Как решить проблему сортировки по GUID в запросе 1С без изменения типовой конфигурации?

Программист 1С v8.3 (Обычные формы) Управленческий учет IT и автоматизация бизнеса
← На главную

При сопровождении и доработке конфигураций на платформе 1С:Предприятие разработчики часто сталкиваются с неочевидным поведением платформы и СУБД. Классический пример — использование агрегатных функций МИНИМУМ или МАКСИМУМ по ссылочному полю в запросах. В этой статье мы подробно разберем, почему возникает такая проблема, как работают механизмы СУБД при сравнении уникальных идентификаторов (GUID), и рассмотрим практические способы исправления ситуации без изменения кода основной конфигурации.

Суть проблемы: почему ломается логика запроса

Рассмотрим типичную ситуацию на примере запроса из конфигурации «Управление производственным предприятием» (УПП). В системе существует предопределенный вид расчета (например, «Командировка» с кодом 1283). Бухгалтер копирует этот вид расчета, создает дубликат с кодом 1284, чтобы настроить для него специфическое отражение в учете. В этот момент ломается логика типового алгоритма.

Типовой запрос выбирает минимальный вид расчета среди подходящих под условия:


ВЫБРАТЬ
    МИНИМУМ(ОсновныеНачисления.Ссылка) КАК ВидРасчета,
    ВидыДоговоровСФизЛицами.Ссылка КАК ВидДоговора
ПОМЕСТИТЬ ВТНачислениеКомандировки
ИЗ
    Перечисление.ВидыДоговоровСФизЛицами КАК ВидыДоговоровСФизЛицами
ВНУТРЕННЕЕ СОЕДИНЕНИЕ 
    ПланВидовРасчета.ОсновныеНачисленияОрганизаций КАК ОсновныеНачисления
ПО 
    (ОсновныеНачисления.СпособРасчета = ЗНАЧЕНИЕ(Перечисление.СпособыРасчетаОплатыТруда.ПоСреднемуЗаработку))
    И (ОсновныеНачисления.ВидВремени = ЗНАЧЕНИЕ(Перечисление.ВидыВремени.ЦелодневноеНеотработанное))
    И (ОсновныеНачисления.ОбозначениеВТабелеУчетаРабочегоВремени = ЗНАЧЕНИЕ(Справочник.КлассификаторИспользованияРабочегоВремени.Командировка))
СГРУППИРОВАТЬ ПО
    ВидыДоговоровСФизЛицами.Ссылка

До копирования запрос всегда возвращал предопределенный вид расчета. После копирования результатом функции МИНИМУМ(Ссылка) внезапно становится новый элемент (код 1284). Это происходит потому, что платформа 1С на уровне СУБД сравнивает бинарные данные ссылок — их уникальные идентификаторы (GUID). Если GUID нового элемента оказывается «меньше» с точки зрения СУБД, то система выбирает именно его, нарушая бизнес-логику программы.

Выясним причину, почему так происходит, проанализировав поведение различных систем управления базами данных.

---

Особенности сравнения GUID в различных СУБД

В базе данных СУБД ссылки 1С хранятся в шестнадцатеричном бинарном формате (тип данных binary(16) в MS SQL или uuid в PostgreSQL). Сравнение этих данных не эквивалентно лексикографическому сравнению строк, к которому привыкли программисты.

1. Поведение в MS SQL Server

MS SQL Server использует для хранения ссылок тип данных uniqueidentifier. При выполнении операции сравнения или сортировки (включая агрегатные функции MIN/MAX) MS SQL Server применяет специфический алгоритм: он оценивает группы байтов справа налево. Наибольший приоритет при сравнении имеют последние 6 байт GUID (так называемая группа "node").

Это означает, что если у нового элемента последние байты GUID окажутся меньше, чем у старого предопределенного элемента, то в SQL-запросе функция MIN(_IDRRef) гарантированно вернет ссылку на новый элемент, независимо от даты его создания, кода или наименования.

