datarekha
SQL Medium Asked at GoogleAsked at StripeAsked at Airbnb

Why does NOT IN (subquery) return zero rows when the subquery contains a NULL, and how do you fix it?

The short answer

SQL uses three-valued logic: comparing any value to NULL yields UNKNOWN, not FALSE. NOT IN evaluates as NOT (a = v1 OR a = v2 OR ...), so a single NULL in the list makes the entire predicate UNKNOWN for every row, suppressing all results. Use NOT EXISTS or a LEFT JOIN anti-pattern instead.

How to think about it

This is one of SQL’s most dangerous silent bugs — the query runs without error, looks right, and returns zero rows when it should return many. Interviewers ask it to see whether you understand three-valued logic and whether you reach for the safe pattern reflexively.

Why it breaks — step by step

Say you want customers who did not order in month 6:

SELECT customer_id, name
FROM customers
WHERE customer_id NOT IN (
  SELECT customer_id FROM orders WHERE order_month = 6
);

If even one row in that subquery has a NULL customer_id, the engine expands the predicate to:

NOT (customer_id = 1 OR customer_id = NULL)

customer_id = NULL evaluates to UNKNOWN — not true, not false. Then x OR UNKNOWN is UNKNOWN (unless x is true), and NOT UNKNOWN is still UNKNOWN. Since a WHERE only passes rows that are true, every row is filtered out:

customer_idname
(zero rows)

No error, no warning — just an empty result that looks like a data problem. With a NULL in the orders table, the query that should list three customers lists none.

The fix — NOT EXISTS is NULL-safe

NOT EXISTS tests row-by-row existence instead of building a value list, so a NULL simply fails to match rather than poisoning the whole predicate:

SELECT c.customer_id, c.name
FROM customers c
WHERE NOT EXISTS (
  SELECT 1 FROM orders o
  WHERE o.customer_id = c.customer_id
    AND o.order_month = 6
);
customer_idname
2Bea
3Chen
4Dara

Three customers, correctly. Aarav is excluded because he did order in month 6; Bea, Chen, and Dara did not — note Bea’s only order was in month 5, so she belongs here too. The NULL-keyed order is simply ignored rather than wiping out the result.

The LEFT JOIN anti-join is equally NULL-safe and sometimes reads better to analysts from a pandas background:

SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o
  ON c.customer_id = o.customer_id
 AND o.order_month = 6
WHERE o.customer_id IS NULL;

NOT EXISTS often wins on speed because it short-circuits at the first match. A third option — appending AND customer_id IS NOT NULL to the subquery — works but is fragile: you must remember it every time, and a future nullable-column change silently reintroduces the bug.

Learn it properly Anti-joins

Keep practising

All SQL questions

Explore further

Skip to content