Почему МАКСИМУМ() в запросе 1С выбирает пустую ссылку вместо заполненной?

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

Каждый разработчик 1С рано или поздно сталкивается с ситуациями в работе запросов, которые на первый взгляд кажутся абсолютно нелогичными и даже мистическими. Представьте стандартную задачу: в выборке присутствуют как пустые, так и заполненные ссылки различных типов документов. Мы пишем привычное групповое поле с агрегатной функцией МАКСИМУМ() и ожидаем получить самый актуальный или «наибольший» документ. Однако платформа возвращает нам пустую ссылку одного из типов.

В этой статье мы подробно разберем, почему СУБД ведет себя именно так, заглянем во внутреннее устройство хранения данных 1С и рассмотрим надежные практические методы решения этой проблемы.

Физическая структура хранения составных типов в СУБД

Для того чтобы понять логику работы СУБД, нам необходимо разобраться, как платформа 1С транслирует объекты метаданных в физические таблицы базы данных (например, под управлением MS SQL Server или PostgreSQL). Если реквизит имеет простой ссылочный тип (например, только ДокументСсылка.РеализацияТоваровУслуг), в базе данных создается ровно одно поле для хранения 16-байтового уникального идентификатора (GUID) — _RRRef.

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

  1. Поле типа _TRef (или _RTRef) — содержит внутренний числовой (бинарный) идентификатор типа метаданных. Это уникальный код таблицы конкретного документа или справочника внутри базы данных.
  2. Поле ссылки _RRRef — содержит уникальный идентификатор (GUID) конкретной записи в соответствующей таблице.

Если значение в поле не определено, или тип пустой, могут создаваться дополнительные поля (например, _Fld123_TYPE для хранения признака типа данных), но ключевую роль в сравнении ссылок играют именно первые два параметра.

Как СУБД выполняет сравнение и поиск максимума

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

  1. В первую очередь СУБД сравнивает значения в поле идентификатора типа _TRef.
  2. И только в том случае, если идентификаторы типов в поле _TRef абсолютно равны, СУБД переходит к сравнению GUID в поле _RRRef.

Давайте проанализируем конкретную ситуацию на примере. Представим, что у нас есть две строки в таблице:

Строка 1: Заполненный документ типа Документ.РеализацияТоваровУслуг. Предположим, что в нашей базе данных этот тип получил внутренний идентификатор _TRef = 0x00000010 (в десятичном виде — 16). Сам GUID документа заполнен реальным значением: _RRRef = 0x80e500155d010b0311e3b56a312a0001.

Строка 2: Пустая ссылка типа Документ.РасходныйКассовыйОрдер. Этот тип получил идентификатор _TRef = 0x00000025 (в десятичном виде — 37). Так как ссылка пустая, её GUID заполнен нулями: _RRRef = 0x00000000000000000000000000000000.

Теперь посмотрим, что происходит, когда СУБД начинает искать МАКСИМУМ() между Строкой 1 и Строкой 2. СУБД сравнивает первые поля составного ключа — _TRef:

0x00000025 больше, чем 0x00000010 (37 > 16)

Поскольку идентификатор типа во второй строке оказался больше, СУБД признает всю вторую строку «большей», даже не приступая к сравнению уникальных идентификаторов _RRRef. В результате выполнения запроса мы получаем пустую ссылку документа РасходныйКассовыйОрдер, а заполненный документ другого типа просто игнорируется.

Главная скрытая опасность: Нестабильность на разных базах

Самое неприятное в этой особенности — её нестабильность. Внутренние идентификаторы типов (значения _TRef) назначаются платформой 1С динамически в процессе создания объектов метаданных в конфигураторе.

Это приводит к тому, что один и тот же запрос на абсолютно идентичных данных может работать по-разному в разных базах:

Такое поведение делает ошибку крайне трудной для локализации и тестирования.

Как правильно решать эту проблему в запросах 1С

Поиск максимума по составному полю напрямую — это архитектурная ошибка. Разберем три надежных способа, которые помогут гарантированно обойти эту проблему.

Вариант А. Явное приведение типов через ВЫРАЗИТЬ()

Если в рамках вашей бизнес-логики группировка и поиск максимума должны происходить по конкретному типу данных, обязательно используйте функцию ВЫРАЗИТЬ(). Это сузит составной тип до одного конкретного типа.

Рассмотрим пример запроса:


ВЫБРАТЬ
    МАКСИМУМ(ВЫРАЗИТЬ(Таблица.СоставноеПоле КАК Документ.РеализацияТоваровУслуг)) КАК МаксимальныйДокумент
ИЗ
    РегистрНакопления.Продажи КАК Таблица

Как только мы применили функцию ВЫРАЗИТЬ(), поле перестало быть составным для СУБД. Теперь SQL-сервер будет сравнивать исключительно GUID (поле _RRRef). При сравнении GUID пустая ссылка (состоящая из одних нулей) всегда будет самым минимальным значением и гарантированно уступит место любому реальному заполненному документу.

Вариант Б. Фильтрация пустых ссылок до группировки

Если пустые ссылки в выборке вам в принципе не нужны и не несут смысловой нагрузки, наиболее эффективным решением будет отсечь их еще на этапе выборки в секции ГДЕ.

Для составного поля проверить «пустоту» всех входящих в него типов можно через обращение к реквизиту Ссылка. Разберем этот пример:


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

При обращении через точку к реквизиту Ссылка для пустой ссылки любого типа (а также для значений НЕОПРЕДЕЛЕНО и NULL) СУБД вернет значение NULL. Условие НЕ ... ЕСТЬ NULL надежно отфильтрует такие строки, и в агрегацию МАКСИМУМ() попадут только заполненные документы.

Вариант В. Хронологический выбор (поиск по Дате или Моменту времени)

Стоит помнить, что поиск максимума по ссылке (даже одного типа) — не всегда корректная задача с точки зрения логики учета. Уникальные идентификаторы (GUID) генерируются платформой и не гарантируют хронологической последовательности. Документ, созданный позже, может иметь GUID «меньше», чем документ, созданный раньше.

Правильным решением является поиск максимального документа через его дату или момент времени. Реализуем этот подход по шагам с использованием временных таблиц:

  1. Сначала найдем максимальную дату для каждого ключевого поля (например, контрагента).
  2. Затем соединим полученную максимальную дату с исходной таблицей, чтобы получить физическую ссылку на документ.

Посмотрим на пример реализации такого запроса:


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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_МаксимальныеДаты.Контрагент КАК Контрагент,
    Взаиморасчеты.СоставноеПоле КАК Документ
ИЗ
    ВТ_МаксимальныеДаты КАК ВТ_МаксимальныеДаты
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.Взаиморасчеты КАК Взаиморасчеты
        ПО ВТ_МаксимальныеДаты.Контрагент = Взаиморасчеты.Contragent
        И ВТ_МаксимальныеДаты.МаксимальнаяДата = Взаиморасчеты.Период

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

← На главную