NULL and missing values
Real data has holes. SQL marks a hole with NULL, and NULL follows rules of its own: most surprises in a query that "looks right" come from them.
What NULL means
NULL is not a value. It is the mark SQL puts in a cell when there is no value: the fact is unknown, or does not apply. It is not zero, which is a number, and it is not the empty string '', which is a piece of text with no letters in it. A price of 0 says "free"; a NULL price says "nobody wrote the price down".
In the bakery database (see the bakery database page), customer.referred_by holds the id of the customer who recommended the shop. Seven customers walked in on their own, so their referred_by is NULL. In the same way staff.manager_id holds the manager of each member of staff, and Margaret Hale, who owns the shop, answers to nobody: her manager_id is NULL. The columns are not wrong for them; there is simply nothing to put in them. Every rule on this page follows from recording an absence that way.
Three answers, not two: true, false and unknown
A comparison usually answers true or false. When one side is NULL, it answers unknown: is a missing manager equal to Tom Becker, staff member 2? Nobody can say. SQL's logic therefore has three values, and the rule that matters most is short: WHERE, HAVING and ON keep a row only when the condition is true. Unknown is thrown away, exactly like false.
The query below asks for the staff not managed by Tom Becker. Four of the six answer to him, so it looks as if it should return the other two, Tom himself and Margaret Hale, but Margaret is missing: for her manager_id <> 2 is unknown.
SELECT name, manager_id
FROM staff
WHERE manager_id <> 2
ORDER BY id;
| name | manager_id |
|---|---|
| Tom Becker | 1 |
If the owner belongs in the answer, say so: WHERE manager_id <> 2 OR manager_id IS NULL. A CASE makes the three outcomes visible. Its first branch catches true, its second catches false, and whatever is left over is unknown.
SELECT name,
CASE
WHEN manager_id = 2 THEN 'yes'
WHEN NOT (manager_id = 2) THEN 'no'
ELSE 'unknown'
END AS managed_by_tom
FROM staff
ORDER BY id;
| name | managed_by_tom |
|---|---|
| Margaret Hale | unknown |
| Tom Becker | no |
| Nina Patel | yes |
| Omar Reyes | yes |
| Lucy Brandt | yes |
| Sam Ito | yes |
Notice that NOT does not rescue the owner: the opposite of unknown is still unknown. AND and OR follow common sense once you read unknown as "could be either":
| a | b | a AND b | a OR b |
|---|---|---|---|
| true | unknown | unknown | true |
| false | unknown | false | unknown |
| unknown | unknown | unknown | unknown |
False AND anything is false, whatever the unknown turns out to be; true OR anything is true. Every other mix stays unknown. Arithmetic behaves the same way: price + NULL is NULL, and so is 'Mr ' || NULL.
Asking about NULL: IS NULL and IS NOT NULL
Because a comparison with NULL is never true, referred_by = NULL keeps no rows at all, even on the rows where the column is NULL. SQL has a dedicated test instead: IS NULL, and its opposite IS NOT NULL. These two always answer true or false, never unknown, so they are safe anywhere a condition goes. Here they find the walk-ins who left an email (customer.email is NULL for those who gave none).
SELECT name, town
FROM customer
WHERE referred_by IS NULL
AND email IS NOT NULL
ORDER BY id;
| name | town |
|---|---|
| Alice Turner | Riverside |
| Stefan Lambert | Old Town |
| Zoe Garcia | Riverside |
Aggregates skip NULL
COUNT(*) counts rows. COUNT(column) counts the rows where that column is not NULL. SUM, AVG, MIN and MAX also ignore NULLs: an average is taken over the known values only, not over all rows with the unknown ones treated as zero. One query shows the difference.
SELECT COUNT(*) AS customers,
COUNT(referred_by) AS referred,
COUNT(DISTINCT referred_by) AS referrers
FROM customer;
| customers | referred | referrers |
|---|---|---|
| 40 | 33 | 14 |
Forty customers, 33 of whom were referred, by 14 different people (Chloe Martin alone referred six). Each column answers a different question, and picking the wrong COUNT gives a plausible number that answers the wrong one.
An aggregate over no rows at all is a special case. COUNT of nothing is 0, but SUM, AVG, MIN and MAX of nothing are NULL: there is no total of an empty list. The Pistachio financier, product 19, was never ordered, so the sum of its quantities is NULL, not 0.
SELECT COUNT(*) AS lines, SUM(quantity) AS pieces
FROM order_item
WHERE product_id = 19;
| lines | pieces |
|---|---|
| 0 | NULL |
Replacing NULL: COALESCE
COALESCE(a, b, ...) returns the first of its arguments that is not NULL. It is the standard way to show a default instead of a hole, and it works in every database (IFNULL is not standard SQL; see Grammar: COALESCE and IS). Its most common use is turning the NULL sum of an empty group back into a zero. Here each cake is joined to its order lines with a LEFT JOIN, so the Pistachio financier, which nobody bought, stays in the result with NULL quantities, and COALESCE reports 0 pieces.
SELECT p.name, COALESCE(SUM(oi.quantity), 0) AS pieces
FROM product AS p
LEFT JOIN order_item AS oi ON oi.product_id = p.id
WHERE p.category = 'cake'
GROUP BY p.id, p.name
ORDER BY p.id;
| name | pieces |
|---|---|
| Lemon tart | 58 |
| Chocolate eclair | 52 |
| Carrot cake slice | 46 |
| Cheesecake slice | 39 |
| Raspberry tart | 51 |
| Coffee eclair | 51 |
| Pistachio financier | 0 |
Use COALESCE for display, or where a NULL really means zero. Do not use it to hide NULLs that mean "unknown": replacing an unknown price by 0 before averaging pulls the average down with prices nobody ever charged. The reverse tool exists too: NULLIF(a, b) gives NULL when a equals b, which is how you avoid dividing by zero (total / NULLIF(orders, 0) is NULL instead of an error: PostgreSQL raises an error on division by zero, while SQLite already returns NULL).
Grammar: COALESCE and IS
The standard way to replace a NULL is COALESCE, as above; IFNULL is not standard SQL. A query with a red edge is one the game's terminal refuses: the answer under it is the terminal's own.
SELECT name, IFNULL(referred_by, 0) AS referrer
FROM customer;
ERROR: function ifnull(integer, integer) does not exist ifnull() is not standard SQL: use COALESCE(a, b) instead.IS goes only with NULL, TRUE or FALSE. To compare with a value, write =; when two NULLs must count as equal, write IS NOT DISTINCT FROM.
SELECT name
FROM customer
WHERE referred_by IS 1;
ERROR: syntax error at or near "1" IS only goes with NULL, TRUE or FALSE: write = 1 to compare with 1, or IS NOT DISTINCT FROM 1 if NULLs must match too.SELECT name
FROM customer
WHERE referred_by = 1
ORDER BY id;
| name |
|---|
| Ben Okafor |
| Chloe Martin |
| Vera Bianchi |
NULL in joins
A join condition is a condition like any other, so a NULL key never matches anything, not even another NULL. Joining each member of staff to their manager with an inner join silently drops the owner: five rows come back, not six.
SELECT s.name, m.name AS manager
FROM staff AS s
JOIN staff AS m ON m.id = s.manager_id
ORDER BY s.id;
| name | manager |
|---|---|
| Tom Becker | Margaret Hale |
| Nina Patel | Tom Becker |
| Omar Reyes | Tom Becker |
| Lucy Brandt | Tom Becker |
| Sam Ito | Tom Becker |
A LEFT JOIN keeps the unmatched rows and fills the right-hand columns with NULL, which brings a trap of its own when you count. With a left join, COUNT(*) counts the padded row too, so a customer with no order still counts one; COUNT(o.id) counts only real orders. Compare the two columns for Hugo Silva, Greta Rossi and Marco Costa, who never ordered.
SELECT c.name, COUNT(*) AS joined_rows, COUNT(o.id) AS orders
FROM customer AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE c.town = 'Hillcrest'
GROUP BY c.id, c.name
ORDER BY c.id;
| name | joined_rows | orders |
|---|---|---|
| Ben Okafor | 14 | 14 |
| Emma Novak | 17 | 17 |
| Hugo Silva | 1 | 0 |
| Daniel Moreno | 16 | 16 |
| Kofi Tanaka | 6 | 6 |
| Ugo Dubois | 2 | 2 |
| Zoe Garcia | 2 | 2 |
| Nadia Jensen | 4 | 4 |
| Jonas Eriksen | 9 | 9 |
| Greta Rossi | 1 | 0 |
| Lena Dubois | 4 | 4 |
| Marco Costa | 1 | 0 |
The NOT IN trap
Which members of staff manage nobody? A natural first attempt asks for the staff whose id is not in the list of managers. It returns nothing at all.
SELECT name
FROM staff
WHERE id NOT IN (SELECT manager_id FROM staff)
ORDER BY id;
| name |
|---|
The list produced by the subquery contains NULL (the owner's empty manager_id). x NOT IN (1, 2, NULL) means x <> 1 AND x <> 2 AND x <> NULL, and the last part is unknown for every row, so no row is ever true. IN is not affected in the same way (a match is still a match), which makes the bug hard to see: the query is correct until one NULL appears in the data. Two fixes, both worth knowing: remove the NULLs from the list, or use NOT EXISTS (see Subqueries), which only asks whether a matching row exists and is never fooled by NULL.
SELECT name
FROM staff
WHERE id NOT IN (SELECT manager_id FROM staff WHERE manager_id IS NOT NULL)
ORDER BY id;
| name |
|---|
| Nina Patel |
| Omar Reyes |
| Lucy Brandt |
| Sam Ito |
NULL in sorting and grouping
For GROUP BY and DISTINCT, all NULLs are treated as one value, so the seven walk-ins form a single group. For sorting, databases disagree: SQLite, which runs these examples, puts NULLs first in ascending order, while PostgreSQL puts them last. When it matters, do not rely on either: say so with NULLS FIRST or NULLS LAST. Here HAVING keeps the referrers who brought at least three customers, and the group of walk-ins comes last.
SELECT referred_by, COUNT(*) AS customers
FROM customer
GROUP BY referred_by
HAVING COUNT(*) >= 3
ORDER BY referred_by NULLS LAST;
| referred_by | customers |
|---|---|
| 1 | 3 |
| 3 | 6 |
| 7 | 5 |
| 10 | 3 |
| 15 | 3 |
| NULL | 7 |
Common mistakes
Writing = NULL or <> NULL instead of IS NULL or IS NOT NULL. Forgetting that <> and NOT also drop the NULL rows. Using COUNT(*) after a left join and counting the padded rows. Using NOT IN on a subquery whose column can be NULL. Expecting SUM of nothing to be 0. Replacing an unknown value with a made-up one through COALESCE before a calculation. Storing the text 'none' or 'N/A' instead of NULL, which makes IS NULL miss the rows and COUNT(column) count them.
Try it: Back again, Untidy emails and Money back, on the exercises page.
In the game: the archives record a missing value as NULL, like the bakery; Chapter X, Never Seen Together, and Chapter XI, The Silence, are where the rules of this page matter most.