Looking for a structured path? Browse all SQL lessons.

Maintained by
Learning Platform content team
Reviewed by
Learning Platform source and executable-example contract

SQL NULL comparisons: why = NULL never matches

NULL represents an unknown or missing value. SQL comparisons with an unknown yield UNKNOWN, not TRUE or FALSE. A WHERE clause keeps only TRUE, so column = NULL returns no rows.

Reproduce and fix it

WITH users(name, nickname) AS (
  VALUES ('Ada', 'ace'), ('Grace', NULL)
)
SELECT name
FROM users
WHERE nickname IS NULL;

Expected output: Grace. Use IS NOT NULL for the opposite check.

Equality when both nullable values should match

PostgreSQL supports a IS NOT DISTINCT FROM b, which treats two nulls as equal. A portable alternative is explicit:

WITH pairs(label, a, b) AS (
  VALUES ('same', 1, 1), ('both-null', NULL, NULL), ('different', 1, 2)
)
SELECT label
FROM pairs
WHERE a = b OR (a IS NULL AND b IS NULL)
ORDER BY label;

Do not replace nulls with an arbitrary sentinel through COALESCE unless that sentinel is impossible in the real data.

The NOT IN trap

SELECT (3 NOT IN (1, 2, NULL)) IS NULL;

This is UNKNOWN, not TRUE, because SQL cannot prove that the unknown value is different from 3. For anti-joins, prefer correlated NOT EXISTS:

WITH products(id) AS (VALUES (1), (2), (3)),
retired(product_id) AS (VALUES (2))
SELECT p.id
FROM products AS p
WHERE NOT EXISTS (
  SELECT 1 FROM retired AS r WHERE r.product_id = p.id
)
ORDER BY p.id;

Aggregates and null

COUNT(column) ignores nulls; COUNT(*) counts rows. Most aggregates ignore null input, while COALESCE(SUM(amount), 0) is useful when an empty result should mean zero in your domain.

Failure mode

Making every nullable field an empty string or zero hides missingness and creates new ambiguity. Model absence deliberately and test filters with both null and non-null fixtures.

Try the examples in the SQL playground, then study NULL Handling and Filtering.

Official references