Many tables, and a table joined to itself
In short: Several JOINs in a row link a chain of tables, and a self join links a table to itself, for example a category to its parent category.
Problem
Revenue by category
The owner wants to know which categories the money comes from. Quantity and price are in order_items. Whether the order was cancelled is in orders. The product's category is in products, and the category name in categories.
Four tables for one report. JOIN does not stop at two.
The idea
A chain of JOINs
Each following JOIN adds a table to what has already been put together. Order 60, with its products and their categories:
The path follows the foreign keys: orders → order_items (by order_id) → products (by product_id) → categories (by category_id). Each link is a separate JOIN … ON.
Both products and categories have a name column. That is why we name them here with AS: product and category. Otherwise the result would have two name columns.
Try it
Who rated what
For every review, show the customer name, the product name and the rating.
Solution
SELECT cu.name AS customer, p.name AS product, r.rating
FROM reviews r
JOIN customers cu ON cu.id = r.customer_id
JOIN products p ON p.id = r.product_id;FROM reviews r JOIN customers cu ON cu.id = r.customer_id JOIN products p ON p.id = r.product_id. 16 rows. The review is in the middle: it points both to the customer and to the product. The coffee beans have three reviews: 5, 4 and 1.
See it
The lost money
Revenue by category, without the cancelled orders:
Looks fine. Now the whole revenue, without categories:
The six categories give 1,537 €, but the revenue is 1,697 €.
In a chain of JOINs, every link can quietly throw rows out. For a product without a category there is no match in categories. With a LEFT JOIN to the categories the sales stay, and COALESCE gives the missing category a name:
Seven rows, and the total is 1,697 €. Whenever a report counts money, compare its total with the overall one. The difference shows the lost rows.
Try it
Revenue by city
Show the revenue by customer city, without the cancelled orders.
Solution
SELECT c.city, SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status <> 'cancelled'
GROUP BY c.city;FROM orders o JOIN customers c ON c.id = o.customer_id JOIN order_items oi ON oi.order_id = o.id, WHERE o.status <> 'cancelled' and GROUP BY c.city. Seven rows: Sofia leads with 658.70 €. The row with NULL is Boris, who has no city: 66.60 €. Nothing is lost here, because every order has a customer.
The idea
A table joined to itself
In the categories, parent_id points to id in the same table. To see a category next to its parent, you join categories to itself. The table takes part twice, with two different short names:
c is the category, parent is the same table seen as the parent. ON parent.id = c.parent_id says: the parent is the row whose id equals the category's parent_id. This is called a self join. Here the short names are required: without them name from the two copies cannot be told apart.
It is a LEFT JOIN so that “Food” and “Home”, which have no parent, stay.
Exercise
Two tasks
1. The new catalogue needs all 25 products with their category and parent category. The gift card must be on the list too.
The product, its category and the parent category, for all products:
Solution
SELECT p.name AS product, cat.name AS category, parent.name AS parent
FROM products p
LEFT JOIN categories cat ON cat.id = p.category_id
LEFT JOIN categories parent ON parent.id = cat.parent_id;FROM products p LEFT JOIN categories cat ON cat.id = p.category_id LEFT JOIN categories parent ON parent.id = cat.parent_id. 25 rows. Both JOINs must be LEFT: if the second is a plain JOIN, the gift card is dropped again, because a NULL category has no parent.
2. What is the revenue of the two top categories, “Food” and “Home”? Without the cancelled orders.
Revenue by top category:
Solution
SELECT parent.name AS parent, SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
JOIN categories parent ON parent.id = cat.parent_id
WHERE o.status <> 'cancelled'
GROUP BY parent.name;To the chain from the category report, add JOIN categories parent ON parent.id = cat.parent_id and group by parent.name. “Food” 824.60 €, “Home” 712.40 €. Together they make 1,537 €: the gift cards, 160 €, are in no top category.
More precisely, if you are curious
The order of plain JOINs in a query does not change the result: the database itself chooses which table to start from. With LEFT JOIN the order matters, because “left” is everything put together before it.
A table joined to itself gives only one level up. A tree with many levels (a subcategory of a subcategory) is walked with WITH RECURSIVE, in the module on subqueries.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: GROUP BY: a report by groups · Primary key and foreign key · JOIN: two tables in one query · LEFT JOIN: rows without a match too
- Continues in: UNION, INTERSECT, EXCEPT and CROSS JOIN · A report in steps: WITH · Trees and sequences: WITH RECURSIVE · Views
- Tasks on this topic: Joining tables