Kodewick
Lesson 13 of 21Contents

Joining more tables

Lesson 13 of 21 · Part 3 · More than one table · about 15 minutes

To see what an order bought, you need three tables: orders says who ordered and when, order_items says which products each order bought, how many and at what price, and only products has the products' names. This lesson joins several tables together, then groups the joined result.

Three tables

You can write one JOIN after another. This query shows the products in each order of customer 1:

main.sql
1SELECT o.id AS order_id, o.order_date, p.name, oi.quantity2FROM orders AS o3JOIN order_items AS oi ON oi.order_id = o.id4JOIN products AS p ON p.id = oi.product_id5WHERE o.customer_id = 16ORDER BY o.id, p.id;
Output
order_idorder_datenamequantity
12025-09-02Notebook3
12025-09-02Pens (10)1
32025-09-18Headphones1
32025-09-18USB cable2
92025-10-15Notebook2
92025-10-15Pens (10)2
6 rows

Read it from the top: first orders; the first JOIN pairs each order with its items, one row per item; the second JOIN pairs each item with its product. Every JOIN takes "the result so far" and matches it against the next table, and its ON says how to match at that step.

Press Step and watch the two JOINs add their columns one after the other.

Four tables

Join customers as well and you get the customers' names. Here is what customers in London bought; amount is the money spent on that row:

main.sql
1SELECT c.name, o.id AS order_id, p.name AS product,2    oi.quantity * oi.unit_price AS amount3FROM customers AS c4JOIN orders AS o ON o.customer_id = c.id5JOIN order_items AS oi ON oi.order_id = o.id6JOIN products AS p ON p.id = oi.product_id7WHERE c.city = 'London'8ORDER BY c.id, o.id, p.id;
Output
nameorder_idproductamount
Ana Silva1Notebook13.50
Ana Silva1Pens (10)6.00
Ana Silva3Headphones59.00
Ana Silva3USB cable17.00
Ana Silva9Notebook9.00
Ana Silva9Pens (10)12.00
Chloe Martin6Notebook22.50
Hana Sato10USB cable8.50
Hana Sato10Phone case15.00
9 rows

c.name and p.name are both called name, so the product's name gets the alias product. The more tables, the more columns share a name; put the table alias in front of every column and nothing gets mixed up.

JOIN first, then group

Now work out how much each customer has spent in total. This is where most mistakes happen, so take it slowly.

Don't group yet; just look at the rows after the JOINs. Here is customer 1, Ana Silva, alone:

main.sql
1SELECT c.name, o.id AS order_id, p.name AS product,2    oi.quantity * oi.unit_price AS amount3FROM customers AS c4JOIN orders AS o ON o.customer_id = c.id5JOIN order_items AS oi ON oi.order_id = o.id6JOIN products AS p ON p.id = oi.product_id7WHERE c.id = 18ORDER BY o.id, p.id;
Output
nameorder_idproductamount
Ana Silva1Notebook13.50
Ana Silva1Pens (10)6.00
Ana Silva3Headphones59.00
Ana Silva3USB cable17.00
Ana Silva9Notebook9.00
Ana Silva9Pens (10)12.00
6 rows

Ana has 3 orders, but there are 6 rows. After the JOINs, one row is one order item: not one order, and not one customer. What she spent is the sum of these 6 amounts.

Now drop the WHERE and group by customer:

main.sql
1SELECT c.name, SUM(oi.quantity * oi.unit_price) AS spent2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4JOIN order_items AS oi ON oi.order_id = o.id5GROUP BY c.id, c.name6ORDER BY spent DESC;
Output
namespent
Ana Silva116.50
Ben Okafor101.80
George Brown59.00
Emma Novak55.80
David Kim54.60
Hana Sato23.50
Chloe Martin22.50
7 rows

Press Step and go through it:

  1. FROM and the two JOINs: 18 rows, each an order item, with its customer and order in front.
  2. GROUP BY: those 18 rows go into groups by customer. Ana's group is the 6 rows above.
  3. SELECT: each group gets one SUM and becomes one row.

GROUP BY groups the rows after the JOIN, not the rows of the customers table as it was. This query doesn't need product names, so it doesn't join products. GROUP BY includes c.id because two customers could share a name. Customers who never ordered aren't in the result; to include them, use LEFT JOIN from the last lesson.

The order-counting trap

To count each customer's orders in the same query, most people write this:

