Relational Model & Relational Algebra

The mathematical contract behind relational databases: relations as sets of typed tuples, integrity constraints as invariants, and a closed algebra that turns queries into expressions the optimizer can safely transform.

The relational model is not “data in rectangular files.” It is a separation of concerns: users describe what relation they want, an algebra gives that request precise meaning, constraints define which database states are legal, and the engine is free to choose a physical representation and execution plan. That logical/physical boundary is why an index, partitioning scheme, or join algorithm can change without changing the meaning of a correct query.

The theory is deliberately small. A relation is a set of typed tuples; every query operator consumes relations and produces another relation. From that closure property comes composability, equivalence reasoning, views, and most rule-based query optimization.

Four layers that must not be conflated #

Layer Question Examples
Meaning What fact does one tuple assert? “Product 42 is supplied by supplier 7 at price 9.50”
Logical schema Which attributes and invariants represent those facts? domains, keys, foreign keys, checks
Logical query Which relation follows from the current facts? selection, projection, join, difference
Physical plan How will the engine compute it? sequential scan, B-tree lookup, hash join, sort-merge join

The formal object #

Let each attribute AiA_i have a domain DiD_i. A relation schema

R(A1:D1,A2:D2,…,An:Dn)R(A_1:D_1, A_2:D_2, \ldots, A_n:D_n)

names the attributes and their domains. A tuple tt is a finite mapping from attribute names to domain values, so t[Ai]∈Dit[A_i] \in D_i. A relation value (often called an instance) is a finite set of such tuples:

r(R)⊆D1×D2×⋯×Dn.r(R) \subseteq D_1 \times D_2 \times \cdots \times D_n.

Using attribute names rather than only positions matters: it makes projection, renaming, and natural join precise without treating the third physical column as part of the fact's identity.

Heading
The schema: attribute names and domains.
Body
The current set of tuples satisfying that heading and the declared constraints.
Degree (arity)
The number of attributes in the heading.
Cardinality
The number of tuples in the body.

A mathematical relation has three consequences that SQL users often overlook:

  • tuples are unordered;
  • attributes are identified by name, not display position;
  • duplicate tuples do not exist because the body is a set.

The empty relation is valid. So are a zero-degree relation containing no tuple (false) and a zero-degree relation containing the single empty tuple (true): they are the relational counterparts of Boolean values and explain why closed relational expressions can represent predicates as well as data.

A running database #

We will use three relation schemas:

Supplier(sid,name,city)Product(pid,label,category)Supplies(sid,pid,price)\begin{aligned} Supplier&(sid, name, city) \\ Product&(pid, label, category) \\ Supplies&(sid, pid, price) \end{aligned}

with the following small instances:

Supplier

sid name city
1 Ada Parts Kyiv
2 Bolt Works Lviv
3 Circuit House Kyiv

Product

pid label category
10 M3 bolt mechanical
20 op-amp electronic
30 encoder electronic

Supplies

sid pid price
1 10 0.12
1 20 1.80
2 10 0.09
3 20 1.65
3 30 8.40

One Supplies tuple means one supplier offers one product at one price. That sentence is part of the schema contract. If prices vary by currency, quantity, or validity interval, those attributes—or separate relations representing those facts—must be added. A table cannot repair an underspecified predicate.

Domains say which individual values are admissible. Constraints say which combinations of tuples form a legal database state.

Keys #

For an attribute set K⊆attrs(R)K \subseteq attrs(R), KK is a superkey when no two distinct tuples in any legal instance agree on every attribute in KK. A candidate key is a minimal superkey: removing any attribute destroys uniqueness. One candidate key may be designated the primary key; the remaining candidate keys are alternate keys. “Primary” is a schema/implementation choice, not a stronger mathematical kind of key.

In the running example:

  • Supplier.sid and Product.pid are candidate keys;
  • (Supplies.sid, Supplies.pid) is a candidate key if each supplier has at most one current offer per product;
  • (sid, pid, price) is a superkey but not a candidate key, because price is redundant for uniqueness.

Referential and general constraints #

