NULLs and Coalescing
Design for missingness, don't stumble into it
NULL is not a value — it is the absence of a value. Every comparison, aggregate and join inherits three-valued logic unless you design for it explicitly.
What you will learn
- Explain three-valued logic (TRUE / FALSE / UNKNOWN)
- Use IS NULL, COALESCE and NULLIF correctly
- Predict how NULLs behave in aggregates, joins and ORDER BY
What is this?
NULL denotes the absence of a value. Comparisons with NULL evaluate to UNKNOWN, not TRUE/FALSE, which filters rows out of WHERE, pads equality untested in joins, and skips rows in sums. SQL gives you IS NULL, COALESCE and NULLIF to handle missingness explicitly.
Why do you need it?
- SUM() silently skips NULLs — a column half-NULL produces an undercount that looks legitimate.
- A join ON a = b pairs 'missing A' with nothing — missing keys vanish.
- Missing-money handling is the #1 source of 'why is the total different' in data teams.
Mental model
An unanswered survey question
NULL
A blank isn't 'no answer' and isn't '0' — it's 'unknown'. Asking 'did 5 people answer 0?' gets a different answer than 'did 5 people skip it?'. SUM ignores blanks; COUNT(*) doesn't; the difference is frequently surprising.
How it works
- Three-valued logic: `NULL = NULL` is UNKNOWN; only `IS NULL`/`IS NOT NULL` test absence.
- Aggregates: SUM/AVG ignore NULLs; COUNT(*) counts all rows, COUNT(col) counts non-NULL.
- COALESCE(a, b, c) returns the first non-NULL; NULLIF(a, b) returns NULL when a = b.
- ORDER BY puts NULLs first or last depending on dialect (NULLS FIRST/LAST) — default differs.
- Joins treat NULL keys as unmatched: on = NULL matches nothing, even in FULL JOIN.
How NULL propagates.
Examples
NULL handling done explicitly
SELECT COALESCE(channel, 'unknown') AS channel, SUM(COALESCE(amount, 0)) AS revenue -- don't undercountFROM salesWHERE amount IS NOT NULL OR channel IS NULL -- explicit intentGROUP BY 1;COALESCE fixes display and arithmetic; IS NOT NULL filters precisely; nothing is left to logic's default.
In production
- A revenue model wraps every financial measure in COALESCE(x, 0) so SUM never undercounts.
- A null-safety audit counts IS NULL per critical column and gates the pipeline when it spikes.
- A join on optional customer keys uses COALESCE(key, '-') so orphans land in an audit bucket.
Best practices
- Decide a NULL policy per dataset: is missing 0, 'unknown', or invalid? Encode it.
- COALESCE at the boundary (read or write), not scattered through every downstream query.
- Test joining on nullable columns: add COALESCE to the key or a NOT NULL guard.
- Document NULL semantics in the catalog: 'amount NULL means cancelled, never 0'.
Common mistakes
- `WHERE amount != 0` silently dropping NULL rows (UNKNOWN not TRUE).
- SUM over a nullable column underreporting revenue with no signal.
- Joining two nullable key columns and 'losing' legitimate matches.
- Treating COALESCE(x, -1) as data instead of a stand-in sentinel that leaks downstream.
Common issues
Revenue report says 0 for a region with dataAmount column NULL-rich; SUM skipped them and COALESCE wasn't applied
COALESCE(amount, 0) at the model boundary and add a null-rate assertion.
JOIN silently lost customersCustomer key nullable with NULLs for anonymous visits
Store a stable fallback key ('anonymous-<id>') or bucket NULLs explicitly to an audit group.
Debugging tips
- When totals differ, COUNT(*) vs COUNT(x) and COUNT(*) - COUNT(x) = rows with NULL x.
- Add `WHERE x IS NULL` alone to see the missing population before deciding a policy.
- Use NULLIF to detect glaring stand-ins: a column of -1s masks 'did not use COALESCE'.
Interview checkpoint
Quick quiz
What is the result of SELECT NULL = NULL;
SUM(amount) counts NULL amounts as zero.