NULL: the value that breaks comparisons
Three-valued logic, and why a WHERE clause silently drops rows.
Open this lesson in the learning hubKey points
- NULL means unknown, not zero and not empty string. Any comparison with it yields NULL rather than true or false, so
WHERE x = NULLmatches nothing at all - even rows where x is NULL. - Use
IS NULLandIS NOT NULL. They are the only operators that test for it. - The trap that costs most people a bug:
NOT INwith a subquery containing a single NULL returns no rows, because the comparison is unknown for every candidate. - Aggregates skip NULL.
COUNT(column)counts non-null values whileCOUNT(*)counts rows, andAVGdivides by the non-null count - which quietly changes the answer. - NULL sorts first ascending in MySQL, and it is treated as equal for
GROUP BYandDISTINCTeven though it is not equal to itself. - Use
COALESCEto substitute a default, andNULLIFto turn a sentinel such as an empty string back into NULL.
Example
-- Matches nothing, including rows where deleted_at IS NULL.
SELECT * FROM orders WHERE deleted_at = NULL; -- always empty
SELECT * FROM orders WHERE deleted_at IS NULL; -- correct
-- THE NOT IN TRAP. If ANY value in the subquery is NULL, this returns
-- zero rows - not "all rows that do not match".
SELECT * FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders); -- empty if any is NULL
-- Safe alternatives:
SELECT * FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
SELECT * FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL);
-- Aggregates ignore NULL, which changes the denominator:
SELECT COUNT(*) AS rows_total, -- 100
COUNT(rating) AS rated, -- 60 (40 are NULL)
AVG(rating) AS avg_of_rated, -- divided by 60, not 100
AVG(COALESCE(rating, 0)) AS avg_treating_null_as_zero
FROM reviews;
-- Substituting and producing NULL:
SELECT COALESCE(nickname, first_name, 'Anonymous') AS display_name,
NULLIF(TRIM(phone), '') AS phone -- empty string becomes NULL
FROM users;
-- NULL-safe equality: <=> treats NULL = NULL as TRUE.
SELECT * FROM a JOIN b ON a.code <=> b.code; -- matches NULL to NULL
NULL is unknown, so every comparison with it is unknown - and NOT IN over a subquery containing NULL returns nothing.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the MySQL Course course, and every lesson in it is listed on the MySQL Course contents page.