Learning Platform
Урок 04.03 · 18 мин
Начальный
NULLThree-valued logicIS DISTINCT FROMNOT INPredicate logic

Зачем углубляться в NULL

В модуле 2 мы установили базовое правило: NULL = NULL возвращает не TRUE, а сам NULL. Это следствие семантики «значение неизвестно». Но из этого простого утверждения вырастает целое дерево неочевидных следствий, и каждый второй баг в продовых запросах — это нарушение трёхзначной логики.

В этом уроке мы пройдём её до конца: посмотрим таблицы истинности для AND, OR, NOT, разберём классическую ловушку WHERE NOT IN (..., NULL), и познакомимся с операторами, специально сделанными, чтобы трактовать NULL как обычное значение.

Три значения вместо двух

Классическая (булева) логика — двухзначная: TRUE или FALSE. SQL-логика трёхзначная: TRUE, FALSE, UNKNOWN (он же NULL в контексте предикатов).

Трёхзначная логика
— это не каприз, а единственный способ согласованно работать с «отсутствующей информацией». Если значение неизвестно, то любое утверждение о нём («оно равно 5», «оно больше 3», «оно не пустое») тоже неизвестно.

Таблица истинности AND

A \ BTRUEFALSENULL
TRUETRUEFALSENULL
FALSEFALSEFALSEFALSE
NULLNULLFALSENULL

Запомни мнемонику: AND с FALSE всегда FALSE (потому что для AND достаточно одного FALSE). Это работает даже с NULL: NULL AND FALSE = FALSE, потому что «что-то неизвестное И ложь» — это всё равно ложь.

Таблица истинности OR

A \ BTRUEFALSENULL
TRUETRUETRUETRUE
FALSETRUEFALSENULL
NULLTRUENULLNULL

Симметрично: OR с TRUE всегда TRUE. NULL OR TRUE = TRUE, потому что «что-то неизвестное ИЛИ истина» — это уже истина.

NOT

NOT TRUE = FALSE, NOT FALSE = TRUE, NOT NULL = NULL. Отрицание неизвестного — неизвестное.

Проверь таблицы истинности на практике:

PostgreSQL

true_and_nullNULL. false_and_nullFALSE (это «короткое замыкание» AND с FALSE). true_or_nullTRUE. И так далее.

Как работает WHERE

WHERE оставляет в результате только те строки, для которых предикат равен TRUE. FALSE и NULL — обе отсеиваются. Это ключ к пониманию всех NULL-багов.

WHERE и трёхзначная логика

Только TRUE проходит фильтр. FALSE и NULL — обе уходят в корзину.

строкаx = 5
предикат x = 5TRUE
WHEREоставляет
строкаx = 7
предикат x = 5FALSE
WHEREотсеивает
строкаx = NULL
предикат x = 5NULL
WHEREотсеивает

Из этого следует

контринтуитивная вещь
: запросы WHERE x = 5 и WHERE x != 5 вместе не покрывают всех строк. Строки с x IS NULL пропадают в обоих случаях.

Проверь: birth_year = 1998 + birth_year != 1998 не дают всех клиентов:

PostgreSQL

Сумма eq + neq + is_null равна total, а вот eq + neq — нет. У Ирины birth_year = NULL, и она не попадает ни в один из двух фильтров. Чтобы получить «всё кроме 1998 года рождения», нужно явно: WHERE birth_year != 1998 OR birth_year IS NULL.

Классическая ловушка: NOT IN с NULL

Это самая частая ошибка в продовых запросах. Если в списке для NOT IN есть хотя бы один NULL, результат становится пустым.

Магия NOT IN с NULL — внимательно посмотри на оба запроса:

PostgreSQL

Почему? Потому что x NOT IN (a, b, c) разворачивается в x != a AND x != b AND x != c. Если один из этих сравнений — x != NULL, он всегда NULL. А что-угодно AND NULL — либо FALSE, либо NULL. Никогда TRUE. Значит, WHERE ничего не пропускает.

Это особенно коварно, когда NULL приходит не из литерала, а из под-запроса:

SELECT * FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);  -- если в orders есть NULL в customer_id, всё ломается

Решение — использовать NOT EXISTS или IS DISTINCT FROM. Они трактуют NULL по-другому.

IS DISTINCT FROM — оператор «равенство, но как у нормальных людей»

