Relational Algebra Overview
Closure Property
Read aloud is unavailable in this browser.
The Property That Makes It All Work
In mathematics, closure is one of those elegant properties that seems almost too simple to matter—yet underpins everything. A set is said to be closed under an operation if applying that operation to members of the set produces another member of the set. For example, integers are closed under addition: adding any two integers yields another integer.
Relational algebra possesses this property in a particularly powerful way: every operation takes relation(s) as input and produces a relation as output. This seemingly simple observation has profound consequences. It's what allows you to compose arbitrarily complex queries from simple operations. It's what enables query optimization through algebraic transformations. It's fundamentally what makes relational databases work.
In this page, we'll explore the closure property in depth—what it means, why it matters, and how it shapes everything from query writing to system architecture.
By the end of this page, you will understand: the formal definition of closure in relational algebra; how closure enables query composition and nesting; the relationship between closure and optimization; practical implications for query design; and connections to functional programming concepts.
Formal Definition of Closure
The Closure Property of Relational Algebra:
Every operation in relational algebra takes one or more relations as input and produces exactly one relation as output.
Let's unpack this definition precisely.
For Unary Operations:
If R is a relation, then:
σ_p(R)is a relationπ_L(R)is a relationρ_S(R)is a relation𝒢_F(R)is a relation
For Binary Operations:
If R and S are relations, then:
R ∪ Sis a relationR ∩ Sis a relationR − Sis a relationR × Sis a relationR ⋈ Sis a relationR ÷ Sis a relation
Formal Statement:
Let ℛ be the set of all relations (all possible sets of tuples over any schema). For every operator Op in relational algebra:
- Unary:
Op: ℛ → ℛ - Binary:
Op: ℛ × ℛ → ℛ
The image of Op is contained in ℛ. The domain and codomain are the same. This is closure.
Technically, each operation produces a relation with a specific schema determined by the input schemas and the operation. Selection and rename preserve the schema. Projection produces a subset schema. Join and product produce combined schemas. But regardless of the specific schema, the output is always a valid relation—a set of tuples conforming to some fixed schema.
What Closure Is NOT:
It's worth clarifying what closure doesn't mean:
- Not preservation of specific schema: Different operations produce different schemas
- Not cardinality preservation: Join might produce more or fewer tuples than its inputs
- Not property preservation: A key in the input might not be a key in the output
- Not referential integrity preservation: Operations don't automatically maintain constraints
Closure is specifically about the type of the result: it's always a relation, which can be represented as a table, and can be input to further operations.
The Empty Relation
An important edge case: the empty relation (a relation with zero tuples but a defined schema) is still a relation. Operations can produce empty results:
σ_false(R)produces the empty relation with R's schemaR − Rproduces the empty relationR ⋈_{false} Sproduces the empty relation
Closure holds even for these degenerate cases—the empty relation is a valid input
Closure Enables Composition
The most immediate consequence of closure is composability: since every operation outputs a relation, and every operation accepts relations as input, we can chain operations arbitrarily.
Simple Composition:
π_name(σ_salary>50000(Employees))
This chains selection and projection. The selection operates on Employees (a relation), produces a relation, which the projection then operates on—also producing a relation.
Complex Composition:
π_name,dept_name(
σ_salary>50000(
Employees ⋈_{Employees.dept_id = Departments.id} Departments
)
)
Here we:
- Join Employees and Departments (two relations → one relation)
- Select from the join result (one relation → one relation)
- Project from the selection result (one relation → one relation)
Every intermediate step is a valid relation. We can conceptually "stop" at any point and examine the intermediate result.
Arbitrary Nesting Depth:
There's no limit to composition depth:
π_customer_name(
σ_total>1000(
customer_id 𝒢_{SUM(amount) as total}(
σ_order_date > '2024-01-01'(
Orders ⋈ OrderItems ⋈ Products ⋈ Customers
)
)
)
)
This query: joins four tables; filters by date; groups and aggregates; filters by aggregate; projects final columns. Each operation nestles within the output of the previous, forming a complex but well-structured query.
- •Modular Query Design — Build complex queries by composing simple, understandable pieces
- •Reusable Subqueries — A common subexpression can be named and reused (views, CTEs)
- •Incremental Development — Build and test queries piece by piece, validating intermediates
- •Uniform Representation — All intermediate and final results have the same type (relation)
- •Analysis Capability — Each operation can be analyzed independently for cost, cardinality
- •Optimization Opportunity — Transformations can be applied at any level of nesting
Comparison to Non-Closed Systems:
Imagine if some operations produced something other than relations—say, if COUNT produced an integer, or if aggregation produced a key-value structure. Suddenly:
- You couldn't apply relational operators to aggregation results
- Different operations would need different subsequent handling
- Query composition would require type matching at each step
- The uniform optimization framework would break down
This is exactly what happens in less principled systems. Relational algebra's closure avoids these problems entirely.
SQL is designed to maintain closure: every SELECT statement produces a table, which can be used in FROM clauses of other queries. This is why subqueries work—they're compositional because the underlying relational algebra is closed. Features like VIEWs and Common Table Expressions (WITH clauses) leverage closure to name and reuse intermediate results.
Closure and Query Optimization
Closure isn't just convenient for writing queries—it's essential for query optimization. The ability to freely compose and decompose expressions enables the algebraic transformations that make efficient execution possible.
Transformation Through Equivalence
Query optimization is fundamentally about finding equivalent expressions—expressions that produce the same result but may execute differently. Closure guarantees that:
- Any subexpression can be replaced with an equivalent subexpression
- The result remains a valid relational algebra expression
- The final result is unchanged
Example of Optimization Through Transformation:
Original expression:
π_name(σ_salary>50000(Employees × Departments))
This computes the full Cartesian product before filtering—extremely expensive if Employees and Departments are large.
Transformed expression:
π_name(Employees) where salary > 50000
Wait—but we can do better if there are join conditions. If the real query involved matching departments:
More realistic original:
π_name(σ_{E.dept_id=D.id ∧ salary>50000}(E × D))
Transformed:
π_name(σ_{salary>50000}(E) ⋈_{E.dept_id=D.id} D)
Here we:
- Push the selection on salary to before the join (possible because salary is in E only)
- Replace product + selection on join condition with a proper join
- Result: fewer tuples enter the join, much faster execution
Why Closure Matters for This:
At every transformation step, we're producing a new relational algebra expression. Closure guarantees:
- The transformed expression is syntactically valid
- The output is a relation (can be further transformed or executed)
- We can apply optimization rules without worrying about type mismatches
| Transformation | Original Form | Optimized Form | Benefit |
|---|---|---|---|
| Selection Pushdown | σ_p(R ⋈ S) | σ_p(R) ⋈ S (when p involves only R) | Fewer tuples in join |
| Projection Pushdown | π_A(R ⋈ S) | π_A(π_{A∪joinAttrs}(R) ⋈ S) | Narrower intermediates |
| Join Reordering | (R ⋈ S) ⋈ T | R ⋈ (S ⋈ T) | Smaller intermediate joins |
| Selection Splitting | σ_{p∧q}(R) | σ_p(σ_q(R)) | Push parts independently |
| Join to Semijoin | π_R(R ⋈ S) | R ⋉ S | Don't need S attributes in result |
| Subquery Flattening | σ(x IN (subq)) | R ⋈ (subq) | Avoid correlated execution |
Query optimizers explore a search space of equivalent expressions. Closure ensures this space is well-defined: every point in the space is a valid relational algebra expression producing a relation. Without closure, the optimizer would need to track and validate types at every transformation, dramatically complicating the optimization process.
Closure and Expression Trees
Relational algebra expressions naturally form tree structures due to closure. Understanding this tree representation is fundamental to query processing.
Expression Tree Structure:
- Leaf nodes: Base relations (tables in the database)
- Internal nodes: Operators (σ, π, ⋈, ∪, etc.)
- Edges: Data flow from child to parent
Because of closure, every node (except leaves) takes its children's outputs (relations) and produces an output (relation) for its parent. The types match perfectly at every connection.
Example Tree:
For the expression:
π_name(σ_{salary>50000}(Employees ⋈ Departments))
Execution as Tree Traversal:
Query execution can be viewed as a bottom-up tree traversal:
- Load leaf relations (Employees, Departments)
- Execute join: combine loaded relations
- Execute selection: filter join results
- Execute projection: extract final columns
- Return root result to user
Each step produces a relation that flows up to its parent. Closure ensures this works uniformly.
Optimization as Tree Transformation:
Query optimization transforms one tree into an equivalent tree:
Before optimization:
π_name
|
σ_sal>50K
|
⋈
/
Emp Dept
After pushing selection down:
π_name
|
⋈
/
σ_sal>50K Dept
|
Emp
Both trees are valid expressions producing the same result. The optimizer systematically explores such transformations, guided by cost estimates.
Pipelining and Materialization:
Closure enables two execution strategies:
- Materialized Evaluation: Compute each operator's full result, store it, then proceed
- Pipelined Evaluation: Stream tuples through operators without fully materializing intermediates
Both work because every operator produces relations—pipelining just doesn't wait for the complete relation before passing tuples upward.
When analyzing complex queries, drawing the expression tree clarifies the structure. You can see where binary operations combine data, where filtering reduces volume, and where the final projection shapes the output. This visualization skill is invaluable for understanding and optimizing queries.
Closure and Views
The view mechanism in relational databases is a direct consequence of closure. Because every relational algebra expression produces a relation, we can name that expression and use it as if it were a base table.
View Definition:
A view is a named relational algebra expression:
HighEarners ← σ_{salary > 100000}(Employees)
Or in SQL:
CREATE VIEW HighEarners AS
SELECT * FROM Employees WHERE salary > 100000;
Using Views:
Once defined, the view can be used anywhere a table can be used:
π_name(HighEarners ⋈ Departments)
This works because HighEarners evaluates to a relation (by closure), and that relation is a valid join input (by closure).
View Composition:
Views can be defined in terms of other views:
EngineeringHighEarners ← σ_{dept='Engineering'}(HighEarners)
This chains closures: HighEarners is a relation, so selection produces a relation, which can be named as another view. Arbitrary nesting is possible—views on views on views—all thanks to closure.
- •Abstraction — Complex queries hidden behind simple names
- •Reusability — Define once, use in multiple queries
- •Security — Grant access to views without exposing base tables
- •Simplification — Users see a simpler schema via customized views
- •Logical Independence — Base schema changes can be hidden by views
- •Query Organization — Break complex queries into manageable pieces
View Expansion and Optimization:
When a query references a view, the database performs view expansion: replacing the view name with its defining expression. The result is a larger relational algebra expression that can be optimized as a whole.
Query:
π_name(HighEarners)
After view expansion:
π_name(σ_{salary > 100000}(Employees))
After optimization:
(Push projection, possibly use salary index)
Closure ensures that view expansion always produces a valid expression. The expanded expression has the same structure as if the user had written it directly.
Materialized Views:
A materialized view stores the computed result rather than just the expression:
- Regular view: Stores expression, computes on each access
- Materialized view: Stores expression AND precomputed result
Closure ensures the precomputed result is a valid relation that can be queried directly. Updates to base tables require view maintenance to keep the materialized result current.
Common Table Expressions (CTEs):
SQL's WITH clause creates temporary named views for a single query:
WITH HighEarners AS (
SELECT * FROM Employees WHERE salary > 100000
)
SELECT name FROM HighEarners WHERE department = 'Engineering';
Again, this works because closure guarantees the CTE produces a relation usable in the main query.
From the query processor's perspective, views are indistinguishable from base tables after expansion. This uniformity—made possible by closure—simplifies the entire system: parsing, optimization, and execution all work identically whether data comes from base tables or views.
Connection to Functional Programming
Relational algebra's closure property has deep connections to concepts in functional programming. Understanding this relationship provides additional insight into why relational databases work so well.
Functions and Types:
In functional programming, functions transform values of certain types to values of (potentially) other types:
map :: (a -> b) -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]
Notice that filter is closed over lists: it takes a list and returns a list. This enables chaining:
map getName . filter (\e -> salary e > 50000) $ employees
Relational algebra operators work identically:
- σ (selection) is like
filter: takes a relation, returns a relation - π (projection) is like
map: takes a relation, returns a relation - ⋈ (join) is like a specialized
flatMapover two collections
Composability in Both Paradigms:
Functional programming emphasizes function composition: f ∘ g means "apply g, then apply f." This works when the output type of g matches the input type of f.
Relational algebra achieves the same: all operators output relations, all operators accept relations, so arbitrary composition is valid. Query expressions are essentially composed functions over the domain of relations.
12345678910111213141516171819202122232425262728293031323334353637
// Functional programming parallel to relational algebra interface Employee { id: number; name: string; salary: number; deptId: number;} interface Department { id: number; name: string;} // Selection (σ) parallel: filterconst highEarners = employees.filter(e => e.salary > 50000);// Input: Employee[] → Output: Employee[] (closed!) // Projection (π) parallel: mapconst names = highEarners.map(e => ({ name: e.name }));// Input: Employee[] → Output: {name: string}[] (closed over collections!) // Join (⋈) parallel: flatMap + filterconst withDepts = employees.flatMap(e => departments .filter(d => d.id === e.deptId) .map(d => ({ ...e, deptName: d.name })));// Input: Employee[], Department[] → Output: combined[] (closed!) // Composition works because each step produces a valid input for the nextconst result = employees .filter(e => e.salary > 50000) // σ .flatMap(e => departments .filter(d => d.id === e.deptId) .map(d => ({ name: e.name, dept: d.name }))) // π ∘ ⋈ .filter(x => x.dept === 'Engineering'); // σThe Monad Connection:
In advanced functional programming, the monad abstraction captures computational patterns that maintain closure. Collections (lists, sets) form a monad where:
returnwraps a value in a collectionbind(flatMap) applies a function returning a collection to each element
Relational operations can be understood as monad operations:
- Base relations are like
return - Joins are like monadic
bind - Selection and projection are functorial
map
This is why database query languages and functional collection APIs feel so similar—they're both instances of the same mathematical structure.
Implications for Modern Systems:
The connection between relational algebra and functional programming explains why:
- DataFrame APIs (Pandas, Spark) feel natural: they're functional APIs over relational-like structures
- LINQ (Language Integrated Query) integrates database queries into C#/F#: same compositional structure
- SQL-like transformations appear in stream processing (Kafka Streams, Flink): closure enables transformation chaining
- Optimization techniques transfer: pushdown, fusion, and reordering apply to both paradigms
If you're comfortable with functional programming, you already understand closure intuitively. Apply that intuition to databases. Conversely, if you master relational algebra, the functional equivalents will feel natural. This conceptual transfer accelerates learning in both domains.
Practical Implications of Closure
Let's consolidate how closure affects practical database work—from query writing to system design.
For Query Writers:
- Any SELECT is composable: Use it in FROM, WHERE IN, or WITH clauses
- Subqueries work universally: Anywhere a table is expected, a subquery can appear
- Views are transparent: No difference between using a view and using a table
- Intermediate results are examinable: Break complex queries to debug step-by-step
For Query Optimizers:
- Uniform representation: All queries, of any complexity, are relational algebra expressions
- Local transformation: Replace any subexpression with an equivalent without global restructuring
- Cost-based selection: Compare alternative expressions for the same query
- Rule-based transformation: Apply patterns (like selection pushdown) confidently
For System Architects:
- Uniform execution model: All operators follow the same input-output pattern
- Pipelining possible: Stream tuples through operators without full materialization
- Parallel execution: Independent subexpressions can execute concurrently
- Simplified APIs: Internal representation is just relational expressions
- •Arbitrary query complexity via composition
- •Views and named intermediate results
- •Algebraic query optimization
- •Uniform execution engine design
- •Pipelining and streaming execution
- •Subquery flattening
- •Query result reuse
- •Limited nesting depth or complexity
- •Type mismatches between operations
- •Ad-hoc handling of different result types
- •Complicated optimizer with special cases
- •Non-uniform execution strategies
- •Manual result transformation
- •Incompatible operation sets
When Closure Seems to Break:
Some SQL features seem to violate closure:
- Scalar subqueries:
SELECT (SELECT COUNT(*) FROM Orders) AS order_count— returns a scalar, not a relation? - Aggregate functions:
SELECT AVG(salary) FROM Employees— returns a single value?
Actually, these maintain closure:
- Scalar subquery produces a one-column, one-row relation; the value is extracted
- Aggregation without GROUP BY produces a one-row relation containing the aggregate
The relation structure is preserved; the result just happens to be a very simple relation. This is why you can still wrap these in further queries.
Some SQL features like LIMIT, ORDER BY, or cursors deal with aspects outside pure relational algebra (ordering, enumeration). These extend the relational model while typically preserving closure for practical purposes. Understanding these extensions and their limits is advanced topic for database specialists.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.