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