← All cheatsheets
Data Analyst · #015 · August 7, 2026 · 2 min read

Why does WHERE email = NULL return zero rows?

The query looks right, runs without an error, and can never match a row. Three-valued logic, IS NULL, COALESCE, and the two NULL traps that empty your results.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

The query runs clean. No error, no warning. And it returns zero rows, every single time, even on a table full of missing emails. One character pair is responsible: = NULL.

Why = NULL can never match

NULL is not a value. It is the absence of one, and you cannot compare a value to absence.

email = NULL      -- unknown
email != NULL     -- unknown too
NULL = NULL       -- still unknown

SQL logic has three outcomes: true, false, and unknown. Any comparison involving NULL is unknown, and WHERE keeps only rows where the condition is true. Unknown rows are silently dropped, which is why the query fails without ever raising an error.

The 3-character fix

Ask about existence, not equality:

-- who has no email?
SELECT * FROM users
WHERE email IS NULL;

-- who has one?
SELECT * FROM users
WHERE email IS NOT NULL;

IS NULL is the only operator designed for absence. It always answers true or false, never unknown.

COALESCE: give absence a face

COALESCE returns the first non-null value in its list, left to right:

SELECT COALESCE(email, 'no email')
FROM users;

COALESCE(nickname, name, 'anon')

Perfect for display defaults and report labels. Resist the urge to use it to paper over data-quality problems that should be fixed upstream.

Trap 1: NOT IN meets NULL

One NULL in the list and every row disappears:

-- returns 0 rows if logs contains a NULL
WHERE id NOT IN
  (SELECT ref_id FROM logs)

-- fix: exclude the NULLs
WHERE id NOT IN
  (SELECT ref_id FROM logs
   WHERE ref_id IS NOT NULL)

id NOT IN (1, NULL) means id != 1 AND id != NULL, and that second test is unknown for every row, which sinks the whole condition. NOT EXISTS is immune to this and is often the safer pattern.

Trap 2: aggregates skip NULLs

Same column, two different answers:

COUNT(*)       -- counts all rows
COUNT(email)   -- counts non-null only
AVG(score)     -- ignores NULLs entirely

-- average over ALL rows instead:
SUM(score) / COUNT(*)

AVG divides by the count of non-null values. If NULL means zero in your data, say so explicitly with COALESCE(score, 0).

The takeaway

Treat NULL as a question the data has not answered yet. Test it with IS NULL, default it with COALESCE, and never let it near = or NOT IN unguarded. The print-ready PDF above fits the whole survival kit on one page.

Frequently asked questions

Why does WHERE column = NULL return no rows?
Because NULL is not a value, it is the absence of one. Any comparison with NULL evaluates to unknown, not true, and WHERE only keeps rows where the condition is true. Use IS NULL instead.
What is the difference between IS NULL and = NULL?
= is a value comparison, and comparing anything to NULL gives unknown, so = NULL never matches. IS NULL is a dedicated test for absence: it always returns true or false and is the only correct way to find missing values.
Why does NOT IN return zero rows when the list contains a NULL?
id NOT IN (1, NULL) expands to id != 1 AND id != NULL. The second comparison is unknown for every row, which makes the whole condition unknown, so no row survives. Filter NULLs out of the subquery, or use NOT EXISTS.
Do COUNT and AVG include NULL values?
COUNT(*) counts all rows, but COUNT(column) and AVG(column) skip NULLs entirely. If NULL should mean zero in your data, make it explicit with COALESCE(column, 0) before aggregating.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

More cheatsheets