Как правильно реализовать двухуровневый обход результата запроса в 1С

Программист
← На главную

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

В этой статье мы подробно разберем, как построить запрос и написать программный код для корректного двухуровневого обхода данных, выясним причины типичных ошибок и проанализируем правильные методы работы с объектом РезультатЗапроса.

Разбор основной проблемы: Итоги против Группировок

Рассмотрим ситуацию: у нас есть периодический регистр сведений ТВ_Программа. Нам необходимо вычислить общую длину блока программ для каждого часа. Если мы используем только СГРУППИРОВАТЬ ПО, система «схлопнет» записи. Мы получим уникальные значения полей группировки, но потеряем доступ к детальным записям внутри этих групп. Если же мы добавим детальные записи в поля группировки, то внешний цикл обхода будет срабатывать на каждую строку, что сводит на нет саму идею иерархического обхода.

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

Шаг 1: Подготовка текста запроса

Проанализируем структуру правильного запроса. Нам нужно получить данные, сгруппированные по началу часа, но при этом сохранить информацию о каждой конкретной передаче. Посмотрим на пример кода:


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

В данном запросе поле НачалоБлока служит верхним уровнем иерархии. Секция ИТОГИ ПО НачалоБлока говорит системе, что нужно подготовить промежуточные агрегаты для каждого часа, сохранив при этом детальные записи (Период и ТВ_Передача) «внутри» этого часа.

Шаг 2: Программный обход результата

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


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

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

Пока ВыборкаЧас.Следующий() Цикл
    
    // В этой точке ВыборкаЧас.НачалоБлока содержит время начала текущего часа
    Сообщить("Обработка часа: " + ВыборкаЧас.НачалоБлока);
    
    // 2. Получаем выборку детальных записей ТОЛЬКО для текущего часа
    // Мы вызываем метод Выбрать() именно у объекта ВыборкаЧас!
    ВыборкаПередача = ВыборкаЧас.Выбрать();
    
    Пока ВыборкаПередача.Следующий() Цикл
        // Здесь мы работаем с конкретной передачей внутри текущего часа
        НаименованиеПередачи = ВыборкаПередача.ТВ_Передача;
        // Выполняем полезные действия, например, суммируем длину
    КонецЦикла;

КонецЦикла;

Анализ типов обхода: ПоГруппировкам против ПоГруппировкамСИерархией

Выясним, в чем разница между этими двумя константами, так как их часто путают:

  1. ОбходРезультатаЗапроса.ПоГруппировкам: Используется для обхода по уровням, заданным в секции ИТОГИ ПО. Это самый быстрый и эффективный способ для большинства задач.
  2. ОбходРезультатаЗапроса.ПоГруппировкамСИерархией: Применяется только в тех случаях, когда сами значения в полях итогов имеют иерархическую природу (например, справочник Номенклатура с группами и элементами). Если вы используете этот метод для обычных дат или строк, вы можете столкнуться с дублированием записей или избыточными итерациями.

В нашем случае (обход по времени) следует использовать строго ОбходРезультатаЗапроса.ПоГруппировкам.

Типичные ошибки и способы их решения

Проанализируем ситуацию, когда «последняя запись часа проходит дважды». Эта проблема часто возникает при неправильном вложенном вызове метода Выбрать(). Если вы ошибочно вызовете РезультатЗапроса.Выбрать() внутри цикла вместо ВыборкаЧас.Выбрать(), система начнет перебор всех детальных записей с самого начала для каждой группы, что приведет к логической ошибке.

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

Пример расчета максимальной длины блока

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


&НаСервере
Функция ОпределитьМаксДлинуБлока(ДатаСтарт, ДатаФиниш)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    НАЧАЛОПЕРИОДА(ТВ_Программа.Период, ЧАС) КАК Час,
        |    ТВ_Программа.ТВ_Передача.Наименование КАК Название
        |ИЗ
        |    РегистрСведений.ТВ_Программа КАК ТВ_Программа
        |ГДЕ
        |    ТВ_Программа.Период МЕЖДУ &ДатаСтарт И &ДатаФиниш
        |ИТОГИ ПО
        |    Час";
        
    Запрос.УстановитьПараметр("ДатаСтарт", ДатаСтарт);
    Запрос.УстановитьПараметр("ДатаФиниш", ДатаФиниш);
    
    Результат = Запрос.Выполнить();
    ВыборкаЧас = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
    
    МаксДлина = 0;
    
    Пока ВыборкаЧас.Следующий() Цикл
        
        ТекущийСоставБлока = "";
        ВыборкаДетали = ВыборкаЧас.Выбрать(); // Берем детали текущего часа
        
        Пока ВыборкаДетали.Следующий() Цикл
            ТекущийСоставБлока = ТекущийСоставБлока + ВыборкаДетали.Название + "/";
        КонецЦикла;
        
        Длина = СтрДлина(ТекущийСоставБлока);
        Если Длина > МаксДлина Тогда
            МаксДлина = Длина;
        КонецЕсли;
        
    КонецЦикла;
    
    Возврат МаксДлина;
    
КонецФункции

Подведем итог: Правильный двухуровневый обход в 1С строится на трех «китах»: использование ИТОГИ ПО в запросе, применение ОбходРезультатаЗапроса.ПоГруппировкам для верхнего уровня и вызов метода Выбрать() от текущей строки выборки для получения вложенных данных.

← На главную