SQLPathLab
Practical SQL Guide · MySQL 8.0

The NOT IN NULL trap: why a result becomes empty

Use three-valued logic to explain empty NOT IN results and write reliable anti-joins with NOT EXISTS.

Problem and reasoning

NOT IN compares the left value with every value in its set. If a subquery returns one NULL and the left value finds no definite match, the combined condition becomes UNKNOWN. WHERE keeps only TRUE, so every apparently unmatched row can disappear.

This behavior follows SQL three-valued logic. x NOT IN (1, NULL) is equivalent to x <> 1 AND x <> NULL, and the second comparison is never TRUE. The defect often appears only when production data contains a NULL absent from test fixtures.

When the business question is “find objects without a related row,” NOT EXISTS usually states the intent directly. Put the relationship predicate inside the correlated subquery; each left row is checked for a match and an unrelated NULL cannot poison the entire set.

If NOT IN remains appropriate, back the subquery column with a trusted NOT NULL constraint or filter NULL explicitly. Handle an application-generated empty list separately too, because NOT IN () is invalid MySQL syntax and an empty array cannot simply be expanded.

Tests should cover a NULL in the subquery, an empty subquery, a NULL on the left, duplicate keys, and both matched and unmatched rows. These boundaries keep an anti-join correct as data changes.

Examples

Sample table

Table:customers
customers
idname
101 Alice
102 Bob
103 Cindy
Table:orders
orders
customer_id
101
102
101
104

Before / Not recommended

SELECT c.id, c.name FROM customers c WHERE c.id NOT IN (SELECT customer_id FROM orders UNION ALL SELECT NULL) ORDER BY c.id;

Before result

result
idname

After / Recommended

SELECT c.id, c.name FROM customers c WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id) ORDER BY c.id;

After result

result
idname
103 Cindy

Why it is better:One NULL in the NOT IN set makes every nonmatch UNKNOWN; NOT EXISTS still finds Cindy, who has no order.

Was this explanation clear?