Technopedia

The technologist’s encyclopedia

Relational Algebra

Relational algebra is the formal, set-theoretic query language of the relational model. It defines what a query computes (semantics) independently of how it is executed. Every SQL query can be expressed as an extended relational algebra expression — although SQL itself goes beyond the pure algebra, with bag semantics, three-valued NULL logic, ORDER BY, and LIMIT.

A relation is a set of tuples over a schema: the notions of relation, tuple, attribute, and key are defined in Schema.

Authoritative sources

Core operations

OperationNotationEffect
Selectσp(R)Rows of R satisfying predicate p
Projectπa₁,…,aₖ(R)Columns a₁,…,aₖ of R (duplicates removed)
Cartesian productR × SAll pairs (r, s) with r ∈ R, s ∈ S
JoinR ⋈ SCartesian product filtered to matching join attributes
UnionR ∪ SRows in R or S (duplicates removed)
DifferenceR − SRows in R but not in S
DivisionR ÷ SRows of R that pair with every row of S
RenameρS(R)Rename relation R to S

The first seven operations (select, project, Cartesian product, union, difference, division, rename) form Codd’s original algebra. Join is a derived operation — an abbreviation for a Cartesian product followed by a select, R⋈S≡σp(R×S)R \bowtie S \equiv \sigma_p(R \times S) — listed in the table because of its overwhelming practical importance. So when a text speaks of “the seven core operations”, join is not counted.

Details

Set vs. bag semantics: pure relational algebra operates on sets (no duplicates, no order). In practice, SQL uses bag (multiset) semantics: SELECT without DISTINCT preserves duplicates. The bag variant of project is written π∗\pi^*.

Closure: the set of all expressions built from these operations is closed: the result of any expression is itself a relation, so operations can be composed arbitrarily.

Expressiveness: relational algebra is equivalent in expressive power to the relational calculus (Codd’s theorem). Any query expressible in one is expressible in the other. The practical consequence: any SQL query can be translated to a relational algebra expression, and the query optimizer works by rewriting algebraic expressions.

Reading an expression: nested expressions are read from the inside out — the innermost operation executes first, and its result (a relation, by closure) feeds the next operation outward.

Worked example — read πname(σcity=′Paris′(customers))\pi_{\text{name}}\bigl(\sigma_{\text{city}='Paris'}(\text{customers})\bigr):

  1. Innermost: σcity=′Paris′(customers)\sigma_{\text{city}='Paris'}(\text{customers}) — keep only the rows of customers whose city is Paris. Say this yields {(1, Alice, Paris), (3, Carol, Paris)}.
  2. Outer: πname(⋅)\pi_{\text{name}}(\cdot) — keep only the name column of that intermediate relation, removing duplicates. Result: {Alice, Carol}.

The same inside-out reading scales to any depth: in πname(σcity=′Paris′(customers)⋈orders)\pi_{\text{name}}(\sigma_{\text{city}='Paris'}(\text{customers}) \bowtie \text{orders}), first filter customers, then join with orders, then project to name.

Example problem: in the expression πcity(σamount>100(orders))\pi_{\text{city}}\bigl(\sigma_{\text{amount}>100}(\text{orders})\bigr), what does the innermost operation produce, and what does the whole expression return, given orders = {(10, 1, 200), (11, 1, 50), (12, 3, 150)} with schema orders(oid, cust_id, amount)?

Solution
  1. Innermost σamount>100(orders)\sigma_{\text{amount}>100}(\text{orders}) keeps rows with amount over 100: {(10, 1, 200), (12, 3, 150)} — the row (11, 1, 50) is eliminated.
  2. Outer πcity\pi_{\text{city}} is ill-typed here: city is not an attribute of orders. This is the point of the exercise — every attribute named in a projection must exist in the schema of its input relation. The correct expression would first join with customers (which has the city column), e.g. πcity(σamount>100(orders)⋈customers)\pi_{\text{city}}(\sigma_{\text{amount}>100}(\text{orders}) \bowtie \text{customers}).

Joins

A join R⋈SR \bowtie S on attribute aa produces all pairs (r,s)(r, s) where r.a=s.ar.a = s.a. The result has all columns of RR and SS (with the join column appearing once). Joins are the most common operation in real queries: almost every query that touches more than one table joins.

