The sample tables
Every example below uses these two small tables. Notice two deliberate edge cases: Carol has no orders, and order 104 belongs to customer 4, who doesn't exist in customers.
| customers.customer_id | name |
|---|---|
| 1 | Alice |
| 2 | Bob |
| 3 | Carol (no orders) |
| orders.order_id | customer_id | amount |
|---|---|---|
| 101 | 1 | 250 |
| 102 | 1 | 90 |
| 103 | 2 | 500 |
| 104 | 4 (no such customer) | 120 |
INNER JOIN — matches only
An INNER JOIN returns a row only when the join key exists in both tables. Unmatched rows on either side are dropped.
SELECT c.name, o.order_id, o.amount
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id;
| name | order_id | amount |
|---|---|---|
| Alice | 101 | 250 |
| Alice | 102 | 90 |
| Bob | 103 | 500 |
Carol (no orders) and order 104 (no customer) are both dropped — they have no match on the other side.
LEFT JOIN — all of the left table
A LEFT JOIN (a.k.a. LEFT OUTER JOIN) keeps every row from the left table (customers) and attaches matching orders. Where there's no match, the order columns come back as NULL.
SELECT c.name, o.order_id, o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;
| name | order_id | amount |
|---|---|---|
| Alice | 101 | 250 |
| Alice | 102 | 90 |
| Bob | 103 | 500 |
| Carol | NULL | NULL |
Carol now appears with NULLs. Order 104 is still gone — it's on the right side, which LEFT JOIN doesn't force-keep.
RIGHT JOIN — all of the right table
A RIGHT JOIN is the mirror: it keeps every row from the right table (orders) and attaches matching customers, with NULL where there's no match.
SELECT c.name, o.order_id, o.amount
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;
| name | order_id | amount |
|---|---|---|
| Alice | 101 | 250 |
| Alice | 102 | 90 |
| Bob | 103 | 500 |
| NULL | 104 | 120 |
Order 104 (the orphan) now appears with a NULL customer. Carol is gone — she's on the left side this time.
LEFT vs RIGHT (and why RIGHT is rare)
LEFT JOIN and RIGHT JOIN are the same operation in opposite directions. Any right join can be rewritten as a left join by swapping the table order — and most teams prefer to, because reading left-to-right matches how we read the query:
-- These two return the same rows:
SELECT * FROM customers c RIGHT JOIN orders o ON c.customer_id = o.customer_id;
SELECT * FROM orders o LEFT JOIN customers c ON o.customer_id = c.customer_id;
Which JOIN should you use?
| You want… | Use |
|---|---|
| Only rows that exist in both tables | INNER JOIN |
| Every row from your main table, matched or not | LEFT JOIN |
| Rows in the main table with no match (e.g. customers with no orders) | LEFT JOIN + WHERE right_key IS NULL |
| Every row from the second table | LEFT JOIN with tables swapped (preferred) or RIGHT JOIN |
LEFT JOIN orders … WHERE o.order_id IS NULL. The NULL row is exactly the "no match" case.The LEFT-JOIN-that-acts-like-INNER gotcha
This is the most common join bug. You write a LEFT JOIN, but a filter on the right table in WHERE silently removes the NULL rows — turning it back into an INNER JOIN:
-- ❌ Carol disappears: WHERE filters her NULL amount away
SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.amount > 100;
-- ✅ Put the condition in ON to keep unmatched left rows
SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id AND o.amount > 100;
Rule: conditions that should filter the final result go in WHERE; conditions that define what counts as a match go in ON. For more on why, see SQL execution order.
Key takeaways
INNER JOIN= rows matched in both tables only.LEFT JOIN= all rows from the left table, with NULLs where the right has no match.RIGHT JOIN= all rows from the right table; it's the mirror of LEFT and rarely needed.- To find unmatched rows, use
LEFT JOIN … WHERE right_key IS NULL. - A right-table filter in
WHEREturns a LEFT JOIN into an INNER JOIN — move it toONto keep unmatched rows.