Почему в выборке по группировкам поле возвращает NULL несмотря на ЕСТЬNULL в запросе 1С?

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

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

Причина поведения: разделение детальных записей и итогов

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

  1. Группировочные записи — формируются на основе выражений и полей, указанных в секции ИТОГИ.
  2. Детальные записи — формируются на основе полей секции ВЫБРАТЬ.

Если мы обходим результат методом РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам), платформа считывает значения именно из группировочных записей. Если поле присутствует в списке ВЫБРАТЬ, но отсутствует в списке агрегатных функций секции ИТОГИ, в группировочной строке для этого поля всегда будет возвращаться значение NULL, так как правило расчета итогов для него не задано.

Решение проблемы: добавление поля в секцию ИТОГИ

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


ВЫБРАТЬ
    ТабДок.Номенклатура КАК Номенклатура,
    ТабДок.Количество КАК Количество,
    ТабДок.Склад КАК Склад,
    ЕСТЬNULL(ОстаткиНоменклатурыОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток,
    ЕСТЬNULL(ОстаткиНоменклатурыОстатки.СуммаОстаток, 0) КАК СуммаОстаток,
    ОстаткиНоменклатурыОстатки.Партия,
    ОстаткиНоменклатурыОстатки.Партия.Дата КАК ПартияДата,
    ЕСТЬNULL(БронированиеТоваровОстатки.КоличествоЗабронированногоОстаток, 0) КАК КоличествоЗабронированногоОстаток
ИЗ
    ТабДок КАК ТабДок
    // ... соединения ...
ИТОГИ
    МАКСИМУМ(Количество),
    МАКСИМУМ(Склад),
    СУММА(КоличествоОстаток),
    СУММА(СуммаОстаток)
ПО
    Номенклатура

Чтобы поле стало доступно на уровне группировки Номенклатура, его необходимо включить в расчет итогов. При этом важно помнить: если агрегатная функция вычисляется по пустому набору строк (например, левое соединение не нашло соответствий), агрегат СУММА(...) вернет NULL. Поэтому безопаснее всего оборачивать агрегатное выражение в ЕСТЬNULL прямо в секции итогов:


ИТОГИ
    МАКСИМУМ(Количество),
    МАКСИМУМ(Склад),
    ЕСТЬNULL(СУММА(КоличествоОстаток), 0) КАК КоличествоОстаток,
    ЕСТЬNULL(СУММА(СуммаОстаток), 0) КАК СуммаОстаток,
    ЕСТЬNULL(СУММА(КоличествоЗабронированногоОстаток), 0) КАК КоличествоЗабронированногоОстаток
ПО
    Номенклатура

Архитектурные ошибки и оптимизация запроса

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

  1. Последовательная цепочка левых соединений (Join Chain). Если таблица БронированиеТоваров.Остатки соединяется не с ведущей таблицей ТабДок, а с таблицей ОстаткиНоменклатурыОстатки, то при отсутствии остатков на складе товар не покажет и забронированное количество, так как промежуточное соединение вернет NULL. Все регистры остатков необходимо связывать с ведущей таблицей документа.
  2. Отсутствие отбора в параметрах виртуальной таблицы. Вызов таблицы РегистрНакопления.БронированиеТоваров.Остатки без параметров приводит к расчету остатков по всей информационной базе с последующей фильтрацией. В параметры виртуальной таблицы обязательно нужно передавать фильтр по номенклатуре: (Номенклатура) В (ВЫБРАТЬ ТабДок.Номенклатура ИЗ ТабДок).
  3. Проблема задвоения остатков (Декартово произведение). Если в таблице ОстаткиНоменклатуры номенклатура разбита по трем партиям, левое соединение с регистром бронирования продублирует строку брони три раза. При вызове СУММА(КоличествоЗабронированногоОстаток) количество брони увеличится втрое.
  4. Разыменование полей через точку. Конструкции вида ПродажаТоваровТовары.Ссылка.Склад вызывают неявные соединения с таблицей шапки документа. Склад рекомендуется передавать параметром &Склад или соединять табличные части напрямую.

Рекомендуемый шаблон: схема с объединением запросов

Для гарантированного исключения задвоений и корректной работы с NULL применим классический шаблон с использованием ОБЪЕДИНИТЬ ВСЕ:


ВЫБРАТЬ
    ДокТовары.Номенклатура КАК Номенклатура,
    ДокТовары.Количество КАК Количество,
    0 КАК КоличествоОстаток,
    0 КАК СуммаОстаток,
    0 КАК КоличествоБронь,
    НЕОПРЕДЕЛЕНО КАК Партия
ПОМЕСТИТЬ ИсходныеДанные
ИЗ
    Документ.ПродажаТоваров.Товары КАК ДокТовары
ГДЕ
    ДокТовары.Ссылка = &Ссылка
    И ДокТовары.Номенклатура.ВидНоменклатуры <> ЗНАЧЕНИЕ(Перечисление.ВидыТоваров.Услуга)

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    Остатки.Номенклатура,
    0,
    Остатки.КоличествоОстаток,
    Остатки.СуммаОстаток,
    0,
    Остатки.Партия
ИЗ
    РегистрНакопления.ОстаткиНоменклатуры.Остатки(
        &Момент,
        (Номенклатура, Склад) В
            (ВЫБРАТЬ
                Док.Номенклатура,
                &Склад
            ИЗ
                Документ.ПродажаТоваров.Товары КАК Док
            ГДЕ
                Док.Ссылка = &Ссылка)) КАК Остатки

ОБЪЕДИНИТЬ ВСЕ

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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Данные.Номенклатура КАК Номенклатура,
    Данные.Партия КАК Партия,
    СУММА(Данные.Количество) КАК Количество,
    СУММА(Данные.КоличествоОстаток) КАК КоличествоОстаток,
    СУММА(Данные.СуммаОстаток) КАК СуммаОстаток,
    СУММА(Данные.КоличествоБронь) КАК КоличествоБронь
ИЗ
    ИсходныеДанные КАК Данные
СГРУППИРОВАТЬ ПО
    Данные.Номенклатура,
    Данные.Партия
ИТОГИ
    МАКСИМУМ(Количество),
    СУММА(КоличествоОстаток),
    СУММА(СуммаОстаток),
    СУММА(КоличествоБронь)
ПО
    Номенклатура

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

← На главную