Почему при обходе запроса по группировкам в 1С возвращается NULL и как это исправить?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

Выясняем причину: почему в группировках появляется NULL?

Давайте проанализируем ситуацию, представленную в первом сообщении автора. В запросе выбираются поля табличной части документа «Акт сверки взаиморасчетов» (такие как ДоговорКонтрагента, Сделка, Дебет, Кредит и другие), и в конце запроса накладывается инструкция ИТОГИ ПО ДокументРегистратор.

Когда мы выполняем обход результатов такого запроса с типом ОбходРезультатаЗапроса.ПоГруппировкам, платформа строит иерархическую структуру. На верхнем уровне находятся строки группировок (в данном случае по полю ДокументРегистратор). На этом уровне платформа заполняет только:

  1. Само поле группировки (ДокументРегистратор).
  2. Поля-ресурсы, для которых в секции ИТОГИ указаны агрегатные функции (например, СУММА(Дебет)).

Все остальные детальные поля, такие как НомерСтроки, ДоговорКонтрагента, Сделка, на уровне группировки не рассчитываются и не агрегируются. Платформа просто не знает, какое именно значение из детальных записей нужно вывести в строке итога, поэтому возвращает системное значение NULL. Попытка обратиться к Выборка.Дебет или Выборка.ДоговорКонтрагента на шаге обхода группировки гарантированно вернет NULL (если только они не описаны как итоги).

Принципиальная разница между «СГРУППИРОВАТЬ ПО» и «ИТОГИ ПО»

Для глубокого понимания работы с запросами важно разграничивать две конструкции языка запросов 1С:

Решение №1: Двухуровневый обход с получением детальных записей

Если нам необходимо обойти все группировки (документы-регистраторы), а затем внутри каждого регистратора обработать все его строки (детальные записи), нам нужно применить двухуровневый обход. Мы сначала выбираем группировки верхнего уровня, а затем для каждой группировки получаем вложенную выборку детальных записей с помощью метода Выбрать() без параметров.

Рассмотрим этот алгоритм по шагам на примере кода:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	АктСверки.ДокументРегистратор КАК ДокументРегистратор,
    |	АктСверки.Ссылка КАК Ссылка,
    |	АктСверки.НомерСтроки КАК НомерСтроки,
    |	АктСверки.ДоговорКонтрагента КАК ДоговорКонтрагента,
    |	АктСверки.Дебет КАК Дебет,
    |	АктСверки.Кредит КАК Кредит
    |ИЗ
    |	Документ.АктСверкиВзаиморасчетов.ПоДаннымОрганизации КАК АктСверки
    |ГДЕ
    |	АктСверки.Ссылка = &Ссылка
    |ИТОГИ ПО
    |	ДокументРегистратор";

Запрос.УстановитьПараметр("Ссылка", ЭтотОбъект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();

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

Пока ВыборкаГруппировка.Следующий() Цикл
    // На этом уровне доступны только поля группировки (ДокументРегистратор)
    Регистратор = ВыборкаГруппировка.ДокументРегистратор;
    
    // Здесь остальные реквизиты (например, Дебет) будут NULL.
    // Чтобы получить их, мы выбираем подчиненную выборку (детальные записи) для текущего регистратора:
    ВыборкаДетальные = ВыборкаГруппировка.Выбрать();
    
    Пока ВыборкаДетальные.Следующий() Цикл
        // На этом уровне доступны все детальные поля каждой строки
        ТекущийДебет = ВыборкаДетальные.Дебет;
        ТекущийКредит = ВыборкаДетальные.Кредит;
        ТекущийДоговор = ВыборкаДетальные.ДоговорКонтрагента;
        
        // Выполняем полезную работу с детальными записями...
    КонецЦикла;
КонецЦикла;

Решение №2: Использование агрегатных функций в предложении ИТОГИ

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

В таких случаях мы можем указать платформе, как именно нужно агрегировать детальные поля при расчете итогов. Для этого в конструкции ИТОГИ мы используем агрегатные функции, такие как СУММА, МАКСИМУМ, МИНИМУМ, СРЕДНЕЕ.

Давайте проанализируем пример: у каждого документа-регистратора в рамках конкретной сверки может быть один договор контрагента. Чтобы получить договор прямо на уровне группировки (не заходя в детальные записи), мы можем применить функцию МАКСИМУМ(ДоговорКонтрагента):


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	АктСверки.ДокументРегистратор КАК ДокументРегистратор,
    |	АктСверки.ДоговорКонтрагента КАК ДоговорКонтрагента,
    |	АктСверки.Дебет КАК Дебет,
    |	АктСверки.Кредит КАК Кредит
    |ИЗ
    |	Документ.АктСверкиВзаиморасчетов.ПоДаннымОрганизации КАК АктСверки
    |ГДЕ
    |	АктСверки.Ссылка = &Ссылка
    |ИТОГИ 
    |	СУММА(Дебет), 
    |	СУММА(Кредит), 
    |	МАКСИМУМ(ДоговорКонтрагента) 
    |ПО 
    |	ДокументРегистратор";

Запрос.УстановитьПараметр("Ссылка", ЭтотОбъект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();

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

Пока Выборка.Следующий() Цикл
    // Теперь на уровне группировки значения НЕ будут NULL!
    Регистратор = Выборка.ДокументРегистратор;
    ОбщийДебетПоРегистратору = Выборка.Дебет; // Будет заполнено суммой дебета
    Договор = Выборка.ДоговорКонтрагента; // Будет заполнено значением договора
КонецЦикла;

Секреты параметров метода Выбрать()

Многие разработчики вызывают метод Выбрать() автоматически, не задумываясь о его параметрах. Давайте разберем его сигнатуру подробнее:

Метод имеет следующий вид: РезультатЗапроса.Выбрать(ТипОбхода, Группировки, ГруппировкиДляЗначенийГруппировок).

Рассмотрим подробнее каждый параметр:

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

Полезные свойства объекта ВыборкаИзРезультатаЗапроса

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

Важное замечание: Влияние RLS (Ограничение прав)

При проектировании отчетов под пользователями с ограниченными правами доступа (когда активен механизм RLS) совместное использование ВЫБРАТЬ РАЗРЕШЕННЫЕ и обхода по группировкам может преподнести сюрпризы. Если у пользователя нет прав на чтение родительского элемента (например, определенной группы справочника или организации), но есть права на дочерние элементы, то при обходе иерархии дочерние элементы могут неожиданно "выплыть" на самый верхний уровень выборки. Всегда учитывайте этот нюанс при тестировании функционала под ограниченными ролями.

← На главную