What NULL actually means
NULL is not a value — it's the absence of one. It means "we don't know" or "not applicable." That's fundamentally different from an empty string '' (a known, zero-length text) or 0 (a known number). Because it represents the unknown, NULL refuses to behave like a normal value in comparisons.
NULL as the word "unknown." "Is unknown equal to 5?" — nobody can say, so the answer is UNKNOWN, not yes or no.Why = NULL never works (three-valued logic)
Most languages have two boolean values. SQL has three: TRUE, FALSE, and UNKNOWN. Any comparison involving NULL produces UNKNOWN:
| Expression | Result |
|---|---|
NULL = NULL | UNKNOWN |
NULL = 5 | UNKNOWN |
NULL <> 5 | UNKNOWN |
NULL + 10 | NULL |
'' = NULL | UNKNOWN |
A WHERE clause keeps a row only when its condition is TRUE. Since col = NULL is UNKNOWN (never TRUE), it matches nothing — no error, just zero rows.
-- ❌ Returns 0 rows, even if some phones really are NULL
SELECT * FROM users WHERE phone = NULL;
IS NULL / IS NOT NULL — the right way
IS NULL and IS NOT NULL are the only correct way to test for NULLs. They return TRUE/FALSE (never UNKNOWN), so they actually filter rows.
-- ✅ Rows with a missing phone
SELECT * FROM users WHERE phone IS NULL;
-- ✅ Rows that do have a phone
SELECT * FROM users WHERE phone IS NOT NULL;
WHERE status <> 'active' silently drops rows where status is NULL, because NULL <> 'active' is UNKNOWN. If you want those rows too, write WHERE status <> 'active' OR status IS NULL.The NOT IN + NULL trap
This one bites even experienced developers. If the list or subquery inside NOT IN contains a single NULL, the entire expression becomes UNKNOWN for every row — so you get zero rows back.
-- ❌ Returns nothing if any manager_id is NULL
SELECT name FROM employees
WHERE id NOT IN (SELECT manager_id FROM employees);
-- ✅ Fix A: NOT EXISTS is NULL-safe
SELECT e.name FROM employees e
WHERE NOT EXISTS (
SELECT 1 FROM employees m WHERE m.manager_id = e.id
);
-- ✅ Fix B: exclude NULLs from the NOT IN list
SELECT name FROM employees
WHERE id NOT IN (
SELECT manager_id FROM employees WHERE manager_id IS NOT NULL
);
How aggregates treat NULL
Aggregate functions ignore NULLs — which is usually what you want, but it changes the math:
SUM,MIN,MAXskip NULLs.AVGdivides by the number of non-NULL values, not the row count — so NULLs don't drag the average toward zero.COUNT(column)skips NULLs;COUNT(*)counts every row. (Full breakdown in COUNT(*) vs COUNT(column).)
AVG(COALESCE(score, 0)) averages over all rows treating missing scores as 0.NULL in GROUP BY, DISTINCT & ORDER BY
For grouping and de-duplication, SQL makes a pragmatic exception: it treats all NULLs as "the same" so they don't scatter.
GROUP BYputs all NULLs into a single group.DISTINCTkeeps just one NULL.ORDER BYsorts NULLs together, but where they land differs by database. PostgreSQL and Oracle treat NULLs as largest (last in ascending); MySQL and SQL Server put them first. Be explicit withNULLS FIRST/NULLS LASTwhere supported.
-- PostgreSQL / Oracle: control NULL placement explicitly
SELECT name, phone FROM users
ORDER BY phone ASC NULLS LAST;
Replacing NULLs: COALESCE, IFNULL, NULLIF
To substitute a default for missing values, use COALESCE — it's standard SQL and returns the first non-NULL argument.
-- Show 'N/A' when phone is missing
SELECT name, COALESCE(phone, 'N/A') AS phone
FROM users;
-- Vendor-specific equivalents (same idea)
-- MySQL: IFNULL(phone, 'N/A')
-- SQL Server: ISNULL(phone, 'N/A')
-- Oracle: NVL(phone, 'N/A')
-- NULLIF does the reverse: return NULL when two values match
SELECT NULLIF(discount, 0) AS discount FROM orders; -- 0 becomes NULL
COALESCE over the vendor functions for portable code — it works everywhere and accepts more than two arguments: COALESCE(mobile, home, work, 'N/A').NULL cheat sheet
| Goal | Do this |
|---|---|
| Test if a value is NULL | col IS NULL |
| Test if a value exists | col IS NOT NULL |
| Exclude a value and keep NULLs | col <> x OR col IS NULL |
| Safe "not in a set" | NOT EXISTS (…) |
| Default for missing values | COALESCE(col, default) |
| Turn a value into NULL | NULLIF(col, x) |
Key takeaways
NULLmeans unknown — not zero, not empty string, and not equal to another NULL.- Any comparison with NULL is
UNKNOWN, so= NULLmatches nothing. UseIS NULL/IS NOT NULL. <>conditions silently drop NULL rows — addOR col IS NULLif you want them.NOT INwith any NULL returns zero rows — useNOT EXISTSinstead.- Aggregates ignore NULLs; replace them with
COALESCEwhen you need a default.