Выборка с NULL в языке запросов 1С: условия, ЕСТЬNULL и подводные камни

· 11 мин

Содержание

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

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

Откуда в результате запроса берётся NULL

Таблицы в базе 1С хранят значения конкретных типов. Если реквизит пустой, там лежит пустое значение своего типа: 0, пустая строка, пустая ссылка, Дата(1,1,1). NULL появляется только в результате самого запроса.

Типичные источники:

  • 🔗 Левое соединение. Для строк левой таблицы, у которых нет пары справа, все поля правой таблицы будут NULL. Правое соединение работает зеркально, полное — в обе стороны.
  • ➕ Агрегаты по пустой выборке. СУММА(...) по набору из нуля строк вернёт NULL, а не 0.
  • 🧩 Объединение запросов. Если в одном запросе поля нет, его заполняют литералом NULL, чтобы выровнять колонки.
  • 📊 Виртуальные таблицы регистров. Остатки и обороты на дату возвращают строки только там, где были движения. При соединении с номенклатурой остальные позиции получат NULL.

Пустая ссылка и NULL — разные вещи. Пустая ссылка — реальное значение реквизита «не заполнено». NULL — отсутствие строки или отсутствие значения у выражения.

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

Почему сравнение с NULL не работает

Любое сравнение с NULL — равенство, неравенство, больше, меньше — не даёт истины. Поэтому условие ГДЕ Остатки.Количество = NULL вернёт ноль строк, даже если NULL там есть. А условие ГДЕ Остатки.Количество <> 0 тоже не вернёт строки с NULL: они молча выпадут.

Арифметика ведёт себя так же. NULL + 5 равно NULL, поэтому один NULL в формуле «заражает» всё выражение целиком. Если в колонке Сумма - Скидка скидка пришла из левого соединения и оказалась NULL, итог тоже будет NULL.

Самый частый симптом: отчёт «теряет» строки или показывает пустые ячейки вместо нулей. Почти всегда причина в том, что NULL не обработан до сравнения или сложения.

Условия ЕСТЬ NULL и НЕ … ЕСТЬ NULL

Для проверки в языке запросов есть оператор ЕСТЬ NULL. Для обратной проверки его оборачивают в НЕ.

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

Запрос вернёт товары, по которым в регистре нет ни одной записи с остатком. Название регистра и измерения подставьте свои: в разных конфигурациях они называются по-разному.

Обратная проверка записывается так:

ГДЕ
	НЕ Остатки.Номенклатура ЕСТЬ NULL

Проверяйте на NULL то поле, которое заведомо заполнено у найденной пары. Хорошая практика — брать поле, по которому идёт соединение. Тогда NULL означает ровно одно: пары не нашлось.

Проверка NULL в коде после запроса

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

Пока Выборка.Следующий() Цикл
	Если Выборка.Количество = Null Тогда
		// записи в регистре нет
		Продолжить;
	КонецЕсли;
	// дальше работаем с числом
КонецЦикла;

Второй вариант — ТипЗнч(Выборка.Количество) = Тип("Null"). Но чище обработать NULL в самом запросе, чтобы в код приходили готовые числа.

ЕСТЬNULL() и другие способы подставить значение вместо NULL

Функция ЕСТЬNULL(Выражение, Замена) возвращает выражение, а если оно NULL — подставляет замену. Это основной инструмент для чисел и дат.

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

Теперь для товаров без движений в колонке стоит 0, и дальше можно складывать, вычитать и сравнивать без сюрпризов.

Пара практических правил:

  1. Тип замены должен соответствовать типу выражения. Для числа — 0, для даты — ДАТАВРЕМЯ(1,1,1), для ссылки — ЗНАЧЕНИЕ(...ПустаяСсылка), для строки — "".
  2. Заворачивайте в ЕСТЬNULL() каждое поле правой таблицы, которое попадает в арифметику или сравнение.
  3. Для сложной логики подходит конструкция ВЫБОР:
ВЫБОР
	КОГДА Остатки.Номенклатура ЕСТЬ NULL
		ТОГДА "Движений нет"
	ИНАЧЕ "Есть остаток"
КОНЕЦ КАК Состояние

ВЫБОР удобен, когда нужно не просто подставить ноль, а вывести текст, выбрать другую колонку или разветвить расчёт.

Условие в ПО или в ГДЕ: где левое соединение превращается во внутреннее

Это самая коварная ловушка. Условие на поле правой таблицы в секции ГДЕ отсекает строки, где это поле NULL. А ведь именно такие строки и нужны левому соединению. Результат становится похож на внутреннее соединение.

Плохо:

ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Цены.СрезПоследних КАК Цены
	ПО Номенклатура.Ссылка = Цены.Номенклатура
ГДЕ
	Цены.Цена > 100

Товары без цены исчезнут: для них Цены.Цена равно NULL, а сравнение с NULL истины не даёт.

