Как выполнить иерархический обход по вычисляемому полю в запросе 1С без СКД?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке отчетов и обработок в 1С программисты часто сталкиваются с необходимостью представить данные в виде иерархической структуры (дерева). Самым простым и очевидным инструментом для этого является Система компоновки данных (СКД). Однако существуют ситуации, когда использовать СКД невозможно или нецелесообразно (например, при программном формировании специфических XML-выгрузок, заполнении сложных печатных форм или отправке структурированных данных через API). В таких случаях разработчики используют иерархический обход выборки запроса с помощью вложенных циклов.

Особую сложность вызывает ситуация, когда одним из уровней группировки выступает вычисляемое поле — поле, значение которого рассчитывается непосредственно в запросе с помощью функций (например, функций работы с датами, строками или логических выражений). Попытка выполнить обход по такому полю стандартным методом часто приводит к ошибке платформы «Измерение не найдено». В этой статье мы подробно разберем, почему возникает эта ошибка и как правильно реализовать иерархический обход по вычисляемому полю без использования СКД.

Выясняем причину: почему возникает ошибка «Измерение не найдено»?

Рассмотрим стандартную ситуацию. Разработчик пытается сгруппировать данные по вычисляемому полю (например, по началу эфирного дня, который рассчитывается со смещением на 6 часов назад относительно физического времени) и выполнить обход результатов запроса по группировкам:


// НЕРАБОЧИЙ ВАРИАНТ
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТВ_Программа.Период КАК Период,
    |	МАКСИМУМ(ТВ_Программа.ТВ_Передача) КАК ТВ_Передача,
    |	НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ) КАК НачалоЭфирногоДня
    |ИЗ
    |	РегистрСведений.ТВ_Программа КАК ТВ_Программа
    |ГДЕ
    |	ТВ_Программа.Period МЕЖДУ &ДатаС И &ДатаПо
    |СГРУППИРОВАТЬ ПО
    |	НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ),
    |	ТВ_Программа.Период
    |УПОРЯДОЧИТЬ ПО
    |	НачалоЭфирногоДня,
    |	Период";

РезультатЗапроса = Запрос.Выполнить();

// Данный вызов вызовет ошибку: Измерение не найдено "НачалоЭфирногоДня"
ВыборкаГруппировка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "НачалоЭфирногоДня");

Проанализируем ситуацию. Ошибка возникает из-за принципиального различия между конструкциями СУБД и механизмами платформы 1С:

  1. Конструкция СГРУППИРОВАТЬ ПО (GROUP BY): обрабатывается непосредственно на уровне СУБД (MS SQL, PostgreSQL и др.). Она «схлопывает» исходные детальные записи в плоскую таблицу. В результирующей выборке СУБД отсутствуют какие-либо промежуточные итоги или метаданные об иерархических связях. Для платформы 1С полученный результат — это просто плоский набор строк.
  2. Метод Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, ...): требует наличия в результате запроса специальных служебных записей — итогов. Если этих записей нет, платформа не может построить древовидную структуру и определить, по какому именно измерению (колонке) требуется выполнить группировку верхнего уровня. Отсюда и возникает закономерное исключение «Измерение не найдено».

Решение проблемы: правильное построение запроса с использованием ИТОГИ ПО

Для того чтобы платформа 1С смогла построить иерархическую выборку (в том числе по вычисляемому полю), в тексте запроса обязательно должна присутствовать секция ИТОГИ ... ПО ....

При использовании конструкции ИТОГИ ПО платформа 1С самостоятельно обрабатывает полученные от СУБД данные, рассчитывает агрегатные функции для каждого указанного уровня и добавляет в результат запроса промежуточные (итоговые) строки-родоначальники. Именно эти строки позволяют методу Выбрать() корректно позиционироваться на узлах дерева.