A foreign key from Supplies.sid to Supplier.sid requires every non-null referencing value to identify an existing supplier. Likewise for pid. This is referential integrity: the database cannot assert an offer for a supplier or product absent from the referenced relation.

Other invariants include:

  • domain constraints: price is numeric;
  • tuple constraints: price > 0;
  • relation constraints: no two active offers share (sid, pid);
  • cross-relation constraints: every offer references a supplier and product.

SQL exposes these through types, NOT NULL, CHECK, UNIQUE, PRIMARY KEY, FOREIGN KEY, and sometimes exclusion constraints or triggers. Their exact null and timing semantics are implementation concerns layered on the logical invariant.

The algebra: operators that stay inside the model #

If RR and SS are relations, relational algebra builds expressions from a small operator set. Closure means every well-typed result is itself a relation, so it can feed the next operator.

Selection chooses tuples #

σθ(R)={t∈R∣θ(t)}\sigma_{\theta}(R) = \{t \in R \mid \theta(t)\}

Selection preserves the heading and keeps tuples satisfying predicate θ\theta. Kyiv suppliers are:

σcity="Kyiv"(Supplier).\sigma_{city = \text{"Kyiv"}}(Supplier).

It is a horizontal restriction: fewer rows, same attributes.

Projection chooses attributes #

πX(R)={t[X]∣t∈R}\pi_X(R) = \{t[X] \mid t \in R\}

Projection keeps attributes XX and removes duplicates created by discarding distinguishing attributes. Supplier cities are πcity(Supplier)={Kyiv,Lviv}\pi_{city}(Supplier)=\{\text{Kyiv},\text{Lviv}\}, not three rows.

Projection is existential quantification in disguise. From a ternary predicate Supplies(sid,pid,price), projecting (sid,pid) asks whether some price makes the original predicate true.

Rename makes schemas composable #

The rename operator ρ\rho changes a relation or attribute name without changing tuples. It is essential for self-joins and for making headings compatible:

ρS1(sid1,name1,city1)(Supplier).\rho_{S_1(sid_1,name_1,city_1)}(Supplier).

Renaming is not cosmetic when names carry operator semantics. A natural join matches equally named attributes; careless names can therefore change a query.

Set operators combine compatible relations #

For union-compatible relations—same attribute names with corresponding domains—set union, intersection, and difference have their ordinary meanings:

R∪S,R∩S,R−S.R \cup S, \qquad R \cap S, \qquad R - S.

Difference is the expressive step that supports “has no,” “all,” and anti-join-style questions. It is also non-monotone: adding input tuples can remove result tuples.

Cartesian product combines every pair #

After resolving duplicate attribute names,

R×S={t∪u∣t∈R,u∈S}.R \times S = \{t \cup u \mid t \in R, u \in S\}.

Its cardinality is ∣R∣∣S∣|R||S|. A product is rarely the intended final answer, but it is the substrate of joins: first consider pairs, then retain pairs that satisfy a relationship.

A theta join is selection over a product:

R⋈θS=σθ(R×S).R \bowtie_{\theta} S = \sigma_{\theta}(R \times S).

An equijoin uses equality conditions. A natural join equates every same-named attribute and emits one copy of each matched attribute. The supplier names attached to offers can be expressed as:

Supplier⋈Supplier.sid=Supplies.sidSupplies.Supplier \bowtie_{Supplier.sid = Supplies.sid} Supplies.

Join does not mean “look up by foreign key.” A foreign key is an integrity constraint; a join is a query operator. Joins can use non-key conditions, and a foreign key can exist without a query joining the tables.

Semijoin and antijoin answer existence questions #

A left semijoin keeps only the left tuples that have at least one match:

R⋉S=πattrs(R)(R⋈S).R \ltimes S = \pi_{attrs(R)}(R \bowtie S).

An antijoin keeps those with no match. These are useful mental models for SQL EXISTS and NOT EXISTS, and they prevent accidental row multiplication when the question asks only whether a related tuple exists.

Division expresses “for every” #

