Why joins matter
Analytics questions often require multiple tables.
Example:
- Orders are in
orders - User attributes are in
users
So we join.
INNER JOIN
Keeps only matching rows.
SELECT
o.order_id,
o.amount,
u.country
FROM orders o
INNER JOIN users u
ON u.user_id = o.user_id;LEFT JOIN
Keeps all rows from the left table (even if no match on the right).
Use case: users with or without orders.
SELECT
u.user_id,
o.order_id,
o.amount
FROM users u
LEFT JOIN orders o
ON o.user_id = u.user_id;Common join pitfalls
1) Duplicated rows
If you join users to events, each user might have many events.
- Metrics can explode if you sum after the join.
Fix:
- Aggregate first, then join.
2) Filters in WHERE vs ON
With LEFT JOIN, putting conditions on the right table in WHERE can turn it into an inner join.
Prefer:
- conditions in the
ONclause when you want to preserve rows.
Visualize it
An INNER JOIN keeps only matching rows, while a LEFT JOIN keeps every row from the left table and fills in NULLs when there's no match.
flowchart LR A["Table A"] --> C["Table A + Table B"] B["Table B"] --> C C --> D["INNER JOIN: only rows where key matches in BOTH"] C --> E["LEFT JOIN: ALL rows from A, NULLs where B has no match"]
Check your understanding
Which join returns all users, including those with no orders?
LEFT JOIN keeps every row from the left table (users) and fills order columns with NULL when there is no match.
Show answer
B — users LEFT JOIN orders — LEFT JOIN keeps every row from the left table (users) and fills order columns with NULL when there is no match.
Joining users to events and then summing a user-level amount inflates the metric. What is this problem and fix?
Each user has many events, so user rows are duplicated per event. Aggregate first, then join.
Show answer
A — A fan-out duplicating rows, fixed by aggregating first and then joining — Each user has many events, so user rows are duplicated per event. Aggregate first, then join.
Why can a WHERE filter on the right table break a LEFT JOIN?
Rows with no match have NULLs on the right, and a WHERE condition on them filters those rows out. Put the condition in ON to preserve rows.
Show answer
A — It removes the NULL rows, turning it into an inner join — Rows with no match have NULLs on the right, and a WHERE condition on them filters those rows out. Put the condition in ON to preserve rows.
What does INNER JOIN keep?
INNER JOIN keeps only matching rows.
Show answer
B — Only rows that match on both sides — INNER JOIN keeps only matching rows.
🧪 Try It Yourself
Exercise 1 – INNER JOIN two tables
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 2 – LEFT JOIN keeps unmatched rows
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 3 – Aggregate before joining to avoid fan-out
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Next
Once joins feel natural, Window Functions (OVER, PARTITION BY) let you rank and total rows
within groups without collapsing them the way GROUP BY does.