Loading learning content...
Every query you write has a hidden structure—a shape that determines how it will be executed, optimized, and understood by the database engine. This structure is the expression tree, a hierarchical representation that captures the complete logic of a relational algebra expression.
When you write SQL, the database parses your text, validates the semantics, and translates everything into an expression tree. This tree becomes the central representation that the query optimizer manipulates, the execution engine interprets, and debuggers display. Understanding expression trees means understanding how databases actually process queries.
In this page, we'll explore expression trees in depth: their structure, how they're built, how they're evaluated, and how they're transformed during optimization. By the end, you'll be able to visualize any query as a tree and understand why certain tree shapes lead to better performance.
By the end of this page, you will understand: the formal structure of relational algebra expression trees; how expressions map to tree representations; bottom-up and top-down tree evaluation strategies; how tree transformations enable query optimization; and practical skills for analyzing query plans.
An expression tree is a hierarchical data structure where:
Node Types:
Leaf Nodes (Operands):
Employees, Departments, OrdersInternal Nodes (Operators):
The Root Node:
Edge Direction:
By convention, edges point FROM child TO parent, representing data flow upward. A join node has two incoming edges (from its two input relations) and one outgoing edge (to its parent or the result).
Tree Properties:
Formal Definition:
An expression tree T is a tuple (N, E, root, label) where:
Pure expression trees allow each subexpression to have exactly one parent. In practice, common subexpressions might be shared (e.g., the same subquery used twice), creating a DAG (Directed Acyclic Graph). Query optimizers often handle both, with DAGs enabling common subexpression elimination.
Translating a relational algebra expression into a tree is straightforward—the nested structure of the expression directly maps to the tree structure.
The Recursive Construction:
Tree(R) where R is a base relation:
Create leaf node labeled R
Tree(Op(E)) where Op is a unary operator:
Create internal node labeled Op
Add Tree(E) as its child
Tree(E1 Op E2) where Op is a binary operator:
Create internal node labeled Op
Add Tree(E1) as left child
Add Tree(E2) as right child
Example Construction:
For the expression:
π_name(σ_{salary>50000}(Employees ⋈_{dept_id=id} Departments))
Step-by-step:
π_nameσ_{salary>50000}(...)Employees ⋈ DepartmentsEmployees and Departments (leaves)Building bottom-up:
Step 1: Create leaf nodes
node_E = Leaf(Employees)
node_D = Leaf(Departments)
Step 2: Create join node
node_J = Internal(⋈, condition=dept_id=id)
node_J.left = node_E
node_J.right = node_D
Step 3: Create selection node
node_S = Internal(σ, predicate=salary>50000)
node_S.child = node_J
Step 4: Create projection node (root)
node_P = Internal(π, columns=[name])
node_P.child = node_S
Result: Tree with root = node_P
| Operator | Symbol | Arity | Tree Structure |
|---|---|---|---|
| Selection | σ | Unary | One child |
| Projection | π | Unary | One child |
| Rename | ρ | Unary | One child |
| Aggregation | 𝒢 | Unary | One child |
| Union | ∪ | Binary | Two children |
| Intersection | ∩ | Binary | Two children |
| Difference | − | Binary | Two children |
| Cartesian Product | × | Binary | Two children |
| Join | ⋈ | Binary | Two children |
| Division | ÷ | Binary | Two children |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// Simplified expression tree representation type OperatorType = 'SELECT' | 'PROJECT' | 'JOIN' | 'UNION' | 'PRODUCT' | 'RENAME'; interface TreeNode { type: 'leaf' | 'unary' | 'binary'; operator?: OperatorType; relation?: string; // For leaf nodes condition?: string; // For selection, join columns?: string[]; // For projection children: TreeNode[];} // Constructing the tree for:// π_name(σ_salary>50000(Employees ⋈ Departments)) const tree: TreeNode = { type: 'unary', operator: 'PROJECT', columns: ['name'], children: [{ type: 'unary', operator: 'SELECT', condition: 'salary > 50000', children: [{ type: 'binary', operator: 'JOIN', condition: 'dept_id = id', children: [ { type: 'leaf', relation: 'Employees', children: [] }, { type: 'leaf', relation: 'Departments', children: [] } ] }] }]}; // Tree traversal for displayfunction printTree(node: TreeNode, indent: number = 0): void { const prefix = ' '.repeat(indent); if (node.type === 'leaf') { console.log(`${prefix}📋 ${node.relation}`); } else { console.log(`${prefix}🔧 ${node.operator}${node.condition ? '(' + node.condition + ')' : ''}`); node.children.forEach(child => printTree(child, indent + 1)); }}In relational algebra notation, parentheses indicate nesting—and nesting directly translates to tree depth. Every pair of parentheses creates a new subtree. Reading expressions inside-out matches traversing the tree from leaves to root.
Expression trees aren't just representations—they're executable specifications. The database evaluator traverses the tree to compute the query result. Two primary evaluation strategies exist.
Bottom-Up (Materialized) Evaluation:
Start at the leaves and work upward:
Characteristics:
Top-Down (Demand-Driven/Pipelined) Evaluation:
Start at the root and propagate requests downward:
Characteristics:
The Iterator Interface:
Most modern databases use the iterator model, where each node implements three operations:
Open() - Initialize the operator, recursively open children
Next() - Return the next output tuple (or null if exhausted)
Close() - Clean up resources, recursively close children
Evaluation Example:
For π_name(σ_{salary>50000}(Employees)):
1. Query executor calls Root.Open()
2. π.Open() calls σ.Open()
3. σ.Open() calls Employees.Open() (init table scan)
4. Executor calls Root.Next()
5. π.Next() calls σ.Next()
6. σ.Next() calls Employees.Next() repeatedly:
- Returns each tuple, σ checks predicate
- If salary > 50000, σ returns tuple to π
- If not, σ calls Next() again, filtering internally
7. π extracts 'name' column, returns to executor
8. Executor receives row, sends to client
9. Repeat 4-8 until Next() returns null
Pipeline Breakers:
Some operators can't stream—they need complete input before producing output:
These "pipeline breakers" force materialization at that point in the tree.
Modern systems often use vectorized execution, where Next() returns a batch of tuples rather than one. This reduces function call overhead and enables SIMD optimizations. The tree structure remains the same; only the granularity of data flow changes.
Real database systems maintain rich metadata in their expression trees beyond simple operator labels. Understanding this metadata is essential for reading query plans.
Node Annotations:
Each tree node typically includes:
Example Annotated Node:
HashJoin
Type: Inner Join
Condition: employees.dept_id = departments.id
Output Schema: (emp_id, name, salary, dept_id, dept_name)
Estimated Rows: 850
Estimated Cost: 1250.0
Build Input: Departments (smaller)
Probe Input: Employees (larger)
12345678910111213141516171819202122
-- QueryEXPLAIN ANALYZESELECT e.name, d.dept_nameFROM Employees eJOIN Departments d ON e.dept_id = d.idWHERE e.salary > 50000; -- Query Plan (simplified representation)/*Projection (name, dept_name) └─ cost=0.00..1250.00 rows=850 └─ Hash Join (e.dept_id = d.id) └─ cost=0.00..1200.00 rows=850 └─ Hash (Departments) │ └─ cost=0.00..50.00 rows=50 │ └─ Seq Scan on Departments │ └─ cost=0.00..50.00 rows=50 └─ Seq Scan on Employees └─ Filter: salary > 50000 └─ cost=0.00..1100.00 rows=900 (after filter) └─ Rows Examined: 10000*/Query Plan Visualization:
Databases display trees in various formats:
Reading an EXPLAIN Plan:
The Difference Between Logical and Physical Plans:
The optimizer converts logical to physical, choosing algorithms based on costs and constraints.
Running EXPLAIN on your queries is one of the best ways to understand expression trees. Start with simple queries to see simple trees. Gradually add complexity—joins, subqueries, aggregations—to see how trees grow. Compare EXPLAIN output before and after adding indexes to see how the tree structure changes.
Query optimization is largely about transforming expression trees into equivalent but more efficient forms. The tree representation enables systematic application of transformation rules.
What Makes Trees Transformable:
Common Transformation Patterns:
Selection Pushdown:
Move selection closer to leaves to filter early:
Before:
σ_p
|
⋈
/
R S
After (when p involves only R):
⋈
/
σ_p S
|
R
Join Reordering:
Change the order of joins to minimize intermediate sizes:
Before:
⋈
/
⋈ T
/
R S
After (if S ⋈ T is smaller):
⋈
/
R ⋈
/
S T
| Transformation | When Applicable | Effect |
|---|---|---|
| Selection Pushdown | Selection predicate references only one child | Filter rows early, reduce intermediate sizes |
| Projection Pushdown | Projected columns subset of child's output | Narrow tuples early, reduce memory |
| Join Reordering | Joins are associative (inner joins) | Minimize intermediate join sizes |
| Selection Splitting | Conjunction of predicates | Push independent parts separately |
| Projection Pulling | Repeated projections | Combine into single projection |
| Join to Semijoin | Only need existence check | Avoid retrieving full joined tuples |
| Subquery Decorrelation | Correlated subquery | Convert to flat join structure |
Transformation as Tree Rewriting:
Each transformation rule can be expressed as a pattern matching and replacement:
Rule: Selection Pushdown over Join
Pattern:
σ_p(R ⋈ S) where p references only R
Replacement:
σ_p(R) ⋈ S
Algorithm:
For each node N in tree:
If N matches pattern left-hand side:
Replace N with pattern right-hand side
Update parent/child pointers
The Optimizer's Task:
Given an input tree, the optimizer:
This process might explore thousands of trees for complex queries, using dynamic programming or genetic algorithms to search efficiently.
Not all transformations preserve semantics in all cases. For example, pushing selection past outer join can change results. Optimizers must check applicability conditions carefully. A wrong transformation produces wrong results—the worst possible optimizer bug.
Expression trees carry properties and maintain invariants that help optimizers make decisions and ensure correctness.
Logical Properties:
Properties determined by the logical expression, independent of physical execution:
Physical Properties:
Properties that depend on how the operator is executed:
Property Propagation:
Properties propagate through the tree:
Example: Ordering Propagation:
For tree: ORDER BY salary(σ_dept='Eng'(Employees))
If Employees has index on (dept, salary):
- Scan returns rows ordered by (dept, salary)
- Selection preserves ordering (single dept value)
- Order by salary satisfied—no additional sort needed!
If Employees has no suitable index:
- Scan returns unordered rows
- Selection produces unordered result
- ORDER BY requires explicit sort operation
Interesting Orders:
The concept of "interesting orders" is crucial for optimization:
The optimizer tracks which orderings are interesting and prefers plans that produce them, even at slight extra cost, to avoid expensive sort operations later.
Property-Based Optimization:
Modern optimizers use properties for:
The Volcano and Cascades optimizer frameworks formalize property-based optimization, treating properties as first-class optimization dimensions.
When analyzing queries, think about what properties each operation produces and requires. Does the join need sorted input? Does the output need to preserve ordering? This property-centric thinking helps you understand optimizer decisions and write queries that are easier to optimize.
Expression trees naturally expose opportunities for parallel execution. Understanding these opportunities is essential for high-performance query processing.
Independent Subtrees:
Subtrees that share no data dependencies can execute concurrently:
⋈
/
/
σ(R) σ(S)
The two selection subtrees (on R and on S) can run simultaneously on different processors. The join waits for both to complete, then combines results.
Pipeline Parallelism:
In pipelining, producer and consumer operators run concurrently:
Producer: Table Scan on Employees
→ Stream tuples to →
Consumer: Selection (salary > 50000)
→ Stream tuples to →
Consumer: Projection (name)
All three operators can be active simultaneously, each processing different tuples.
Partition Parallelism:
Data is partitioned, and the same operator runs on multiple partitions:
Gather
/ |
σ σ σ
| | |
R(p1) R(p2) R(p3)
The relation R is split into partitions p1, p2, p3. Parallel selections run on each partition. Results are gathered at the end.
Exchange Operators:
Distributed databases insert exchange operators into trees to manage data distribution:
Gather
|
π(name)
|
⋈ (on hash-partitioned dept_id)
/
Repartition Repartition
| |
σ(Employees) σ(Departments)
Blockers and Parallelism:
Pipeline-breaking operators create synchronization points:
These blockers limit parallelism but are sometimes unavoidable.
Tree Width and Parallelism:
In distributed databases (Spark, Presto, CockroachDB), the expression tree is partitioned across nodes. The optimizer must consider network costs, data locality, and partition strategies. The tree structure remains the foundation, augmented with distribution metadata.
We've explored expression trees—the fundamental representation of relational algebra queries. Let's consolidate the key insights:
What's Next
With expression trees understood, we'll next explore query equivalence—the formal foundation that justifies tree transformations. We'll examine equivalence rules, the conditions under which they apply, and how they enable the optimization transformations we've previewed.
You now understand expression trees—the core representation that databases use for query processing. Every query plan you examine, every optimization the system applies, and every execution strategy chosen operates on this tree structure. Next, we'll explore the equivalence rules that make tree transformations valid.