The Relational Algebra

Suppose a university database stores millions of `Student`, `Course`, and `Enrollment` records. Someone asks: "which students got an A in a database course taught in Fall 2023?" A human can answer this by describing what they want. A machine needs to know how to compute it. It must decide which rows to pick, which columns to keep, and how to combine tables. The relational algebra is the formal bridge between those two worlds. It is a small, closed set of operators that transforms relations into relations. This gives every declarative query a precise, mechanical meaning. This matters because every SQL query a system executes is, underneath, translated into an expression in this algebra (or an equivalent). Query optimizers do not reason about SQL text directly. Instead, they reason about algebraic expression trees. They rewrite these trees into equivalent but cheaper forms using algebraic identities. Without a formal algebra, "cheaper" and "equivalent" would have no rigorous meaning. Query optimization would then be guesswork. The big picture is this: the Relational Model gives us relations — sets of tuples over named attributes — as the sole data structure. The relational algebra gives us a minimal set of operators that are closed over relations. Feed relations in, and get a relation out. That closure property lets operators compose into arbitrarily deep expression trees. This works exactly the way arithmetic operators compose into expression trees over numbers. This article builds the algebra from its six fundamental operators. These are selection, projection, union, set difference, Cartesian product, and rename. It shows how join is derived from product and selection. It gives working pseudocode for evaluating an expression tree. It also proves the basic complexity and correctness properties an implementer needs to trust the machinery. It closes by pointing toward Relational Calculus, the declarative twin of this algebra. It also points toward how algebraic reasoning underlies Lossless-Join Decomposition in schema design.

Key takeaways
  • The relational algebra is a small, closed set of operators: relations in, a relation out — always.
  • Six operators are fundamental: selection (σ), projection (π), union (∪), difference (−), Cartesian product (×), and rename (ρ).
  • Join (⋈) is not primitive — it is defined as product followed by selection, a fact that drives query optimization.
  • Every algebra expression is a tree; evaluating it bottom-up computes the query result one relation at a time.
  • Equivalent expressions can have wildly different costs — algebraic rewriting (e.g., pushing selections down) is how optimizers save time.
  • The algebra is procedural (it specifies how), while Relational Calculus is declarative (it specifies what) — they are provably equivalent in expressive power.
The core insight

The relational algebra is a minimal, closed set of operators over relations that turns declarative queries into mechanically evaluable, rewritable expression trees.

The idea

A relation is a set of tuples over a fixed schema of named attributes. The relational algebra defines operators that take one or two relations as input and produce a new relation as output. This closure property — relations map to relations — is the algebra's defining feature, analogous to how `+` and `×` on integers produce integers, letting you nest `(2+3)×4` without leaving the world of integers. Because relational operators are closed, you can nest them: `σ(π(R ⋈ S))` is itself a relation, ready to be fed into another operator. There are six fundamental operators, chosen because no smaller set can express everything the algebra expresses, and because every other useful operator (join, intersection, division) can be derived from them:

  • Selection (σ), written σ_predicate(R): keeps rows of R satisfying a Boolean predicate. It filters horizontally.
  • Projection (π), written π_attributes(R): keeps only specified columns, discarding the rest, and removes any duplicate rows that result (since a relation is a set). It filters vertically.
  • Union (∪), written R ∪ S: the set union of two relations, valid only when R and S are union-compatible — same number of attributes, matching domains.
  • Set difference (−), written R − S: tuples in R that are not in S, also requiring union-compatibility.
  • Cartesian product (×), written R × S: every tuple of R paired with every tuple of S, with no compatibility restriction — attribute names are simply concatenated (and renamed if they clash).
  • Rename (ρ), written ρ_S(A₁,…,Aₙ)(R): relabels a relation and/or its attributes without changing its contents, resolving name clashes before a product or join.

Every other operator is derived — expressible as a combination of the six. Intersection R ∩ S equals R − (R − S). Join, the algebra's most celebrated derived operator, is defined as ``` R ⋈_θ S ≡ σ_θ(R × S) ``` a Cartesian product followed by a selection on the join condition θ. The common special case, natural join, further projects away duplicate columns that share a name across R and S, matching tuples where those shared attributes agree. This derivation is not a curiosity — it is the single fact that makes cost-based query optimization possible, because it tells the optimizer that a join can legally be rewritten as its constituent product-plus-selection, and, crucially, that selections and projections can often be pushed inside a join to shrink intermediate results before the expensive pairing happens.

