Why does a NOT IN subquery sometimes return zero rows when you can clearly see rows that should match?
Why they ask this
It is the cleanest test of whether you understand three-valued logic, and it is a bug that reaches production regularly because it fails silently rather than loudly.
Say this
If the subquery returns even one NULL, `NOT IN` evaluates to UNKNOWN for every row, and UNKNOWN is not TRUE — so nothing passes the filter. `NOT EXISTS` does not have this problem, which is why it is the safer default.
The reasoning
`x NOT IN (a, b, c)` is shorthand for `x <> a AND x <> b AND x <> c`. Comparing anything to NULL yields UNKNOWN rather than TRUE or FALSE, and `TRUE AND UNKNOWN` is UNKNOWN. So the moment one element of that list is NULL, the whole predicate can never be TRUE for any row — it is either FALSE (when x matches something) or UNKNOWN (when it does not). A WHERE clause keeps only rows where the predicate is TRUE, so the result is empty.
The asymmetry catches people: plain `IN` still works fine with a NULL present, because it only needs one comparison to be TRUE. It is specifically the negation that collapses. That is why the two look interchangeable in testing on clean data and diverge the first time the column is nullable.
`NOT EXISTS` is written as a correlated existence check rather than a chain of comparisons, so a NULL row in the subquery simply fails to match and is ignored. Anti-joining with `LEFT JOIN ... WHERE right.key IS NULL` behaves the same way. Both are NULL-safe.
See it verified against SQLite
One NULL in the list is enough to empty the result.
Given these rows
| id |
|---|
| 1 |
| 2 |
| 3 |
The query
SELECT id, 'in list' AS via FROM ids WHERE id IN (1, NULL) UNION ALL SELECT id, 'not in list' FROM ids WHERE id NOT IN (1, NULL);
Returns
| id | via |
|---|---|
| 1 | in list |
The answer most people give
"`NOT IN` and `NOT EXISTS` are the same thing, one is just faster." They are not equivalent — they return different results the moment a NULL is present, and the difference is correctness rather than speed.
They’ll ask next
So when would you still reach for NOT IN? (Answer: when the list is a literal you control, or the column is provably NOT NULL — and say which of those you are relying on.)
