skip to content

Why does CASE status WHEN NULL THEN 'unknown' ELSE status END never return 'unknown'?

level: middleimportance: should knowfreq 45%

answer

  1. Simple CASE is shorthand for something
  2. Expand it and read the predicate
  3. Comparison with the null value is not TRUE
  4. Only TRUE selects a branch
  5. A predicate, not a value, tests for null

basics

~20 s

Simple CASE matches with equality, and status = NULL evaluates to UNKNOWN, never TRUE, so the branch cannot fire; a NULL status falls through to ELSE. Use a searched CASE with WHEN status IS NULL instead.

solid answer

~50 s

The simple form is defined as shorthand for a searched CASE whose predicates are `expr = value`, so `WHEN NULL` really means `WHEN status = NULL`. In SQL's three-valued logic that comparison yields **UNKNOWN**, and only TRUE selects a branch — so the branch is dead and every NULL `status` drops through to `ELSE`, returning NULL here since `ELSE status` is itself NULL. The portable fix is a searched CASE with an explicit null test: `CASE WHEN status IS NULL THEN 'unknown' ELSE status END`. Note the symmetry with the rest of the language: `WHERE x = NULL` matches nothing for the same reason. If you need a NULL branch inside a simple CASE, you cannot — that is precisely the case where the simple form is not expressive enough and you switch to the searched form.

code

sql · 12 lines
sql
-- dead branch: expands to WHEN status = NULL, which is UNKNOWN
SELECT order_id,
       CASE status WHEN NULL THEN 'unknown' ELSE status END AS label
FROM orders;   -- NULL status -> NULL, never 'unknown'

-- searched CASE with an explicit null predicate, tested first
SELECT order_id,
       CASE WHEN status IS NULL THEN 'unknown'
            WHEN status = 'N'   THEN 'new'
            ELSE 'other'
       END AS label
FROM orders;

go deeper

for a junior

Remember that testing for a missing value always uses IS NULL, never = NULL — inside CASE just as in WHERE.

for a middle

Derive the behaviour rather than reciting it: expand the simple form into its expr = value predicates and show that UNKNOWN never selects a branch.

for a senior

Connect it to the other null comparison traps you have hit in production — silent exclusion by <>, unmatched join keys — and describe the count check you run to prove null rows landed where you intended.

for a principal

Argue about the schema level: a status column that is nullable forces every query author to remember this rule, so a NOT NULL column with an explicit 'unknown' code is often the cheaper long-term design.

## What the simple form actually means The SQL standard defines the simple CASE as shorthand. Writing ```sql CASE status WHEN 'N' THEN 'new' WHEN NULL THEN 'unknown' ELSE status END ``` is defined to mean ```sql CASE WHEN status = 'N' THEN 'new' WHEN status = NULL THEN 'unknown' ELSE status END ``` Once the shorthand is expanded the behaviour is obvious. `status = NULL` is a comparison with the null value, and in SQL a comparison where either side is NULL evaluates to **UNKNOWN** — not TRUE, not FALSE. A `WHEN` branch is selected only when its condition is TRUE. UNKNOWN is not TRUE, so the branch is unreachable for every row, whatever `status` holds. The expression is nevertheless perfectly legal syntax: no engine rejects it, no warning appears, and the query runs. It is dead code that looks like a null-handling branch, which is why it survives review. ## What a NULL row actually gets A row with `status IS NULL` fails every equality branch (they are all UNKNOWN) and lands on `ELSE`. In the example above `ELSE status` returns the NULL itself, so the column shows nothing — exactly the outcome the author was trying to prevent. If there were no `ELSE`, the implicit `ELSE NULL` would give the same visible result by a different route. Either way, the label never appears. ## The fix Switch to the searched form and use the null **predicate**: ```sql SELECT CASE WHEN status IS NULL THEN 'unknown' WHEN status = 'N' THEN 'new' WHEN status = 'S' THEN 'shipped' ELSE 'other' END AS status_label FROM orders; ``` `IS NULL` is a predicate that returns TRUE or FALSE — never UNKNOWN — so the branch fires. Put it **first** in the ladder: it costs nothing and it makes the null case visible at the top of the decision list, before any branch that might otherwise absorb those rows. Where the standard's `IS NOT DISTINCT FROM` comparison is available, `CASE WHEN status IS NOT DISTINCT FROM 'N' THEN ...` gives null-safe equality (two NULLs compare as equal, and NULL versus a value as unequal), which is occasionally handy when you are comparing two nullable expressions rather than testing for null. Support differs between engines, so check yours before relying on it in portable code. ## Why this generalises The same rule drives several superficially different traps in SQL, and recognising the shared cause is what an interviewer is really testing: - `WHERE status = NULL` returns no rows. - `WHERE status <> 'N'` silently excludes rows where `status` is NULL, because the comparison is UNKNOWN. - A join predicate `a.key = b.key` never matches two NULL keys. All of them come from the single fact that comparison with NULL yields UNKNOWN and only TRUE passes. The simple CASE trap is the same rule wearing different syntax — which is exactly why expanding the shorthand in your head is the fastest way to reason about it. ## The mirror-image trap The reverse mistake also appears: assuming a NULL controlling expression makes the whole CASE return NULL. It does not. `CASE status WHEN 'N' THEN 'new' ELSE 'other' END` returns `'other'` for a NULL status, because the NULL simply fails every equality test and reaches `ELSE`. So a null value is not propagated — it is silently *categorised as the default*, which can be worse than a NULL because it looks like real data. When "missing" and "anything else" are genuinely different categories, you must add the `IS NULL` branch; the default will not do it for you. ## Reviewing for it Two cheap habits catch this class of bug. First, treat any literal `NULL` appearing directly after `WHEN` in a simple CASE as an automatic defect — there is no correct use of it. Second, when a CASE assigns categories over a nullable column, check the counts: run a grouped count of the derived label and confirm the null bucket's size matches `SELECT COUNT(*) FROM t WHERE col IS NULL`. If the null rows are hiding inside your default category, the numbers will say so.

  • What does `CASE status WHEN 'N' THEN 'new' ELSE 'other' END` return when status is NULL?
    'other'. The NULL fails the equality test — the comparison is UNKNOWN, not TRUE — so the row falls through to ELSE. Note this is worse than getting a NULL back: the missing value is silently filed under a real category. If missing must be distinguishable, add `WHEN status IS NULL THEN 'unknown'` as the first branch of a searched CASE.
  • Is there any null-safe equality comparison you could use inside a CASE?
    The standard defines `IS NOT DISTINCT FROM`, which treats two NULLs as equal and NULL versus a value as unequal, always returning TRUE or FALSE. `CASE WHEN a IS NOT DISTINCT FROM b THEN ... END` is therefore null-safe. Engine support varies, so confirm it exists on your target before using it in portable code; otherwise write the explicit `IS NULL` branches.
  • Where should the IS NULL branch sit in a CASE ladder?
    First, as a rule. Branches are tested in order and the null case is the one most easily absorbed by a later default or by a range test that evaluates to UNKNOWN. Putting it on top makes the treatment of missing data explicit to any reader and guarantees no earlier branch quietly claims those rows.

saying these in an interview costs you the question

  • Believing WHEN NULL catches missing values
  • Saying the query fails with a syntax error
  • Claiming a NULL controlling expression makes the whole CASE NULL
  • Using = NULL anywhere as a null test
  • Assuming ELSE always returns a non-null label

context