Одна из самых неприятных ловушек SQL — поведение
NOT IN при наличии NULL. Запрос выглядит абсолютно корректно, но внезапно перестаёт возвращать строки.Таблицы:
users(id)
bans(user_id)
Допустим, нужно получить пользователей, которых нет в
bans. Часто пишут так:SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM bans
);
Пока в
bans.user_id нет NULL — всё работает нормально. Но представим данные:bans
-------
1
2
NULL
Теперь запрос внезапно вернет:
0 rows
Хотя пользователи без бана есть. Почему так происходит — SQL сравнивает условие примерно как:
id <> 1
AND id <> 2
AND id <> NULL
Но:
id <> NULL
не даёт TRUE или FALSE. Результат: UNKNOWN. А в
WHERE проходят только TRUE. Из-за этого всё условие целиком перестаёт выполняться.Это особенность трёхзначной логики SQL: TRUE, FALSE, UNKNOWN
Любое сравнение с NULL даёт UNKNOWN:
NULL = 1
NULL <> 1
NULL = NULL
Поэтому
NOT IN с NULL внутри подзапроса становится опасным.Безопасный вариант —
NOT EXISTS:SELECT *
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM bans b
WHERE b.user_id = u.id
);
Почему
EXISTS работает нормально: сравнение идёт построчно; NULL не ломает всю проверку; оптимизатор обычно хорошо превращает это в anti-join.Ещё вариант — явно убрать NULL:
SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM bans
WHERE user_id IS NOT NULL
);
Но на практике:
NOT EXISTS обычно считается более безопасным и читаемым решением.Отдельный момент:
NOT IN () и NOT EXISTS могут давать разные планы выполнения в зависимости от СУБД. Но логически для nullable данных: NOT EXISTS почти всегда предпочтительнее.Быстрая проверка проблемы:
SELECT COUNT(*)
FROM bans
WHERE user_id IS NULL;
Если такие строки есть —
NOT IN уже потенциально опасен.🔥
NOT IN и NULL плохо сочетаются. Если в подзапросе появляется хотя бы один NULL, условие может перестать возвращать строки вообще. Для таких проверок надёжнее использовать NOT EXISTS.➡️ SQL Ready | #практика