How do you find rows in one table that have no match in another (anti-join)?
Quick Answer
Use a `LEFT JOIN` and filter for `WHERE right_table.key IS NULL` (a classic "left anti-join"), or use `NOT EXISTS` with a correlated subquery — the latter is generally preferred because it's NULL-safe and its intent ("no matching row exists") is more explicit than a `LEFT JOIN`/`IS NULL` combination.
Detailed Answer
An anti-join answers "give me rows from A that have no corresponding row in B" — e.g., customers who have never placed an order.
Approach 1: LEFT JOIN + IS NULL
SELECT c.*
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
The LEFT JOIN keeps every customer even without a match, filling unmatched columns with NULL; filtering WHERE o.id IS NULL then keeps only the customers where no match occurred.
Approach 2: NOT EXISTS (generally preferred)
SELECT c.*
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
Why NOT EXISTS is usually the better choice
- Clarity of intent —
NOT EXISTSdirectly states "no matching row exists";LEFT JOIN ... IS NULLrequires the reader to infer the anti-join pattern from a filter condition on an outer-joined column. - NULL-safety — if you instead reached for
NOT IN (SELECT customer_id FROM orders), a singleNULLinorders.customer_idsilently breaks the whole query (see the NULL/three-valued-logic question);NOT EXISTShas no such trap. - No risk of picking the wrong column to check — with
LEFT JOIN ... IS NULL, you must check a column that's guaranteedNOT NULLon the right side (like its primary key); accidentally checking a nullable right-side column produces wrong results when a match exists but that particular column happens to beNULL.
Most modern optimizers (PostgreSQL, SQL Server) recognize both patterns and produce the same anti-join execution plan (typically a hash anti-join or nested-loop anti-join), so performance is usually a non-issue — the choice comes down to correctness and readability, and NOT EXISTS wins on both.