При разработке отчетов и обработок в 1С программисты часто сталкиваются с необходимостью представить данные в виде иерархической структуры (дерева). Самым простым и очевидным инструментом для этого является Система компоновки данных (СКД). Однако существуют ситуации, когда использовать СКД невозможно или нецелесообразно (например, при программном формировании специфических XML-выгрузок, заполнении сложных печатных форм или отправке структурированных данных через API). В таких случаях разработчики используют иерархический обход выборки запроса с помощью вложенных циклов.
Особую сложность вызывает ситуация, когда одним из уровней группировки выступает вычисляемое поле — поле, значение которого рассчитывается непосредственно в запросе с помощью функций (например, функций работы с датами, строками или логических выражений). Попытка выполнить обход по такому полю стандартным методом часто приводит к ошибке платформы «Измерение не найдено». В этой статье мы подробно разберем, почему возникает эта ошибка и как правильно реализовать иерархический обход по вычисляемому полю без использования СКД.
Рассмотрим стандартную ситуацию. Разработчик пытается сгруппировать данные по вычисляемому полю (например, по началу эфирного дня, который рассчитывается со смещением на 6 часов назад относительно физического времени) и выполнить обход результатов запроса по группировкам:
// НЕРАБОЧИЙ ВАРИАНТ
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТВ_Программа.Период КАК Период,
| МАКСИМУМ(ТВ_Программа.ТВ_Передача) КАК ТВ_Передача,
| НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ) КАК НачалоЭфирногоДня
|ИЗ
| РегистрСведений.ТВ_Программа КАК ТВ_Программа
|ГДЕ
| ТВ_Программа.Period МЕЖДУ &ДатаС И &ДатаПо
|СГРУППИРОВАТЬ ПО
| НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ),
| ТВ_Программа.Период
|УПОРЯДОЧИТЬ ПО
| НачалоЭфирногоДня,
| Период";
РезультатЗапроса = Запрос.Выполнить();
// Данный вызов вызовет ошибку: Измерение не найдено "НачалоЭфирногоДня"
ВыборкаГруппировка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "НачалоЭфирногоДня");
Проанализируем ситуацию. Ошибка возникает из-за принципиального различия между конструкциями СУБД и механизмами платформы 1С:
СГРУППИРОВАТЬ ПО (GROUP BY): обрабатывается непосредственно на уровне СУБД (MS SQL, PostgreSQL и др.). Она «схлопывает» исходные детальные записи в плоскую таблицу. В результирующей выборке СУБД отсутствуют какие-либо промежуточные итоги или метаданные об иерархических связях. Для платформы 1С полученный результат — это просто плоский набор строк.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, ...): требует наличия в результате запроса специальных служебных записей — итогов. Если этих записей нет, платформа не может построить древовидную структуру и определить, по какому именно измерению (колонке) требуется выполнить группировку верхнего уровня. Отсюда и возникает закономерное исключение «Измерение не найдено».Для того чтобы платформа 1С смогла построить иерархическую выборку (в том числе по вычисляемому полю), в тексте запроса обязательно должна присутствовать секция ИТОГИ ... ПО ....
При использовании конструкции ИТОГИ ПО платформа 1С самостоятельно обрабатывает полученные от СУБД данные, рассчитывает агрегатные функции для каждого указанного уровня и добавляет в результат запроса промежуточные (итоговые) строки-родоначальники. Именно эти строки позволяют методу Выбрать() корректно позиционироваться на узлах дерева.
Посмотрим на пример правильного запроса, который решает поставленную задачу:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТВ_Программа.Период КАК Период,
| ТВ_Программа.ТВ_Передача КАК ТВ_Передача,
| НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ) КАК НачалоЭфирногоДня
|ИЗ
| РегистрСведений.ТВ_Программа КАК ТВ_Программа
|ГДЕ
| ТВ_Программа.Период МЕЖДУ &ДатаС И &ДатаПо
|УПОРЯДОЧИТЬ ПО
| НачалоЭфирногоДня,
| Период
|ИТОГИ
| МАКСИМУМ(ТВ_Передача)
|ПО
| НачалоЭфирногоДня";
Запрос.УстановитьПараметр("ДатаС", ДатаС);
Запрос.УстановитьПараметр("ДатаПо", ДатаПо);
РезультатЗапроса = Запрос.Выполнить();
Обратите внимание на важные изменения в структуре запроса:
СГРУППИРОВАТЬ ПО. Сгруппировать детальные записи теперь помогает конструкция ИТОГИ.ИТОГИ МАКСИМУМ(ТВ_Передача) ПО НачалоЭфирногоДня. Это указывает платформе 1С, что поле НачалоЭфирногоДня является измерением верхнего уровня (группировкой), по которому необходимо рассчитать итоги.После того как мы правильно составили запрос и получили результат со служебными записями итогов, мы можем приступить к обходу структуры с помощью вложенных циклов. Разберем этот процесс по шагам.
Первый (внешний) цикл будет обходить группировочные записи — уникальные значения вычисляемого поля НачалоЭфирногоДня. Второй (внутренний) цикл будет обходить детальные записи, относящиеся к текущему дню.
// Шаг 1. Выбираем группировку верхнего уровня (по вычисляемому полю)
ВыборкаГруппировка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "НачалоЭфирногоДня");
Пока ВыборкаГруппировка.Следующий() Цикл
// Выводим информацию по группировке верхнего уровня
Сообщить("Эфирный день: " + Формат(ВыборкаГруппировка.НачалоЭфирногоДня, "ДФ=dd.MM.yyyy"));
// Шаг 2. Из текущей строки группировки получаем подчиненную выборку детальных записей
ВыборкаДетальныеЗаписи = ВыборкаГруппировка.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// Обрабатываем детальные записи внутри группы
Сообщить(" Время передачи: " + Формат(ВыборкаДетальныеЗаписи.Период, "ДФ=HH:mm")
+ " - " + ВыборкаДетальныеЗаписи.ТВ_Передача);
КонецЦикла;
КонецЦикла;
При таком подходе платформа 1С не выдаст ошибку, так как поле НачалоЭфирногоДня явно объявлено в секции ИТОГИ ПО, и система подготовила всю необходимую информацию для позиционирования выборки.
Использование сложных функций, особенно связанных со смещением и округлением дат (таких как НАЧАЛОПЕРИОДА и ДОБАВИТЬКДАТЕ), непосредственно в секциях расчета итогов и сортировки основного запроса может негативно сказываться на производительности БД при больших объемах данных. СУБД будет вынуждена вычислять эти тяжелые выражения многократно.
Рассмотрим более производительный и профессиональный подход — расчет вычисляемого поля на этапе формирования Временной Таблицы:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТВ_Программа.Период КАК Период,
| ТВ_Программа.ТВ_Передача КАК ТВ_Передача,
| НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ) КАК НачалоЭфирногоДня
|ПОМЕСТИТЬ ВТ_ПодготовленныеДанные
|ИЗ
| РегистрСведений.ТВ_Программа КАК ТВ_Программа
|ГДЕ
| ТВ_Программа.Период МЕЖДУ &ДатаС И &ДатаПо
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ПодготовленныеДанные.Период КАК Период,
| ВТ_ПодготовленныеДанные.ТВ_Передача КАК ТВ_Передача,
| ВТ_ПодготовленныеДанные.НачалоЭфирногоДня КАК НачалоЭфирногоДня
|ИЗ
| ВТ_ПодготовленныеДанные КАК ВТ_ПодготовленныеДанные
|УПОРЯДОЧИТЬ ПО
| НачалоЭфирногоДня,
| Период
|ИТОГИ
| МАКСИМУМ(ТВ_Передача)
|ПО
| НачалоЭфирногоДня";
РезультатЗапроса = Запрос.Выполнить();
Преимущества данного подхода:
НачалоЭфирногоДня выбирается как готовое значение, что значительно ускоряет расчет итогов и сортировку.Если ваша конечная цель состоит не в построчном выводе текстовой информации, а в получении готовой иерархической структуры (например, для вывода ее на управляемую форму в таблицу со структурой "Дерево" или для последующей сложной логической обработки), то писать вложенные циклы обхода выборки необязательно.
Платформа 1С позволяет выгрузить результат запроса с итогами напрямую в специализированный объект ДеревоЗначений с сохранением всех уровней группировки. Для этого используется метод Выгрузить() с указанием типа обхода:
// Выгружаем иерархию напрямую в Дерево Значений
ДеревоРезультата = РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
// Теперь у нас есть готовая древовидная структура.
// Мы можем связать ее с реквизитом формы или обойти рекурсивной процедурой:
ОбходСтрокДереваРекурсивно(ДеревоРезультата.Строки);
Для понимания того, как работать с полученным деревом программно, приведем пример простой рекурсивной процедуры для обхода строк полученного дерева значений:
Процедура ОбходСтрокДереваРекурсивно(СтрокиДерева, Уровень = 0)
Для Каждого СтрокаДерева Из СтрокиДерева Цикл
Отступ = "";
Для Счетчик = 1 По Уровень Цикл
Отступ = Отступ + " ";
КонецЦикла;
// Если это строка группировки (у нее есть подчиненные строки)
Если СтрокаДерева.Строки.Количество() > 0 Тогда
Сообщить(Отступ + "Группировка: " + СтрокаДерева.НачалоЭфирногоДня);
// Рекурсивный вызов для подчиненных строк
ОбходСтрокДереваРекурсивно(СтрокаДерева.Строки, Уровень + 1);
Иначе
// Это детальная запись
Сообщить(Отступ + "Детальная запись: " + СтрокаДерева.Период + " - " + СтрокаДерева.ТВ_Передача);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
При работе с конструкцией ИТОГИ ПО и вычисляемыми полями важно учитывать две важные особенности поведения платформы 1С:
1. Автоматическая сортировка группировок. Когда мы используем ИТОГИ ПО Поле1, платформа автоматически сортирует группировочные записи по возрастанию значений этого поля. Если вам нужна стандартная сортировка по возрастанию, указывать вычисляемое поле в секции УПОРЯДОЧИТЬ ПО не обязательно. Однако, если вам требуется сортировать детальные записи внутри группировки (например, хронологически по полю Период), то поле Период обязательно должно быть указано в секции УПОРЯДОЧИТЬ ПО.
2. Обработка значений NULL. Если вычисляемое поле строится на основе данных из левых соединений или полей, которые физически могут быть не заполнены в базе данных, результат вычисления формулы может вернуть значение NULL. Если поле со значением NULL попадает в секцию ИТОГИ ПО, платформа создаст для него отдельную группировочную строку (обычно она выводится самой первой в списке и выглядит как пустая строка).
Чтобы избежать появления пустых родительских узлов в иерархии, всегда оборачивайте вычисляемые выражения в функцию ЕСТЬNULL. Например:
ЕСТЬNULL(НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(ТВ_Программа.Период, ЧАС, -6), ДЕНЬ), ДАТАВРЕМЯ(1, 1, 1)) КАК НачалоЭфирногоДня
Это гарантирует, что даже при отсутствии исходных данных группировка получит корректное значение по умолчанию (пустую дату) вместо неопределенного значения NULL, что сделает вывод иерархического дерева стабильным и предсказуемым.