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