Suppose R(X,Y)R(X,Y) records pairs and S(Y)S(Y) lists required values. Division returns each XX related to every YY in SS:

R÷S={x∣∀y∈S, (x,y)∈R}.R \div S = \{x \mid \forall y \in S,\ (x,y) \in R\}.

An expression using primitive set operators is:

πX(R)−πX((πX(R)×S)−R).\pi_X(R) - \pi_X\big((\pi_X(R) \times S) - R\big).

Read it causally:

  1. form every candidate xx paired with every required yy;
  2. subtract the pairs that actually exist—what remains are missing requirements;
  3. project the failing xx values;
  4. subtract failures from all candidates.

For “suppliers that supply every electronic product,” let

E=πpid(σcategory="electronic"(Product))E = \pi_{pid}(\sigma_{category=\text{"electronic"}}(Product))

and then compute

πsid,pid(Supplies)÷E.\pi_{sid,pid}(Supplies) \div E.

Only supplier 3 qualifies in the running data.

A complete query derivation #

Question: Which Kyiv suppliers offer an electronic product for less than 2?

Work from meaning to operators:

  1. restrict suppliers by city;
  2. restrict products by category;
  3. restrict offers by price;
  4. join the three relations on their identifiers;
  5. project the requested supplier attributes.

πsid,name(σcity="Kyiv"(Supplier)⋈σprice<2(Supplies)⋈σcategory="electronic"(Product))\pi_{sid,name}\left( \sigma_{city=\text{"Kyiv"}}(Supplier) \bowtie \sigma_{price<2}(Supplies) \bowtie \sigma_{category=\text{"electronic"}}(Product) \right)

The result is {(1, Ada Parts), (3, Circuit House)}.

One SQL rendering is:

sql
SELECT DISTINCT s.sid, s.name
FROM supplier AS s
JOIN supplies AS x ON x.sid = s.sid
JOIN product AS p ON p.pid = x.pid
WHERE s.city = 'Kyiv'
  AND x.price < 2
  AND p.category = 'electronic';

DISTINCT preserves the set-valued algebraic result when one supplier has multiple matching offers. Without it, SQL returns one output row per matching join combination.

Why equivalent expressions matter #

Because operators have algebraic laws, many syntactically different expressions denote the same relation. A query optimizer exploits exactly that freedom.

For predicates θ1\theta_1 and θ2\theta_2:

σθ1∧θ2(R)=σθ1(σθ2(R)).\sigma_{\theta_1 \land \theta_2}(R) = \sigma_{\theta_1}(\sigma_{\theta_2}(R)).

If θ\theta mentions only attributes of RR:

σθ(R⋈S)=(σθ(R))⋈S.\sigma_{\theta}(R \bowtie S) = (\sigma_{\theta}(R)) \bowtie S.

The second form can discard tuples before the join. If Supplier has one million rows but only 2,000 Kyiv rows, early selection can shrink both memory and comparison work without changing meaning.

Projection can similarly be pushed down when every attribute needed by later predicates and joins is retained. The caveat is the lesson: transformations are justified by schemas and laws, not by visual resemblance.

RewriteUsually valid whenFailure boundary
Split selectionsPredicates use ordinary two-valued relational logicSQL nulls and volatile expressions require SQL-specific rules
Push selection below joinPredicate mentions only one inputMoving a filter across an outer join can remove null-extended rows
Push projectionJoin keys and later attributes remainDropping a needed attribute makes the later expression ill-typed
Reorder inner joinsPredicates and schemas are preservedOuter, lateral, or order-sensitive operations add constraints

SQL is relationally inspired, not identical #

The distinction is not pedantry; it predicts real bugs.

Relational model Typical SQL behavior
Relation body is a set Query results use bag/multiset semantics unless DISTINCT or a set operator removes duplicates
Tuples have no order Output order is unspecified unless the outermost query has ORDER BY
Predicates are true or false NULL introduces UNKNOWN; WHERE keeps only rows for which the condition is true
Every tuple component is a value from its domain SQL permits null markers and implementation-specific types
Projection removes duplicates SELECT column preserves duplicates; SELECT DISTINCT column removes them
Union is set union UNION removes duplicates; UNION ALL preserves multiplicity
Join combines matching tuples Outer joins additionally create null-extended rows for unmatched input

