NULL в 1С и его ловушки

«Соединяю продажи с планом, а в итоговой сумме появляются пустые ячейки, и арифметика ломается. Почему?» — знакомьтесь, это NULL.

NULL в языке запросов 1С — это не ноль и не пустая строка, а особое значение «данных нет». Оно возникает там, где соединение не нашло пары или в виртуальной таблице не оказалось записей.

Вопрос про NULL на собеседовании 1С-разработчика проверяет аккуратность мышления. NULL «заразен»: любая арифметика с ним даёт NULL, сравнения ведут себя не так, как ждёшь, и невнимательный код тихо выдаёт неверные суммы. Кто понимает NULL — тот пишет отчёты, которые не врут.

Вопрос-крючок

Показывают запрос: соединяем список всей номенклатуры с оборотами продаж, чтобы увидеть, что и на сколько продано. По товарам без продаж в колонке суммы — не ноль, а пусто, и последующее сложение «съезжает». Спрашивают: «Откуда там NULL и как правильно его обработать?»

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

Здесь у товара, которого нет в оборотах, Продажи.СуммаОборот будет равна NULL. Это и есть ловушка.

Откуда берётся NULL

Два главных источника, которые надо назвать на собеседовании:

1. Левое (и полное) соединение без пары

ЛЕВОЕ СОЕДИНЕНИЕ берёт все строки левой таблицы и подтягивает подходящие строки правой. Если пары справа нет — все поля правой таблицы заполняются NULL. В примере выше товары без продаж есть слева (в справочнике), но их нет справа (в оборотах) — отсюда NULL в сумме.

2. Пустая виртуальная таблица или отсутствующий итог

Если по каким-то измерениям в регистре вообще нет движений, поля вроде КоличествоОстаток или СуммаОборот тоже вернутся как NULL, а не как ноль. Регистр не «додумывает» ноль за вас — нет записи, значит, «данных нет».

Почему NULL — это не ноль и не пустая строка

Главная мысль, которую хотят услышать: NULL не равен ничему, даже самому себе. Любое сравнение с NULL даёт не «истину» и не «ложь», а неопределённый результат, который в ГДЕ ведёт себя как «ложь». Отсюда контринтуитивные вещи:

  • NULL = 0 — не истина. И NULL <> 0 — тоже не истина.
  • NULL = "" — не истина. И NULL <> "" — тоже.
  • Условие ГДЕ Сумма <> 0 не пропустит строки, где сумма NULL — хотя интуитивно кажется, что «NULL ведь не ноль».

Именно поэтому арифметика ломается: NULL + 100 = NULL, NULL * Количество = NULL. Одна пустая ячейка «заражает» весь расчёт.

Как обрабатывать: ЕСТЬNULL

В языке запросов для замены NULL на нормальное значение есть функция ЕСТЬNULL(Выражение, ЗначениеЕслиNULL). Оборачиваем ею всё, что пришло из «правой» части соединения или из виртуальной таблицы:

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

Теперь по товарам без продаж будет честный 0, арифметика и итоги работают. Важно оборачивать ЕСТЬNULL до вычислений: правильно ЕСТЬNULL(А, 0) + ЕСТЬNULL(Б, 0), а не ЕСТЬNULL(А + Б, 0) — во втором случае, если хоть одно слагаемое NULL, вся сумма станет NULL ещё до защиты, и вы получите 0 там, где должна быть частичная сумма.

Часто ЕСТЬNULL сочетают с ВЫБОР, когда нужно не просто ноль, а логика:

ВЫБОР
    КОГДА Продажи.СуммаОборот ЕСТЬ NULL
        ТОГДА "Нет продаж"
    ИНАЧЕ "Продавался"
КОНЕЦ КАК Статус

Обратите внимание на проверку ЕСТЬ NULL (два слова) — это единственно правильный способ проверить NULL в запросе. Писать = NULL нельзя: как мы выяснили, сравнение с NULL никогда не истинно.

NULL против Неопределено и ЗначениеЗаполнено()

Тут любят проверить, не путает ли кандидат два разных «пустых» значения:

  • NULL — значение из языка запросов: «данных нет» (не нашлось пары, пустой регистр). Это про базу.
  • Неопределено — значение встроенного языка: переменная объявлена, но значение не присвоено; реквизит составного типа не заполнен; функция ничего не вернула. Это про код.

Когда результат запроса попадает в код, NULL из выборки приходит именно как NULL, и сравнивать его надо с NULL, а не с Неопределено. А вот универсальная функция встроенного языка ЗначениеЗаполнено() удобна тем, что считает «незаполненными» и NULL, и Неопределено, и пустую строку, и ноль, и пустую ссылку — то есть все «пустые» значения сразу:

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    Если НЕ ЗначениеЗаполнено(Выборка.Сумма) Тогда
        // сюда попадут и NULL, и 0, и пустая ссылка
        Сообщить("Нет данных по: " + Выборка.Номенклатура);
    КонецЕсли;
КонецЦикла;

Тонкость для сильного ответа: ЗначениеЗаполнено() — функция встроенного языка, в тексте запроса её нет. Внутри запроса для проверки «пусто ли» используют ЕСТЬ NULL, а ЗначениеЗаполнено() применяют уже к строкам выборки в коде.

Частые ошибки на собеседовании

  • Считать, что регистр вернёт ноль. Пустой остаток/оборот — это NULL, а не 0. Всегда оборачивайте поля виртуальных таблиц в ЕСТЬNULL, если дальше будет арифметика.
  • Проверять NULL через = NULL. Не сработает никогда. Только ЕСТЬ NULL.
  • Думать, что ГДЕ Поле <> 0 оставит и NULL-строки. Нет: сравнение с NULL не истинно, такие строки уйдут. Если их надо оставить — сперва ЕСТЬNULL(Поле, 0).
  • Путать NULL и Неопределено. NULL — из запроса, Неопределено — из кода. Сравнивать значение выборки с Неопределено вместо NULL — типичный баг.
  • Обернуть ЕСТЬNULL уже вокруг суммы. Защищать надо каждое слагаемое до сложения, иначе один NULL обнулит весь результат.

Итоги-шпаргалка

  • NULL = «данных нет»; появляется при ЛЕВОМ СОЕДИНЕНИИ без пары и в пустых виртуальных таблицах.
  • NULL не равен ни 0, ни "", ни самому себе; любая арифметика с NULL даёт NULL.
  • Замена — ЕСТЬNULL(Выражение, 0), оборачиваем до вычислений и по каждому слагаемому.
  • Проверка в запросе — только ЕСТЬ NULL (не = NULL).
  • NULL (язык запросов) ≠ Неопределено (встроенный язык); ЗначениеЗаполнено() в коде ловит все «пустые» значения сразу.
Проверьте себя
1. Товар есть в справочнике, но продаж по нему не было. Что вернёт СуммаОборот при ЛЕВОМ СОЕДИНЕНИИ с оборотами продаж?
A0
BПустую строку ""
CNULL — потому что пары в правой таблице не нашлось
DНеопределено
2. Как в языке запросов 1С правильно проверить, что поле равно NULL, и заменить его на 0?
AУсловие ГДЕ Поле = NULL, а замена через ЗначениеЗаполнено()
BПроверка через ЕСТЬ NULL, а замена через ЕСТЬNULL(Поле, 0)
CУсловие ГДЕ Поле <> 0 автоматически уберёт NULL, замена не нужна
DСравнить поле с Неопределено и подставить 0