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