The Relational Model
Relational Algebra
So the output of any operation can be the input
JrCodex·7 min read
Jr Codex DBMS Notes
Level: Beginner–Intermediate Prerequisites: Chapter 2: Keys and Constraints Time to complete: ~20 minutes
Table of Contents
- Why an Algebra
- Selection and Projection
- The Set Operations
- Cartesian Product and Join
- Rename, Grouping and Division
- Composing Queries
- Why This Makes Optimisation Possible
- Summary & Next Steps
1. Why an Algebra
Relational algebra is a small set of operations that take relations as input and produce relations as output.
The Closure Property
─────────────────────────────────────────
Every operation returns a RELATION.
So the output of any operation can be the input
to the next, without limit. This is why queries
compose — subqueries, nested joins, CTEs all
work for this one reason.
Same idea as functions returning the same type
they consume.
─────────────────────────────────────────
Six Primitives
─────────────────────────────────────────
σ SELECTION choose ROWS
π PROJECTION choose COLUMNS
∪ UNION rows in either
− DIFFERENCE rows in the first, not second
× CARTESIAN every pairing
ρ RENAME rename a relation or attribute
Everything else — joins, intersection, division —
is DEFINED in terms of these six.
─────────────────────────────────────────
2. Selection and Projection
The two most common operations, and the two most often confused.
SELECTION σ (sigma) — filters ROWS
─────────────────────────────────────────
σ_condition(R)
σ_city='Pune'(students)
┌────┬───────┬───────┬───────┐
│ 1 │ Asha │ Pune │ 88 │
│ 3 │ Meera │ Pune │ 91 │
└────┴───────┴───────┴───────┘
Same columns. Fewer rows.
PROJECTION π (pi) — chooses COLUMNS
─────────────────────────────────────────
π_attributes(R)
π_name,city(students)
┌───────┬───────┐
│ Asha │ Pune │
│ Ravi │ Delhi │
│ Meera │ Pune │
└───────┴───────┘
Same rows. Fewer columns.
─────────────────────────────────────────
The Detail People Miss
─────────────────────────────────────────
Projection REMOVES DUPLICATES, because the result
is a set (Chapter 1).
π_city(students) ──► {Pune, Delhi} — 2 rows,
not 3
SQL's SELECT does NOT deduplicate by default —
you need SELECT DISTINCT. This is one of the
places SQL departs from the algebra (Chapter 4).
─────────────────────────────────────────
# The algebra, in Python, to make the shapes concrete.
def select(rel, predicate): # σ
return {t for t in rel if predicate(t)}
def project(rel, indices): # π
return {tuple(t[i] for i in indices) for t in rel} # set ──► dedupes
students = {(1,"Asha","Pune",88), (2,"Ravi","Delhi",74), (3,"Meera","Pune",91)}
print(select(students, lambda t: t[2] == "Pune")) # 2 tuples
print(project(students, [2])) # {('Pune',), ('Delhi',)} — 2, not 33. The Set Operations
Union Compatibility
─────────────────────────────────────────
∪, ∩ and − require both relations to have:
- the same NUMBER of attributes
- corresponding attributes from the same
DOMAINS
You cannot union students with courses. You can
union two relations of student names.
─────────────────────────────────────────
The Three
─────────────────────────────────────────
UNION R ∪ S in R, or S, or both
INTERSECTION R ∩ S in both
DIFFERENCE R − S in R but not S
INTERSECTION is not primitive:
R ∩ S = R − (R − S)
DIFFERENCE is the one that expresses NEGATION —
"students not enrolled in anything" — and it is
the hardest to express and the slowest to
compute.
─────────────────────────────────────────
4. Cartesian Product and Join
CARTESIAN PRODUCT ×
─────────────────────────────────────────
R × S pairs EVERY tuple of R with EVERY tuple
of S.
|R| = 1,000 and |S| = 1,000 ──► 1,000,000 rows
Almost never what you want alone. It is the raw
material joins are built from — and an
accidental Cartesian product is the classic
cause of a query that never finishes.
THETA JOIN ⋈_condition
─────────────────────────────────────────
R ⋈_c S = σ_c(R × S)
Pair everything, then keep only pairs satisfying
the condition. Conceptually. No real database
materialises the product first (Module 6).
NATURAL JOIN ⋈
─────────────────────────────────────────
Joins on ALL commonly-named attributes, and
removes the duplicated column.
students ⋈ enrollments
joins on student id automatically
Elegant in theory, risky in practice: add a
column named `created_at` to both tables and the
join silently changes meaning. Prefer an explicit
condition.
OUTER JOINS
─────────────────────────────────────────
Inner join DROPS tuples with no match. Outer
joins KEEP them, padding with NULL.
LEFT ⟕ keep all of R
RIGHT ⟖ keep all of S
FULL ⟗ keep all of both
"Students with their enrollments, INCLUDING
students enrolled in nothing" is a left outer
join — and the NULLs it produces are how you
find them.
─────────────────────────────────────────
5. Rename, Grouping and Division
RENAME ρ (rho)
─────────────────────────────────────────
ρ_new(R) or ρ_new(a→b)(R)
Needed for SELF-JOINS: to join students to
students, one copy must be renamed, or the
attribute names are ambiguous.
GROUPING AND AGGREGATION γ (gamma)
─────────────────────────────────────────
γ_city; COUNT(id)→n (students)
Not part of the original algebra — the original
had no aggregates — but standard in extended
versions, and it is what SQL's GROUP BY compiles
to.
DIVISION ÷
─────────────────────────────────────────
Answers "for ALL" questions.
"Students enrolled in EVERY course"
= π_student,course(enrollments) ÷ π_course(courses)
Rare, and worth recognising because SQL has no
division operator. It must be written as a
double NOT EXISTS — the notorious
"no course exists that this student is not
enrolled in".
─────────────────────────────────────────
6. Composing Queries
A Full Query, Built Up
─────────────────────────────────────────
"Names of Pune students scoring above 80, and
the titles of courses they are enrolled in."
π_name,title(
σ_city='Pune' ∧ marks>80 (students)
⋈_students.id = enrollments.student_id
enrollments
⋈_enrollments.course_id = courses.id
courses
)
Read it inside out: filter, then join, then join,
then keep two columns.
─────────────────────────────────────────
Order Matters for SPEED, Not for RESULT
─────────────────────────────────────────
These two produce IDENTICAL results:
σ_city='Pune'(students ⋈ enrollments)
(σ_city='Pune'(students)) ⋈ enrollments
The second filters BEFORE joining, so the join
processes far fewer rows.
That rewrite is called PUSHING DOWN THE
SELECTION, and it is the single most valuable
optimisation a query planner performs.
─────────────────────────────────────────
7. Why This Makes Optimisation Possible
This is why a whole chapter of algebra earns its place in a practical curriculum.
The Chain of Reasoning
─────────────────────────────────────────
1. Relational algebra has ALGEBRAIC LAWS, like
ordinary arithmetic:
σ_a(σ_b(R)) = σ_b(σ_a(R)) commutative
σ_a(R ⋈ S) = σ_a(R) ⋈ S pushdown
(when a only mentions R)
π_x(π_y(R)) = π_x(R) when x ⊆ y
R ⋈ S = S ⋈ R commutative
2. These laws are PROVABLE, because relations are
sets and the operations are set operations.
3. So the optimiser can transform your query into
a different expression and KNOW the result is
identical.
4. Which means it can search for the CHEAPEST
equivalent expression — the subject of
Module 6.
─────────────────────────────────────────
The Payoff, Stated Plainly
─────────────────────────────────────────
You write a query in whatever order reads
clearly. The database rewrites it into something
potentially very different, and is
MATHEMATICALLY CERTAIN the answer is unchanged.
That certainty is what the algebra buys, and it
is why the declarative promise from Module 1 is
keepable rather than aspirational.
─────────────────────────────────────────
8. Summary & Next Steps
Key Takeaways
- Every operation takes relations and returns a relation, and that closure property is why queries compose without limit.
- Selection filters rows, projection chooses columns and removes duplicates — SQL's
SELECTdoes not deduplicate, which is a deliberate departure. - Joins are Cartesian product plus selection conceptually; outer joins keep unmatched tuples padded with NULL, which is how you find "rows with nothing matching".
- The algebra's provable laws let the optimiser rewrite a query into a cheaper equivalent and be certain the result is identical — pushing selections below joins being the highest-value rewrite.
Concept Check
π_city(students)on a 3-row table returns 2 rows. Why, and what does SQL do differently?- Write out why
σ_city='Pune'(students ⋈ enrollments)and(σ_city='Pune'(students)) ⋈ enrollmentsgive the same answer but different performance. - Why can an optimiser reorder your joins without risking a wrong answer?
Next Chapter
→ Chapter 4: From Algebra to SQL
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DBMS Index