Посмотрим на пример правильного запроса, который решает поставленную задачу:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТВ_Программа.Период КАК Период,
    |	ТВ_Программа.ТВ_Передача КАК ТВ_Передача,
    |	НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ) КАК НачалоЭфирногоДня
    |ИЗ
    |	РегистрСведений.ТВ_Программа КАК ТВ_Программа
    |ГДЕ
    |	ТВ_Программа.Период МЕЖДУ &ДатаС И &ДатаПо
    |УПОРЯДОЧИТЬ ПО
    |	НачалоЭфирногоДня,
    |	Период
    |ИТОГИ
    |	МАКСИМУМ(ТВ_Передача)
    |ПО
    |	НачалоЭфирногоДня";

Запрос.УстановитьПараметр("ДатаС", ДатаС);
Запрос.УстановитьПараметр("ДатаПо", ДатаПо);

РезультатЗапроса = Запрос.Выполнить();

Обратите внимание на важные изменения в структуре запроса:

Реализация иерархического обхода во вложенных циклах

После того как мы правильно составили запрос и получили результат со служебными записями итогов, мы можем приступить к обходу структуры с помощью вложенных циклов. Разберем этот процесс по шагам.

Первый (внешний) цикл будет обходить группировочные записи — уникальные значения вычисляемого поля НачалоЭфирногоДня. Второй (внутренний) цикл будет обходить детальные записи, относящиеся к текущему дню.


// Шаг 1. Выбираем группировку верхнего уровня (по вычисляемому полю)
ВыборкаГруппировка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "НачалоЭфирногоДня");

Пока ВыборкаГруппировка.Следующий() Цикл
    
    // Выводим информацию по группировке верхнего уровня
    Сообщить("Эфирный день: " + Формат(ВыборкаГруппировка.НачалоЭфирногоДня, "ДФ=dd.MM.yyyy"));
    
    // Шаг 2. Из текущей строки группировки получаем подчиненную выборку детальных записей
    ВыборкаДетальныеЗаписи = ВыборкаГруппировка.Выбрать();
    
    Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
        
        // Обрабатываем детальные записи внутри группы
        Сообщить("    Время передачи: " + Формат(ВыборкаДетальныеЗаписи.Период, "ДФ=HH:mm") 
            + " - " + ВыборкаДетальныеЗаписи.ТВ_Передача);
            
    КонецЦикла;

КонецЦикла;

При таком подходе платформа 1С не выдаст ошибку, так как поле НачалоЭфирногоДня явно объявлено в секции ИТОГИ ПО, и система подготовила всю необходимую информацию для позиционирования выборки.

Оптимизация: расчет вычисляемого поля во временной таблице (Best Practice)

Использование сложных функций, особенно связанных со смещением и округлением дат (таких как НАЧАЛОПЕРИОДА и ДОБАВИТЬКДАТЕ), непосредственно в секциях расчета итогов и сортировки основного запроса может негативно сказываться на производительности БД при больших объемах данных. СУБД будет вынуждена вычислять эти тяжелые выражения многократно.

Рассмотрим более производительный и профессиональный подход — расчет вычисляемого поля на этапе формирования Временной Таблицы:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТВ_Программа.Период КАК Период,
    |	ТВ_Программа.ТВ_Передача КАК ТВ_Передача,
    |	НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ) КАК НачалоЭфирногоДня
    |ПОМЕСТИТЬ ВТ_ПодготовленныеДанные
    |ИЗ
    |	РегистрСведений.ТВ_Программа КАК ТВ_Программа
    |ГДЕ
    |	ТВ_Программа.Период МЕЖДУ &ДатаС И &ДатаПо
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ВТ_ПодготовленныеДанные.Период КАК Период,
    |	ВТ_ПодготовленныеДанные.ТВ_Передача КАК ТВ_Передача,
    |	ВТ_ПодготовленныеДанные.НачалоЭфирногоДня КАК НачалоЭфирногоДня
    |ИЗ
    |	ВТ_ПодготовленныеДанные КАК ВТ_ПодготовленныеДанные
    |УПОРЯДОЧИТЬ ПО
    |	НачалоЭфирногоДня,
    |	Период
    |ИТОГИ
    |	МАКСИМУМ(ТВ_Передача)
    |ПО
    |	НачалоЭфирногоДня";