IS DISTINCT FROM — это специальный оператор, который сравнивает значения, считая NULL обычным значением. Два NULL для него равны.

Сравнение=IS DISTINCT FROM
5 = 5TRUEFALSE
5 = 7FALSETRUE
5 = NULLNULLTRUE
NULL = NULLNULLFALSE

Обратный — IS NOT DISTINCT FROM, который ведёт себя как «равенство с NULL = NULL».

Сравни поведение = и IS DISTINCT FROM:

PostgreSQL

Когда полезно? Когда сравниваешь два значения, и оба могут быть NULL, и тебе нужно понимать «они одинаковые». Типичный случай — UPDATE-trigger или сравнение «было vs стало» в audit log:

-- "Изменилось ли значение?"
SELECT * FROM audit
WHERE old_value IS DISTINCT FROM new_value;  -- работает корректно с любыми NULL

С обычным != строки, где old_value IS NULL и new_value IS NULL, неправильно считались бы «изменившимися» (потому что NULL != NULL = NULL = не TRUE = отсеивается) — то есть, наоборот, неизменными.

Исключения: set-операции, GROUP BY, DISTINCT

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

  • UNION, INTERSECT, EXCEPT — при устранении дубликатов NULL считается равным NULL.
  • GROUP BY — две строки с NULL в группирующем столбце попадают в одну группу.
  • DISTINCT — несколько NULL свернутся в один.
  • Уникальные индексы —
    тут наоборот
    : до Postgres 15 несколько NULL в уникальной колонке разрешались (NULL != NULL), теперь есть опция NULLS NOT DISTINCT.

GROUP BY и DISTINCT сворачивают NULL в одну группу:

PostgreSQL

Две строки с NULL группируются вместе — cnt = 2. Если бы NULL считался уникальным, было бы две группы по одному NULL.

NULL в JOIN — где сюрприз

INNER JOIN ON a.x = b.y отсеивает строки, где a.x или b.y равны NULL — потому что предикат = NULL это NULL, а JOIN ON оставляет только TRUE. Это редко бывает желаемым поведением для «опциональных» внешних ключей.

LEFT JOIN сохраняет левые строки, и NULL не отсеивает — но он создаёт новые NULL для правой части, когда совпадения не нашлось. После такого JOIN всегда внимательно с WHERE: WHERE b.x IS NULL — стандартный паттерн для anti-join.

Anti-join: клиенты без заказов через LEFT JOIN + IS NULL:

PostgreSQL

В нашей вселенной таких может не быть (плотные seed-данные) — но синтаксис теперь у тебя в руках.

Проверка знанийKnowledge check
Запрос SELECT * FROM products WHERE category_id NOT IN (SELECT id FROM categories_to_exclude) внезапно возвращает 0 строк, хотя должен много. Никто ничего не менял. Гипотеза?
ОтветAnswer
Скорее всего, в таблице categories_to_exclude появилась строка с NULL в колонке id. Раз NULL попал в список NOT IN, весь предикат для каждой проверяемой строки превращается в «что-то AND NULL» — то есть либо FALSE, либо NULL, но никогда TRUE. Результат — пустая выборка. Починка: либо WHERE id IS NOT NULL внутри подзапроса, либо переписать на NOT EXISTS — он трёхзначной логикой не страдает (NOT EXISTS возвращает TRUE/FALSE, ни одного NULL). Хороший префикс защиты — добавить NOT NULL constraint на критичные колонки.
NULL и трёхзначная логика: теоретическое обоснование Friendly SQL в DuckDB: FROM-first и другие отличия

Чек-лист

  • SQL-логика трёхзначная: TRUE, FALSE, NULL. Предикат может вернуть любой из трёх.
  • WHERE оставляет только строки с предикатом TRUE. FALSE и NULL отсеиваются вместе.
  • x = 5 и x != 5 не покрывают строки с x IS NULL. Нужен явный OR x IS NULL.
  • NOT IN (..., NULL) всегда даёт пустой результат — классическая ловушка с подзапросами.
  • IS DISTINCT FROM — равенство, в котором NULL считается обычным значением.
  • В UNION, GROUP BY, DISTINCT NULL равен сам себе (исключение из общей логики).
  • INNER JOIN ON a.x = b.y отбрасывает строки с NULL в ключе соединения.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чему равно выражение `FALSE AND NULL` в PostgreSQL?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6