Join types:

How joins are executed (nested loop, hash join, sort-merge join, and their complexities) is covered in SQL & Query Planning.

Details

Join correctness: the inner join R⋈R.a=S.aSR \bowtie_{R.a = S.a} S is equivalent to σR.a=S.a(R×S)\sigma_{R.a = S.a}(R \times S). The outer join extends this: a left outer join is (R⋈S)∪(R−πR(R⋈S))(R \bowtie S) \cup (R - \pi_R(R \bowtie S)) with NULLs filled in the SS columns.

Natural join: R⋈SR \bowtie S joins on all shared attribute names. An equi-join specifies the join predicate explicitly: R⋈R.a=S.bSR \bowtie_{R.a = S.b} S.

Theta join: the join predicate is any comparison (R.a<S.bR.a < S.b, R.a≠S.bR.a \neq S.b). An equi-join is the special case where the predicate is equality.

Multi-way joins: R⋈S⋈TR \bowtie S \bowtie T is ambiguous without parentheses. The query optimizer (see SQL & Query Planning) chooses the join order that minimizes cost. Join is associative and commutative (for inner joins), so the optimizer is free to reorder.

Semi-join (R⋉SR \ltimes S): returns rows of RR that have a match in SS, without duplicating SS columns. Useful for EXISTS subqueries.

Anti-join: rows of RR with no match in SS. Useful for NOT EXISTS.

Example problem: with customers = {(1, Alice, Paris), (2, Bob, Lyon)} and orders = {(10, 1, 200), (11, 1, 50)}, what is the left outer join customers⋈leftorders\text{customers} \bowtie_{\text{left}} \text{orders} on id = cust_id?

Solution

Alice (id 1) matches both orders, so she contributes two rows. Bob (id 2) has no matching order, so he contributes one row with the orders columns filled with NULL:

(1, Alice, Paris, 10, 1, 200)
(1, Alice, Paris, 11, 1, 50)
(2, Bob,   Lyon,  NULL, NULL, NULL)

An inner join would have dropped Bob entirely; the left outer join keeps him. In SQL: SELECT * FROM customers LEFT JOIN orders ON customers.id = orders.cust_id;

Visual: how a join works

R (customers) id | name | city 1 | Alice| Paris 2 | Bob | Lyon S (orders) oid | cust_id | amt 10 | 1 | 200 11 | 1 | 50 ⋈ on id=cust_id R ⋈ S id | name | city | oid | cust_id | amt 1 | Alice| Paris | 10 | 1 | 200 1 | Alice| Paris | 11 | 1 | 50 (Bob has no matching order → excluded)

Interactive: step-by-step relational algebra

The simulation below walks through two scenarios step by step: a join query (“names of Paris customers with an order over 100”) and a division query (“students enrolled in all courses”).

Division

Division R÷SR \div S answers “all” queries: it returns the rows of RR that are associated with every row of SS.

Details

Formal definition: let AA be the set of attributes of RR that do not appear in SS, and let rAr_A denote the tuple rr restricted to the attributes AA. Then:

R÷S={ rA∣∀sB∈S,  (rA,sB)∈R }R \div S = \{\, r_A \mid \forall s_B \in S,\; (r_A, s_B) \in R \,\}

In words: keep each AA-value that appears in RR paired with every tuple of SS. Note the condition is membership in RR itself — not in R×SR \times S, which would be trivially true and make the definition vacuous.

Equivalently, using only the core operations (with AA the attributes of RR not in SS):

R÷S=πA(R)−πA ⁣(πA(R)×S−R)R \div S = \pi_A(R) - \pi_A\!\bigl(\pi_A(R) \times S - R\bigr)

Read it inside-out: πA(R)×S\pi_A(R) \times S builds every combination of an AA-value with a tuple of SS; subtracting RR leaves the combinations that are missing; projecting those to AA gives the AA-values that fail the “all” test; subtracting them from πA(R)\pi_A(R) leaves the ones that pass.

Worked example: “Find students enrolled in all courses.” Let Enrolled(student, course) contain:

