pbPassingBI
/
Combining data intermediate 7 min

LEFT and RIGHT JOIN

Preserving unmatched rows, finding missing data, and the WHERE trap.

What you'll be able to do
  • Write outer joins
  • Find rows with no match
  • Explain why WHERE can break a LEFT JOIN

LEFT JOIN

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

Every customer appears. Those with no orders get nulls in the order columns.

RIGHT JOIN is the mirror image. It is rarely used in practice — most people reorder the tables and use LEFT, because reading a query is easier when the preserved table comes first.

Finding what is missing

The anti-join pattern:

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

The IS NULL test keeps only rows where the join found nothing — customers who have never ordered. This is one of the most-used patterns in analytics.

The WHERE trap

This is asked in almost every SQL interview

A filter on the right-hand table in WHERE silently converts a LEFT JOIN into an INNER JOIN.

-- silently becomes an INNER JOIN
SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.status = 'shipped';

WHERE is applied after the join. Customers with no orders have o.status = NULL, which fails the test, so they are discarded — defeating the entire purpose of the LEFT JOIN.

Put the condition in ON instead:

LEFT JOIN orders o
  ON o.customer_id = c.customer_id
 AND o.status = 'shipped'

The rule: conditions on the preserved table go in WHERE; conditions on the optional table go in ON. This is asked in almost every SQL interview.

Watch out for counts

SELECT c.name, COUNT(*) FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.name;

A customer with no orders gets 1, not 0 — because COUNT(*) counts the null-extended row. Use COUNT(o.order_id), which skips nulls and correctly returns 0.

Key points
  • LEFT JOIN keeps all rows from the left table
  • A WHERE filter on the right table turns a LEFT JOIN into an INNER JOIN
  • Use COUNT(right.column), not COUNT(*), when counting after a LEFT JOIN
Check yourself