Multiple Joins — Combining 3+ Tables
title: “Multiple Joins (3+ Tables)” part: 2 topic_number: 4 slug: “multiple-joins” difficulty: “Intermediate” prerequisites: “inner-join, left-join” —
Multiple Joins — Combining 3+ Tables
What Is It?
Multiple joins connect three or more tables in a single query. This lets you pull related data from across your entire database — like showing employee names, their department, and the projects they’re working on, all in one result.
Real-world analogy: Like connecting the dots in a network — following relationships from person → company → project → client.
When you’d use it:
- Complex reporting across many entities
- Denormalizing data for analysis
- Building comprehensive dashboards
- Tracking relationships across your data model
Syntax Breakdown
1
2
3
4
5
6
7
SELECT ...
FROM table1
JOIN table2 ON table1.key = table2.key
JOIN table3 ON table2.key = table3.key
JOIN table4 ON table3.key = table4.key
...
WHERE ...
Key points:
- Chain joins one after another
- Each JOIN has its own ON condition
- Can mix INNER, LEFT, RIGHT joins
- Order matters for readability (but not always for results)
- Be mindful of Cartesian explosion (result multiplying)
Basic Examples
Three-Table Join: Employees, Departments, Projects
1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT
e.first_name,
e.last_name,
d.department_name,
p.project_name,
ep.role,
ep.hours_allocated
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN projects p ON ep.project_id = p.project_id
ORDER BY e.last_name, p.project_name
LIMIT 10;
Expected output:
| first_name | last_name | department_name | project_name | role | hours_allocated |
|---|---|---|---|---|---|
| Jack | Anderson | Human Resources | CRM System | Developer | 120 |
| Jack | Anderson | Human Resources | Mobile App | Lead Developer | 160 |
| David | Brown | Finance | API Development | Developer | 100 |
| Sam | Clark | Engineering | Website Redesign | Lead Developer | 150 |
| … | … | … | … | … | … |
Complete picture: who works where on what.
Four-Table Join with Manager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Add employee alias for manager
SELECT
e.first_name AS employee,
d.department_name,
p.project_name,
ep.role,
m.first_name AS manager_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN projects p ON ep.project_id = p.project_id
LEFT JOIN employees m ON e.manager_id = m.employee_id
ORDER BY p.project_name, e.last_name
LIMIT 10;
Expected output:
| employee | department_name | project_name | role | manager_name |
|---|---|---|---|---|
| David | Finance | API Development | Developer | Bob |
| Henry | Finance | API Development | Lead Developer | NULL |
| Jack | Human Resources | CRM System | Developer | Alice |
| Ivy | Human Resources | CRM System | Developer | NULL |
| … | … | … | … | … |
Full organizational context for each project assignment.
Going Deeper
Mix INNER and LEFT Joins
Show all employees, their departments, and any projects (even if they have none):
1
2
3
4
5
6
7
8
9
10
11
SELECT
e.first_name,
e.last_name,
d.department_name,
p.project_name,
ep.hours_allocated
FROM employees e
JOIN departments d ON e.department_id = d.department_id
LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id
LEFT JOIN projects p ON ep.project_id = p.project_id
ORDER BY e.last_name;
Expected output (partial):
| first_name | last_name | department_name | project_name | hours_allocated |
|---|---|---|---|---|
| Jack | Anderson | Human Resources | CRM System | 120 |
| Jack | Anderson | Human Resources | Mobile App | 160 |
| David | Brown | Finance | API Development | 100 |
| Leo | Jackson | Sales | NULL | NULL |
| Quinn | Martinez | Marketing | NULL | NULL |
| … | … | … | … | … |
Employees without projects show NULL (Leo, Quinn have no assignments).
Aggregation Across Multiple Tables
Count projects per department:
1
2
3
4
5
6
7
8
9
10
SELECT
d.department_name,
COUNT(DISTINCT ep.project_id) AS unique_projects,
COUNT(ep.employee_id) AS total_assignments,
SUM(ep.hours_allocated) AS total_hours
FROM departments d
LEFT JOIN employees e ON d.department_id = e.department_id
LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id
GROUP BY d.department_id, d.department_name
ORDER BY unique_projects DESC;
Expected output:
| department_name | unique_projects | total_assignments | total_hours |
|---|---|---|---|
| Engineering | 5 | 7 | 780 |
| Finance | 4 | 5 | 540 |
| Human Resources | 3 | 4 | 480 |
| Sales | 2 | 3 | 310 |
| Marketing | 0 | 0 | NULL |
| … | … | … | … |
Departmental workload analysis.
Key: COUNT(DISTINCT ep.project_id) avoids counting the same project multiple times when multiple employees from same department work on it.
Complex Filtering
Find high-earning engineers on active projects with more than 2 team members:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SELECT
e.first_name,
e.last_name,
e.salary,
d.department_name,
p.project_name,
p.status AS project_status,
(SELECT COUNT(*)
FROM employee_projects ep2
WHERE ep2.project_id = p.project_id) AS team_size
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN projects p ON ep.project_id = p.project_id
WHERE d.department_name = 'Engineering'
AND e.salary >= 80000
AND p.status = 'In Progress'
AND (SELECT COUNT(*)
FROM employee_projects ep2
WHERE ep2.project_id = p.project_id) > 2
ORDER BY p.project_name;
Expected output:
| first_name | last_name | salary | department_name | project_name | project_status | team_size |
|---|---|---|---|---|---|---|
| Sam | Clark | 95000.00 | Engineering | Website Redesign | In Progress | 4 |
| Paul | Garcia | 87000.00 | Engineering | Data Pipeline | In Progress | 3 |
High-value engineers on collaborative, active projects.
Pause and Predict: What happens to result size when you add more INNER JOINs?
Answer
The result can **grow exponentially** (Cartesian explosion)! If Employee A is in 3 projects and each project has 4 tasks, joining to tasks creates 3 × 4 = 12 rows for that one employee. This is called "data explosion" and can make queries very slow. Use `DISTINCT` or careful aggregation to manage this.Watch Out — Common Mistakes
Mistake #1: Wrong Join Order Causing Confusion
1
2
3
4
5
6
-- CONFUSING (employees -> projects -> departments)
SELECT ...
FROM employees e
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN departments d ON e.department_id = d.department_id
JOIN projects p ON ep.project_id = p.project_id;
Not wrong technically, but hard to follow.
Better:
1
2
3
4
5
6
-- • CLEARER (employees -> departments, then -> projects)
SELECT ...
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN projects p ON ep.project_id = p.project_id;
Logical flow: Start with main entity (employees), add direct relationships (departments), then associations (projects).
Mistake #2: Forgetting LEFT JOIN for Optional Data
1
2
3
4
5
-- INCOMPLETE (misses employees without projects)
SELECT e.first_name, p.project_name
FROM employees e
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN projects p ON ep.project_id = p.project_id;
Fix: Use LEFT JOIN for optional relationships:
1
2
3
4
5
-- • COMPLETE (includes employees without projects)
SELECT e.first_name, p.project_name
FROM employees e
LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id
LEFT JOIN projects p ON ep.project_id = p.project_id;
Mistake #3: Cartesian Explosion Without GROUP BY
1
2
3
4
5
6
7
8
-- DATA EXPLOSION
SELECT
d.department_name,
COUNT(*) AS count -- Wrong! This counts ROWS, not unique entities
FROM employees e
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN departments d ON e.department_id = d.department_id
GROUP BY d.department_name;
Problem: If an employee has 3 projects, they’re counted 3 times!
Fix: Use DISTINCT:
1
2
3
4
5
6
7
8
-- • CORRECT
SELECT
d.department_name,
COUNT(DISTINCT e.employee_id) AS unique_employees
FROM employees e
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN departments d ON e.department_id = d.department_id
GROUP BY d.department_name;
Edge Case Spotlight
Joining Through Many-to-Many
1
2
3
4
5
6
7
8
9
-- Show departments and projects they're involved in (through employees)
SELECT DISTINCT
d.department_name,
p.project_name
FROM departments d
JOIN employees e ON d.department_id = e.department_id
JOIN employee_projects ep ON e.employee_id = ep.employee_id
JOIN projects p ON ep.project_id = p.project_id
ORDER BY d.department_name, p.project_name;
Expected output:
| department_name | project_name |
|---|---|
| Engineering | Data Pipeline |
| Engineering | Website Redesign |
| Finance | API Development |
| Finance | CRM System |
| … | … |
Department-project relationships (even though they’re not directly connected in the schema).
NULL Propagation in Chains
1
2
3
4
5
6
7
8
-- LEFT JOIN chain
SELECT
e.first_name,
ep.role,
p.project_name
FROM employees e
LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id
LEFT JOIN projects p ON ep.project_id = p.project_id;
Important: Once a LEFT JOIN produces NULL, subsequent LEFT JOINs also produce NULL (NULL doesn’t match anything).
Try This
Exercise 1 (Guided)
Create a comprehensive employee report: first_name, last_name, department_name, department location, number of projects they’re assigned to (even if 0). Sort by last_name.
Hint
Start with employees LEFT JOIN to all other tables, use COUNT with GROUP BY, remember to count project_id not *.Exercise 2 (Independent)
Find departments that have employees working on projects with status ‘Completed’. Show department_name, number of unique completed projects, total hours spent. Sort by total hours descending.
Hint
Join departments -> employees -> employee_projects -> projects, filter WHERE status = 'Completed', GROUP BY department, use COUNT(DISTINCT) and SUM.Exercise 3 (Challenge)
Create a “project collaboration matrix”: For each project, show project_name, total team size, number of different departments involved, and list the department names (comma-separated). Only include active projects with 3+ team members.
Hint
Multiple joins to projects -> employee_projects -> employees -> departments. Use COUNT(DISTINCT), GROUP_CONCAT for comma-separated list, HAVING for filtering grouped results.Answer Key
Exercise 1 Answer
```sql SELECT e.first_name, e.last_name, d.department_name, d.location, COUNT(ep.project_id) AS project_count FROM employees e JOIN departments d ON e.department_id = d.department_id LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id GROUP BY e.employee_id, e.first_name, e.last_name, d.department_name, d.location ORDER BY e.last_name; ``` **Expected output:** | first_name | last_name | department_name | location | project_count | |------------|-----------|-----------------|----------|---------------| | Jack | Anderson | Human Resources | Boston | 2 | | David | Brown | Finance | Chicago | 1 | | Sam | Clark | Engineering | San Francisco | 1 | | Leo | Jackson | Sales | Seattle | 0 | | ... | ... | ... | ... | ... | Complete employee overview with project involvement.Exercise 2 Answer
```sql SELECT d.department_name, COUNT(DISTINCT p.project_id) AS completed_projects, SUM(ep.hours_allocated) AS total_hours FROM departments d JOIN employees e ON d.department_id = e.department_id JOIN employee_projects ep ON e.employee_id = ep.employee_id JOIN projects p ON ep.project_id = p.project_id WHERE p.status = 'Completed' GROUP BY d.department_id, d.department_name ORDER BY total_hours DESC; ``` **Expected output:** | department_name | completed_projects | total_hours | |-----------------|-------------------|-------------| | Engineering | 2 | 420 | | Finance | 1 | 220 | | Sales | 1 | 110 | Departmental productivity on completed work.Exercise 3 Answer
```sql SELECT p.project_name, COUNT(DISTINCT ep.employee_id) AS team_size, COUNT(DISTINCT d.department_id) AS departments_involved, GROUP_CONCAT(DISTINCT d.department_name ORDER BY d.department_name) AS department_list FROM projects p JOIN employee_projects ep ON p.project_id = ep.project_id JOIN employees e ON ep.employee_id = e.employee_id JOIN departments d ON e.department_id = d.department_id WHERE p.status = 'In Progress' GROUP BY p.project_id, p.project_name HAVING COUNT(DISTINCT ep.employee_id) >= 3 ORDER BY team_size DESC; ``` **Expected output:** | project_name | team_size | departments_involved | department_list | |--------------|-----------|----------------------|-----------------| | Website Redesign | 4 | 3 | Engineering,Finance,Marketing | | Data Pipeline | 3 | 2 | Engineering,Sales | | Mobile App | 3 | 2 | Human Resources,Sales | Cross-functional project analysis. **Note:** GROUP_CONCAT creates comma-separated lists (MySQL-specific function).Quick Recap
• Multiple joins connect 3+ tables in one query
• Chain joins with multiple JOIN…ON clauses
• Mix INNER/LEFT/RIGHT joins based on requirements
• Join order matters for readability
• Watch for Cartesian explosion with many-to-many relationships
• Use COUNT(DISTINCT) to avoid overcounting
• LEFT JOIN chains propagate NULLs
• Start with main entity, add relationships logically
Up Next
Time for a Challenge! → Mini Challenge 8
You’ve mastered CASE WHEN, self joins, and multiple joins! Practice these intermediate techniques.