29 INNER and LEFT JOIN
Joins combine rows from two (or more) tables based on a matching condition. They are how relational data is rebuilt into the shape your application needs. INNER JOIN keeps only rows where the condition matches on both sides; LEFT JOIN (also called LEFT OUTER JOIN) keeps every row from the left side and fills in NULL where the right side has no match. Mastering joins is mastering SQL.
What you’ll learn
INNER JOINwithON: keep matched rows onlyLEFT JOIN: keep everything on the left, NULL on the right when unmatched- Table aliases (
c,o) for readability - Filtering on the JOIN target vs. in the
WHEREclause USING (col)as a shorthand forON a.col = b.col
Example
The ON clause is the join condition. It can be any boolean expression, though usually it is parent.id = child.parent_id. Conditions in ON are part of the join itself — for a LEFT JOIN, a row from the left side survives even if the right side has no matching row that satisfies ON. Conditions in WHERE are applied after the join, so a WHERE child.col = 'x' on a LEFT JOIN filters out unmatched (NULL) rows — turning the join effectively into an INNER JOIN.
USING (col) is shorthand when the columns are named the same on both sides: USING (customer_id) is equivalent to ON a.customer_id = b.customer_id, and the resulting row has just one customer_id column instead of two.
Order 3 has customer_id = 99 (no customer with that id). INNER JOIN drops it; LEFT JOIN keeps customers without orders (Bob, Carol). The two together give the full picture.
Run the example
To run:
Common pitfalls
WHERE right_table.col = 'x'on aLEFT JOINaccidentally drops the unmatched rows you wanted to keep. Put the condition inONif it should be part of the match, inWHEREif you want to filter after the join.- Joining on a column without an index: the planner falls back to a hash join or merge join that may scan the entire table. Index your foreign-key columns.
- Cartesian products:
CROSS JOINor a comma-separatedFROMlist without a connecting predicate producesm × nrows. A plainINNER JOINrequiresONorUSING; PostgreSQL does not warn about a valid cross join. LEFT JOIN ... GROUP BYis the natural shape for “count of children per parent, including parents with zero.” Usecount(child.id)(notcount(*)) so unmatched rows yield 0.
Pick short, meaningful aliases. customers_example c and orders_example o reads better than customers_example cs1 and orders_example o1. Many style guides recommend a 1-3 character alias derived from the table name.
Change INNER JOIN to LEFT JOIN and see Carol appear with NULLs. Try the two filter variants above and confirm only one keeps unmatched customers. Then use USING: SELECT * FROM customers_example c JOIN orders_example o ON o.customer_id = c.id; becomes SELECT * FROM customers_example c JOIN orders_example o USING (id); — wait, the columns are named differently here, so you would need to rename one first. Try with two tables that share a column name.
Downloads
SQL file: inner-and-left-join.sql · All lesson files · Setup guide