Null changes logical laws #

In SQL, x = NULL is not true or false; it is unknown. Use IS NULL to test the marker. A CHECK constraint in PostgreSQL is satisfied when its expression is true or null, so CHECK (price > 0) does not imply price IS NOT NULL. Declare both when both are invariants.

Unique constraints also need explicit null reasoning. PostgreSQL normally treats nulls as distinct for uniqueness, while UNIQUE NULLS NOT DISTINCT makes them collide. Neither behavior should be smuggled into the mathematical definition of a key; decide the domain semantics first, then choose matching SQL constraints.

Bags explain duplicate explosions #

If a supplier has three matching offers and the query projects only its name, plain SQL can return that name three times. That is not the engine “duplicating data”; it is the correct multiplicity of the joined bag. Ask which semantics the result needs:

  • preserve occurrences because each match is meaningful;
  • use EXISTS because only existence matters;
  • aggregate because the question asks for a count or total;
  • use DISTINCT because the intended mathematical result is a set.

Using DISTINCT blindly at the top often hides an incorrect join predicate.

Logical design before physical design #

A reliable modelling pass proceeds in this order:

  1. Write the fact sentence. What does one tuple assert?
  2. Name attributes and domains. Include units, time scope, currency, and identity semantics.
  3. State candidate keys. Explain why each is guaranteed across all legal states.
  4. State dependencies and constraints. Which values determine others? Which references must exist?
  5. Normalize where dependencies create anomalies. This is covered in Functional Dependencies & Normalization.
  6. Write representative relational queries. Ensure the schema can answer required questions without inventing meaning.
  7. Only then choose indexes and storage. Those optimize the workload; they do not define the facts.

Diagnostic reasoning #

When a query or schema behaves unexpectedly, ask in this order:

  1. What fact does one input tuple mean?
  2. Which keys and foreign keys are actually enforced?
  3. Is the intended result a set, a bag, or an aggregate?
  4. Can one left tuple match several right tuples? Should it?
  5. Does a predicate evaluate to unknown because of null?
  6. Is a filter applied before or after an outer join creates null-extended rows?
  7. Is ordering explicitly requested?
  8. Which algebraic expression describes the intended result before SQL syntax?
  9. Only after semantics are correct: which physical plan did the optimizer choose and why?

This ordering separates two very different failures: a wrong result relation and an inefficient plan for a correct relation.

Exercises #

  1. Express “products supplied by no Kyiv supplier” with difference or antijoin.
  2. Explain why projecting city from Supplier changes cardinality from 3 to 2 in relational algebra but not necessarily in SQL.
  3. Show the difference between “suppliers that provide at least one electronic product” and “suppliers that provide every electronic product.”
  4. Decide whether (sid,pid,price) is a candidate key when (sid,pid) is already guaranteed unique.
Solutions
  1. Let K=πsid(σcity="Kyiv"(Supplier))K=\pi_{sid}(\sigma_{city=\text{"Kyiv"}}(Supplier)). Products supplied by a Kyiv supplier are πpid(K⋈Supplies)\pi_{pid}(K \bowtie Supplies). Subtract that from πpid(Product)\pi_{pid}(Product).
  2. The mathematical projection forms a set, so the two Kyiv occurrences collapse. SQL SELECT city is a bag projection; SELECT DISTINCT city matches the set result.
  3. “At least one” is a semijoin/existential query. “Every” is division or a double NOT EXISTS: there must be no required product for which the supplier lacks an offer.
  4. No. It is a superkey, but price can be removed while uniqueness remains, so it is not minimal.

Sources

  1. paperA Relational Model of Data for Large Shared Data Banks (Codd, 1970)
  2. docsPostgreSQL 18 — Table Expressions
  3. docsPostgreSQL 18 — SELECT
  4. docsPostgreSQL 18 — Constraints

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close