Intermediate: combining and changing data · JOINs · topic 23 of 62 · 35 min
Joining three or more tables
Explanation
Following the keys
The question "how much has each customer spent on each category?" touches four tables. The customer's name is in customers, the order status in orders, the quantity and price paid in order_items, and the category in products. Before writing SQL, draw the path between them using the foreign keys.
customers.id <-- orders.customer_id
orders.id <-- order_items.order_id
products.id <-- order_items.product_idA query with many joins is just this path written down. Each JOIN adds one table and one ON rule that connects it to a table already in the query.
Build it step by step
Do not write a five-table query in one go. Add one table, run it, look at the rows, and only then add the next. If the row count jumps unexpectedly, you know which join caused it.
-- Step 1: orders with customer names (8 rows)
SELECT o.id, c.name
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id;
-- Step 2: add the order lines (12 rows: one per line now)
SELECT o.id, c.name, oi.product_id, oi.qty, oi.price
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
JOIN order_items AS oi ON oi.order_id = o.id;
-- Step 3: add product names and categories (still 12 rows)
SELECT o.id, c.name, p.name AS product, p.category, oi.qty * oi.price AS line_total
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
JOIN order_items AS oi ON oi.order_id = o.id
JOIN products AS p ON p.id = oi.product_id;The grain of a result means "what one row stands for". After step 1 one row is one order; after step 2 it is one order line. Always know the grain, because it decides what SUM and COUNT will mean.
Revenue per customer
Now add WHERE and GROUP BY exactly as in module 4. The joins run first (they are part of FROM), then WHERE filters the joined rows, then GROUP BY makes one row per group.
SELECT c.name, SUM(oi.qty * oi.price) AS revenue
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
JOIN order_items AS oi ON oi.order_id = o.id
WHERE o.status = 'Delivered'
GROUP BY c.id, c.name
ORDER BY revenue DESC;
-- Ravi Kumar | 6497
-- Asha Verma | 4028Revenue per category
SELECT p.category, SUM(oi.qty * oi.price) AS revenue
FROM order_items AS oi
JOIN orders AS o ON o.id = oi.order_id
JOIN products AS p ON p.id = oi.product_id
WHERE o.status <> 'Cancelled'
GROUP BY p.category
ORDER BY revenue DESC;
-- Fashion | 5196
-- Electronics | 3998
-- Grocery | 2750
-- Kitchen | 1900Does the order of JOINs matter?
For inner joins, no: the database's query planner (the part that decides how to run a query) reorders them for speed, and the result is the same. With LEFT JOINs the order does matter, because "keep every row of the left side" depends on what is on the left. A common trap: customers LEFT JOIN orders, then INNER JOIN order_items. The inner join drops the NULL rows of customers without orders, so Arjun disappears again. Once you start a LEFT JOIN chain, keep the following joins LEFT too.
Common mistakes
- Skipping a table on the path, such as joining customers straight to order_items. There is no shared key; you must go through orders.
- Losing track of the grain and counting order lines when you meant orders. Use COUNT(DISTINCT o.id) when the grain is lines.
- Mixing a LEFT JOIN with a later INNER JOIN and losing the unmatched rows. Make the later joins LEFT JOINs too.
- Writing all the joins at once and not knowing which one is wrong. Build step by step and check the row count.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Joining three or more tables · 1/6
Real questions often need three or four tables. Joins chain together, one table at a time.
Prefer a video by a teacher? Find free videos on “sql join multiple tables three tables group by tutorial” on YouTube ↗
In real life: A category sales report for a merchandising team
An online store's category managers look at revenue by category every week to decide which products to promote. The report is a join from order lines to orders (for the date and status) and products (for the category), grouped by category. Many teams put such a query behind a dashboard tool, so it runs fresh each morning.
SELECT p.category, COUNT(DISTINCT o.id) AS orders, SUM(oi.qty) AS units
FROM order_items AS oi
JOIN orders AS o ON o.id = oi.order_id
JOIN products AS p ON p.id = oi.product_id
WHERE o.status <> 'Cancelled'
GROUP BY p.category;Practice (0/4 done)
How much has each customer spent on Delivered orders? Return the customer name and revenue (the sum of qty * price from order_items), highest revenue first.
Which product categories has each customer bought (any order status)? Return each customer name and category pair once, sorted by name and then category.
For every order, return the order id, the customer name and items (the total quantity of all its lines).
Revenue per category, ignoring Cancelled orders. Return category and revenue (sum of qty * price from order_items), highest revenue first.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Self joins and cross joinsJOINs · same module
- Joins: combining tablesBackend Development · SQL in depth
- Project: e-commerce analytics, and your next stepsInterview questions and practice projects
- GROUP BYAggregation: GROUP BY and HAVING
- Design project: an online storeSchema design and normalisation
- Conditional aggregation and string aggregationAggregation: GROUP BY and HAVING
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
To get each customer's product categories in SHOP, which tables must the query pass through?
Answer: customers, orders, order_items and products
Keys connect customers → orders → order_items → products; you cannot skip a link.
orders JOIN order_items: what does one row of the result stand for?
Answer: One order line
The grain becomes the more detailed table, order_items.
customers LEFT JOIN orders ... JOIN order_items (an inner join). What happens to Arjun, who has no orders?
Answer: He is dropped by the inner join, because his NULL order id matches no order line
A later inner join removes the unmatched rows the LEFT JOIN kept. Use LEFT JOIN for the later joins too.
Why use order_items.price rather than products.price for revenue?
Answer: order_items stores the price actually paid, which does not change when the product price changes later
History must keep the amount charged at the time.
Why build a big join one table at a time?
Answer: So you can check the row count after each join and spot the one that goes wrong
Small steps make mistakes easy to locate.
Customers has 7 rows; 2 of them have no orders. What does an INNER JOIN of customers with orders do with those 2 customers?Related
Answer: Leaves them out
INNER JOIN keeps only rows that have a match on both sides. A LEFT JOIN would keep them with NULLs.
Which is a good check after building revenue per month?Related
Answer: The months add up to the total revenue with the same WHERE
Groups must add up to the total worked out separately.
After customers LEFT JOIN orders ... GROUP BY customer, which gives 0 for a customer with no orders?Related
Answer: COUNT(o.id)
COUNT(column) skips NULLs; the unmatched row has o.id NULL.
Why is the status filter put in the ON clause of the LEFT JOIN in the cohort query?Related
Answer: So customers with no matching orders are kept instead of being filtered out
A WHERE condition on the right-hand table removes the NULL rows and turns the LEFT JOIN into an inner join.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “JOINs” before its exam: Self joins and cross joins, Join pitfalls and how to avoid them.
- Module 5 exam: JOINs · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Subqueries, CTEs and set operations. 62 topics left in the course before the final exam.