Imagine every table in your database is a piece of one big puzzle. The students table knows who is studying, the courses table knows what is being taught, and the enrollments table knows who is enrolled in what. But on their own, these pieces don’t tell you much. Want to see the whole picture? You gotta connect them—and that’s where multiple JOINs come in.
In real life, data is often organized into related tables to keep things structured and avoid redundancy. For example, in our university database, we have these tables:
students— info about students.enrollments— info about students enrolling in courses.courses— info about courses.
If we want a full list of students, their courses, and teachers, we need to join all three tables using JOIN.
Order of JOIN Execution
When you use several JOINs, PostgreSQL processes them left to right. That means it joins the first two tables, then joins the result with the third table, and so on.
Example:
SELECT *
FROM students
INNER JOIN enrollments ON students.id = enrollments.student_id
INNER JOIN courses ON enrollments.course_id = courses.id;
- First, the
studentsandenrollmentstables are joined onstudents.id = enrollments.student_id. - The result of the first join is then joined with the
coursestable onenrollments.course_id = courses.id.
The order of execution is especially important when you’re dealing with big tables. A bad JOIN structure can seriously slow things down.
Example: List of Students, Their Courses, and Teachers
Let’s say we have these tables with data:
Table students:
| id | name |
|---|---|
| 1 | Otto Song |
| 2 | Maria Chi |
| 3 | Alex Lin |
Table courses:
| id | name | teacher |
|---|---|---|
| 101 | Mathematics | Ellen Moore |
| 102 | Physics | James Okoro |
| 103 | Computer Science | Nina Delgado |
Table enrollments:
| student_id | course_id |
|---|---|
| 1 | 101 |
| 1 | 103 |
| 2 | 102 |
| 3 | 101 |
Query:
SELECT
students.name AS student_name,
courses.name AS course_name,
courses.teacher AS teacher_name
FROM students
INNER JOIN enrollments ON students.id = enrollments.student_id
INNER JOIN courses ON enrollments.course_id = courses.id;
Result:
| student_name | course_name | teacher_name |
|---|---|---|
| Otto Song | Mathematics | Ellen Moore |
| Otto Song | Computer Science | Nina Delgado |
| Maria Chi | Physics | James Okoro |
| Alex Lin | Mathematics | Ellen Moore |
Filtering in Queries with Multiple JOINs
You can use filtering conditions in JOIN queries to limit the amount of data returned and speed things up. For example, if you only want students who are taking the "Mathematics" course:
SELECT
students.name AS student_name,
courses.name AS course_name
FROM students
INNER JOIN enrollments ON students.id = enrollments.student_id
INNER JOIN courses ON enrollments.course_id = courses.id
WHERE courses.name = 'Mathematics';
Result:
| student_name | course_name |
|---|---|
| Otto Song | Mathematics |
| Alex Lin | Mathematics |
Optimizing Queries with Multiple JOINs
When you’re working with big tables, query optimization is super important. Here are a few tips:
- Use indexes
Indexes let PostgreSQL work faster, especially when joining on key fields. Make sure you have indexes on the student_id and course_id columns in the enrollments table.
Example of creating an index:
CREATE INDEX idx_enrollments_student_id ON enrollments(student_id);
CREATE INDEX idx_enrollments_course_id ON enrollments(course_id);
You’ll learn more about indexes in later levels, but I wanted to mention them here. They’re super often used with JOINs.
- Filter data early
PostgreSQL’s optimizer is usually smart enough to apply WHERE conditions as early as possible (predicate pushdown), so adding filters is a good practice. The formal logical order is FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY, but the planner is allowed to reorder steps for performance. For example:
SELECT
students.name AS student_name,
courses.name AS course_name
FROM students
INNER JOIN enrollments ON students.id = enrollments.student_id
INNER JOIN courses ON enrollments.course_id = courses.id
WHERE
courses.teacher = 'Ivan Petrov';
- Minimize the number of rows to join
Instead of joining all records from two tables, try filtering them first with subqueries:
SELECT
students.name AS student_name,
courses.name AS course_name
FROM
(SELECT * FROM students WHERE id IN (1, 2)) sub_students
INNER JOIN enrollments ON sub_students.id = enrollments.student_id
INNER JOIN courses ON enrollments.course_id = courses.id;
You’ll learn more about nested SELECTs literally in the next level :P
Example of a Complex Join: Students, Courses, and Faculties
Let’s say we add another table faculties:
Table faculties:
| id | name |
|---|---|
| 10 | Engineering |
| 20 | Natural Sciences |
Table courses updated:
| id | name | teacher | faculty_id |
|---|---|---|---|
| 101 | Mathematics | Ellen Moore | 10 |
| 102 | Physics | James Okoro | 20 |
| 103 | Computer Science | Nina Delgado | 10 |
To get a list of students, courses, and faculties, we add one more JOIN:
SELECT
students.name AS student_name,
courses.name AS course_name,
faculties.name AS faculty_name
FROM students
INNER JOIN enrollments ON students.id = enrollments.student_id
INNER JOIN courses ON enrollments.course_id = courses.id
INNER JOIN faculties ON courses.faculty_id = faculties.id;
Result:
| student_name | course_name | faculty_name |
|---|---|---|
| Otto Song | Mathematics | Engineering |
| Otto Song | Computer Science | Engineering |
| Maria Chi | Physics | Natural Sciences |
| Alex Lin | Mathematics | Engineering |
SQL queries with multiple JOINs can get tricky, but they let you build powerful reports and get valuable insights. If you optimize and structure them right, they’ll be your go-to tool for working with big databases.
GO TO FULL VERSION