Как в запросе 1С отличить Неопределено, NULL и пустую ссылку

· 9 мин

Содержание

Код Если Поле = Неопределено Тогда работает во встроенном языке, но в запросе та же проверка часто молчит: строки с «пустым» полем в результат не попадают, а должны. Причина в том, что из соединения приходит не Неопределено, а NULL — другое значение с другими правилами сравнения.

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

Почему NULL и Неопределено в запросе — это два разных значения

В тексте запроса поле может оказаться в одном из трёх состояний, и путать их нельзя:

  • 🟦 NULL — значения нет вообще. Чаще всего появляется, когда левое соединение (ЛЕВОЕ СОЕДИНЕНИЕ) не нашло пару. Это не «ноль» и не пустая строка, а отсутствие данных.
  • 🟨 Неопределено — конкретное значение типа Неопределено. Хранится, например, в поле составного типа, которое не заполнили, или в ресурсе регистра сведений с типом Характеристика.
  • 🟩 Пустая ссылка — заполненный реквизит-ссылка со значением «пусто» (Справочник.Контрагенты.ПустаяСсылка). Это полноценное значение, а не отсутствие.

Главная ловушка в сравнении. Оператор = с NULL не работает так, как ожидают: любое сравнение с NULL через = или <> не даёт «истину». Поэтому проверка вида ГДЕ Поле = НЕОПРЕДЕЛЕНО не поймает строки, где поле пришло из незаполненного соединения.

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

Этот отбор вернёт пустой результат, даже если цен нет ни у одной позиции. Для позиций без цены поле Цены.Цена равно NULL, а не Неопределено, поэтому условие = НЕОПРЕДЕЛЕНО ложно.

Правильно — специальный оператор ЕСТЬ NULL:

ГДЕ
    Цены.Цена ЕСТЬ NULL

А для обратной задачи (оставить только заполненные строки) — ЕСТЬ NOT NULL. Запомните правило: значения из соединений проверяют на ЕСТЬ NULL, а собственные значения поля — на = НЕОПРЕДЕЛЕНО.

Способ 1. Подмена значения через ВЫБОР КОГДА

Конструкция ВЫБОР КОГДА … ТОГДА … ИНАЧЕ … КОНЕЦ — аналог CASE WHEN. Она позволяет прямо в выборке заменить NULL на понятное значение и дальше работать уже с ним.

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

ВЫБОР незаменим, когда замена зависит от нескольких условий или когда нужно вернуть разные типы в разных ветках — например, текстовую метку «нет данных» вместо числа.

Внутри одного ВЫБОР можно перечислить несколько КОГДА. Проверки идут сверху вниз, срабатывает первая истинная — порядок условий имеет значение.

Минус способа — громоздкость. Если задача только «поставить ноль вместо пустоты», проще взять функцию из следующего раздела.

Способ 2. Функция ЕСТЬNULL для значения по умолчанию

ЕСТЬNULL(<выражение>, <значение_замены>) — короткая форма того же приёма. Если первый аргумент равен NULL, возвращается второй; иначе — само выражение.

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

Два важных нюанса:

  • ЕСТЬNULL заменяет только NULL. Значение Неопределено она не трогает — для него нужна проверка = НЕОПРЕДЕЛЕНО или ВЫБОР.
  • Тип значения замены должен быть совместим с типом поля. Подставлять строку "нет" вместо числовой цены нельзя — результат получит составной тип, и дальнейшая арифметика по нему сломается.

Отдельный частый случай — агрегаты. СУММА, МАКСИМУМ, МИНИМУМ по пустому набору строк возвращают NULL, а не ноль. Поэтому итоговые суммы почти всегда оборачивают:

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

Ещё одно применение проверки — необязательный отбор по параметру запроса. Когда параметр могут не заполнить (передали Неопределено), условие в ГДЕ строят так, чтобы при пустом параметре фильтр отключался:

ГДЕ
    (&Контрагент = НЕОПРЕДЕЛЕНО ИЛИ Продажи.Контрагент = &Контрагент)

Если в параметр &Контрагент ничего не передали, первая часть условия истинна и в результат попадают все строки. Передали конкретного контрагента — работает отбор. Этот приём избавляет от сборки текста запроса из кусков в коде: один запрос обслуживает и отчёт по всем, и отчёт по одному. Для параметра здесь используют именно = НЕОПРЕДЕЛЕНО, потому что это собственное значение параметра, а не NULL из соединения.

Способ 3. Приведение типа через ВЫРАЗИТЬ

С полями составного типа возникает своя задача. Когда поле может хранить ссылки разных типов (классический пример — Регистратор в регистре накопления), обратиться к его реквизитам напрямую нельзя: у документа реализации есть Контрагент, а у документа списания его нет.

Оператор ВЫРАЗИТЬ(<поле> КАК <тип>) приводит значение к одному типу. Если фактический тип строки не совпал — результат становится NULL, и его уже можно проверить привычным ЕСТЬ NULL.

ВЫБРАТЬ
    Движения.Регистратор,
    ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг).Контрагент КАК Контрагент
ИЗ
    РегистрНакопления.Продажи КАК Движения

В строках, где регистратор — не реализация, поле Контрагент вернёт NULL. Это ожидаемо и правильно: тип просто не подошёл.

Без ВЫРАЗИТЬ попытка взять реквизит через точку у поля составного типа либо вернёт неожиданный NULL по всем строкам, либо приведёт к неоптимальному соединению со всеми таблицами типов сразу. Приведение типа — не только про проверку, но и про скорость запроса.

ВЫРАЗИТЬ нужен именно для составных типов. Для простого поля (число, дата, одиночная ссылка) он избыточен.

