skip to content

questions

6

Why does WHERE email = NULL return no rows, and what do you write instead?

level: juniorimportance: must knowfreq 88%

answer

  1. Predicates have three outcomes, not two
  2. Comparing with an unknown value tells you nothing
  3. WHERE keeps only one of the three
  4. There is a dedicated predicate for the marker
  5. IS NULL never returns UNKNOWN

basics

~20 s

Comparing anything with NULL yields UNKNOWN rather than TRUE, and WHERE keeps only rows whose predicate is TRUE. So email = NULL matches nothing, not even rows whose email is NULL. Write email IS NULL.

solid answer

~50 s

SQL predicates are three-valued: they evaluate to TRUE, FALSE or UNKNOWN. NULL is a marker for a missing value, so any comparison operator applied to it — `=`, `<>`, `<`, `>` — returns UNKNOWN, because SQL cannot know whether an unknown value equals the thing you compared it to. Even `NULL = NULL` is UNKNOWN. The `WHERE` clause keeps a row **only when its predicate is TRUE**; FALSE and UNKNOWN are both discarded, which is why `email = NULL` returns an empty result and `email <> NULL` does too. The dedicated predicate for the test you actually meant is `IS NULL` / `IS NOT NULL`. Unlike comparison operators, `IS NULL` inspects the marker instead of comparing values, so it always returns TRUE or FALSE and never UNKNOWN. The same TRUE-only rule governs `HAVING` and a join's `ON` clause.

code

sql · 9 lines
sql
-- Wrong: returns zero rows no matter what the data looks like
SELECT user_id, email
FROM users
WHERE email = NULL;

-- Right: IS NULL tests the marker instead of comparing values
SELECT user_id, email
FROM users
WHERE email IS NULL;

go deeper

for a junior

Memorise the fix and the reason: never compare to NULL with =, always use IS NULL or IS NOT NULL. Be able to state that a comparison with NULL yields UNKNOWN and that WHERE keeps only TRUE.

for a middle

Explain the mechanics: three-valued logic, comparison operators returning UNKNOWN, and WHERE/HAVING/ON all retaining rows only on TRUE. Point out that IS NULL is separate grammar and can never itself be UNKNOWN.

for a senior

Show you catch this in review and in production: silently empty result sets from NULL bind parameters, filters that quietly lose rows, and data-quality checks that must test both IS NULL and the empty string.

for a principal

Own the upstream decision — which columns are genuinely allowed to be absent, and whether a sentinel or a nullable column better expresses the domain, given that every nullable column pushes three-valued reasoning onto every query that touches it.

