NULL causes more silently-wrong SQL than any other feature, and almost every surprise comes from one misunderstanding: NULL does not mean "empty" or "zero". It means "unknown". Once that clicks, every rule below follows from it.
Three-valued logic
Since NULL is unknown, comparing it to anything is also unknown — not true, not false:
NULL = NULL → UNKNOWN
NULL <> 5 → UNKNOWN
NULL > 0 → UNKNOWN
NULL + 5 → NULL
'abc' || NULL → NULL (concatenation, most engines)And WHERE keeps only rows where the predicate is TRUE — UNKNOWN rows are dropped just
like FALSE ones. That is why this returns nothing:
SELECT * FROM employees WHERE commission = NULL; -- always empty
SELECT * FROM employees WHERE commission IS NULL; -- correctThe truth tables, which are worth knowing cold:
WHERE keeps only TRUE. That single rule explains the NOT IN trap and the disappearing rows from a <> filter.Note FALSE AND UNKNOWN = FALSE and TRUE OR UNKNOWN = TRUE — the known operand can
still decide the result. That is exactly why IN tolerates NULLs while NOT IN does not:
a match makes the OR-chain TRUE regardless, but in the negated AND-chain a single UNKNOWN
prevents TRUE forever. See
filtering for that trap in full.
A NOT filter drops NULL rows
The most common real-world NULL bug, and it looks nothing like a NULL bug:
-- Intent: everyone not in Sales. Employees with NULL department are NOT returned.
SELECT * FROM employees WHERE department <> 'Sales';
-- Fix: say what you mean about the unknowns
SELECT * FROM employees WHERE department <> 'Sales' OR department IS NULL;
-- Or use the NULL-safe comparison
SELECT * FROM employees WHERE department IS DISTINCT FROM 'Sales'; -- Postgres
SELECT * FROM employees WHERE NOT (department <=> 'Sales'); -- MySQLIS DISTINCT FROM treats NULL as a value that can differ from things — it is the
comparison people usually meant to write.
Which functions ignore NULLs
| Behaviour | Functions | ||
|---|---|---|---|
| Skip NULLs | SUM, AVG, MIN, MAX, COUNT(col), COUNT(DISTINCT col) | ||
| Count NULLs | COUNT(*) | ||
| Return NULL if any input is NULL | Arithmetic, ` | ` concatenation, most scalar functions |
AVG is the one that bites: it divides by the count of non-NULL values.
-- 10 employees, 6 with NULL commission
SELECT AVG(commission) FROM employees; -- sum / 4
SELECT AVG(COALESCE(commission,0)) FROM employees; -- sum / 10Both are defensible; the interview point is knowing they differ and asking which is wanted.
GROUP BY does the opposite of comparison — it collects all NULLs into one group,
because grouping uses "not distinct" rather than =. So does DISTINCT, and so do
set operations.
The toolkit
COALESCE(a, b, c) -- first non-NULL argument; standard, any number of args
NULLIF(a, b) -- NULL when a = b, else a
IFNULL(a, b) -- MySQL, two arguments
ISNULL(a, b) -- SQL Server, two arguments
NVL(a, b) -- OraclePrefer COALESCE — it is standard SQL and works everywhere.
NULLIF has one job that everyone eventually needs, guarding division:
SELECT revenue / NULLIF(order_count, 0) AS avg_order_value FROM daily_stats;
-- zero orders ⇒ NULL, instead of a divide-by-zero errorSorting
NULL ordering is not standardised. PostgreSQL and Oracle put NULLs last ascending; MySQL and SQL Server put them first. Be explicit when it matters:
ORDER BY commission DESC NULLS LAST; -- PostgreSQL, Oracle
ORDER BY commission IS NULL, commission DESC; -- MySQL — the boolean sorts 0 before 1NULL in joins and constraints
- A NULL join key never matches, including another NULL — rows simply do not join.
UNIQUEallows multiple NULLs in most engines, because two unknowns are not provably
equal. SQL Server is the exception, allowing only one.
PRIMARY KEYforbids NULL entirely.CHECKconstraints pass on UNKNOWN —CHECK (age > 0)does not reject a NULL age.
Add NOT NULL if that is the intent.
Mistakes that cost the interview
- `= NULL` or `<> NULL` instead of
IS [NOT] NULL. - `NOT IN` over a nullable subquery, returning zero rows.
- A `<>` filter that silently excludes NULL rows.
- Assuming `AVG` divides by the row count.
- Assuming NULLs group separately. They form one group.
- Unguarded division where the denominator can be zero or NULL.
Practise these on the SQL sheet.
Frequently asked
Why is NULL = NULL not true in SQL?
Because NULL means 'unknown', not 'empty'. Two unknown values cannot be proven equal, so the comparison evaluates to UNKNOWN rather than TRUE, and WHERE keeps only TRUE rows. Use IS NULL to test for it, or IS DISTINCT FROM (MySQL: <=>) for a NULL-safe comparison.
Does AVG ignore NULL values?
Yes — AVG, SUM, MIN, MAX and COUNT(column) all skip NULLs, so AVG divides by the count of non-NULL values rather than the row count. If missing values should count as zero, write AVG(COALESCE(col, 0)); the two give different answers and knowing which the question wants is the point.
How do I avoid division by zero in SQL?
Wrap the denominator in NULLIF: revenue / NULLIF(order_count, 0). When the count is zero, NULLIF turns it into NULL and the whole expression becomes NULL instead of raising an error. Wrap the result in COALESCE if you would rather show 0.