Skip to content

MySQL Tutorial JOINs: Combining Tables

Joining Three or More Tables

Lesson 146 of 308 2 min read By Deepak Paswan Updated Oct 5, 2026

Real queries often combine three, four or more tables. The method is simple: join them one at a time, each with its own ON condition.

Here is a small shop with four tables:

query.sql
SQL
CREATE TABLE customers (id INT PRIMARY KEY, name VARCHAR(20), city VARCHAR(20));
CREATE TABLE products  (id INT PRIMARY KEY, name VARCHAR(20), price DECIMAL(8,2));
CREATE TABLE orders    (id INT PRIMARY KEY, customer_id INT NOT NULL, ordered_on DATE NOT NULL);
CREATE TABLE order_items (
    order_id INT NOT NULL,
    product_id INT NOT NULL,
    quantity INT NOT NULL,
    unit_price DECIMAL(8,2) NOT NULL,
    PRIMARY KEY (order_id, product_id)
);

INSERT INTO customers VALUES (1, 'Maya', 'Pune'), (2, 'Rohan', 'Delhi'), (3, 'Sana', 'Pune');
INSERT INTO products VALUES (1, 'Notebook', 80), (2, 'Pen', 15), (3, 'Backpack', 999);
INSERT INTO orders VALUES (1, 1, '2026-05-02'), (2, 1, '2026-05-20'), (3, 2, '2026-05-21');
INSERT INTO order_items VALUES (1, 1, 2, 80), (1, 2, 5, 15), (2, 3, 1, 999), (3, 1, 1, 80), (3, 2, 10, 15);

order_items is a link table between orders and products: each order has many items, and each product appears in many orders. This is the many-to-many pattern (Many-to-Many).

Build the query in steps

Step 1: orders with customers.

query.sql
SQL
SELECT o.id AS order_id, c.name AS customer, o.ordered_on
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.id;
Output
OUTPUT
+----------+----------+------------+
| order_id | customer | ordered_on |
+----------+----------+------------+
|        1 | Maya     | 2026-05-02 |
|        2 | Maya     | 2026-05-20 |
|        3 | Rohan    | 2026-05-21 |
+----------+----------+------------+
3 rows in set

Step 2: add the items.

query.sql
SQL
SELECT o.id AS order_id, c.name AS customer, i.product_id, i.quantity
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items i ON i.order_id = o.id
ORDER BY o.id, i.product_id;
Output
OUTPUT
+----------+----------+------------+----------+
| order_id | customer | product_id | quantity |
+----------+----------+------------+----------+
|        1 | Maya     |          1 |        2 |
|        1 | Maya     |          2 |        5 |
|        2 | Maya     |          3 |        1 |
|        3 | Rohan    |          1 |        1 |
|        3 | Rohan    |          2 |       10 |
+----------+----------+------------+----------+
5 rows in set

The order rows are now repeated, once per item.

Step 3: add the product names.

query.sql
SQL
SELECT o.id AS order_id, c.name AS customer, p.name AS product, i.quantity, i.unit_price, i.quantity * i.unit_price AS line_total
FROM orders o
JOIN customers c   ON c.id = o.customer_id
JOIN order_items i ON i.order_id = o.id
JOIN products p    ON p.id = i.product_id
ORDER BY o.id, p.name;
Output
OUTPUT
+----------+----------+----------+----------+------------+------------+
| order_id | customer | product  | quantity | unit_price | line_total |
+----------+----------+----------+----------+------------+------------+
|        1 | Maya     | Notebook |        2 |      80.00 |     160.00 |
|        1 | Maya     | Pen      |        5 |      15.00 |      75.00 |
|        2 | Maya     | Backpack |        1 |     999.00 |     999.00 |
|        3 | Rohan    | Notebook |        1 |      80.00 |      80.00 |
|        3 | Rohan    | Pen      |       10 |      15.00 |     150.00 |
+----------+----------+----------+----------+------------+------------+
5 rows in set

One row per order line: who bought what, how many, at which price.

Summarise

Turn the detail into totals with GROUP BY:

query.sql
SQL
SELECT o.id AS order_id, c.name AS customer, COUNT(*) AS line_count, SUM(i.quantity * i.unit_price) AS order_total
FROM orders o
JOIN customers c   ON c.id = o.customer_id
JOIN order_items i ON i.order_id = o.id
GROUP BY o.id, c.name
ORDER BY o.id;
Output
OUTPUT
+----------+----------+------------+-------------+
| order_id | customer | line_count | order_total |
+----------+----------+------------+-------------+
|        1 | Maya     |          2 |      235.00 |
|        2 | Maya     |          1 |      999.00 |
|        3 | Rohan    |          2 |      230.00 |
+----------+----------+------------+-------------+
3 rows in set

And by product, across all orders:

query.sql
SQL
SELECT p.name AS product, SUM(i.quantity) AS units_sold, SUM(i.quantity * i.unit_price) AS revenue
FROM products p
JOIN order_items i ON i.product_id = p.id
GROUP BY p.id, p.name
ORDER BY revenue DESC;
Output
OUTPUT
+----------+------------+---------+
| product  | units_sold | revenue |
+----------+------------+---------+
| Backpack |          1 |  999.00 |
| Notebook |          3 |  240.00 |
| Pen      |         15 |  225.00 |
+----------+------------+---------+
3 rows in set

Mixing INNER and LEFT joins

To include all products, even ones never sold, start from products and use LEFT JOIN:

query.sql
SQL
INSERT INTO products VALUES (4, 'Marker', 145);

SELECT p.name AS product, COALESCE(SUM(i.quantity), 0) AS units_sold
FROM products p
LEFT JOIN order_items i ON i.product_id = p.id
GROUP BY p.id, p.name
ORDER BY units_sold DESC, p.name;
Output
OUTPUT
+----------+------------+
| product  | units_sold |
+----------+------------+
| Pen      |         15 |
| Notebook |          3 |
| Backpack |          1 |
| Marker   |          0 |
+----------+------------+
4 rows in set

The Marker has never been ordered, and still appears with 0. If you then join another table with an inner join, you must be careful: an inner join after a left join can remove those rows again, unless its condition is satisfied by the unmatched rows too.

The order of joins

For inner joins, the order you write them in does not change the result. MySQL’s optimizer chooses the best order itself. For outer joins, the order matters, because the left table is the one kept. Write the joins in the order that tells the story: from the main table outward.

The double-counting trap

The order total could be calculated from orders.total if such a column existed. But after joining items, any order-level column is repeated for each item. Summing it gives a wrong number (Aggregate Pitfalls). Always sum the item-level values, or aggregate in a subquery first.

Habits for big joins

  • Alias every table, and prefix every column.
  • Join on keys that are indexed on both sides.
  • Add tables one at a time and run the query each time, checking the row count.
  • Select only the columns you need.
  • Look at EXPLAIN for the join order and the indexes used (EXPLAIN).
  • Keep a limit on how many tables you join in one query. Ten tables is a sign that the query should be split, or that a view or a summary table would help.
lightbulb
Read it like a sentence
“Each order, with its customer, with its items, with each item’s product.” If you can say it that way, the join chain is right.