How do you find rows in one table that have no match in another (anti-join)?

4 minintermediatejoinsanti-joinleft-joinnot-exists

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

  1. Clarity of intent. NOT EXISTS directly states "no matching row exists." LEFT JOIN ... IS NULL requires the reader to infer the anti-join pattern from a filter condition on an outer-joined column.
  2. NULL-safety. If you instead reached for NOT IN (SELECT customer_id FROM orders), a single NULL in orders.customer_id silently breaks the whole query (see the NULL/three-valued-logic question). NOT EXISTS has no such trap.
  3. No risk of picking the wrong column to check. With LEFT JOIN ... IS NULL, you must check a column that's guaranteed NOT NULL on 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 be NULL.

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.