LEFT, RIGHT, and FULL OUTER JOIN
When you need rows from one side even where the other side has no match — the joins that surface what's missing.
What you'll learn
- When to use LEFT JOIN, RIGHT JOIN, or FULL OUTER JOIN
- How NULLs appear in the result, and how to handle them
- The 'all users with their order count, including zero' pattern
- Why a filter on the optional side belongs in ON, not WHERE
Before you start
INNER JOIN drops rows that do not match on both sides, which is sometimes exactly right. But “all users — and their orders if they have any” is a different question, and it calls for an outer join: one that keeps rows from a side even when the other side has nothing to offer, filling the gaps with NULL.
| Join | Keeps |
|---|---|
LEFT JOIN | all rows from the left table, plus matches from the right (NULLs where none) |
RIGHT JOIN | the mirror — all rows from the right |
FULL OUTER JOIN | all rows from both sides, NULL-filled wherever there is no match |
Shaded = rows kept. Wherever a side has no match, its columns come back as NULL.
LEFT JOIN keeps every left-side row
Recall our three users — Asha and Carlos have orders, Bo has none. An inner join would lose Bo; a left join keeps her, with NULLs where the order would be:
SELECT u.name, u.country, o.id AS order_id, o.product
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
ORDER BY u.name;
| name | country | order_id | product |
|---|---|---|---|
| Asha | IN | 101 | Pencil |
| Asha | IN | 102 | Laptop |
| Bo | CN | NULL | NULL |
| Carlos | US | 103 | Coffee |
Bo survives, and her right-side columns are NULL — the engine’s way of saying “no match here.” That is exactly what powers the include-zeros pattern: counting per group while keeping the empty groups.
SELECT u.name, COUNT(o.id) AS num_orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.name;
| name | num_orders |
|---|---|
| Asha | 2 |
| Bo | 0 |
| Carlos | 1 |
Two details earn their keep here. COUNT(o.id) counts non-NULL order ids, so Bo gets 0 — whereas COUNT(*) would have counted her single result row as 1. And for a sum, COALESCE(SUM(...), 0) turns the NULL that SUM returns over nothing into a clean zero.
The ON-vs-WHERE trap
When you filter the optional (right) side, where you put the filter changes everything. Filter inside ON and the left side is preserved:
... LEFT JOIN orders o ON o.user_id = u.id AND o.product = 'Laptop'
| name | order_id | product |
|---|---|---|
| Asha | 102 | Laptop |
| Bo | NULL | NULL |
| Carlos | NULL | NULL |
Every user stays; only matching Laptop orders attach. But move that same filter to WHERE, and the NULL rows fail the test and vanish — silently collapsing the left join back into an inner join:
... LEFT JOIN orders o ON o.user_id = u.id
WHERE o.product = 'Laptop' -- drops Bo and Carlos!
| name | order_id | product |
|---|---|---|
| Asha | 102 | Laptop |
RIGHT JOIN is just LEFT JOIN with the tables swapped — almost nobody writes it, since you can always put the table you care about on the left. FULL OUTER JOIN keeps everything from both sides and is the tool for reconciliation reports (“every row in the source, every row in the warehouse, and where they line up”).
Practice
Quick check
Practice this in an interview
All questionsA FULL OUTER JOIN combined with IS NULL checks on each side isolates rows that exist in only one table, making it ideal for data reconciliation, pipeline audits, and finding discrepancies between a source and a target table.
INNER JOIN returns only rows where the join condition matches in both tables. LEFT OUTER JOIN returns every row from the left table plus matching rows from the right, filling NULLs where there is no match. FULL OUTER JOIN returns all rows from both sides, with NULLs wherever one side has no match.
A LEFT JOIN produces NULLs for right-table columns when there is no match. Adding a WHERE condition that demands a specific non-NULL value from the right table then eliminates those NULL rows, leaving only matched rows — identical to an INNER JOIN result.
A semi-join returns each row from the left table at most once when at least one match exists on the right, without returning any columns from the right table. An INNER JOIN can duplicate left rows when the right side has multiple matches. In SQL, semi-joins are written with EXISTS or IN subqueries.