Разработка запросов к полям с составным ссылочным типом данных — одна из частых задач при работе с 1С, будь то работа с полем Регистратор, субконто или произвольным реквизитом составного типа. Однако при попытке разыменования таких полей («через точку») разработчики нередко сталкиваются с резким падением производительности СУБД или даже аварийным завершением запроса. Разберем механику этой проблемы и рассмотрим лучшие практики построения производительных запросов.
Проанализируем, как платформа 1С:Предприятие транслирует конструкции запроса на язык СУБД. На уровне базы данных составной ссылочный тип хранится в виде набора колонок: код типа данных, код таблицы метаданных и бинарный идентификатор ссылки (GUID).
Когда мы обращаемся через одну точку к реквизиту составного поля (например, Источник.СоставноеПоле.Реквизит), платформа не знает заранее, какой именно тип объекта хранится в каждой строке. Поэтому на уровне SQL формируется неявное левое соединение (LEFT JOIN) со всеми таблицами метаданных, типы которых входят в состав реквизита.
Если же мы пытаемся получить «реквизит реквизита» составного типа (обращение через две точки), происходит каскадное лавинообразное умножение соединений: платформа сначала неявно соединяет все таблицы первого уровня, а затем для каждого полученного ссылочного типа строит соединения со связанными таблицами второго уровня. Если тип широкий (например, ЛюбаяСсылка или регистратор с десятками документов), число формируемых соединений может достигать нескольких сотен.
Включение ограничений прав на уровне записей (RLS) усугубляет ситуацию, так как к каждой автоматически присоединенной таблице СУБД добавляет внутренние подзапросы безопасности. В секциях ГДЕ, ПО и УПОРЯДОЧИТЬ ПО такое разыменование полностью блокирует использование индексов и переводит запрос в режим полного сканирования таблиц.
Если нам достоверно известно, что в рамках конкретной выборки нас интересует строго определенный ссылочный тип данных, наилучший способ исключить лишние неявные соединения — использовать конструкцию ВЫРАЗИТЬ() КАК. Посмотрим на пример кода:
ВЫБРАТЬ
ВЫРАЗИТЬ(Источник.СоставноеПоле КАК Документ.РеализацияТоваровУслуг).Контрагент.ИНН КАК ИННКонтрагента
ИЗ
РегистрНакопления.Взаиморасчеты КАК Источник
Уточним важную деталь: оператор ВЫРАЗИТЬ позволяет привести значение только к одному конкретному типу метаданных. Он указывает транслятору запросов платформы соединять только одну целевую таблицу, отсекая все остальные варианты и ликвидируя избыточную нагрузку.
Если поле может содержать разные типы ссылок, и нам требуется обработать несколько вариантов или безопасно получить данные, не вызывая ошибок, разберем подход с оператором ССЫЛКА. Посмотрим на пример:
ВЫБРАТЬ
ВЫБОР
КОГДА Источник.СоставноеПоле ССЫЛКА Документ.РеализацияТоваровУслуг
ТОГДА ВЫРАЗИТЬ(Источник.СоставноеПоле КАК Документ.РеализацияТоваровУслуг).Контрагент.ИНН
КОГДА Источник.СоставноеПоле ССЫЛКА Документ.ПоступлениеТоваровУслуг
ТОГДА ВЫРАЗИТЬ(Источник.СоставноеПоле КАК Документ.ПоступлениеТоваровУслуг).Контрагент.ИНН
ИНАЧЕ NULL
КОНЕЦ КАК ИННКонтрагента
ИЗ
РегистрНакопления.Взаиморасчеты КАК Источник
Обратите внимание: даже внутри конструкции ВЫБОР КОГДА обязательно нужно применять ВЫРАЗИТЬ, иначе платформа попытается неявно соединить все потенциальные таблицы для ветки разыменования.
Вместо неявного разыменования через точку надежнее и нагляднее использовать явное соединение с требуемой таблицей метаданных. В этом случае мы полностью контролируем план выполнения запроса. Разберем пример реализации:
ВЫБРАТЬ
ТаблицаРеализаций.Контрагент.ИНН КАК ИННКонтрагента
ИЗ
РегистрНакопления.Взаиморасчеты КАК Источник
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК ТаблицаРеализаций
ПО Источник.СоставноеПоле = ТаблицаРеализаций.Ссылка
При таком подходе соединение происходит исключительно с одной таблицей Документ.РеализацияТоваровУслуг, минуя все остальные типы, входящие в составной реквизит.
Когда требуется выбрать реквизиты второго уровня для нескольких типов документов из большой таблицы, наивысшую производительность обеспечивает разделение задачи на независимые части с последующим объединением. Рассмотрим, как это сделать:
ВЫБРАТЬ
ВЫРАЗИТЬ(Источник.СоставноеПоле КАК Документ.РеализацияТоваровУслуг).Контрагент.ИНН КАК ИННКонтрагента
ИЗ
РегистрНакопления.Взаиморасчеты КАК Источник
ГДЕ
Источник.СоставноеПоле ССЫЛКА Документ.РеализацияТоваровУслуг
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ВЫРАЗИТЬ(Источник.СоставноеПоле КАК Документ.ПоступлениеТоваровУслуг).Контрагент.ИНН
ИЗ
РегистрНакопления.Взаиморасчеты КАК Источник
ГДЕ
Источник.СоставноеПоле ССЫЛКА Документ.ПоступлениеТоваровУслуг
В данном сценарии оптимизатор СУБД строит максимально простой и эффективный план для каждого отдельного блока, выполняя сканирование по индексам с жестким фильтром по типу объекта.
Если объем данных в выборке велик, разберем по шагам алгоритм с временными таблицами:
ССЫЛКА.
ВЫБРАТЬ РАЗЛИЧНЫЕ
Источник.СоставноеПоле КАК ДокументСсылка
ПОМЕСТИТЬ ВТ_Ссылки
ИЗ
РегистрНакопления.Взаиморасчеты КАК Источник
ГДЕ
Источник.СоставноеПоле ССЫЛКА Документ.РеализацияТоваровУслуг
ИНДЕКСИРОВАТЬ ПО
ДокументСсылка
;
ВЫБРАТЬ
ВТ_Ссылки.ДокументСсылка КАК Документ,
Контрагенты.ИНН КАК ИНН
ИЗ
ВТ_Ссылки КАК ВТ_Ссылки
СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Документы
ПО ВТ_Ссылки.ДокументСсылка = Документы.Ссылка
СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
ПО Документы.Контрагент = Контрагенты.Ссылка
Помимо оптимизации языка запросов, проанализируем архитектурные методы решения проблемы:
ЛюбаяСсылка, если в этом нет жесткой необходимости). Каждое исключение типа уменьшает число потенциальных соединений в базе.