2. Поведение в PostgreSQL

В СУБД PostgreSQL тип данных uuid сравнивается стандартным побайтовым методом слева направо (аналог функции memcmp). Результат выполнения одного и того же запроса с функцией МИНИМУМ(Ссылка) на MS SQL Server и на PostgreSQL может кардинально отличаться именно из-за разницы алгоритмов сравнения на уровне СУБД!

3. Последовательные GUID (Sequential UUIDs) в 1С

Начиная с версии платформы 8.3, 1С:Предприятие генерирует упорядоченные (последовательные) уникальные идентификаторы для уменьшения фрагментации индексов таблиц СУБД при добавлении новых записей. Обычно новые элементы получают GUID, который больше ранее созданных. Однако при ручном копировании элементов, обмене данными или миграциях баз эта последовательность нарушается, что и приводит к подобным коллизиям.

---

Способы решения проблемы без изменения кода типовой конфигурации

Рассмотрим по шагам несколько практических способов исправления ситуации, когда нам категорически нельзя или крайне нежелательно редактировать код типового общего модуля напрямую.

Способ А. Полная рокировка уникальных идентификаторов (GUID) объектов

Поскольку напрямую отредактировать GUID существующего элемента в базе данных невозможно, нам необходимо создать новый объект с правильным (заведомо «большим») идентификатором и перенести на него все ссылки. Разберем этот процесс по шагам.

  1. Создание временного элемента-дубля. Создайте в плане видов расчета (или справочнике) временный элемент «Временный Командировка».
  2. Перенаправление ссылок. С помощью стандартной обработки «Поиск и замена значений» перенаправьте все ссылки с проблемного нового элемента (код 1284) на созданный «Временный Командировка». Проблемный элемент (код 1284) теперь нигде не используется.
  3. Удаление проблемного элемента. Физически удалите старый элемент с кодом 1284 из базы данных.
  4. Программное создание элемента с нужным GUID. Напишите простую внешнюю обработку, которая создаст новый элемент с помощью метода УстановитьСсылкуНового(). Нам нужно сгенерировать такой GUID, который при сравнении в вашей СУБД гарантированно будет «больше» GUID предопределенного элемента.

Пример кода для внешней обработки по генерации и записи элемента с новым GUID:


// 1. Получаем GUID предопределенного элемента, который должен остаться минимальным
ПредопределенныйЭлемент = ПланыВидовРасчета.ОсновныеНачисленияОрганизаций.Командировка;
УИДПредопределенного = ПредопределенныйЭлемент.УникальныйИдентификатор();

// 2. Генерируем новый GUID, который гарантированно больше старого для нашей СУБД
// Для надежности можно использовать циклическую генерацию, пока не получим нужные байты
НовыйУИД = НайтиПравильныйGUID(УИДПредопределенного);

// 3. Создаем новый элемент плана видов расчета с принудительной установкой ссылки
НоваяСсылка = ПланыВидовРасчета.ОсновныеНачисленияОрганизаций.ПолучитьСсылка(НовыйУИД);

НовыйОбъект = ПланыВидовРасчета.ОсновныеНачисленияОрганизаций.СоздатьВидРасчета();
НовыйОбъект.УстановитьСсылкуНового(НоваяСсылка);

// Заполняем реквизиты нового элемента данными из временного объекта
ЗаполнитьЗначенияСвойств(НовыйОбъект, ВременныйЭлемент);
НовыйОбъект.Код = "1284";
НовыйОбъект.Записать();

После записи нового элемента снова запустите обработку «Поиск и замена значений» и перенесите все данные с элемента «Временный Командировка» на наш вновь созданный элемент. Временный элемент удалите. Теперь GUID нового элемента физически больше, и запрос вернет предопределенный элемент.

---

Способ Б. Метод «Лотереи GUID» (циклическая генерация во внешней обработке)