How it works

An algebra query is not evaluated as flat text; it is parsed into an expression tree, where leaves are base relations and internal nodes are operators. Evaluation proceeds bottom-up: each node consumes the relation(s) produced by its children and produces a new relation, until the root yields the final answer. Consider the query "names of students who got an A in a course taught by 'Kirmani'," over relations `Enrollment(sid, cid, grade)`, `Course(cid, title, instructor)`, `Student(sid, name)`. ``` π_name( σ_(grade='A' ∧ instructor='Kirmani')( Enrollment ⋈_(Enrollment.cid = Course.cid) Course ) ⋈_(sid) Student ) ``` Below is language-agnostic pseudocode for evaluating an arbitrary expression tree, plus the concrete implementations of the physical operators it dispatches to. Each function returns a materialized relation — a set of tuples with a schema — matching the closure property. ``` // A relation is a pair: (schema: list of attribute names, tuples: set of tuples) function EVALUATE(node): if node is a LEAF (base relation R): return R.load() // read from storage else if node.op == SELECT(predicate): child = EVALUATE(node.child) result.schema = child.schema result.tuples = { t in child.tuples : predicate(t) } return result else if node.op == PROJECT(attrs): child = EVALUATE(node.child) result.schema = attrs result.tuples = SET({ t.restrict(attrs) : t in child.tuples }) // dedup! return result else if node.op == UNION: left = EVALUATE(node.left) right = EVALUATE(node.right) assert left.schema ~ right.schema // union-compatible result.schema = left.schema result.tuples = left.tuples ∪ right.tuples return result else if node.op == DIFFERENCE: left = EVALUATE(node.left) right = EVALUATE(node.right) assert left.schema ~ right.schema result.schema = left.schema result.tuples = { t in left.tuples : t not in right.tuples } return result else if node.op == PRODUCT: left = EVALUATE(node.left) right = EVALUATE(node.right) result.schema = left.schema + right.schema // concatenate, rename clashes result.tuples = { concat(t1, t2) : t1 in left.tuples, t2 in right.tuples } return result else if node.op == RENAME(newName, newAttrs): child = EVALUATE(node.child) return relabel(child, newName, newAttrs) else if node.op == JOIN(theta): // derived operator return EVALUATE( SELECT(theta, PRODUCT(node.left, node.right)) ) ``` Two implementation notes matter for correctness. First, projection must deduplicate — because a relation is a set of tuples, projecting onto fewer columns can make previously distinct tuples collide, and the algebra's semantics demand the duplicates be removed (this is exactly why `π` is more expensive than it looks: it needs a sort or hash pass, not just a column copy). Second, union and difference require union-compatibility — same arity, corresponding domains — otherwise the result is not a well-formed relation and the expression is simply illegal, not merely inefficient. A real query engine does not evaluate `JOIN` literally as `SELECT(PRODUCT(...))` — it substitutes a physical operator (hash join, sort-merge join, nested-loop join) that computes the same result set far more efficiently by never materializing the full product. The algebra guarantees these physical operators are interchangeable precisely because they all compute the same relation as `σ_θ(R × S)` — the algebra is the specification, the physical operator is the implementation.

Figure
An algebra expression tree evaluated bottom-up
Enrollment Course Student ⋈ cid = cid Enrollment ⋈ Course σ grade='A' ∧ instructor='Kirmani' ⋈ sid = sid result ⋈ Student π name Evaluation order: leaves → σ/⋈ → π (bottom-up, each node yields a relation)

Complexity & correctness

Correctness rests on an invariant maintained by every operator: the input to each node is a well-formed relation (a finite set of tuples with a fixed schema), and the output is also a well-formed relation. This is proved by structural induction on the expression tree.

  • Base case: a leaf is a stored relation, well-formed by definition (the Relational Model guarantees this at the storage layer).
  • Inductive step: assume the child/children of a node are well-formed relations. Selection filters a set by a predicate — still a set, same schema, so well-formed. Projection maps tuples to a shorter tuple and takes the resulting set (removing duplicates) — still a well-formed relation, by construction. Union and difference are standard set operations on two sets of the same schema (guaranteed by the union-compatibility check), so the result is a well-formed set of that schema. Product pairs every tuple of one relation with every tuple of another, producing a set of concatenated tuples over the concatenated schema — well-formed as long as attribute name clashes are resolved by rename.