РезультатЗапроса = Запрос.Выполнить();

Преимущества данного подхода:

Альтернативный вариант: Выгрузка результатов в Дерево Значений

Если ваша конечная цель состоит не в построчном выводе текстовой информации, а в получении готовой иерархической структуры (например, для вывода ее на управляемую форму в таблицу со структурой "Дерево" или для последующей сложной логической обработки), то писать вложенные циклы обхода выборки необязательно.

Платформа 1С позволяет выгрузить результат запроса с итогами напрямую в специализированный объект ДеревоЗначений с сохранением всех уровней группировки. Для этого используется метод Выгрузить() с указанием типа обхода:


// Выгружаем иерархию напрямую в Дерево Значений
ДеревоРезультата = РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

// Теперь у нас есть готовая древовидная структура.
// Мы можем связать ее с реквизитом формы или обойти рекурсивной процедурой:
ОбходСтрокДереваРекурсивно(ДеревоРезультата.Строки);

Для понимания того, как работать с полученным деревом программно, приведем пример простой рекурсивной процедуры для обхода строк полученного дерева значений:


Процедура ОбходСтрокДереваРекурсивно(СтрокиДерева, Уровень = 0)
    
    Для Каждого СтрокаДерева Из СтрокиДерева Цикл
        
        Отступ = "";
        Для Счетчик = 1 По Уровень Цикл
            Отступ = Отступ + "    ";
        КонецЦикла;
        
        // Если это строка группировки (у нее есть подчиненные строки)
        Если СтрокаДерева.Строки.Количество() > 0 Тогда
            Сообщить(Отступ + "Группировка: " + СтрокаДерева.НачалоЭфирногоДня);
            // Рекурсивный вызов для подчиненных строк
            ОбходСтрокДереваРекурсивно(СтрокаДерева.Строки, Уровень + 1);
        Иначе
            // Это детальная запись
            Сообщить(Отступ + "Детальная запись: " + СтрокаДерева.Период + " - " + СтрокаДерева.ТВ_Передача);
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Тонкости сортировки и обработки значений NULL

При работе с конструкцией ИТОГИ ПО и вычисляемыми полями важно учитывать две важные особенности поведения платформы 1С:

1. Автоматическая сортировка группировок. Когда мы используем ИТОГИ ПО Поле1, платформа автоматически сортирует группировочные записи по возрастанию значений этого поля. Если вам нужна стандартная сортировка по возрастанию, указывать вычисляемое поле в секции УПОРЯДОЧИТЬ ПО не обязательно. Однако, если вам требуется сортировать детальные записи внутри группировки (например, хронологически по полю Период), то поле Период обязательно должно быть указано в секции УПОРЯДОЧИТЬ ПО.

2. Обработка значений NULL. Если вычисляемое поле строится на основе данных из левых соединений или полей, которые физически могут быть не заполнены в базе данных, результат вычисления формулы может вернуть значение NULL. Если поле со значением NULL попадает в секцию ИТОГИ ПО, платформа создаст для него отдельную группировочную строку (обычно она выводится самой первой в списке и выглядит как пустая строка).

Чтобы избежать появления пустых родительских узлов в иерархии, всегда оборачивайте вычисляемые выражения в функцию ЕСТЬNULL. Например:


ЕСТЬNULL(НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ), ДАТАВРЕМЯ(1, 1, 1)) КАК НачалоЭфирногоДня

Это гарантирует, что даже при отсутствии исходных данных группировка получит корректное значение по умолчанию (пустую дату) вместо неопределенного значения NULL, что сделает вывод иерархического дерева стабильным и предсказуемым.

← На главную