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