## The mistake and the fix ```sql SELECT * FROM users WHERE email = NULL; -- always empty SELECT * FROM users WHERE email IS NULL; -- the rows you wanted ``` The first query is not an error and produces no warning on most engines. It simply returns zero rows, forever, whatever the data looks like. That silence is why this is the single most common SQL trap in interviews and in production. ## NULL is a marker, not a value A NULL in a column does not mean zero, an empty string, or "a special value that equals other NULLs". It records that the value is *absent* — unknown, inapplicable, or not yet supplied. Two rows with a NULL `email` are not two rows that share an email address; they are two rows about which the same thing is unknown. That framing explains every rule that follows. Ask "is this unknown value equal to `'[email protected]'`?" and the only honest answer is "I cannot tell". Ask "is this unknown value equal to that other unknown value?" and the answer is still "I cannot tell". ## Three-valued logic Because of that, a SQL predicate does not return a boolean with two states. It returns one of **TRUE**, **FALSE**, or **UNKNOWN**. Every comparison operator — `=`, `<>`, `<`, `<=`, `>`, `>=` — returns UNKNOWN as soon as either operand is NULL: ```sql SELECT NULL = NULL; -- UNKNOWN (not TRUE) SELECT NULL <> NULL; -- UNKNOWN (not TRUE either) SELECT 5 > NULL; -- UNKNOWN ``` Note that `<>` does not "flip" UNKNOWN into something useful: the negation of "I cannot tell" is still "I cannot tell". ## Why the row disappears The `WHERE` clause is specified to retain a row **if and only if its search condition evaluates to TRUE**. Rows whose condition is FALSE are dropped, and so are rows whose condition is UNKNOWN — the clause makes no distinction between "definitely not a match" and "cannot be determined". `email = NULL` is UNKNOWN for every row (whether the email is missing or present), so every row is discarded. The same TRUE-only rule applies to `HAVING` and to a join's `ON` condition, which is why NULLs also quietly refuse to match in join predicates. ## IS NULL: a predicate, not a comparison `IS NULL` and `IS NOT NULL` are separate grammar in the standard. They ask a question about the *marker*, not about the value behind it, so they are two-valued: they always return TRUE or FALSE and can never yield UNKNOWN. ```sql -- rows with no email at all SELECT user_id FROM users WHERE email IS NULL; -- rows that definitely have one SELECT user_id FROM users WHERE email IS NOT NULL; ``` Because the two are exact complements, `COUNT` of the first plus `COUNT` of the second always equals the table's row count — something that is *not* true for `= 'x'` and `<> 'x'`. ## Empty string is not NULL A very common follow-up: `email = ''` and `email IS NULL` are different tests in standard SQL. An empty string is a known value of length zero; NULL is the absence of a value. A column can hold either, and a data-quality check often has to look for both: ```sql SELECT user_id FROM users WHERE email IS NULL OR email = ''; ``` Oracle is the notable exception here: it stores an empty string as NULL, so the distinction collapses on that engine. Elsewhere, keep the two apart deliberately. ## Related shapes worth recognising - `CASE WHEN email = NULL THEN 'missing' ELSE 'present' END` never takes the first branch, for exactly the same reason — the `WHEN` condition must be TRUE. - Parameterised queries hit this hard: a predicate like `WHERE email = :param` silently returns nothing when the application passes a NULL parameter, rather than "all rows" or "the NULL rows". If both behaviours are wanted, the query needs an explicit NULL branch or a NULL-safe comparison. - Sorting and grouping do *not* follow the comparison rule: `GROUP BY` and `DISTINCT` treat NULLs as alike and collapse them into one group. That inconsistency surprises people, and it is worth being able to say out loud that comparison semantics and grouping semantics are defined separately. ## How to say it in an interview Three sentences carry the whole answer: NULL means unknown; comparing with an unknown yields UNKNOWN; `WHERE` keeps only TRUE, so the row is dropped. Then give the fix, `IS NULL`, and mention that it is the one test that can never return UNKNOWN.

  • Can IS NULL itself ever evaluate to UNKNOWN?
    No. `IS NULL` and `IS NOT NULL` are two-valued predicates: they inspect whether the marker is present rather than comparing values, so they always return TRUE or FALSE. That is what makes them exact complements — every row satisfies exactly one of them, which is not true of `= 'x'` and `<> 'x'`.
  • Is a column holding an empty string the same as a column holding NULL?
    In standard SQL, no. An empty string is a known value of length zero; NULL records that no value exists. `email = ''` and `email IS NULL` therefore select different rows, and data-quality checks often test both. Oracle is the exception: it stores the empty string as NULL, collapsing the distinction on that engine.
  • Which other clauses drop a row when its condition evaluates to UNKNOWN?
    `HAVING` and a join's `ON` condition follow the same rule as `WHERE`: the row or group survives only if the condition is TRUE. A `CASE` expression behaves analogously — a `WHEN` branch is taken only on TRUE, so `CASE WHEN col = NULL THEN ...` never fires and falls through to `ELSE`.

NULL is a sealed envelope. Asking whether one sealed envelope holds the same number as another is not answered "yes" or "no" — it is answered "I cannot tell", and SQL files that answer under UNKNOWN.

saying these in an interview costs you the question

  • Claiming NULL = NULL is TRUE because both sides are NULL
  • Saying NULL equals zero or the empty string
  • Thinking <> NULL returns the rows that = NULL missed
  • Believing WHERE keeps rows whose predicate is UNKNOWN
  • Treating IS NULL as just another comparison operator

context

open as a page

How do AND, OR and NOT behave when an operand is UNKNOWN in SQL's three-valued logic?

level: middleimportance: must knowfreq 60%

basics

~10 s

UNKNOWN propagates unless the other operand already decides the result: FALSE AND UNKNOWN is FALSE, TRUE OR UNKNOWN is TRUE, and every other combination with UNKNOWN — including NOT UNKNOWN — stays UNKNOWN.

open as a page

Why does base_salary + bonus come back NULL for some rows, and how do you fix it?

level: juniorimportance: should knowfreq 68%

basics

~10 s

Arithmetic on an absent value is undefined, so any expression with a NULL operand evaluates to NULL — the whole sum vanishes when bonus is missing. Substitute a default first: base_salary + COALESCE(bonus, 0).

open as a page

In SQL, how do you compare two nullable columns so that two NULLs count as equal?

level: middleimportance: should knowfreq 42%

basics

~20 s

Use the standard NULL-safe comparison IS NOT DISTINCT FROM, which returns TRUE when both sides are NULL and never yields UNKNOWN. Where an engine lacks it, write a = b OR (a IS NULL AND b IS NULL).

open as a page

Why don't the counts from status = 'active' and status <> 'active' add up to the table total?

level: seniorimportance: should knowfreq 47%

basics

~10 s

Rows with a NULL status make both predicates UNKNOWN, and WHERE keeps only TRUE, so those rows fall out of both buckets. A predicate and its negation are not exhaustive over a nullable column.

open as a page

What does NULLIF(x, 0) return, and why is it the portable division-by-zero guard?

level: middleimportance: nice to knowfreq 33%

basics

~10 s

NULLIF(x, 0) returns NULL when x equals 0 and x otherwise. Dividing by it turns a would-be division-by-zero error into a NULL result, since any arithmetic with a NULL operand yields NULL.

open as a page