datarekha

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.

6 min read Beginner SQL Lesson 8 of 27

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.

JoinKeeps
LEFT JOINall rows from the left table, plus matches from the right (NULLs where none)
RIGHT JOINthe mirror — all rows from the right
FULL OUTER JOINall rows from both sides, NULL-filled wherever there is no match
ABLEFT JOINall of A + matchesABRIGHT JOINall of B + matchesABFULL OUTERall of both

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;
namecountryorder_idproduct
AshaIN101Pencil
AshaIN102Laptop
BoCNNULLNULL
CarlosUS103Coffee

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;
namenum_orders
Asha2
Bo0
Carlos1

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'
nameorder_idproduct
Asha102Laptop
BoNULLNULL
CarlosNULLNULL

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!
nameorder_idproduct
Asha102Laptop

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

0/3
Q1You want every user with their order count (zero for users who never ordered). Correct query?
Q2What happens with LEFT JOIN orders o ON … WHERE o.status = 'paid'?
Q3What does FULL OUTER JOIN return for a row that matches on neither side?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions

Related lessons

Explore further

Skip to content