Почему не выводятся детальные записи при обходе запроса в 1С и как это исправить

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

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

Причины возникновения проблемы

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

  1. Уровень общих итогов: вычисляется для всей таблицы в целом (если не отключен).
  2. Уровни группировок: строки, сформированные по полям, указанным после ключевого слова ПО (например, ПО Номенклатура, Склад).
  3. Уровень детальных записей: исходные строки таблиц, которые объединяются в группировки. Этот уровень всегда располагается на следующем шаге после самой нижней группировки (число уровней группировок + 1).

Если поле не указано в списке группировок секции ПО и к нему не применена агрегатная функция (СУММА, МАКСИМУМ, МИНИМУМ, КОЛИЧЕСТВО), платформа 1С на уровнях группировок всегда возвращает для этого поля значение NULL. Реальное значение присутствует только на уровне детальных записей.

Посмотрим на исходный пример, в котором возникла ошибка. В тексте запроса объявлены группировки по трем полям:


ИТОГИ ПО
    Номенклатура1,
    Склад1,
    Период

При этом в коде обхода было создано только два уровня вложенных циклов:


Номенклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Номенклатура1");
Пока Номенклатура.Следующий() Цикл
    Склады = Номенклатура.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Склад1");
    Пока Склады.Следующий() Цикл
        // Обращение к Склады.Остаток возвращает NULL!
        Ост1 = Склады.Остаток;
    КонецЦикла;
КонецЦикла;

В этом коде обращение к полю Остаток происходит на уровне группировки Склад1. Поскольку поле Остаток не агрегировано в секции ИТОГИ, платформа закономерно возвращает NULL. Кроме того, цикл не спускается до группировки Период и до детальных записей движений.

Способ 1. Полноценный обход всех уровней иерархии с переходом к детальным записям

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

Посмотрим на пример кода с правильной структурой обхода:


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

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

// 1-й уровень: Номенклатура
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Номенклатура1");
Пока ВыборкаНоменклатура.Следующий() Цикл

    // 2-й уровень: Склад
    ВыборкаСклад = ВыборкаНоменклатура.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Склад1");
    Пока ВыборкаСклад.Следующий() Цикл
        
        // Значение остатка доступно в группировке благодаря агрегатной функции МАКСИМУМ
        ТекущийОстаток = ВыборкаСклад.Остаток;
        
        // 3-й уровень: Детальные записи движений
        ВыборкаДетали = ВыборкаСклад.Выбрать(ОбходРезультатаЗапроса.Прямой);
        Пока ВыборкаДетали.Следующий() Цикл
            
            // Здесь доступны все детальные поля конкретной строки
            ПериодПрихода = ВыборкаДетали.Период;
            КоличествоПрихода = ВыборкаДетали.Количество;
            
        КонецЦикла;
        
    КонецЦикла;
    
КонецЦикла;

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

Способ 2. Прямой линейный обход выборки без иерархии

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

Разберем пример реализации прямого обхода:


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

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

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

Способ 3. Проверка типа записи через свойство ТипЗаписи

При работе с выборкой запроса полезно проверять тип текущей строки с помощью свойства ТипЗаписи и системного перечисления ТипЗаписиЗапроса. Это позволяет однозначно определить, на каком уровне иерархии находится итератор:


Если Выборка.ТипЗаписи() = ТипЗаписиЗапроса.ИтогПоГруппировке Тогда
    // Обработка данных строки итогов
ИначеЕсли Выборка.ТипЗаписи() = ТипЗаписиЗапроса.ДетальнаяЗапись Тогда
    // Обработка конкретной детальной строки
КонецЕсли;

Рекомендации и выводы

  1. Если требуется обращаться к числовым полям на уровнях группировок, обязательно объявляем их в секции ИТОГИ с соответствующими агрегатными функциями (СУММА, МАКСИМУМ, МИНИМУМ).
  2. Помним правило уровней выборки: детальные записи всегда располагаются на уровне N + 1, где N — количество группировок в секции ИТОГИ ПО.
  3. Для получения детальных записей внутри группировки используем вызов РодительскаяВыборка.Выбрать() без передачи второго параметра имени группировки.
← На главную