Since every operator preserves well-formedness given well-formed inputs, induction gives well-formedness for the whole tree, including the root — i.e., every syntactically valid algebra expression denotes a relation. This is exactly the closure property the whole system depends on.

Complexity. Let R have `r` tuples and S have `s` tuples, both over relations with tuples of bounded size (schema is fixed and small compared to `r, s`).

OperatorTime (naive)Time (with index/sort/hash)Space
σ (selection)O(r) — scan every tuple, test predicateO(log r) or O(1) with an index on the predicate's attributeO(r) worst case (all tuples match)
π (projection)O(r log r) — sort to dedupeO(r) expected with hashingO(r)
∪ (union)O(r + s) with sorted merge or hashingsameO(r + s)
− (difference)O(r + s) with sorted merge or hashingsameO(r)
× (product)O(r·s) — every pair of tuplesno asymptotic improvement possible; product is inherently quadraticO(r·s)
⋈ (join, via σ∘×)O(r·s) naivelyO(r + s) expected with a hash join, O((r+s) log(r+s)) with sort-mergeO(r + s) for hash join

The critical honesty point: product is genuinely quadratic, and a naive evaluator that literally computes `σ(R × S)` for every join pays O(r·s) time and space, materializing a huge intermediate relation before filtering it down. This is why real systems never implement join this way. A hash join builds a hash table on the smaller relation's join attribute (O(r) to build, assuming r ≤ s) and then streams S, probing the hash table in expected O(1) per tuple, giving expected O(r + s) total time — orders of magnitude better than O(r·s) for large r, s. A sort-merge join sorts both sides (O(r log r + s log s)) and merges in O(r + s), giving O((r+s) log(r+s)) overall, which is worse than hash join in expectation but has better worst-case guarantees and produces sorted output "for free," useful when a subsequent operator wants sorted input. The algebra's equivalence rules — e.g., σ commutes past projection when the predicate only mentions retained attributes, and selection distributes over union/difference/product ("push selections down") — are what let an optimizer replace a query tree that computes a huge product early with an equivalent tree that filters first and joins last, shrinking every intermediate relation the query touches. The rewriting is sound precisely because both trees denote the same relation by the well-formedness/closure proof above — algebraic equivalence, not guesswork, licenses the optimization.

Worked example

Take three small relations. `Enrollment(sid, cid, grade)`:

sidcidgrade
101CS340A
101CS210B
102CS340A
103CS340C

`Course(cid, title, instructor)`:

cidtitleinstructor
CS340DatabasesKirmani
CS210AlgorithmsRao

`Student(sid, name)`:

sidname
101Amara
102Ben
103Chen

Query: names of students with grade A in a course taught by Kirmani.

Step 1 — Cartesian product `Enrollment × Course` conceptually produces all 4×2 = 8 combinations (shown only as a concept — a real engine never materializes this): 8 rows total, pairing every enrollment with every course row.

Step 2 — Selection `σ_(Enrollment.cid = Course.cid ∧ grade='A' ∧ instructor='Kirmani')` on that product keeps only rows where the join condition holds and the extra predicate holds:

sidcidgradetitleinstructor
101CS340ADatabasesKirmani
102CS340ADatabasesKirmani

(The (101, CS210) row is dropped — Rao, not Kirmani, and grade B. The (103, CS340) row is dropped — grade C.)

Step 3 — Join with Student on `sid`, keeping only matching rows:

sidcidgradetitleinstructorname
101CS340ADatabasesKirmaniAmara
102CS340ADatabasesKirmaniBen

Step 4 — Projection `π_name` keeps only the `name` column and deduplicates:

name
Amara
Ben

