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 — 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

  1. Clarity of intentNOT 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.