Хорошо — перенести условие в соединение:

ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Цены.СрезПоследних КАК Цены
	ПО Номенклатура.Ссылка = Цены.Номенклатура
		И Цены.Цена > 100

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

Где стоит условиеЧто происходит со строками без пары
ПО ... И УсловиеОстаются в результате, поля правой таблицы NULL
ГДЕ Условие на поле правой таблицыИсчезают, если условие не про NULL
ГДЕ ... ЕСТЬ NULLОстаются только они — «антисоединение»

Последняя строка — рабочий приём для поиска «лишнего»: контрагентов без договоров, товаров без цен, документов без проведённых движений.

Комбинации с И, ИЛИ и НЕ

Логика вокруг NULL строится на том, что сравнение с ним ложно. Отсюда несколько неочевидных следствий.

  • Условие Цена > 100 ИЛИ Цена ЕСТЬ NULL корректно пропустит строки и с ценой, и без неё.
  • Условие НЕ Цена > 100 не вернёт строки с NULL. Для них исходное сравнение ложно, но запрос не считает его «отрицанием истины».
  • Условие Цена <> 100 тоже не вернёт NULL, хотя по смыслу «нет цены» не равно ста.

Если вам нужны и строки с другим значением, и строки без значения, добавьте проверку явно:

ГДЕ
	Цены.Цена <> 100
	ИЛИ Цены.Цена ЕСТЬ NULL

Либо обработайте поле заранее: ЕСТЬNULL(Цены.Цена, 0) <> 100. Во втором варианте запрос короче, но подстановка нуля значит, что «нет цены» и «цена ноль» станут неразличимы. Решайте по задаче.

Скобки ставьте всегда. И и ИЛИ в одном условии без скобок читаются плохо, а ошибка в приоритете вместе с NULL даёт трудноуловимые потери строк.

Пустые ссылки и объекты

Пустая ссылка проверяется не через NULL, а сравнением с пустым значением нужного типа:

ГДЕ
	Договор.Контрагент = ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)

Если реквизит составного типа, у него могут быть и пустая ссылка конкретного типа, и значение Неопределено. В запросе Неопределено задают как НЕОПРЕДЕЛЕНО, а проверяют ЕСТЬ NULL не нужно: это другое значение. Для поиска таких строк используйте Поле = НЕОПРЕДЕЛЕНО.

Другой частый случай — обращение через точку к реквизитам связанных объектов: Документ.Контрагент.ИНН. Платформа превращает такую запись в неявное левое соединение. Если контрагент не заполнен, ИНН будет NULL. Именно поэтому поле, полученное через точку по пустой ссылке, нужно оборачивать в ЕСТЬNULL() так же, как поле явного левого соединения.

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

Агрегатные функции и группировка

Агрегаты работают с NULL по-своему, и это нужно знать до первого отчёта.

  • СУММА, СРЕДНЕЕ, МИНИМУМ, МАКСИМУМ игнорируют NULL в исходных строках. Если не осталось ни одной строки с числом, результат NULL.
  • КОЛИЧЕСТВО(Поле) считает только строки, где поле не NULL. КОЛИЧЕСТВО(*) считает все строки.
  • КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Поле) тоже не учитывает NULL.

Из этого получаются два практических приёма:

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

Здесь ЕСТЬNULL стоит снаружи агрегата: для товара без продаж сумма по пустому набору даст NULL, и его заменяет ноль.

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

В СГРУППИРОВАТЬ ПО все NULL попадают в одну группу. Строка с NULL в колонке группировки — нормальный результат, а не ошибка.

Объединение запросов и литерал NULL

При ОБЪЕДИНИТЬ ВСЕ в каждом подзапросе должно быть одинаковое число колонок. Если в одном из источников нужного поля нет, его заполняют литералом:

ВЫБРАТЬ
	Приход.Номенклатура КАК Номенклатура,
	Приход.Количество КАК Количество,
	NULL КАК Причина
ИЗ
	РегистрНакопления.Приход КАК Приход

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	Брак.Номенклатура,
	Брак.Количество,
	Брак.Причина
ИЗ
	РегистрНакопления.Брак КАК Брак

Литерал NULL без типа ведёт себя как «любой тип», и колонка получает тип по другим подзапросам. Если нужен строгий тип, используйте ВЫРАЗИТЬ(NULL КАК СТРОКА(100)). Это спасает от ошибок «несовместимые типы» в итоговой таблице и при последующих соединениях.

Скорость запросов с NULL

Сам по себе NULL запрос не тормозит. Тормозит то, как мы его обходим.

Что действительно влияет на скорость:

  • ⚡ Функции над полями в условии. ЕСТЬNULL(Цены.Цена, 0) > 100 в секции ГДЕ мешает СУБД использовать индекс по этому полю. Если можно, проверяйте поле напрямую и добавляйте ИЛИ ... ЕСТЬ NULL.
  • ⚡ Соединение по неиндексированным полям. Левое соединение по произвольному реквизиту заставляет СУБД перебирать таблицу целиком. Соединяйте по ссылкам и измерениям регистров, которые входят в индекс.
  • ⚡ Соединение с большими виртуальными таблицами. Передавайте отбор в параметры виртуальной таблицы (Остатки(&Дата, Склад = &Склад)), а не фильтруйте результат в ГДЕ. Тогда NULL получает ровно то, что нужно.
  • ⚡ Лишние вложенные запросы. Если соединение нужно только ради проверки «есть ли запись», подойдёт условие с СУЩЕСТВУЕТ вместо левого соединения и фильтра по NULL.

