В отчёте нужно исключить оплаченные заказы. Какие строки вернёт запрос, если столбец status допускает NULL?
WITH orders(id, status) AS (
VALUES (1, 'paid'), (2, 'pending'), (3, NULL)
)
SELECT id
FROM orders
WHERE NOT (status = 'paid');
Запрос вернёт только строку с id = 2. Для строки с status = NULL условие status = 'paid' имеет результат UNKNOWN, а NOT UNKNOWN также даёт UNKNOWN; оператор WHERE оставляет только строки, для которых условие равно TRUE.
SQL должен был различать известное значение, отсутствие значения и логический результат проверки. Для этого используется NULL, который означает неизвестное или отсутствующее значение, а логика предикатов становится трёхзначной: TRUE, FALSE и UNKNOWN.
Такой подход позволяет не трактовать неизвестное значение как обычное равенство или неравенство. Однако он требует учитывать, что привычные преобразования булевой логики не всегда интуитивны при наличии NULL.
В запросе разработчик пытается выбрать все заказы, которые не имеют статуса paid. Интуитивно кажется, что NULL тоже должен попасть в результат, поскольку он точно не равен paid.
Но сравнение с NULL не возвращает TRUE или FALSE. Если не учесть это поведение, отчёт может молча исключить заказы с неизвестным статусом, что приведёт к неполному результату.
Выражение status = 'paid' вычисляется так:
'paid' результат — TRUE;'pending' результат — FALSE;NULL результат — UNKNOWN.Затем применяется NOT: NOT TRUE превращается в FALSE, NOT FALSE — в TRUE, а NOT UNKNOWN остаётся UNKNOWN. Поскольку WHERE пропускает только строки с результатом TRUE, строка с NULL отбрасывается.
Если бизнес-правило требует считать неизвестный статус не-оплаченным, условие нужно записать явно:
Для проверки NULL нельзя использовать = NULL или <> NULL; применяются предикаты IS NULL и IS NOT NULL. В некоторых СУБД существуют специальные операторы безопасного сравнения с NULL, но их синтаксис и семантику нужно проверять для конкретной СУБД.
В отчёте о просроченных платежах условие WHERE NOT (status = 'paid') исключило заказы, у которых платёжный шлюз ещё не передал статус и в базе сохранился NULL. Это исказило показатель количества потенциально проблемных заказов.
Рассматривались два варианта. Первый — заменить NULL заранее через COALESCE(status, 'unknown'): это делает условие компактным, но может скрыть различие между отсутствующим значением и реальным статусом и иногда мешает использованию индекса по столбцу. Второй — явно написать status <> 'paid' OR status IS NULL: условие длиннее, зато бизнес-правило видно непосредственно и семантика не зависит от подстановочного значения.
Был выбран второй вариант. Отчёт стал включать заказы с неизвестным статусом, а отдельная метрика позволила контролировать качество обмена с платёжным шлюзом.
Почему условие status <> 'paid' тоже не включает строки с NULL?
Для status = NULL сравнение не может установить ни равенство, ни неравенство. Поэтому status <> 'paid' даёт UNKNOWN, а не TRUE. Чтобы включить такие строки, нужно явно добавить OR status IS NULL.
Почему выражение NOT (A OR B) нельзя бездумно заменять на NOT A AND NOT B при наличии NULL?
В SQL действуют таблицы истинности трёхзначной логики, а не только двухзначной. Например, если A равно UNKNOWN, а B — FALSE, то NOT (A OR B) даёт UNKNOWN; правая часть NOT A AND NOT B также даёт UNKNOWN. Но при более сложных выражениях промежуточные значения UNKNOWN нужно прослеживать явно, поскольку они могут исключить строку в WHERE, хотя ни одно сравнение не дало обычного FALSE.
Чем отличается фильтрация в WHERE от проверки результата выражения в списке SELECT?
WHERE использует предикат как фильтр и оставляет только строки с результатом TRUE; значения FALSE и UNKNOWN отбрасываются. В SELECT выражение может быть вычислено и выведено как значение: например, сравнение status = 'paid' для NULL даст UNKNOWN, которое обычно отображается клиентом как NULL или специальное неизвестное значение. Поэтому наличие строки в результате SELECT не означает, что любое вычисленное условие внутри неё было истинным.