При разработке сложных отчетов на схеме компоновки данных (СКД) разработчики часто сталкиваются с ситуацией, когда "великий и ужасный" оптимизатор запросов СКД самостоятельно вырезает из итогового SQL-запроса поля. Особенно часто это происходит, если поле во вложенном запросе представляет собой не просто прямую выборку, а некоторое выражение (например, конкатенацию строк, арифметическую операцию или конструкцию ВЫБОР КОГДА). В итоге в результирующем отчете вместо ожидаемых данных мы получаем пустоту или ошибки компиляции, хотя в настройках компоновки это поле явно выбрано.
В этой статье мы подробно разберем, почему компоновщик макета СКД ведет себя подобным образом, и рассмотрим пять практических способов решения этой проблемы — от простых манипуляций со структурой запроса до использования встроенных механизмов самой платформы 1С.
Механизм оптимизации СКД работает по принципу построения дерева зависимостей полей "снизу вверх" — от финальных настроек, выбранных пользователем в схеме компоновки, до исходных таблиц базы данных. Если на каком-то этапе поле передается напрямую (например, Выборка.Поле КАК Поле), СКД без труда сопоставляет псевдонимы и отслеживает путь поля.
Однако, если внутри вложенного запроса поле преобразуется с помощью выражения (например, ГлючноеПоле + "выражение" КАК ГлючноеПоле), цепочка псевдонимов в глазах оптимизатора прерывается. Компоновщик начинает воспринимать это поле как изолированную сущность. Если на самом верхнем уровне запроса поле-выражение не выбрано пользователем в настройках напрямую в строго определенном виде или логическая цепочка прерывается другими вложенными таблицами, СКД считает эту ветку избыточной для оптимизации СУБД. В результате выражение просто удаляется из итогового SQL-текста, и поле "исчезает".
Проанализируем классический пример проблемного запроса, где поле ГлючноеПоле пропадает после компоновки:
ВЫБРАТЬ
ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2
ПОМЕСТИТЬ ВТ
ИЗ
(ВЫБРАТЬ
"г" КАК ГлючноеПоле,
"нг" КАК НеглючноеПоле,
"нн1" КАК НенужноеПоле1,
"нн2" КАК НенужноеПоле2
{ВЫБРАТЬ
ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2}
) КАК ВложенныйЗапрос;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2
{ВЫБРАТЬ
ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2}
ИЗ
(ВЫБРАТЬ
ГлючноеПоле + "выражение" КАК ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2
ИЗ
(ВЫБРАТЬ
ВыборкаИзВТ.ГлючноеПоле,
ВыборкаИзВТ.НеглючноеПоле,
ПрисоединеннаяВТ.НенужноеПоле1,
ПрисоединеннаяВТ.НенужноеПоле2
ИЗ
(ВЫБРАТЬ
ВТ.ГлючноеПоле,
ВТ.НеглючноеПоле
ИЗ
ВТ КАК ВТ
) КАК ВыборкаИзВТ
ЛЕВОЕ СОЕДИНЕНИЕ ВТ КАК ПрисоединеннаяВТ
ПО ВыборкаИзВТ.ГлючноеПоле = ПрисоединеннаяВТ.ГлючноеПоле
) КАК СоединениеС_ВТ
) КАК ЗдесьПолеИсчезает
Если пользователь выберет в настройках только ГлючноеПоле и НеглючноеПоле, оптимизатор попытается вырезать неиспользуемые поля. При этом из-за наличия операции конкатенации строк во вложенном запросе ЗдесьПолеИсчезает, компоновщик полностью исключит расчет поля ГлючноеПоле.
Рассмотрим по шагам основные и наиболее эффективные варианты решения этой проблемы.
Это наиболее надежный архитектурный подход, который полностью решает проблему оптимизации и одновременно повышает производительность отчета на уровне СУБД. Процессор компоновки данных гораздо корректнее и предсказуемее работает с последовательными временными таблицами, нежели с многократно вложенными подзапросами в секции ИЗ (FROM).
Для исправления ситуации перепишем запрос, поместив результат соединения во временную таблицу СоединениеС_ВТ непосредственно перед вычислением выражения. Разберем этот пример:
ВЫБРАТЬ
ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2
ПОМЕСТИТЬ ВТ
ИЗ
(ВЫБРАТЬ
"г" КАК ГлючноеПоле,
"нг" КАК НеглючноеПоле,
"нн1" КАК НенужноеПоле1,
"нн2" КАК НенужноеПоле2
{ВЫБРАТЬ
ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2}
) КАК ВложенныйЗапрос;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВыборкаИзВТ.ГлючноеПоле,
ВыборкаИзВТ.НеглючноеПоле,
ПрисоединеннаяВТ.НенужноеПоле1,
ПрисоединеннаяВТ.НенужноеПоле2
ПОМЕСТИТЬ СоединениеС_ВТ
ИЗ
(ВЫБРАТЬ
ВТ.ГлючноеПоле,
ВТ.НеглючноеПоле
ИЗ
ВТ КАК ВТ
) КАК ВыборкаИзВТ
ЛЕВОЕ СОЕДИНЕНИЕ ВТ КАК ПрисоединеннаяВТ
ПО ВыборкаИзВТ.ГлючноеПоле = ПрисоединеннаяВТ.ГлючноеПоле;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2
{ВЫБРАТЬ
ГлючноеПоле,
НеглючноеПоле,
НенужноеПоле1,
НенужноеПоле2}
ИЗ
(ВЫБРАТЬ
СоединениеС_ВТ.ГлючноеПоле + "выражение" КАК ГлючноеПоле,
СоединениеС_ВТ.НеглючноеПоле,
СоединениеС_ВТ.НенужноеПоле1,
СоединениеС_ВТ.НенужноеПоле2
ИЗ
СоединениеС_ВТ КАК СоединениеС_ВТ
) КАК ЗдесьПолеИсчезает
Почему это работает? Помещение промежуточных данных во временную таблицу разрывает "слепую зону" оптимизатора СКД. Теперь структура полей жестко зафиксирована в таблице СоединениеС_ВТ. СКД больше не пытается оптимизировать логику внутри предыдущих шагов соединения на основании финального запроса пакета, и выражение собирается без сбоев.
Если по каким-то причинам плодить временные таблицы не хочется, мы можем вручную подсказать компоновщику, как управлять составом полей во вложенных подзапросах. Для этого используются специальные расширения языка запросов СКД — фигурные скобки {ВЫБРАТЬ...}.
Важное правило: фигурные скобки для выбора полей должны быть объявлены на том уровне вложенного запроса, где эти поля впервые извлекаются или формируются. Проанализируем пример настройки:
ВЫБРАТЬ
ВТ1.А ПОМЕСТИТЬ ВТ1
ИЗ
(ВЫБРАТЬ
"А" КАК А
{ВЫБРАТЬ
А}
) КАК ВТ1;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ2.Б,
ВТ2.Х
ПОМЕСТИТЬ ВТ2
ИЗ
(ВЫБРАТЬ
"Б" КАК Б,
"Х" КАК Х
{ВЫБРАТЬ
Б,
Х}
) КАК ВТ2;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ1.А,
ВТ2.Б,
ВТ2.Х
{ВЫБРАТЬ
А,
Б,
Х}
ИЗ
ВТ1 КАК ВТ1
{ЛЕВОЕ СОЕДИНЕНИЕ ВТ2 КАК ВТ2
ПО ВТ1.А = ВТ2.Х}
Если пользователь выбирает только поля А и Б, благодаря фигурной скобке компоновщик корректно исключит поле Х и само левое соединение из итоговой схемы, не ломая при этом логику запроса. Это позволяет динамически сокращать нагрузку на сервер СУБД, убирая неиспользуемые связи.
Существует элегантный способ запретить оптимизатору СКД исключать поле из запроса на любом из этапов компоновки без изменения исходного SQL-кода. Этот вариант реализуется через визуальные настройки схемы компоновки данных.
Разберем пошаговый алгоритм настройки:
.xsd) в конфигураторе или в консоли отчетов.ГлючноеПоле).Результат: Установка этого свойства жестко указывает генератору запросов СКД, что данное поле должно присутствовать во всех промежуточных запросах пакета и временных таблицах в обязательном порядке, даже если конечный пользователь отключил его видимость в настройках структуры или в выбранных полях отчета. Это гарантирует стабильное вычисление выражения.
Если выражение не содержит специфических функций СУБД и его не требуется рассчитывать строго в процессе выполнения SQL-запроса (например, это простая конкатенация строк, сложение чисел или несложное условие), то наиболее правильным решением будет перенос логики вычислений из тела запроса на уровень процессора компоновки данных.
Проанализируем этот процесс:
Выборка.ГлючноеПоле КАК ГлючноеПоле).ГлючноеПолеСВыражением).ГлючноеПоле + "выражение" или Выбор Когда НеглючноеПоле = "нг" Тогда "Значение1" Иначе "Значение2" Конец.
При таком подходе итоговый SQL-запрос, отправляемый к СУБД, остается максимально простым и легко поддается оптимизации, а все ресурсоемкие операции по форматированию и расчету строк СКД выполнит самостоятельно на этапе вывода результата.
Если вам крайне важно оставить вычисления внутри тела запроса, но при этом нет возможности использовать свойство «Обязательное» (например, при динамическом формировании текста запроса в коде), можно использовать небольшую хитрость — применить фиктивное условие в секции ГДЕ.
Для этого в итоговом запросе добавьте условие, которое заведомо всегда будет истинным, но заставит оптимизатор СКД принудительно включить поле во все выборки:
ГДЕ
ГлючноеПоле <> "ЗаведомоНесуществующееЗначениеУникальное"
ИЛИ ГлючноеПоле ЕСТЬ НЕ NULL
Поскольку поле явно задействовано в секции фильтрации (ГДЕ), оптимизатор СКД не имеет права исключить его из дерева зависимостей, и ваше выражение благополучно дойдет до финального этапа компоновки макета.
Согласно стандартам разработки фирмы 1С, опубликованным на ИТС, использование вложенных запросов в секции ИЗ (особенно левых соединений с вложенными запросами) крайне не рекомендуется. СУБД не всегда способны построить эффективный план выполнения таких запросов из-за отсутствия статистик по виртуальным таблицам вложенных выборок.
Поэтому решение проблемы через **временные таблицы** (описанное в Способе 1) является не просто обходным маневром для исправления багов оптимизатора СКД, а единственно верным архитектурным решением с точки зрения оптимизации производительности базы данных. Переводя вложенные запросы во временные таблицы, мы гарантируем быструю работу отчета даже на терабайтных базах данных.