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 (язык запросов) ≠ Неопределено (встроенный язык);
ЗначениеЗаполнено()в коде ловит все «пустые» значения сразу.