LEFT JOIN & Finding What's Missing (Anti-Joins)

SQL & Querying
28 / 47

LEFT JOIN keeps every row from the left table even when nothing matches (the right-hand columns come back NULL). Keeping only those NULL rows answers 'which ones have none?'.

The Analogy (Read This First)

A class roster and a pile of homework. An inner join lists the students who handed something in. A LEFT JOIN starts from the full roster, so students with no homework still appear - with a blank next to their name. Keep only the blanks and you have the list of who did not submit.

Deep Dive Analysis

LEFT JOIN returns every row from the left table. Where no right-hand row matches, the right-hand columns are NULL:

SELECT c.name, o.id AS order_id
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id;

The anti-join pattern. To find left rows that have no match at all, LEFT JOIN and then keep only the rows where a right-hand column that is never NULL in a real match (its primary key) is NULL:

SELECT c.name
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.id IS NULL;

Read it as: 'customers, with their orders if any; keep the ones where the order is missing' - customers who never ordered. The same shape answers 'accounts with no sessions', 'rooms with no bookings' and 'tickets nobody replied to'.

Where you put the filter matters. A condition on the right-hand table in WHERE (WHERE o.status = 'paid') turns the LEFT JOIN back into an inner join, because the NULL rows fail the test. To get 'every customer, with only their paid orders', put the condition in ON instead: LEFT JOIN orders AS o ON o.customer_id = c.id AND o.status = 'paid'.

Equivalent forms. WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id) says the same thing. WHERE c.id NOT IN (SELECT customer_id FROM orders) looks equivalent but returns nothing at all if the subquery yields even one NULL, so prefer LEFT JOIN ... IS NULL or NOT EXISTS.

Counting with a LEFT JOIN. COUNT(o.id) counts only matched orders (0 for customers with none), while COUNT(*) would count the unmatched row itself as 1. Always count a column from the optional side.

Key Bullets
  • LEFT JOIN keeps all left rows; missing matches show up as NULL on the right.
  • 'Rows with no match' = LEFT JOIN, then WHERE right_table.primary_key IS NULL.
  • A WHERE filter on the right table silently converts a LEFT JOIN into an inner join - use ON for that.
  • Prefer LEFT JOIN ... IS NULL or NOT EXISTS over NOT IN (NULLs break NOT IN).
  • COUNT(right.id) gives 0 for unmatched rows; COUNT(*) gives 1.
Real-World Examples
Customers who never orderedEmployees with no badge swipe on a given daySessions that were opened and never closed

Curated Curation & Deep Insights

ScaleDojo Certified
Video Tutorial Pending

Our system architects are vetting high-quality, authorized video guides for LEFT JOIN & Finding What's Missing (Anti-Joins) with zero third-party platform links.

Verification in Progress
Reading Material Pending

We are preparing premium, zero-competitor deep-dives for LEFT JOIN & Finding What's Missing (Anti-Joins). Authorized reading references will appear automatically.

Verification in Progress