Оценивать, что получилось, лучше замером, а не на глаз. Прогоните запрос в консоли запросов на реальной базе до и после правки и сравните время выполнения. Для разбора тяжёлых случаев смотрят, какие SQL-запросы платформа отправила в СУБД: способ зависит от того, файловая у вас база или клиент-серверная.

Пример замены соединения на СУЩЕСТВУЕТ:

ВЫБРАТЬ
	Контрагенты.Ссылка КАК Контрагент
ИЗ
	Справочник.Контрагенты КАК Контрагенты
ГДЕ
	НЕ СУЩЕСТВУЕТ (
		ВЫБРАТЬ ПЕРВЫЕ 1 1
		ИЗ Справочник.ДоговорыКонтрагентов КАК Договоры
		ГДЕ Договоры.Владелец = Контрагенты.Ссылка)

Запрос находит контрагентов без единого договора, и NULL в нём вообще не участвует.

Типичные ошибки с NULL

Каждая из них встречается в чужих отчётах и в нашем коде.

  1. = NULL в запросе. Не работает , всегда ложно. Нужен ЕСТЬ NULL.
  2. Фильтр правой таблицы в ГДЕ. Левое соединение превращается во внутреннее. Условие переносят в ПО.
  3. Арифметика без ЕСТЬNULL. Одно NULL обнуляет всё выражение, итоговая колонка пустая.
  4. Замена ЕСТЬNULL не того типа. Число подставили в ссылку или строку в число, итоговая колонка получила составной тип, и сортировка или соединение ведёт себя странно.
  5. Путаница пустой ссылки и NULL. Проверка ЕСТЬ NULL для незаполненного реквизита ничего не найдёт: там не NULL, а пустая ссылка.
  6. Пустые ячейки в СКД. Если поле из левого соединения не обработано, в отчёте на СКД будут пустоты вместо нулей. Решение — ЕСТЬNULL прямо в тексте запроса набора данных, а не в формате ячейки.

Три примера от простого к сложному

Простой. Найти номенклатуру без единой записи в регистре цен: левое соединение, ГДЕ Цены.Номенклатура ЕСТЬ NULL.

Средний. Остатки по всем товарам с нулями вместо пустых ячеек: левое соединение с виртуальной таблицей остатков, ЕСТЬNULL(Остатки.КоличествоОстаток, 0) в списке выборки. Отбор по складу передаётся параметром виртуальной таблицы.

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

ВЫБРАТЬ
	ЕСТЬNULL(Текущий.Номенклатура, Прошлый.Номенклатура) КАК Номенклатура,
	ЕСТЬNULL(Текущий.Количество, 0) КАК Сейчас,
	ЕСТЬNULL(Прошлый.Количество, 0) КАК Раньше,
	ВЫБОР
		КОГДА ЕСТЬNULL(Прошлый.Количество, 0) = 0
			ТОГДА NULL
		ИНАЧЕ (ЕСТЬNULL(Текущий.Количество, 0) - Прошлый.Количество) * 100 / Прошлый.Количество
	КОНЕЦ КАК Изменение
ИЗ
	ВТТекущий КАК Текущий
		ПОЛНОЕ СОЕДИНЕНИЕ ВТПрошлый КАК Прошлый
		ПО Текущий.Номенклатура = Прошлый.Номенклатура

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

Для проверки арифметики возьмите числа: было 40, стало 50, тогда изменение равно (50 - 40) * 100 / 40 = 25. Если было 0, результат NULL, и его показывают как прочерк.

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

Чем NULL отличается от пустого значения в 1С?
Пустое значение — реальное значение типа: 0, "", пустая ссылка. NULL — отсутствие значения в результате запроса, например, нет пары в левом соединении.
Можно ли написать `ГДЕ Поле <> NULL`?
Нет, условие никогда не вернёт строк. Правильная запись — ГДЕ НЕ Поле ЕСТЬ NULL.
Как заменить NULL на ноль?
Функцией ЕСТЬNULL(Поле, 0). Для сумм по группе ставьте её снаружи агрегата: ЕСТЬNULL(СУММА(Поле), 0).
Почему левое соединение вернуло меньше строк, чем в левой таблице?
Скорее всего, условие на поле правой таблицы стоит в ГДЕ. Перенесите его в секцию ПО.
Считает ли `КОЛИЧЕСТВО(Поле)` строки с NULL?
Нет, только строки, где поле не NULL. Для подсчёта всех строк используйте КОЛИЧЕСТВО(*).