Trace the cost: the naive plan materializes the full 8-row product before filtering, i.e. O(r·s) = O(4×2). A real optimizer instead pushes the `grade='A'` selection down to `Enrollment` before the join (shrinking it from 4 rows to 2), and pushes `instructor='Kirmani'` down to `Course` before the join (shrinking it from 2 rows to 1) — so the join itself pairs 2 rows with 1 row instead of 4 with 2. On this toy example the saving is small, but on tables with 10⁶ and 10⁴ rows respectively, filtering first versus filtering after a full product is the difference between a fast query and one that never returns.

Figure
Pushing selection below join shrinks intermediate relations
Naive (filter after join) σ grade='A' ∧ instructor='Kirmani' ⋈ (4×2 pairs) Enrollment Course Optimized (filter before join) ⋈ (2×1 pairs) σ grade='A' σ instr='Kirmani' Enrollment Course Same result, less intermediate data — equivalence licenses the rewrite

Common pitfalls & traps

PitfallWhy it happensFix
Forgetting projection dedupsTreating π like a plain column copy, ignoring set semanticsRemember a relation is a set — π must remove duplicate resulting tuples
Union/difference on mismatched schemasCombining relations with different arity or incompatible domainsCheck union-compatibility first; rename/cast attributes to align schemas
Confusing θ-join with natural joinAssuming ⋈ always drops duplicate columnsθ-join keeps all columns and filters by θ; natural join additionally projects shared columns away
Materializing the full Cartesian productEvaluating join literally as σ(R × S) on large tablesUse physical operators (hash join, sort-merge join) that never build the full product
Assuming rewritten queries always helpPushing selection down blindly, even when it's not selectiveCost-based optimizers estimate selectivity; push down only when it shrinks data
Ambiguous attribute names after productTwo joined relations share a column name (e.g., both have `id`)Apply ρ (rename) before product/join to disambiguate

Pioneers & history

The relational algebra was introduced alongside the relational model itself, as the formal query mechanism that gave the model teeth: a rigorous way to say what a query means, independent of any particular database system's internals.

Origins · 1 minEdgar F. Codd and how this was discovered
EF
Edgar F. Codd
1923–2003 · IBM Research, San Jose, 1970s

Codd, a mathematician and computer scientist, proposed the relational model in 1970 to free applications from the brittle, pointer-based navigation of hierarchical and network databases then dominant in industry. He defined the relational algebra as the model's formal query language, proving that a small set of set-theoretic operators — selection, projection, union, difference, product, join — suffices to express arbitrary relational queries, and later showed this procedural algebra is equivalent in power to a declarative relational calculus based on first-order logic. His work won the 1981 Turing Award and directly seeded SQL and every relational database system that followed.

History
1970
— Codd publishes "A Relational Model of Data for Large Shared Data Banks," introducing relations and the algebra.
1972
— Codd formalizes the relational algebra and relational calculus and proves their equivalence in expressive power.
1974
— IBM's System R project begins, implementing SQL (originally SEQUEL) as a practical syntax over algebraic semantics.
1979
— Oracle ships the first commercially available SQL-based relational database management system.
1981
— Codd receives the ACM Turing Award for his foundational contributions to database theory.

Where this matters

Every production relational database — PostgreSQL, MySQL, Oracle, SQL Server, SQLite, and cloud warehouses like BigQuery, Snowflake, and Redshift — parses SQL into an internal algebraic (or near-algebraic) representation before executing it. Query optimizers work entirely in this algebraic space: they enumerate equivalent expression trees using rewrite rules derived directly from the algebra's laws (selection pushdown, join reordering, projection pruning) and choose the cheapest one using cost estimates built on the complexity facts above. Distributed query engines such as Apache Spark's Catalyst optimizer and Apache Calcite (used inside Hive, Flink, and Druid) are explicit, general-purpose implementations of relational-algebra rewriting and cost-based planning, decoupled from any single storage engine. Even NoSQL and dataframe libraries that reject full SQL — pandas, Spark DataFrames, Polars — still expose selection, projection, join, and union as their core operations, because the algebra captures the minimal, composable vocabulary that any tabular data-processing system eventually needs. Understanding the algebra is also the direct prerequisite for reasoning about Relational Calculus, its declarative counterpart, and for understanding why certain schema designs guarantee Lossless-Join Decomposition — a property stated and proved entirely in the algebra's own vocabulary of join and projection.