Способ 4. Отбор пустых значений через временные таблицы

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

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

Что это даёт:

  • 🧱 NULL «гасится» один раз на входе, во второй части запроса поле уже гарантированно заполнено.
  • ⚡ По временной таблице можно построить индекс (ИНДЕКСИРОВАТЬ ПО Номенклатура), и последующие соединения ускоряются.
  • 🔍 Логику проще отлаживать: промежуточную таблицу видно в консоли запросов отдельным шагом.

Способ особенно выручает в сложных отчётах на СКД и в пакетных запросах, где одно и то же проверенное множество используется несколько раз. Собранную таблицу можно соединять с другими и фильтровать повторно, не возвращаясь каждый раз к исходным регистрам и не плодя проверки на NULL в каждом соединении.

Если проверенное множество нужно только для того, чтобы отсеять по нему строки, вместо соединения иногда удобнее конструкция В (ВЫБРАТЬ …) или НЕ В (ВЫБРАТЬ …) — она не добавляет полей и не создаёт новых NULL.

Способ 5. Проверка в условии соединения и в отборе ГДЕ

Пятый приём — не отдельная функция, а правильное размещение проверки. От того, где стоит условие — в ПО, в ГДЕ или в выборке, — зависит, какие строки останутся.

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

ВЫБРАТЬ
    Товары.Ссылка КАК Номенклатура
ИЗ
    Справочник.Номенклатура КАК Товары
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
    ПО Цены.Номенклатура = Товары.Ссылка
ГДЕ
    Цены.Цена ЕСТЬ NULL

Здесь важная тонкость, на которой спотыкаются чаще всего:

Любой обычный отбор по полю правой таблицы в ГДЕ (скажем, Цены.Цена > 100) отбрасывает строки с NULL и фактически превращает левое соединение во внутреннее. Если нужно сохранить все строки слева, переносите условие в секцию ПО соединения или оборачивайте поле в ЕСТЬNULL.

Сравните два варианта. В первом останутся только товары с ценой выше 100, товары без цены исчезнут:

// ГДЕ — левое соединение превращается во внутреннее
ГДЕ Цены.Цена > 100

Во втором сохранятся все товары, а дорогие будут помечены:

// ЕСТЬNULL в ВЫБОР — все строки на месте
ВЫБОР КОГДА ЕСТЬNULL(Цены.Цена, 0) > 100 ТОГДА ИСТИНА ИНАЧЕ ЛОЖЬ КОНЕЦ КАК ДорогойТовар

Какой способ выбрать

Все пять приёмов решают разные задачи — их не выбирают «по вкусу», а берут под ситуацию.

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

Чтобы быстро определить свой случай, сверьтесь с деревом решений — оно отталкивается от того, откуда взялось «пустое» поле.

Откуда пришло пустое поле в запросе?

Ошибки, из-за которых проверка срабатывает не так

Собрал промахи, которые чаще всего приводят к «пустому» или, наоборот, раздутому результату.

Сравнение NULL через знак равенства. Запись ГДЕ Поле = NULL или Поле <> NULL не работает: сравнение с NULL оператором = никогда не истинно. Только ЕСТЬ NULL и ЕСТЬ NOT NULL.

= НЕОПРЕДЕЛЕНО для поля из соединения. Самый частый случай. Поле, которого не оказалось в правой таблице, равно NULL, а не Неопределено. Проверка на НЕОПРЕДЕЛЕНО его пропустит.

Путаница пустой ссылки и NULL. Незаполненный реквизит Контрагент в документе — это пустая ссылка, конкретное значение. Он не равен NULL и не равен Неопределено. Чтобы отобрать документы без контрагента, сравнивайте с ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка).

Несовместимый тип в ЕСТЬNULL. Если подставить замену другого типа (строку вместо числа), поле получит составной тип. Дальнейшие суммирование, группировка или сортировка по такому полю дадут неожиданный результат.

Отбор правой таблицы в ГДЕ вместо ПО. Разобрали выше: это молча превращает левое соединение во внутреннее и теряет строки, которые вы рассчитывали сохранить.

Частые вопросы

Чем NULL отличается от пустой ссылки?
NULL — это отсутствие значения, обычно из несработавшего соединения. Пустая ссылка — заполненное поле со значением «пусто» конкретного типа справочника или документа. Проверяются они по-разному: ЕСТЬ NULL против = ЗНАЧЕНИЕ(…ПустаяСсылка).
Почему `Поле = НЕОПРЕДЕЛЕНО` иногда работает, а иногда нет?
Работает, когда поле действительно хранит значение типа Неопределено (например, незаполненный ресурс составного типа). Не работает, когда «пустота» пришла из соединения — там лежит NULL, и нужен оператор ЕСТЬ NULL.
Можно ли проверить на Неопределено прямо в условии ГДЕ?
Да. Для значений из соединений — ГДЕ Поле ЕСТЬ NULL или ЕСТЬ NOT NULL. Для собственного значения поля — ГДЕ Поле = НЕОПРЕДЕЛЕНО. Смешивать операторы нельзя.
Как заменить NULL на пустую дату или строку?
Той же функцией: ЕСТЬNULL(Поле, ДАТАВРЕМЯ(1, 1, 1)) для даты, ЕСТЬNULL(Поле, "") для строки. Главное, чтобы тип замены совпадал с типом поля.
Нужен ли ВЫРАЗИТЬ для обычного реквизита?
Нет. ВЫРАЗИТЬ нужен только для полей составного типа, когда вы обращаетесь к реквизитам через точку. Для числа, даты или одиночной ссылки он лишний.