Для того чтобы гарантированно сгенерировать GUID, который СУБД будет считать большим, чем GUID предопределенного элемента, воспользуемся алгоритмом циклической проверки байтов. Напишем функцию для сравнения уникальных идентификаторов по правилам MS SQL Server:


Функция GUIDБольшеДляMSSQL(GUID1, GUID2) Экспорт
    Строка1 = НРег(СтрЗаменить(Строка(GUID1), "-", ""));
    Строка2 = НРег(СтрЗаменить(Строка(GUID2), "-", ""));
    
    // MS SQL Server сравнивает группы байтов в следующем порядке:
    // Подстроки: [10-15] байты (символы 20-32), затем [8-9] (16-20), [6-7] (12-16), [0-5] (0-12)
    
    Группа1_1 = Сред(Строка1, 21, 12);
    Группа2_1 = Сред(Строка2, 21, 12);
    Если Группа1_1 <> Группа2_1 Тогда
        Возврат Группа1_1 > Группа2_1;
    КонецЕсли;
    
    Группа1_2 = Сред(Строка1, 17, 4);
    Группа2_2 = Сред(Строка2, 17, 4);
    Если Группа1_2 <> Группа2_2 Тогда
        Возврат Группа1_2 > Группа2_2;
    КонецЕсли;
    
    Группа1_3 = Сред(Строка1, 13, 4);
    Группа2_3 = Сред(Строка2, 13, 4);
    Если Группа1_3 <> Группа2_3 Тогда
        Возврат Группа1_3 > Группа2_3;
    КонецЕсли;
    
    Группа1_4 = Лев(Строка1, 12);
    Группа2_4 = Лев(Строка2, 12);
    Возврат Группа1_4 > Группа2_4;
КонецФункции

С помощью этой функции в цикле генерируйте новые уникальные идентификаторы через метод Новый УникальныйИдентификатор() до тех пор, пока функция GUIDБольшеДляMSSQL не вернет значение Истина по отношению к GUID предопределенного элемента. Обычно для этого требуется не более нескольких десятков итераций, что занимает доли секунды.

---

Способ В. Использование расширения конфигурации (наиболее современный и безопасный метод)

Если ваша база данных работает на платформе 1С:Предприятие 8.3 (даже если сама конфигурация работает в режиме совместимости со старыми версиями), лучшим решением будет использование механизма Расширений конфигурации. Это позволит не трогать данные в СУБД и сохранить конфигурацию на поддержке.

Разберем этот метод по шагам:

  1. Создайте новое расширение конфигурации в конфигураторе.
  2. Найдите общий модуль, в котором находится проблемный запрос (в нашем примере это модуль АнализНеявокПереопределяемый), и добавьте его в расширение.
  3. Создайте в расширении метод-перехватчик с аннотацией &Вместо для функции, возвращающей текст запроса:

&Вместо("ВернутьТекстВТНачислениеКомандировки")
Функция Расш_ВернутьТекстВТНачислениеКомандировки()
    // Мы можем полностью переопределить текст запроса, избавившись от ненадежной функции МИНИМУМ
    // и заменив ее на явный отбор по предопределенному значению
    ТекстЗапроса = 
    "ВЫБРАТЬ
    |	ЗНАЧЕНИЕ(ПланВидовРасчета.ОсновныеНачисленияОрганизаций.Командировка) КАК ВидРасчета,
    |	ВидыДоговоровСФизЛицами.Ссылка КАК ВидДоговора
    |ПОМЕСТИТЬ ВТНачислениеКомандировки
    |ИЗ
    |	Перечисление.ВидыДоговоровСФизЛицами КАК ВидыДоговоровСФизЛицами";
    
    Возврат ТекстЗапроса;
КонецФункции

Этот способ полностью решает проблему, исключает риск повторения ситуации при создании других копий видов расчета и гарантирует стабильную работу программы при последующих обновлениях конфигурации поставщика.

← На главную