NULL, and the third truth value
NULL is not a value. It is the absence of one, and every comparison it touches returns neither true nor false. Half of all real SQL bugs live here.
14 min · runs against the shop dataset
SQL has three truth values: true, false, and unknown. Any comparison involving NULL produces unknown, and a WHERE clause keeps a row only when its predicate is true. Unknown is not a reason to keep a row, and it is not a reason to drop it either — it is simply not true, and that is enough for the row to disappear.
Two nulls are not equal. They are not unequal either.
| null_equals_nullboolean | null_differsboolean | actually_unknownboolean |
|---|---|---|
| null | null | t |
1 row · 0 examined · 0 pages read
A predicate and its opposite do not cover the table
This is the consequence people are surprised by. If a column is nullable, WHERE total > 100 and WHERE total <= 100 do not partition the table between them — the rows where total is null fall out of both.
The two halves do not add up to the whole.
| bigbigint | smallbigint | all_rowsbigint |
|---|---|---|
| 44 | 38 | 90 |
1 row · 90 examined · 1 page read
NOT IN is the trap worth memorising
x IN (1, 2, NULL) is true for 1, and unknown — not false — for 3, because SQL cannot rule out that the null was a 3. NOT IN is therefore never true the moment the list contains a null, and the query returns nothing at all however obviously the value is absent.
run it
| in_is_unknownboolean | not_in_is_fineboolean |
|---|---|
| t | t |
1 row · 0 examined · 0 pages read
The fix is not to remember this at the moment you write it, because you will not. The fix is NOT EXISTS, which asks a question that has an answer whatever the nulls are doing.
The operator that will compare a null
IS DISTINCT FROM is the null-safe comparison: two nulls are not distinct, and a null and a value are. It is what = refuses to say, and it is why it exists.
run it
| sameboolean | differentboolean |
|---|---|
| t | t |
1 row · 0 examined · 0 pages read
Where grouping disagrees with equality
GROUP BY, DISTINCT and the set operations all treat two nulls as the same value, which = flatly refuses to do. Grouping and equality are different questions, and this is the clearest place that shows.
Many null rows, one group.
| totalnumeric(10,2) |
|---|
| null |
1 row · 90 examined · 1 page read
Did it land?
- The orders nobody priced
Return the ids of every order with no total, in ascending order.
- Everyone who is not signed up
Return the names of every customer who is not signed up to the newsletter — including the ones who never answered. Order by name.