Каждый разработчик 1С рано или поздно сталкивается с коварным поведением типа NULL. На первый взгляд простая задача — получить начальный остаток по регистру на определенную дату — может обернуться аварийным завершением работы программы с невнятной ошибкой: «Операции сравнения на больше-меньше допустимы только для значений совпадающих типов». При этом попытки «заткнуть» проблему стандартной проверкой ЕСТЬNULL внутри запроса часто не приносят желаемого результата.
В этой статье мы подробно разберем, почему возникает эта ошибка во встроенном языке 1С (BSL), в чем заключается принципиальная разница между конструкциями обработки пустых значений в запросах и как раз и навсегда решить проблему с получением корректных остатков на «начало времен».
Давайте проанализируем ситуацию, с которой столкнулся автор темы. В коде встроенного языка выполняется обход выборки запроса:
ВыборкаДетальныеЗаписи = Запрос.Выполнить().Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.СуммаОстаток > 0 Тогда
// Какая-то логика обработки
КонецЕсли;
КонецЦикла;
Если в поле СуммаОстаток из базы данных приходит значение NULL, платформа 1С честно пытается выполнить сравнение:
Если Null > 0 Тогда
Поскольку встроенный язык 1С строго типизирован в отношении операций сравнения, сравнивать величины операторами >, <, >=, <= можно только в том случае, если они принадлежат к одному типу данных (например, Число с Числом, Дата с Датой). Попытка сравнить значение типа NULL с числовым нулем мгновенно вызывает ошибку времени выполнения. Платформа просто «не понимает», как сопоставить абстрактную пустоту с конкретным числом.
Разработчик в сообщении темы пытается исправить ситуацию в самом запросе, оборачивая поле в функцию ЕСТЬNULL внутри агрегатора СУММА:
ВЫБРАТЬ
СУММА(ЕСТЬNULL(ДвиженияДенежныхСредствОстатки.СуммаОстаток, 0)) КАК СуммаОстаток
ИЗ
РегистрНакопления.ДвиженияДенежныхСредств.Остатки(&НачалоПериода, ) КАК ДвиженияДенежныхСредствОстатки
Почему этот код продолжает возвращать NULL во встроенный язык, если остатков нет на указанную дату (например, до начала ведения учета)? Выясним причину этого поведения, которая кроется в стандартах работы СУБД с агрегатными функциями.
Рассмотрим по шагам, как СУБД обрабатывает этот запрос:
&НачалоПериода в регистре нет ни одной записи, удовлетворяющей условиям (или если таблица полностью пуста), виртуальная таблица остатков вернет пустой набор строк (0 строк).ЕСТЬNULL внутри СУММА ни разу не вызовется, так как СУБД не к чему её применить.СУММА, СРЕДНЕЕ, МАКСИМУМ, МИНИМУМ) над абсолютно пустым множеством строк всегда является значение NULL (исключением является только функция КОЛИЧЕСТВО, которая в таком случае вернет 0).СуммаОстаток содержит NULL. Далее этот NULL попадает в выборку во встроенный язык и валит систему на этапе сравнения.Чтобы гарантированно избежать появления NULL на выходе из запроса, мы должны применить функцию ЕСТЬNULL к результату работы агрегатной функции, а не к её аргументу. Посмотрим на правильный пример:
ВЫБРАТЬ
ЕСТЬNULL(СУММА(ДвиженияДенежныхСредствОстатки.СуммаОстаток), 0) КАК СуммаОстаток
ИЗ
РегистрНакопления.ДвиженияДенежныхСредств.Остатки(&НачалоПериода, ) КАК ДвиженияДенежныхСредствОстатки
При таком подходе, даже если виртуальная таблица пуста, агрегатная функция СУММА вернет NULL, который тут же будет перехвачен внешней функцией ЕСТЬNULL и преобразован в полноценное числовое значение 0. Во встроенный язык вернется число, и проверка ВыборкаДетальныеЗаписи.СуммаОстаток > 0 отработает идеально.
В первой попытке решения проблемы автор темы пытался использовать условную конструкцию ВЫБОР КОГДА:
ВЫБОР
КОГДА ДвиженияДенежныхСредствОстатки.СуммаОстаток = NULL ТОГДА 0
ИНАЧЕ ДвиженияДенежныхСредствОстатки.СуммаОстаток
КОНЕЦ
Это классическая ошибка, связанная с непониманием троичной (трехзначной) логики, используемой в базах данных SQL. В отличие от привычной нам булевой логики (Истина / Ложь), в SQL существует третье логическое значение — UNKNOWN (Неизвестно). Сам по себе NULL интерпретируется СУБД как неизвестное значение (данные отсутствуют, их физически нет).
Поскольку значение неизвестно, его невозможно сравнить ни с чем другим при помощи стандартных операторов сравнения (равно, не равно, больше, меньше). СУБД не знает, чему равен NULL, поэтому:
Поле = NULL всегда возвращает результат UNKNOWN.Поле <> NULL также всегда возвращает результат UNKNOWN.В логических условиях (таких как ГДЕ или КОГДА в конструкции ВЫБОР) значение UNKNOWN приравнивается к значению Ложь. Таким образом, ветка ТОГДА 0 в коде автора не выполнится никогда, а управление сразу перейдет в ветку ИНАЧЕ, возвращая исходный NULL.
Чтобы проверить поле на равенство NULL в запросе, необходимо использовать специальный оператор сравнения ЕСТЬ NULL (или ЕСТЬ НЕ NULL):
ВЫБОР
КОГДА ДвиженияДенежныхСредствОстатки.СуммаОстаток ЕСТЬ NULL ТОГДА 0
ИНАЧЕ ДвиженияДенежныхСредствОстатки.СуммаОстаток
КОНЕЦ
Хотя, безусловно, использование специализированной функции ЕСТЬNULL(Поле, ЗначениеЗамены) выглядит гораздо лаконичнее и работает быстрее.
Для закрепления материала разберем три понятия, которые начинающие разработчики часто путают между собой. Все они обозначают «пустоту», но имеют абсолютно разную физическую природу в платформе 1С:
NULL — это тип данных, обозначающий физическое отсутствие значения в базе данных. Он возникает главным образом при использовании ЛЕВОГО СОЕДИНЕНИЯ (когда в правой таблице нет подходящих записей), при получении итогов по группировкам или при вычислении агрегатных функций над пустыми таблицами. Существует как в языке запросов, так и во встроенном языке.Неопределено — это тип данных, существующий исключительно во встроенном языке 1С. Он указывает, что переменной еще не присвоено конкретное значение, либо реквизиту составного типа на форме не выбран конкретный тип. В таблицах баз данных SQL типа «Неопределено» не существует, СУБД о нем ничего не знает.Пустая ссылка — это абсолютно реальное, заполненное значение конкретного ссылочного типа данных (например, Справочники.Номенклатура.ПустаяСсылка()). В базе данных SQL оно хранится в виде уникального идентификатора, состоящего из одних нулей (00000000-0000-0000-0000-000000000000). С ним можно производить любые стандартные операции сравнения, и оно никогда не вызывает ошибку несовпадения типов при сравнении на равенство или неравенство.Проанализируем лучшие практики написания кода, которые рекомендует фирма «1С» для избежания подобных коллизий:
NULL на стороне базы данных. Старайтесь приводить потенциальные NULL к дефолтным значениям (0 для чисел, пустая строка для текста, Ложь для булево) прямо в запросе с помощью ЕСТЬNULL(). Это гарантирует, что ваш код BSL не упадет при обработке выборки.ЛЕВОЕ СОЕДИНЕНИЕ, потенциально может вернуть NULL. Всегда оборачивайте такие поля в ЕСТЬNULL() при выводе в выборку.ЗначениеЗаполнено() во встроенном языке перед сравнением. Функция ЗначениеЗаполнено(Null) вернет Ложь и предохранит систему от аварийного завершения:
Если ЗначениеЗаполнено(ВыборкаДетальныеЗаписи.СуммаОстаток)
И ВыборкаДетальныеЗаписи.СуммаОстаток > 0 Тогда
// Безопасный код
КонецЕсли;