Lesson 46

Joining Multiple Tables in SQL: 3+ Table Joins

Write JOIN queries that connect three or more tables so you can pull related data from across an entire database schema in a single query.

Your operations team wants one report: every order, the customer who placed it, the employee who handled it, and the payment method used. That data lives across four tables. You can't get it from one. You need to join them, in sequence, until the full picture is in a single result set.

How Chaining Joins Works

Every [JOIN](learn/sql-inner-join) adds one table to the query. When you need three tables, you write two JOIN clauses. When you need four, you write three. The pattern is always the same: JOIN a table, specify the ON condition, then JOIN the next one.

Start with two tables and build from there. Here's orders joined to customers:

SELECT o.order_id, c.first_name, c.last_name, o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id

Now add employees to bring in the employee who handled each order:

SELECT o.order_id, c.first_name, c.last_name, e.department, o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN employees e ON o.employee_id = e.employee_id

Each JOIN connects to the existing result using a column from a table already in the query. orders has both customer_id and employee_id, so both joins anchor back to it.

Key idea: Each new JOIN connects to a column that already exists in the query, either from the base table or from a previously joined table.

Adding a Fourth Table

Adding payments gives you the payment method for each order. payments connects to orders through order_id:

SELECT
  o.order_id,
  c.first_name,
  c.last_name,
  e.department,
  pay.method,
  pay.amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN employees e ON o.employee_id = e.employee_id
JOIN payments pay ON o.order_id = pay.order_id

Four tables, three JOIN clauses. Each one adds a layer of context. The query reads from top to bottom: start with orders, bring in who bought, bring in who processed it, bring in how it was paid.

Formatting matters here. Aligning each JOIN on its own line and indenting the ON clause makes the chain readable at a glance. When a query gets this wide, readability is not a nicety; it's how you catch mistakes.

Going Through a Bridge Table

Some tables don't connect directly. To get from orders to products, you have to go through order_items. That table sits between them and holds the foreign keys for both.

SELECT
  o.order_id,
  c.first_name,
  p.product_name,
  oi.quantity,
  oi.line_total
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id

order_items is the bridge. You join it first, then use its product_id column to reach products. You can't skip it. There's no direct link between orders and products, so the chain must pass through order_items.

This is exactly the junction table pattern described in the [star schema lesson](learn/sql-star-schema). Understanding which table is the bridge is a reading exercise on the schema before you write anything.

Key idea: If two tables have no shared column, look for a table in between that holds foreign keys for both.

Filtering and Aggregating Across Multiple Joins

Once the tables are joined, you can filter and aggregate exactly as you would with a single table. [WHERE](learn/sql-where-clause) applies before aggregation, and every column in the query is available to filter on.

SELECT
  c.country,
  COUNT(o.order_id) AS order_count,
  SUM(oi.line_total) AS total_revenue
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.status = 'delivered'
GROUP BY c.country
ORDER BY total_revenue DESC

This query crosses three tables, filters on order status, then aggregates by customer country. The joins run first, the WHERE filters the combined rows, and then GROUP BY collapses them. The order of operations doesn't change just because more tables are involved.

How Multi-Table Joins Connect to Other Concepts

The join type you use for each clause can vary. If some orders have no matching payment record, switching JOIN payments to a [LEFT JOIN](learn/sql-left-join) keeps those orders in the result with NULL values for the payment columns instead of dropping them.

Column ambiguity becomes a real issue when multiple tables share column names. Both orders and payments have a status column. Always prefix with the alias: o.status and pay.status are unambiguous. status alone causes an error.

Common Mistakes

1. Joining tables in the wrong order and losing the bridge

Trying to join products directly to orders fails because there's no shared key between them. order_items must come first.

-- wrong
SELECT o.order_id, p.product_name
FROM orders o
JOIN products p ON o.order_id = p.product_id

Join order_items first to create the path, then join products through order_items.

