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