SQL Fundamentals
Joins
Good design splits data across tables so nothing is stored twice (Module 4 makes this rigorous). Joins put it back together on demand.
JrCodex·8 min read
Jr Codex DBMS Notes
Level: Beginner–Intermediate Prerequisites: Chapter 2: Querying with SELECT Time to complete: ~25 minutes
Table of Contents
- Why Joins Exist
- INNER JOIN
- LEFT JOIN and Finding Absences
- RIGHT, FULL and CROSS
- Self Joins
- Multi-Table Joins
- The Four Join Mistakes
- Summary & Next Steps
1. Why Joins Exist
Good design splits data across tables so nothing is stored twice (Module 4 makes this rigorous). Joins put it back together on demand.
The Trade
─────────────────────────────────────────
Storing the student's name on every enrollment
row would make queries simple and updates
dangerous — change a name and you must find
every copy.
Storing it once and JOINING costs a little at
read time and eliminates an entire class of
inconsistency.
Joins are the price of correctness, and the
price is low because of indexes (Module 5).
─────────────────────────────────────────
2. INNER JOIN
Returns only rows that match on both sides.
SELECT s.name, c.title, e.grade
FROM students s
JOIN enrollments e ON s.id = e.student_id
JOIN courses c ON e.course_id = c.id
ORDER BY s.name, c.title;What Comes Back
─────────────────────────────────────────
Asha | Databases | A
Asha | Networks | B
Asha | Statistics | A
Divya | Databases | B
Divya | Ethics | NULL
Karan | Networks | D
...
Sam is ABSENT — no enrollments, so no matching
rows. This is the defining behaviour of an inner
join, and the source of most "where did my rows
go" confusion.
─────────────────────────────────────────
ON vs WHERE
─────────────────────────────────────────
ON defines HOW the tables relate
WHERE filters the joined result
For an INNER join the two are interchangeable in
result. For an OUTER join they are NOT — see
Section 7.
Keep the relationship in ON and the filtering in
WHERE. It reads better and it stays correct when
someone later changes the join type.
─────────────────────────────────────────
3. LEFT JOIN and Finding Absences
Keeps every row from the left table, padding with NULL where there is no match.
SELECT s.name, c.title, e.grade
FROM students s
LEFT JOIN enrollments e ON s.id = e.student_id
LEFT JOIN courses c ON e.course_id = c.id
ORDER BY s.name;Now Sam Appears
─────────────────────────────────────────
Asha | Databases | A
...
Sam | NULL | NULL ← kept, padded
─────────────────────────────────────────
-- The anti-join: rows on the left with NO match on the right.
SELECT s.name
FROM students s
LEFT JOIN enrollments e ON s.id = e.student_id
WHERE e.student_id IS NULL; -- only unmatched rows surviveThe Anti-Join Pattern
─────────────────────────────────────────
LEFT JOIN, then WHERE right_column IS NULL.
This is how you answer every "which X has no Y"
question:
customers who never ordered
products never sold
users with no login in 90 days
Test the right table's JOIN KEY for NULL, not
some other column — a nullable column could be
NULL in a matched row too, which would silently
include rows that DO match.
─────────────────────────────────────────
4. RIGHT, FULL and CROSS
-- RIGHT JOIN: keeps all right-hand rows. Identical to a LEFT JOIN with the
-- tables swapped, which is why most codebases just use LEFT consistently.
SELECT c.title, e.student_id
FROM enrollments e
RIGHT JOIN courses c ON e.course_id = c.id;
-- FULL OUTER JOIN: keeps unmatched rows from BOTH sides.
SELECT s.name, c.title
FROM students s
FULL OUTER JOIN enrollments e ON s.id = e.student_id
FULL OUTER JOIN courses c ON e.course_id = c.id;
-- CROSS JOIN: every combination. Deliberate here — a grid of all
-- student/course pairs, to find which are missing.
SELECT s.name, c.title
FROM students s
CROSS JOIN courses c; -- 6 students × 4 courses = 24 rowsPractical Notes
─────────────────────────────────────────
RIGHT JOIN is rarely used. Prefer LEFT and order
your tables accordingly — mixing directions in
one query is hard to read.
FULL OUTER JOIN is genuinely useful for
reconciliation ("what is in A but not B, and B
but not A"). SQLite gained it recently; MySQL
still lacks it and needs a UNION of two outer
joins.
CROSS JOIN is right when you want a complete
grid — report scaffolding, date spines. An
ACCIDENTAL cross join is Section 7's first
mistake.
─────────────────────────────────────────
5. Self Joins
A table joined to itself, which requires aliasing — the ρ rename from Module 2, Chapter 3.
-- Pairs of students in the same city.
SELECT a.name AS student_a, b.name AS student_b, a.city
FROM students a
JOIN students b ON a.city = b.city
AND a.id < b.id -- avoids self-pairs AND mirrored duplicates
ORDER BY a.city;Why a.id < b.id Rather Than a.id <> b.id
─────────────────────────────────────────
With <>, you get BOTH (Asha, Meera) and
(Meera, Asha) — each pair twice.
With <, each unordered pair appears exactly once,
and self-pairing is excluded for free.
A small idiom worth remembering; it comes up
whenever you enumerate pairs.
─────────────────────────────────────────
-- Self join on a hierarchy: employees and their managers.
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id; -- LEFT keeps the CEO, who has no manager6. Multi-Table Joins
-- CS-department courses taken by Pune students, graded A or B.
SELECT s.name, c.title, c.dept, e.grade
FROM students s
JOIN enrollments e ON s.id = e.student_id
JOIN courses c ON e.course_id = c.id
WHERE s.city = 'Pune'
AND c.dept = 'CS'
AND e.grade IN ('A','B')
ORDER BY s.name, c.title;You Write the Order; the Optimiser Ignores It
─────────────────────────────────────────
The join order in your SQL is a suggestion at
most. The optimiser reorders joins freely,
because relational algebra guarantees the result
is the same (Module 2, Chapter 3).
So write joins in the order that READS clearly —
usually following the relationships outward from
the main entity. Optimising by hand-ordering
joins is wasted effort; Module 6 covers what
actually influences the plan.
─────────────────────────────────────────
7. The Four Join Mistakes
1. THE ACCIDENTAL CROSS JOIN
─────────────────────────────────────────
FROM students s, enrollments e -- no ON!
WHERE s.city = 'Pune'
Every student paired with every enrollment.
6 × 12 = 72 rows instead of the intended handful,
and it grows multiplicatively with the data.
Symptom: far too many rows, or a query that
never finishes.
2. WHERE ON THE OUTER SIDE
─────────────────────────────────────────
SELECT s.name, e.grade
FROM students s
LEFT JOIN enrollments e ON s.id = e.student_id
WHERE e.grade = 'A'; -- ← silently kills the LEFT JOIN
Sam's padded row has grade NULL, and NULL = 'A'
is UNKNOWN, so the WHERE drops him. You now have
an INNER join with extra steps.
FIX: put the condition in ON.
LEFT JOIN enrollments e
ON s.id = e.student_id AND e.grade = 'A'
3. FAN-OUT INFLATING AGGREGATES
─────────────────────────────────────────
Joining a one-to-many relationship MULTIPLIES
rows on the "one" side.
Join students to their 3 enrollments and each
student's row appears 3 times — so SUM(s.marks)
now triple-counts.
FIX: aggregate the many-side FIRST in a subquery
(Chapter 5), then join the result.
4. COUNT(*) IN AN OUTER JOIN
─────────────────────────────────────────
COUNT(*) counts ROWS, including the NULL-padded
ones — so Sam counts as 1 enrollment.
COUNT(e.course_id) counts NON-NULL values, so
Sam correctly counts as 0.
In an outer join, count a COLUMN from the outer
side, never *.
─────────────────────────────────────────
8. Summary & Next Steps
Key Takeaways
- Inner joins drop rows without a match on either side; left joins keep the left rows padded with NULL, which is how absences become visible.
- The anti-join —
LEFT JOINthenWHERE right.key IS NULL— answers every "which X has no Y" question, and must test the join key rather than any nullable column. - A filter on the outer table placed in
WHERErather thanONsilently converts a left join into an inner join. - Joining a one-to-many relationship multiplies rows, so aggregates over the "one" side over-count unless the many side is aggregated first.
Concept Check
- Sam disappears from an inner join but appears in a left join. Explain both, in one sentence each.
- Why must an anti-join test the right table's join key for NULL rather than any convenient column?
- In a left join, why does
COUNT(*)give the wrong answer whereCOUNT(e.course_id)gives the right one?
Next Chapter
→ Chapter 4: Aggregation and Grouping
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DBMS Index