Ask the tutor: How does a cost-based query optimizer decide which algebraically equivalent expression tree to actually execute?

Is the relational algebra actually used to write real queries, or is it purely theoretical?

It is mostly a conceptual and implementation tool rather than something end users type directly. Application developers write SQL, which is declarative — it states what result is wanted. The database's query optimizer translates that SQL into an internal algebraic expression tree, rewrites it into an equivalent but cheaper tree using the algebra's identities, and only then executes it using physical operators. So while you rarely write σ or π by hand, every SQL query you run is compiled into exactly this algebra internally, making it essential for understanding what the optimizer is actually doing and why some equivalent-looking queries run at very different speeds.

What is the difference between a θ-join and a natural join?

A θ-join, written R ⋈_θ S, is literally σ_θ(R × S): it pairs every tuple of R with every tuple of S and keeps only pairs satisfying condition θ, retaining all columns from both relations, including any duplicated attribute names. A natural join additionally requires equality on all attributes with matching names across R and S, and then projects away the duplicate copies, keeping just one instance of each shared attribute. Natural join is a convenient shorthand for the extremely common case of joining on a shared foreign key, but it is strictly less general — it cannot express a join condition like "R.start_date < S.end_date" that isn't equality on identically named columns.

Why is Cartesian product included as a fundamental operator if it's rarely used directly?

Product is included because it is the only operator that combines two relations without any restriction on how their tuples relate, which makes it maximally general — every other binary combination, including join, can be defined in terms of it plus a filter. Its inclusion is a deliberate minimalist design: rather than adding "join" as a separate primitive with its own semantics, Codd defined join derivatively from product and selection, keeping the fundamental operator set small while still being able to express every join condition imaginable, including inequality joins that a natural join cannot express.

How does the relational algebra relate to the relational calculus mentioned as a follow-up topic?

The relational algebra is procedural — an expression specifies a sequence of operations to perform. The Relational Calculus is declarative — a formula specifies properties the desired tuples must satisfy, using first-order logic with quantifiers, without prescribing how to find them. Codd proved these two are equivalent in expressive power: any query expressible in the calculus can be translated into an equivalent algebra expression, and vice versa, a result often called Codd's theorem. SQL's `SELECT`/`WHERE` syntax reads much like calculus (declarative), yet compiles into algebra (procedural) for execution — the algebra is what actually gets optimized and run.

Does relational algebra assume relations are sets, and does that ever cause problems in real SQL?

Yes — the pure algebra treats relations as sets, meaning duplicate tuples are impossible and projection must deduplicate. Real SQL, however, treats tables as multisets (bags) by default for performance reasons — deduplication is expensive, so `SELECT` doesn't remove duplicates unless you explicitly write `SELECT DISTINCT`. This is a deliberate, documented divergence from pure relational algebra semantics, made for practical efficiency, and it's a classic source of confusion for students translating textbook algebra exercises directly into SQL without accounting for the bag-versus-set distinction.

What is the connection between relational algebra and lossless-join decomposition in schema design?

When a schema is normalized — split into smaller relations to eliminate redundancy — it must be possible to reconstruct the original data exactly by joining the pieces back together. This property, Lossless-Join Decomposition, is stated and proved entirely in the vocabulary of this algebra: a decomposition of R into R1 and R2 is lossless if and only if R equals the natural join of R1 and R2, no more and no fewer tuples. Verifying this join-equality condition using functional dependencies is a direct application of algebraic reasoning about projection and natural join, which is why mastering the algebra first is a prerequisite for schema-design theory.

Can every SQL query be expressed in this six-operator core algebra?

The pure relational algebra as presented here — with its set semantics and lack of aggregation — cannot directly express features like `GROUP BY`/aggregate functions (`SUM`, `COUNT`, `AVG`), sorting (`ORDER BY`), or recursive queries, since these go beyond the original six operators and their derivatives. Real database theory extends the algebra with an explicit aggregation operator (often written γ) and, for recursion, a transitive-closure operator, precisely to cover these SQL features. The core six-operator algebra remains foundational, though, because it captures the relational part of virtually every query: filtering, combining, and restructuring tables, with extensions layered cleanly on top.

Before this

Where this leads