Каждый разработчик на платформе 1С:Предприятие рано или поздно сталкивается с парадоксальной ситуацией: написан запрос для поиска дублей или анализа данных, в Консоли запросов он выполняется без ошибок и возвращает сотни строк, но при переносе в Систему компоновки данных (СКД) отчет выводит лишь пустую таблицу — только заголовки колонок без единой строки данных. При этом в настройках отчета нет ни параметров, ни скрытых отборов, а структура группировок кажется абсолютно верной.
В этой статье мы подробно разберем, почему возникает эта проблема, как особенности оптимизатора СКД влияют на выполнение запросов СУБД и каким образом рефакторинг запроса с использованием конструкции ОБЪЕДИНИТЬ ВСЕ позволяет полностью решить проблему пустых результатов и колоссально увеличить производительность отчета.
Рассмотрим классический пример запроса, который нацелен на поиск дублирующихся записей в справочнике Контрагенты. Разработчик соединяет таблицу саму с собой по несовпадающим ссылкам и совпадению ключевых реквизитов (наименования, ИНН или краткого наименования):
ВЫБРАТЬ
Контрагенты.Ссылка КАК Оригинал,
Контрагенты1.Ссылка КАК Дубль,
ВЫБОР
КОГДА Контрагенты.Наименование = Контрагенты1.Наименование
ТОГДА "Совпадает полное наименование"
КОГДА Контрагенты.КороткоеНаименование = Контрагенты1.КороткоеНаименование
ТОГДА "Совпадает короткое наименование"
ИНАЧЕ "Совпадает ИНН"
КОНЕЦ КАК ТипДублирования
ИЗ
Справочник.Контрагенты КАК Контрагенты
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты1
ПО Контрагенты.Ссылка <> Контрагенты1.Ссылка
И Контрагенты.Код < Контрагенты1.Код
И (Контрагенты.Наименование = Контрагенты1.Наименование
ИЛИ Контрагенты.ИНН = Контрагенты1.ИНН
ИЛИ Контрагенты.КороткоеНаименование = Контрагенты1.КороткоеНаименование)
ГДЕ
НЕ Контрагенты.ПометкаУдаления
И НЕ Контрагенты1.ПометкаУдаления
В Консоли запросов этот код работает и находит дубликаты. Однако время его выполнения составляет порядка 30 секунд. При переносе в СКД отчет формирует пустоту. Чтобы понять природу этого явления, нам необходимо заглянуть во внутренние механизмы работы платформы 1С.
Главное заблуждение разработчиков заключается в том, что СКД якобы отправляет в СУБД ровно тот запрос, который написан в схеме компоновки данных. Это не так. По умолчанию в наборе данных включен флаг Автозаполнение. СКД анализирует настройки отчета (какие поля выбраны пользователем, какие используются в группировках, какие отборы применены) и на основе этого динамически перестраивает исходный SQL-текст.
Когда мы выполняем самосоединение (таблица соединяется сама с собой), оптимизатор СКД может совершить логическую ошибку:
Контрагенты1.Ссылка), СКД может посчитать это соединение «избыточным» и исключить его поля из финальной выборки для оптимизации трафика.Контрагенты.Ссылка <> Контрагенты1.Ссылка, и СУБД возвращает пустой набор записей.Для предотвращения такого поведения в настройках набора данных СКД на вкладке Поля для всех ключевых полей, участвующих в связях (даже если они напрямую не выводятся в отчет), следует принудительно устанавливать свойство Обязательное.
Вторая и наиболее критичная проблема исходного запроса — это наложение условия ИЛИ в секции связи ПО (ON) левого соединения:
ПО ... И (Контрагенты.Наименование = Контрагенты1.Наименование ИЛИ Контрагенты.ИНН = Контрагенты1.ИНН ИЛИ ...)
Для СУБД (будь то Microsoft SQL Server или PostgreSQL) оператор ИЛИ в условиях соединения является крайне тяжелой операцией. База данных не может использовать стандартные индексы по отдельным полям (происходит так называемый Index Seek) и вынуждена переходить к полному сканированию всей таблицы (Table Scan) или к неоптимальным вложенным циклам (Nested Loops).
Когда СКД генерирует итоговый запрос, она оборачивает исходный текст во внешние выборки для вычисления итогов и наложения системных отборов. Под воздействием этих «надстроек» план выполнения СУБД ухудшается в геометрической прогрессии. Запрос, выполнявшийся в консоли 30 секунд, в недрах СКД либо уходит в бесконечный расчет (таймаут соединения), либо СУБД аварийно завершает поток выполнения из-за нехватки ресурсов TempDB, возвращая пустой результат.
Проанализируем следующую конструкцию в исходном запросе:
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты1 ... ГДЕ НЕ Контрагенты1.ПометкаУдаления
Использование проверки полей правой таблицы левого соединения в секции ГДЕ — это классическая логическая ошибка. Если для контрагента из левой таблицы не найдется дубля в правой, то все поля таблицы Контрагенты1 примут значение NULL. Проверка НЕ NULL (или попытка сравнения NULL = ЛОЖЬ) в секции ГДЕ вернет значение Unknown (ложь). В результате СУБД отсечет эту строку.
Таким образом, левое соединение неявно превращается во внутреннее соединение (INNER JOIN). Такое расхождение логики также мешает оптимизатору СКД правильно построить итоговый запрос и может приводить к непредсказуемой фильтрации данных.
Чтобы гарантировать стабильный вывод результатов в СКД и ускорить работу отчета в сотни раз, необходимо отказаться от оператора ИЛИ в связях и разбить исходный запрос на три простых независимых запроса, объединив их результаты с помощью конструкции ОБЪЕДИНИТЬ ВСЕ.
Рассмотрим правильный оптимизированный вариант запроса для СКД:
ВЫБРАТЬ
Контрагенты.Ссылка КАК Оригинал,
Контрагенты1.Ссылка КАК Дубль,
"Совпадает полное наименование" КАК ТипДублирования
ИЗ
Справочник.Контрагенты КАК Контрагенты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты1
ПО Контрагенты.Ссылка <> Контрагенты1.Ссылка
И Контрагенты.Код < Контрагенты1.Код
И Контрагенты.Наименование = Контрагенты1.Наименование
ГДЕ
НЕ Контрагенты.ПометкаУдаления
И НЕ Контрагенты1.ПометкаУдаления
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Контрагенты.Ссылка КАК Оригинал,
Контрагенты1.Ссылка КАК Дубль,
"Совпадает короткое наименование" КАК ТипДублирования
ИЗ
Справочник.Контрагенты КАК Контрагенты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты1
ПО Контрагенты.Ссылка <> Контрагенты1.Ссылка
И Контрагенты.Код < Контрагенты1.Код
И Контрагенты.КороткоеНаименование = Контрагенты1.КороткоеНаименование
ГДЕ
НЕ Контрагенты.ПометкаУдаления
И НЕ Контрагенты1.ПометкаУдаления
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Контрагенты.Ссылка КАК Оригинал,
Контрагенты1.Ссылка КАК Дубль,
"Совпадает ИНН" КАК ТипДублирования
ИЗ
Справочник.Контрагенты КАК Контрагенты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты1
ПО Контрагенты.Ссылка <> Контрагенты1.Ссылка
И Контрагенты.Код < Контрагенты1.Код
И Контрагенты.ИНН = Контрагенты1.ИНН
ГДЕ
НЕ Контрагенты.ПометкаУдаления
И НЕ Контрагенты1.ПометкаУдаления
Проанализируем изменения и их влияние на поведение системы компоновки данных:
ОБЪЕДИНИТЬ ВСЕ представляют собой линейную структуру. СКД гораздо легче транслирует их в итоговый SQL-код без искажения логики связей.ЛЕВОЕ СОЕДИНЕНИЕ на эффективное ВНУТРЕННЕЕ СОЕДИНЕНИЕ (INNER JOIN). Это исключило неопределенности со значениями NULL при наложении фильтров в секции ГДЕ.Если в процессе разработки вы снова столкнулись с пустым результатом в СКД при работающем запросе, выполните следующие шаги: