Как исправить ошибку «Операции сравнения на больше-меньше допустимы...» и правильно обработать NULL в запросах 1С

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

Каждый разработчик 1С рано или поздно сталкивается с коварным поведением типа NULL. На первый взгляд простая задача — получить начальный остаток по регистру на определенную дату — может обернуться аварийным завершением работы программы с невнятной ошибкой: «Операции сравнения на больше-меньше допустимы только для значений совпадающих типов». При этом попытки «заткнуть» проблему стандартной проверкой ЕСТЬNULL внутри запроса часто не приносят желаемого результата.

В этой статье мы подробно разберем, почему возникает эта ошибка во встроенном языке 1С (BSL), в чем заключается принципиальная разница между конструкциями обработки пустых значений в запросах и как раз и навсегда решить проблему с получением корректных остатков на «начало времен».

Почему возникает ошибка сравнения во встроенном языке?

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


ВыборкаДетальныеЗаписи = Запрос.Выполнить().Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    Если ВыборкаДетальныеЗаписи.СуммаОстаток > 0 Тогда
        // Какая-то логика обработки
    КонецЕсли;
КонецЦикла;

Если в поле СуммаОстаток из базы данных приходит значение NULL, платформа 1С честно пытается выполнить сравнение:

Если Null > 0 Тогда

Поскольку встроенный язык 1С строго типизирован в отношении операций сравнения, сравнивать величины операторами >, <, >=, <= можно только в том случае, если они принадлежат к одному типу данных (например, Число с Числом, Дата с Датой). Попытка сравнить значение типа NULL с числовым нулем мгновенно вызывает ошибку времени выполнения. Платформа просто «не понимает», как сопоставить абстрактную пустоту с конкретным числом.

Ловушка агрегатных функций: Разница между СУММА(ЕСТЬNULL()) и ЕСТЬNULL(СУММА())

Разработчик в сообщении темы пытается исправить ситуацию в самом запросе, оборачивая поле в функцию ЕСТЬNULL внутри агрегатора СУММА:


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

Почему этот код продолжает возвращать NULL во встроенный язык, если остатков нет на указанную дату (например, до начала ведения учета)? Выясним причину этого поведения, которая кроется в стандартах работы СУБД с агрегатными функциями.

Рассмотрим по шагам, как СУБД обрабатывает этот запрос:

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

Чтобы гарантированно избежать появления NULL на выходе из запроса, мы должны применить функцию ЕСТЬNULL к результату работы агрегатной функции, а не к её аргументу. Посмотрим на правильный пример:


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

При таком подходе, даже если виртуальная таблица пуста, агрегатная функция СУММА вернет NULL, который тут же будет перехвачен внешней функцией ЕСТЬNULL и преобразован в полноценное числовое значение 0. Во встроенный язык вернется число, и проверка ВыборкаДетальныеЗаписи.СуммаОстаток > 0 отработает идеально.

Трехзначная логика SQL и почему нельзя писать «= NULL»

В первой попытке решения проблемы автор темы пытался использовать условную конструкцию ВЫБОР КОГДА:


ВЫБОР 
    КОГДА ДвиженияДенежныхСредствОстатки.СуммаОстаток = NULL ТОГДА 0 
    ИНАЧЕ ДвиженияДенежныхСредствОстатки.СуммаОстаток 
КОНЕЦ

Это классическая ошибка, связанная с непониманием троичной (трехзначной) логики, используемой в базах данных SQL. В отличие от привычной нам булевой логики (Истина / Ложь), в SQL существует третье логическое значение — UNKNOWN (Неизвестно). Сам по себе NULL интерпретируется СУБД как неизвестное значение (данные отсутствуют, их физически нет).

Поскольку значение неизвестно, его невозможно сравнить ни с чем другим при помощи стандартных операторов сравнения (равно, не равно, больше, меньше). СУБД не знает, чему равен NULL, поэтому:

В логических условиях (таких как ГДЕ или КОГДА в конструкции ВЫБОР) значение UNKNOWN приравнивается к значению Ложь. Таким образом, ветка ТОГДА 0 в коде автора не выполнится никогда, а управление сразу перейдет в ветку ИНАЧЕ, возвращая исходный NULL.

Чтобы проверить поле на равенство NULL в запросе, необходимо использовать специальный оператор сравнения ЕСТЬ NULL (или ЕСТЬ НЕ NULL):


ВЫБОР 
    КОГДА ДвиженияДенежныхСредствОстатки.СуммаОстаток ЕСТЬ NULL ТОГДА 0 
    ИНАЧЕ ДвиженияДенежныхСредствОстатки.СуммаОстаток 
КОНЕЦ

Хотя, безусловно, использование специализированной функции ЕСТЬNULL(Поле, ЗначениеЗамены) выглядит гораздо лаконичнее и работает быстрее.

Различие между NULL, Неопределено и Пустой ссылкой

Для закрепления материала разберем три понятия, которые начинающие разработчики часто путают между собой. Все они обозначают «пустоту», но имеют абсолютно разную физическую природу в платформе 1С:

  1. NULL — это тип данных, обозначающий физическое отсутствие значения в базе данных. Он возникает главным образом при использовании ЛЕВОГО СОЕДИНЕНИЯ (когда в правой таблице нет подходящих записей), при получении итогов по группировкам или при вычислении агрегатных функций над пустыми таблицами. Существует как в языке запросов, так и во встроенном языке.
  2. Неопределено — это тип данных, существующий исключительно во встроенном языке 1С. Он указывает, что переменной еще не присвоено конкретное значение, либо реквизиту составного типа на форме не выбран конкретный тип. В таблицах баз данных SQL типа «Неопределено» не существует, СУБД о нем ничего не знает.
  3. Пустая ссылка — это абсолютно реальное, заполненное значение конкретного ссылочного типа данных (например, Справочники.Номенклатура.ПустаяСсылка()). В базе данных SQL оно хранится в виде уникального идентификатора, состоящего из одних нулей (00000000-0000-0000-0000-000000000000). С ним можно производить любые стандартные операции сравнения, и оно никогда не вызывает ошибку несовпадения типов при сравнении на равенство или неравенство.

Рекомендации по написанию безопасного кода

Проанализируем лучшие практики написания кода, которые рекомендует фирма «1С» для избежания подобных коллизий:


Если ЗначениеЗаполнено(ВыборкаДетальныеЗаписи.СуммаОстаток) 
   И ВыборкаДетальныеЗаписи.СуммаОстаток > 0 Тогда
    // Безопасный код
КонецЕсли;
← На главную