(Alice, Math)  (Alice, CS)   (Alice, Bio)
(Bob, Math)
(Carol, Math)  (Carol, CS)  (Carol, Bio)

and Courses(course) contain (Math), (CS), (Bio). Then Enrolled ÷ Courses = {(Alice), (Carol)}: Alice and Carol each appear paired with Math, CS and Bio, while Bob is missing CS and Bio.

Enrolled(student, course) Alice | Math Alice | CS Alice | Bio Bob | Math Carol | Math Carol | CS Carol | Bio Courses: Math, CS, Bio ÷ Check each student against ALL courses Alice: Math ✓ CS ✓ Bio ✓ → kept Bob: Math ✓ CS ✗ Bio ✗ → dropped Carol: Math ✓ CS ✓ Bio ✓ → kept Enrolled ÷ Courses Alice, Carol

SQL equivalent:

SELECT student
FROM Enrolled e
WHERE NOT EXISTS (
  SELECT c.course FROM Courses c
  WHERE NOT EXISTS (
    SELECT 1 FROM Enrolled e2
    WHERE e2.student = e.student AND e2.course = c.course
  )
);

Example problem: with Enrolled as above but Courses = {(Math), (CS)} (Bio no longer offered), what is Enrolled ÷ Courses?

Solution

Now every student must be paired with Math and CS.

  • Alice: Math ✓, CS ✓ → kept.
  • Bob: Math ✓, CS ✗ → dropped.
  • Carol: Math ✓, CS ✓ → kept.

Answer: {(Alice), (Carol)}. Notice the answer grew relative to the three-course version: shrinking SS makes the “all” condition easier to satisfy. Division is monotone decreasing in SS.

Aggregation

Aggregation (γ\gamma): groups rows by a set of attributes and computes a summary function (SUM, COUNT, AVG, MIN, MAX) over each group. This is an extension beyond pure relational algebra, but it is the most important one in practice — every practical SQL query that uses GROUP BY relies on it.

Details

Notation: γa1,…,ak;  F1(b1),…,Fm(bm)(R)\gamma_{a_1,\ldots,a_k;\; F_1(b_1),\ldots,F_m(b_m)}(R) where a1,…,aka_1,\ldots,a_k are the grouping attributes and FiF_i are aggregate functions applied to attribute bib_i.

How it works: partition the rows of RR into groups that share the same values of the grouping attributes, then collapse each group into a single output row carrying the group key and the aggregate values.

orders ⋈ customers Alice | Paris | 200 Alice | Paris | 50 Bob | Lyon | 120 Carol | Paris | 150 group by city Groups Paris: 200, 50, 150 Lyon: 120 SUM Result Paris | 400 Lyon | 120

Example: “Total amount per city”:

γcity;  SUM(amount)(orders⋈customers)\gamma_{\text{city};\; \text{SUM}(\text{amount})}(\text{orders} \bowtie \text{customers})

With the data in the diagram, the groups are {Paris: 200+50+150} and {Lyon: 120}, so the result is {(Paris, 400), (Lyon, 120)}.

SQL equivalent:

SELECT c.city, SUM(o.amount)
FROM orders o JOIN customers c ON o.customer_id = c.id
GROUP BY c.city;

Why it is an extension: pure relational algebra (the seven core operations) cannot express aggregation. Codd’s theorem holds for the unextended algebra; aggregation requires the extended relational algebra. See Silberschatz, Korth & Sudarshan, Ch. 5.2.

Example problem: with orders ⋈ customers = {(Alice, Paris, 200), (Alice, Paris, 50), (Bob, Lyon, 120), (Carol, Paris, 150)}, compute γcity;  COUNT(∗),  MAX(amount)(orders⋈customers)\gamma_{\text{city};\; \text{COUNT}(*),\; \text{MAX}(\text{amount})}(\text{orders} \bowtie \text{customers}).

Solution

Partition by city:

  • Group Paris: rows (Alice, Paris, 200), (Alice, Paris, 50), (Carol, Paris, 150) → COUNT = 3, MAX(amount) = 200.
  • Group Lyon: row (Bob, Lyon, 120) → COUNT = 1, MAX(amount) = 120.

Result: {(Paris, 3, 200), (Lyon, 1, 120)}. Each group collapses to exactly one row, carrying the group key plus one value per aggregate function.

