Joins that keep the right rows

A join is a question about rows: which ones survive, and what fills the gaps when there’s no match.

The idea

Two tables, one key. When you join customers to orders, the join type decides the fate of a customer who never ordered. INNER throws them away — only matched rows survive. LEFT keeps every customer and pads the missing order columns with NULL. And an anti-join flips it around to keep only the customers with no match — the “who signed up but never bought” question.

Pick the wrong join and rows quietly appear or vanish. So the real skill is reading a question — “including the ones who never ordered” — and knowing which join keeps them.

join lab · customers × orders

customers

idname
c1Ada
c2Ben
c3Cleo
c4Dan

orders

customer_idproduct
c1Book
c1Pen
c2Book
c4Lamp

Note: Cleo (c3) appears in customers but nowhere in orders — she never ordered. Ada (c1) placed two orders.

customers orders joined on customer id

stays in the result dropped

result

c.nameo.productstatus
rows kept
0
rows dropped
0
stage
ready
Choose a join type, then press play to watch each customer try to pair with an order.

How it works

Read a join as a loop. Walk the left table (customers) row by row and try to match it against the right table (orders) on the key. What you do with a customer who finds no match is the whole difference:

for each customer c:
    matches = orders where o.customer_id = c.id
    if matches:  emit one row per match   (Ada -> Book, Ada -> Pen, ...)
    else:        INNER  -> drop c        (Cleo disappears)
                 LEFT   -> emit c, o.* = NULL   (Cleo, product NULL)
                 ANTI   -> keep c, drop the rest (only Cleo survives)

The anti-join is just a LEFT join with a filter that reads “the match failed”:

SELECT c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.customer_id IS NULL;   -- only the no-match rows remain

A self-join is the same machinery pointed at one table twice — to attach each employee to their manager, join employees to employees. Use LEFT so the CEO (whose manager_id is NULL) isn’t dropped:

SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

When to use it

Reach for…When the question is…Watch the trade-off
INNER JOIN“orders and the customer who placed them” — both sides must exist.Silently hides anyone with no match.
LEFT JOIN“every customer, including those who never ordered.”Padding rows carry NULLs — COUNT and SUM must handle them.
Anti-join (LEFT … IS NULL / NOT EXISTS)“customers who did A but never B.”Returns only the gaps — the right table’s columns are all NULL.

Watch out for

Worked example

An interviewer asks: “How many of our customers have never placed an order?” The word never is the tell — it’s an anti-join. Start from customers (the side you must keep all of), LEFT JOIN orders on the customer id, and keep only the rows where the order side came back NULL:

SELECT COUNT(*)
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.customer_id IS NULL;   -- answer: 1  (Cleo)

If instead they ask “total spend per customer, including those who’ve spent nothing,” that’s a LEFT JOIN with a NULL-safe total: COALESCE(SUM(o.amount), 0), grouped by customer — so the never-ordered customers land at 0 instead of vanishing. Naming which join keeps the non-matching rows is the answer the interviewer is listening for.

Check yourself

You need a report of every customer with their order count — and customers who’ve never ordered must still appear, showing 0. Which join?

A teammate writes LEFT JOIN orders o ON o.customer_id = c.id WHERE o.product = 'Book' and is surprised the never-ordered customers vanished. What happened?