Guess first

Ana Silva has placed 3 orders. What number does this query give her?

1SELECT c.name, COUNT(*) AS orders2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4JOIN order_items AS oi ON oi.order_id = o.id5WHERE c.id = 16GROUP BY c.id, c.name;

To count orders, count "how many different order numbers" with COUNT(DISTINCT o.id), from lesson 7. DISTINCT inside the parentheses removes repeated values before counting. Here are the two side by side:

main.sql
1SELECT c.name, COUNT(*) AS item_rows, COUNT(DISTINCT o.id) AS orders2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4JOIN order_items AS oi ON oi.order_id = o.id5GROUP BY c.id, c.name6ORDER BY c.id;
Output
nameitem_rowsorders
Ana Silva63
Ben Okafor32
Chloe Martin11
David Kim22
Emma Novak32
George Brown11
Hana Sato21
7 rows

Tip: Before you write a JOIN with GROUP BY, run it once without grouping and ask "what is one row now?" If a row is an order item, COUNT(*) counts items, not orders.

Joining a table to itself

In the employees table, manager_id is the employee number of that person's manager, and the manager is in the same table. To put each person and their manager's name on one row, join employees to itself. The table is used twice, so give it two different aliases: e for the employee, m for the manager:

main.sql
1SELECT e.name, e.title, m.name AS manager2FROM employees AS e3JOIN employees AS m ON e.manager_id = m.id4ORDER BY e.id;
Output
nametitlemanager
Omar HaddadSales leadGrace Lee
Lucia RomeroWarehouse leadGrace Lee
Tom BeckerSalesOmar Haddad
Mei TanakaSalesOmar Haddad
Sam WilsonPackerLucia Romero
5 rows

Only 5 rows: employee 1, the manager Grace Lee, has no manager; her manager_id is NULL and matches nobody. To keep her in, use LEFT JOIN again:

main.sql
1SELECT e.name, e.title, m.name AS manager2FROM employees AS e3LEFT JOIN employees AS m ON e.manager_id = m.id4ORDER BY e.id;
Output
nametitlemanager
Grace LeeManagerNULL
Omar HaddadSales leadGrace Lee
Lucia RomeroWarehouse leadGrace Lee
Tom BeckerSalesOmar Haddad
Mei TanakaSalesOmar Haddad
Sam WilsonPackerLucia Romero
6 rows

Practice

Write the query, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Sales per category

Work out each product category's sales (the sum of quantity × unit_price; cancelled orders count too): two columns, category and revenue, highest revenue first.

It should print
categoryrevenue
Electronics173.50
Bags101.80
Kitchen95.40
Stationery63.00
4 rows
exercise1.sql
1SELECT oi.order_id, oi.quantity * oi.unit_price AS amount2FROM order_items AS oi;
Output
Press Run or Ctrl+Enter
Exercise 2

Orders and units

For every customer who has ordered, show name, the number of orders orders and the total units bought items, ordered by customer id. The starting code gets orders wrong. Fix it.

It should print
nameordersitems
Ana Silva311
Ben Okafor24
Chloe Martin15
David Kim25
Emma Novak25
George Brown11
Hana Sato12
7 rows
exercise2.sql
1SELECT c.name, COUNT(*) AS orders, SUM(oi.quantity) AS items2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4JOIN order_items AS oi ON oi.order_id = o.id5GROUP BY c.id, c.name6ORDER BY c.id;
Output
Press Run or Ctrl+Enter
Exercise 3

How many people each manager has

For everyone who manages someone, show how many people report to them directly: two columns, manager and reports, ordered by the manager's id.

It should print
managerreports
Grace Lee2
Omar Haddad2
Lucia Romero1
3 rows
exercise3.sql
1SELECT e.name, m.name AS manager2FROM employees AS e3JOIN employees AS m ON e.manager_id = m.id4ORDER BY e.id;
Output
Press Run or Ctrl+Enter

What you learned

  • You can write one JOIN after another; each matches the result so far against the next table.
  • With many tables, put the table alias in front of column names, and alias columns that share a name.
  • JOIN with GROUP BY: the JOIN comes first, then the joined rows are grouped. Run it once ungrouped to see what one row is.
  • When a row is an order item, COUNT(*) counts items; count orders with COUNT(DISTINCT o.id).
  • A table can be joined to itself with two different aliases; to keep rows with no match, use LEFT JOIN.