-- correct
SELECT o.order_id, p.product_name
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id

2. Referencing an ambiguous column name without an alias prefix

When two joined tables share a column name, SQL can't resolve which one you mean and throws an error.

-- wrong: status exists in both orders and payments
SELECT order_id, status
FROM orders o
JOIN payments pay ON o.order_id = pay.order_id

Always prefix every column with its table alias when the query touches more than one table.

-- correct
SELECT o.order_id, o.status AS order_status, pay.status AS payment_status
FROM orders o
JOIN payments pay ON o.order_id = pay.order_id

3. Forgetting an ON clause and creating a cross join

Writing JOIN without ON multiplies every row in one table against every row in the other, producing a result set that is almost never what you want.

-- wrong: no ON clause means every order row pairs with every customer row
SELECT o.order_id, c.first_name
FROM orders o
JOIN customers c

Every JOIN in a multi-table query needs its own ON clause with the correct matching columns.

-- correct
SELECT o.order_id, c.first_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id

Quick Recap

  • To join three or more tables, chain JOIN clauses one after another, each with its own ON condition.
  • Every ON clause connects a column from the new table to a column already present in the query.
  • When two tables have no shared column, find the bridge table between them and join through it.
  • Alias prefixes on every column are not optional in multi-table queries; ambiguous column names cause errors.
  • WHERE, GROUP BY, and ORDER BY all work the same way once the tables are joined.
  • Mixing JOIN types in the same query is valid: use INNER JOIN where both sides must match, and LEFT JOIN where one side may have no matching rows.
Exercise 1 Easy
Write a query to return the order_id, order_date, and status for every order in the orders table, sorted by order_date descending.
Hint: SELECT the three columns from orders and use ORDER BY order_date DESC. No joins needed.
Solution
SELECT o.order_id, o.order_date, o.status
FROM orders o
ORDER BY o.order_date DESC
Exercise 2 Easy
Write a query to return the first_name and last_name of each customer alongside the order_id and total_amount of every order they placed.
Hint: JOIN customers and orders on customer_id.
Solution
SELECT c.first_name, c.last_name, o.order_id, o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
Exercise 3 Medium
Write a query to return the first_name and last_name of each customer, the order_id, and the department of the employee who handled each order.
Hint: JOIN orders to customers on customer_id, then JOIN employees on employee_id.
Solution
SELECT c.first_name, c.last_name, o.order_id, e.department
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN employees e ON o.employee_id = e.employee_id
Exercise 4 Medium
Write a query to return the first_name and last_name of each customer, the order_id, the employee department, and the payment method, but only for orders where the payment method is 'credit_card'.
Hint: Add a third JOIN to bring in payments on order_id, then filter with WHERE on pay.method.
Solution
SELECT c.first_name, c.last_name, o.order_id, e.department, pay.method
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN employees e ON o.employee_id = e.employee_id
JOIN payments pay ON o.order_id = pay.order_id
WHERE pay.method = 'credit_card'
Exercise 5 Medium
Write a query to return each customer's country and the total line_total from all their orders, using the column alias total_spent, sorted from highest to lowest.
Hint: JOIN orders to customers on customer_id, then JOIN order_items on order_id. GROUP BY country and SUM line_total.
Solution
SELECT c.country, SUM(oi.line_total) AS total_spent
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY c.country
ORDER BY total_spent DESC
Frequently Asked Questions

Add a second JOIN clause after the first. Each JOIN connects one new table to the query using an ON clause that matches the foreign key in one table to the primary key in another.

There's no hard limit in most databases, but queries with many joins become harder to read and slower to run. In practice, most analytical queries join between two and six tables.

The order affects readability and occasionally performance, but not the final result when you're using INNER JOINs with correct ON clauses. Start from the table most central to your question.

Yes. You can chain an INNER JOIN followed by a LEFT JOIN in the same query. Each JOIN clause is independent and can use whichever type fits that relationship.