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