Algebraic properties

The algebraic laws of relational algebra are what make query optimization possible: the optimizer rewrites an expression into an equivalent one that is cheaper to execute.

LawForm
Commutativity of unionR ∪ S = S ∪ R
Commutativity of joinR ⋈ S = S ⋈ R (inner join)
Associativity of union(R ∪ S) ∪ T = R ∪ (S ∪ T)
Associativity of join(R ⋈ S) ⋈ T = R ⋈ (S ⋈ T) (inner join)
Distributivity of select over joinσp(R ⋈ S) = σp(R) ⋈ S (when p references only R)
Distributivity of project over unionπA(R ∪ S) = πA(R) ∪ πA(S)
AbsorptionR ∪ (R ∩ S) = R; R ∩ (R ∪ S) = R
Details

Why these matter: the distributivity of select over join is the basis of predicate pushdown — pushing a filter below a join reduces the number of rows entering the join. The commutativity and associativity of join let the optimizer reorder multi-way joins to find the cheapest plan. Together, these laws give the optimizer a search space of equivalent expressions from which it picks the most efficient one.

Predicate pushdown: same result, far fewer rows joined Before pushdown σ (customers ⋈ orders) join input: 1,000,000 × 5,000,000 rows entering join: 5 × 10¹² After pushdown σ(customers) ⋈ σ(orders) filters applied first: 500 × 100 rows entering join: 50,000

Worked example: consider σcity=′Paris′(customers⋈orders)\sigma_{\text{city}='Paris'}(\text{customers} \bowtie \text{orders}). Since the predicate references only attributes of customers, distributivity gives σcity=′Paris′(customers)⋈orders\sigma_{\text{city}='Paris'}(\text{customers}) \bowtie \text{orders}. If customers has 1,000,000 rows of which 500 are in Paris, the join input shrinks from 1,000,000 rows to 500 — before a single pair is matched.

Example problem: using the algebraic laws, rewrite πname(σcity=′Paris′(customers⋈orders))\pi_{\text{name}}\bigl(\sigma_{\text{city}='Paris'}(\text{customers} \bowtie \text{orders})\bigr) into an equivalent expression that is likely cheaper to execute, and justify each rewrite.

Solution
  1. Push the select down (distributivity of select over join, predicate references only customers): πname(σcity=′Paris′(customers)⋈orders)\pi_{\text{name}}\bigl(\sigma_{\text{city}='Paris'}(\text{customers}) \bowtie \text{orders}\bigr).
  2. Push the projection down (distributivity of project over join): only name (from customers) and cust_id (the join column of orders) are needed: πname(πname,id(σcity=′Paris′(customers))⋈πcust_id(orders))\pi_{\text{name}}\bigl(\pi_{\text{name},\text{id}}(\sigma_{\text{city}='Paris'}(\text{customers})) \bowtie \pi_{\text{cust\_id}}(\text{orders})\bigr).

Both rewrites preserve the result exactly (they are instances of the laws in the table), but the second plan touches far fewer columns and rows — this is precisely the kind of transformation a query planner performs, as described in SQL & Query Planning.

See SQL & Query Planning for how these laws are applied in practice.

Example problem

Problem: Given tables orders(id, customer_id, amount) and customers(id, name, city), write the relational algebra expression for: “names of customers in Paris who have placed at least one order over 100.”

Solution
π_name( σ_city='Paris'(customers) ⋈ σ_amount>100(orders) )

Step by step:

  1. σamount>100(orders)\sigma_{\text{amount} > 100}(\text{orders}) — select orders over 100.
  2. σcity=′Paris′(customers)\sigma_{\text{city} = 'Paris'}(\text{customers}) — select Paris customers.
  3. ⋈\bowtie on customer_id = id — join the two filtered relations.
  4. πname\pi_{\text{name}} — project to just the name column (removes duplicates).

Equivalent SQL:

SELECT c.name
FROM customers c
JOIN orders o ON c.id = o.customer_id
WHERE c.city = 'Paris' AND o.amount > 100;

The optimizer may reorder these steps using the algebraic laws above; see SQL & Query Planning for the execution strategies.