Joins: INNER, LEFT and Self Joins – SQL Server | Lesson 17
Introduction
Normalization split the school's data into 22 tables, each storing one kind of fact.
That keeps data correct, but no single table answers "who is in 10-A?". The student's
name is in Students, the section is in Enrollments, and the section's name is in
Sections.
A join puts tables back together for one query. In this lesson, you write the query behind the Class 10-A list from Lesson 2, and several more.
By the end of this lesson, you will be able to:
- Join two or more tables with
INNER JOINand anONcondition. - Use table aliases to keep long queries readable.
- Keep unmatched rows with
LEFT JOIN, and recognize the NULLs it produces. - Join a table to itself.
- Explain why a join can multiply rows.
Why this matters
Almost every real screen needs a join:
- A class list needs students and enrollments.
- A report card needs marks, papers, subjects, and exams.
- A fee receipt needs payments, invoices, enrollments, and students.
If you cannot join confidently, you cannot build any of those. Worse, a join written slightly wrong does not fail: it quietly returns duplicated or missing rows, and the report looks fine until someone counts.
The concept
A join matches rows from two tables using a condition, usually "this table's foreign key equals that table's primary key".
Think of the school office matching fee receipts to the student register. Each receipt has a student's number, and the clerk finds that number in the register.
The clerk then writes the receipt and the student's name on one line. A join does the same for every row, in milliseconds.
Example: INNER JOIN
SELECT s.FirstName, e.RollNumber
FROM dbo.Students AS s
INNER JOIN dbo.Enrollments AS e ON e.StudentId = s.Id;
AS sandAS eare table aliases: short names for the tables in this query.ON e.StudentId = s.Idis the join condition: which rows belong together.INNER JOINkeeps only rows that have a match on both sides.
Example: kinds of joins
| Join | Keeps | SMS example |
|---|---|---|
INNER JOIN | Only rows that match on both sides | Students with their enrollments |
LEFT JOIN | Every row from the left table, matched or not | Every teacher, with a section if they are a class teacher |
RIGHT JOIN | Every row from the right table | Rarely used; write it as a LEFT JOIN instead |
FULL JOIN | Every row from both sides | Comparing two lists, such as an import and the database |
CROSS JOIN | Every row combined with every row | Building all class and subject combinations |
When a LEFT JOIN finds no match, the right table's columns are NULL in that row.
In daily work, INNER JOIN and LEFT JOIN cover almost everything. The word INNER
is optional, since a plain JOIN means the same, but writing it makes your intent obvious.
Apply it to the SMS
Save the full script below as queries\017-joins.sql and run one query at a time.
Example: the Class 10-A list
Four tables, joined one after another. Read it as: start from students, find their enrollments, then each enrollment's section and year:
SELECT
s.Id,
s.AdmissionNumber,
s.FirstName,
s.LastName,
e.RollNumber
FROM dbo.Students AS s
INNER JOIN dbo.Enrollments AS e ON e.StudentId = s.Id
INNER JOIN dbo.Sections AS sec ON sec.Id = e.SectionId
INNER JOIN dbo.AcademicYears AS ay ON ay.Id = e.AcademicYearId
WHERE sec.ClassId = 10
AND sec.Name = 'A'
AND ay.IsCurrent = 1
ORDER BY e.RollNumber;
| Id | AdmissionNumber | FirstName | LastName | RollNumber |
|---|---|---|---|---|
| 1 | ADM-2017-0001 | Aarav | Sharma | 1 |
| 2 | ADM-2017-0002 | Diya | Patel | 2 |
| 3 | ADM-2017-0003 | Kabir | Singh | 3 |
| 4 | ADM-2017-0004 | Meera | Iyer | 4 |
This is exactly the data the API returned as JSON in Lesson 2. In Module 11, the API runs this query.
Example: Kabir's attendance
Students, enrollments, academic years, and attendance:
| FirstName | AttendanceDate | Status | Remarks |
|---|---|---|---|
| Kabir | 2026-07-01 | Absent | Fever |
| Kabir | 2026-07-02 | Present | NULL |
| Kabir | 2026-07-03 | Present | NULL |
Example: a report card row
Aarav's Unit Test 1 marks need four tables: Marks, ExamSubjects, Exams, and
Subjects. This is the report card from Lesson 13, built with joins:
| Exam | Subject | MarksObtained | MaxMarks | PassMarks |
|---|---|---|---|---|
| Unit Test 1 | Mathematics | 92.00 | 100.00 | 35.00 |
| Unit Test 1 | Science | 88.00 | 100.00 | 35.00 |
Example: LEFT JOIN and NULL
Every teacher, and the section they are class teacher of this year:
SELECT
t.EmployeeCode,
t.FirstName,
t.LastName,
ct.SectionId
FROM dbo.Teachers AS t
LEFT JOIN dbo.ClassTeachers AS ct
ON ct.TeacherId = t.Id
AND ct.AcademicYearId = (SELECT Id FROM dbo.AcademicYears WHERE IsCurrent = 1)
ORDER BY t.EmployeeCode;
| EmployeeCode | FirstName | LastName | SectionId |
|---|---|---|---|
| TCH-001 | Anitha | Rao | 19 |
| TCH-002 | Rahul | Verma | 20 |
| TCH-003 | Sarah | Thomas | NULL |
| TCH-004 | Imran | Khan | NULL |
| TCH-005 | Priya | Nair | NULL |
| TCH-006 | Deepak | Joshi | NULL |
| TCH-007 | Kavya | Menon | 11 |
| TCH-008 | Arjun | Mehta | 12 |
All eight teachers appear. The four who are not class teachers have NULL. An
INNER JOIN would have shown only four rows.
Notice the year condition is in ON, not in WHERE. In WHERE, ct.AcademicYearId = 2
would remove the NULL rows again and turn the LEFT JOIN back into an INNER JOIN. The
(SELECT Id ...) part is a subquery; Lesson 19 covers them.
Example: many-to-many through a junction table
The Sharma children and their guardians: Students, then StudentGuardians, then Guardians.
| Student | Guardian | Relationship | Phone |
|---|---|---|---|
| Aarav | Rajesh | Father | 9876540101 |
| Aarav | Neha | Mother | 9876540102 |
| Advika | Rajesh | Father | 9876540101 |
| Advika | Neha | Mother | 9876540102 |
Example: a self join
A self join joins a table to itself, with two different aliases. Which students
share a guardian? Join StudentGuardians to itself on the same GuardianId:
SELECT DISTINCT
s1.FirstName AS Student1,
s2.FirstName AS Student2
FROM dbo.StudentGuardians AS sg1
INNER JOIN dbo.StudentGuardians AS sg2
ON sg2.GuardianId = sg1.GuardianId
AND sg2.StudentId > sg1.StudentId
INNER JOIN dbo.Students AS s1 ON s1.Id = sg1.StudentId
INNER JOIN dbo.Students AS s2 ON s2.Id = sg2.StudentId;
| Student1 | Student2 |
|---|---|
| Aarav | Advika |
sg2.StudentId > sg1.StudentId stops a student matching themselves, and stops the
same pair appearing twice in reverse order. DISTINCT removes the second match: they
share two guardians, the father and the mother.
Example: joins can multiply rows
There are 12 fixed students. Join them to StudentGuardians and count:
SELECT COUNT(*) AS RowsAfterJoin
FROM dbo.Students AS s
INNER JOIN dbo.StudentGuardians AS sg ON sg.StudentId = s.Id
WHERE s.Id <= 12;
The result is 24, not 12: each student has two guardians, so each student appears twice. That is correct for a guardian list, and wrong if you meant to count students. Before adding a join, ask: "How many rows on the other side match each row?"
Show queries/017-joins.sql
-- Lesson 17: joins. Read-only: it changes nothing.
USE SchoolDb;
GO
-- 1. INNER JOIN: the Class 10-A list for the current year (the request from Lesson 2).
SELECT
s.Id,
s.AdmissionNumber,
s.FirstName,
s.LastName,
e.RollNumber
FROM dbo.Students AS s
INNER JOIN dbo.Enrollments AS e ON e.StudentId = s.Id
INNER JOIN dbo.Sections AS sec ON sec.Id = e.SectionId
INNER JOIN dbo.AcademicYears AS ay ON ay.Id = e.AcademicYearId
WHERE sec.ClassId = 10
AND sec.Name = 'A'
AND ay.IsCurrent = 1
ORDER BY e.RollNumber;
-- 2. Kabir's attendance this year, with dates and status.
SELECT
s.FirstName,
a.AttendanceDate,
a.Status,
a.Remarks
FROM dbo.Students AS s
INNER JOIN dbo.Enrollments AS e ON e.StudentId = s.Id
INNER JOIN dbo.AcademicYears AS ay ON ay.Id = e.AcademicYearId
INNER JOIN dbo.AttendanceRecords AS a ON a.EnrollmentId = e.Id
WHERE s.AdmissionNumber = 'ADM-2017-0003'
AND ay.IsCurrent = 1
ORDER BY a.AttendanceDate;
-- 3. Aarav's Unit Test 1 marks with subject names: one report card row per paper.
SELECT
ex.Name AS Exam,
sub.Name AS Subject,
m.MarksObtained,
es.MaxMarks,
es.PassMarks
FROM dbo.Marks AS m
INNER JOIN dbo.ExamSubjects AS es ON es.Id = m.ExamSubjectId
INNER JOIN dbo.Exams AS ex ON ex.Id = es.ExamId
INNER JOIN dbo.Subjects AS sub ON sub.Id = es.SubjectId
WHERE m.EnrollmentId = 13
ORDER BY sub.Name;
-- 4. LEFT JOIN: every teacher, and the section they are class teacher of this year (if any).
SELECT
t.EmployeeCode,
t.FirstName,
t.LastName,
ct.SectionId
FROM dbo.Teachers AS t
LEFT JOIN dbo.ClassTeachers AS ct
ON ct.TeacherId = t.Id
AND ct.AcademicYearId = (SELECT Id FROM dbo.AcademicYears WHERE IsCurrent = 1)
ORDER BY t.EmployeeCode;
-- 5. Many-to-many: the Sharma children and their guardians, through StudentGuardians.
SELECT
s.FirstName AS Student,
g.FirstName AS Guardian,
sg.Relationship,
g.Phone
FROM dbo.Students AS s
INNER JOIN dbo.StudentGuardians AS sg ON sg.StudentId = s.Id
INNER JOIN dbo.Guardians AS g ON g.Id = sg.GuardianId
WHERE s.LastName = N'Sharma'
AND s.Id <= 12
ORDER BY s.FirstName, sg.Relationship;
-- 6. Self join: pairs of students who share a guardian (brothers and sisters).
SELECT DISTINCT
s1.FirstName AS Student1,
s2.FirstName AS Student2
FROM dbo.StudentGuardians AS sg1
INNER JOIN dbo.StudentGuardians AS sg2
ON sg2.GuardianId = sg1.GuardianId
AND sg2.StudentId > sg1.StudentId
INNER JOIN dbo.Students AS s1 ON s1.Id = sg1.StudentId
INNER JOIN dbo.Students AS s2 ON s2.Id = sg2.StudentId;
GO
Common mistakes
- Joining on the wrong columns.
ON e.Id = s.Idcompares two unrelated numbers and returns nonsense that looks real. Fix: join a foreign key to the primary key it references. - Putting a LEFT JOIN's filter in
WHERE. It removes the NULL rows. Fix: put conditions on the right table in theONclause. - Forgetting a join condition. The old comma style
FROM A, Bwithout a condition pairs every row with every row: 12 students and 6 subjects give 72 rows. Fix: always writeINNER JOIN ... ON. - Counting after a one-to-many join. Counts come out doubled. Fix: count the right column, or join after aggregating (Lesson 19).
- No aliases in multi-table queries. Ambiguous columns like
Idcause errors. Fix: alias every table and prefix every column.
In real projects
The most common report bug I fix is a total that is exactly double, or triple, what it should be. Every time, someone added one more join to show an extra column, and that join matched two rows instead of one. The numbers looked believable, so nobody noticed for months.
My habit: after writing a join, I check the row count against something I know. If 10-A has four students, the class list must have four rows. If it has eight, a join is multiplying rows, and I find it before anyone builds a report on top.
Best practices
- Join foreign keys to the primary keys they reference.
- Alias every table and prefix every column with its alias.
- Put conditions on a LEFT JOIN's right-hand table in the
ONclause. - Check row counts against a number you already know.
- Prefer
LEFT JOINoverRIGHT JOIN; read every query from left to right.
Recap
- A join combines rows from tables that match on a condition.
INNER JOINkeeps matches only;LEFT JOINkeeps every left row, with NULLs.- Many-to-many data is joined through the junction table.
- A self join uses one table twice with two aliases.
- One-to-many joins multiply rows; check counts.
Homework
Library module task: List every book issue with the book title, copy number, student's first and last name, issue date, and return date, oldest first.
A correct result looks like: two rows, Aarav with "Wings of Fire" (returned) and Advika with "The Jungle Book" (return date NULL).
Show the solution
-- Library homework, Lesson 17. Read-only.
USE SchoolDb;
GO
-- Every issue with the book title and the student's name.
SELECT
b.Title,
c.CopyNumber,
s.FirstName,
s.LastName,
i.IssuedOn,
i.ReturnedOn
FROM dbo.BookIssues AS i
INNER JOIN dbo.BookCopies AS c ON c.Id = i.BookCopyId
INNER JOIN dbo.Books AS b ON b.Id = c.BookId
INNER JOIN dbo.Students AS s ON s.Id = i.StudentId
ORDER BY i.IssuedOn;
GO
Quiz
What does INNER JOIN keep?
Show answer
Answer: B. Only rows that match on both sides Unmatched rows on either side are left out.
Four teachers are not class teachers. In a LEFT JOIN from Teachers to ClassTeachers, what does their SectionId show?
Show answer
Answer: C. NULL A LEFT JOIN keeps the teacher and fills the missing side with NULL.
12 students each have 2 guardians. How many rows does Students INNER JOIN StudentGuardians return?
Show answer
Answer: C. 24 Each student matches two rows, so each appears twice.
What is a self join?
Show answer
Answer: B. A table joined to itself using two aliases The same table plays two roles, such as two students sharing a guardian.
Where should a condition on the right-hand table of a LEFT JOIN go?
Show answer
Answer: B. In the ON clause In WHERE it would remove the NULL rows and turn it into an inner join.
Interview questions
Questions are grouped by experience level. Click a question to see a model answer. All questions: SQL Server interview questions.
Fresher (0-2 yrs)
F What is the difference between INNER JOIN and LEFT JOIN? concept
INNER JOIN returns only rows that match on both sides. LEFT JOIN returns every row
from the left table, and fills the right table's columns with NULL when there is no
match. Every teacher with their class (LEFT) shows teachers who are not class teachers;
INNER would hide them.
F What is a join condition, and what usually goes in it? concept
The join condition, written after ON, says which rows of the two tables belong
together. It is usually a foreign key equal to the primary key it references, such as
ON e.StudentId = s.Id.
F Write a query that lists the students of Class 10-A for the current year. coding
Join Students to Enrollments, then to Sections and AcademicYears, and filter on
the class, the section name, and IsCurrent = 1, ordered by roll number. Four tables,
each joined on its foreign key.
Mid (3-5 yrs)
M Why does adding a join sometimes multiply the number of rows? concept
If each row on the left matches several rows on the right, the left row appears once for each match. Joining 12 students to their guardians gives 24 rows, because each has two. Totals calculated after such a join are multiplied too.
M What is a self join? Give an example. coding
A self join joins a table to itself using two aliases. To find students who share a
guardian, join StudentGuardians to itself on the same GuardianId, with a condition
like sg2.StudentId > sg1.StudentId so a student does not match themselves.
M Why does a condition on the right table belong in the ON clause of a LEFT JOIN, not in WHERE? concept
WHERE runs after the join. A condition there on the right table removes the rows
where that table is NULL, which turns the LEFT JOIN into an INNER JOIN. In the ON
clause, it only limits which right rows can match, and unmatched left rows are kept.
Senior (6-9 yrs)
S A fee report total is exactly double what it should be. How do you find the cause? scenario
Doubled totals almost always come from a one-to-many join added after the data was already at the right level. Remove joins one at a time and watch the row count, or count rows per key to find the join that matches twice. Then aggregate before joining, or join only what the report needs.
How to answer: Show a systematic method (compare row counts with a known number, isolate the join), then the fix (pre-aggregate in a CTE). Use the SMS example of joining payments to guardians. Mention adding a test that checks the total against a simpler query.
S When would you use FULL JOIN or CROSS JOIN in a real system? concept
FULL JOIN compares two lists where either side can have rows the other lacks, such as
reconciling an imported fee file with the payments table. CROSS JOIN builds every
combination, such as creating exam papers for every class and subject. Both are rare in
daily screens.
How to answer: Give one concrete SMS use for each, and say how you keep CROSS JOIN results under control (filter both inputs first, check the expected row count). Mention that the bulk generator in Lesson 15 uses CROSS JOIN deliberately.
Architect / Lead (10+ yrs)
A A report needs eight joins and runs slowly every morning. Would you keep the joins or build a reporting table? design
First check whether the joins are the real problem: missing indexes on the join columns are the usual cause. If the report is still too slow, or many reports repeat the same joins, a reporting table or indexed view refreshed on a schedule can serve them, accepting slightly delayed data.
How to answer: Compare the options on freshness, cost, and complexity: tuned joins (always fresh), indexed views (fresh, slower writes), and scheduled reporting tables (fast reads, delayed data). Make the decision from measured timings and how fresh the report must be.
Resources
- Code for this lesson: git tag
lesson-17-joins(code repository coming soon) - Files:
queries/017-joins.sql,library-assignment/017-library-joins.sql - Video: coming soon
What's next
Joins give you rows. Next, you turn rows into numbers: counts, totals, and averages. Lesson 18: GROUP BY and aggregates
Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.