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, which 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.