ZQL Engine Deep Dive
Zero’s query language (ZQL) is powered by an incremental view maintenance (IVM) engine that efficiently materializes and updates query results. This guide explores the engine’s architecture, design patterns, and implementation details.Architecture Overview
The ZQL engine consists of three main layers:- Query Layer (
packages/zql/src/query/): High-level query API and AST generation - Planner Layer (
packages/zql/src/planner/): Cost-based query optimization - IVM Layer (
packages/zql/src/ivm/): Incremental view maintenance execution
Data Flow
The IVM Engine
Core Concepts
The IVM engine maintains query results incrementally by processing change notifications instead of re-executing queries: Traditional approach (re-query):Operator Pipeline
Queries are compiled into operator pipelines. Each operator:- Implements
InputandOutputinterfaces (packages/zql/src/ivm/operator.ts:103) - Processes fetch requests for initial materialization
- Handles push updates for incremental maintenance
- Maintains internal state for efficient updates
Operator Types
Source
Location:packages/zql/src/ivm/source.ts
Purpose: Root data source, feeds data into the pipeline
- Multiple outputs (one per query using the table)
- Applies filters and ordering
- Splits edit changes when sort keys change
Filter
Location:packages/zql/src/ivm/filter.ts
Purpose: Evaluates conditions and filters rows
Fetch behavior:
Join
Location:packages/zql/src/ivm/join.ts
Purpose: Joins parent and child data, creates hierarchical results
Key insight: Zero’s joins produce hierarchical data, not flat tables:
packages/zql/src/ivm/join.ts:43 for full implementation.
Flipped Join
Location:packages/zql/src/ivm/flipped-join.ts
Purpose: Optimized join when child table is smaller or more selective
Difference from regular join:
- Child table is much smaller than parent
- Child has highly selective filters
- Query has LIMIT but parent has no effective filters
packages/zql/src/planner/) automatically chooses between regular and flipped joins based on cost estimates.
Take (Limit)
Location:packages/zql/src/ivm/take.ts
Purpose: Limits output to first N rows
Fetch behavior:
Union Fan-In/Out
Location:packages/zql/src/ivm/union-fan-in.ts, packages/zql/src/ivm/union-fan-out.ts
Purpose: Handles OR conditions by splitting/merging data streams
Change Types
Location:packages/zql/src/ivm/change.ts
The IVM engine processes four types of changes:
Add Change
Remove Change
Edit Change
- Primary key stays the same (or changes in a compatible way)
- Relationships are unchanged
- The row no longer passes filters
- Sort order changes (relationships must be rebuilt)
packages/zql/src/ivm/view-apply-change.ts:46 for split logic.
Child Change
Streams and Yielding
Location:packages/zql/src/ivm/stream.ts
Stream Type
- Consumed using
for...ofloops - Single-use (can’t restart after exhaustion)
- Enable cleanup when iteration stops early
Yield for Responsiveness
Long-running operations can yield control:- If an input yields
'yield', caller must yield it immediately - Prevents blocking the UI thread during large operations
- Allows incremental rendering and cancellation
packages/zql/src/ivm/operator.ts:41 for yield contract details.
Query Planner
Location:packages/zql/src/planner/
Purpose
The query planner optimizesWHERE EXISTS queries by choosing optimal join execution strategies.
What it optimizes:
- Join direction (semi vs flipped)
- Cost estimation (expected query cost)
- Constraint propagation (how filters enable index usage)
Planning Algorithm
The planner uses exhaustive enumeration of join flip patterns:packages/zql/src/planner/README.md for comprehensive planner documentation.
Cost Model
- Number of rows scanned in each table
- Filter selectivity (fraction of rows passing predicates)
- Join selectivity (fraction of parent rows with matching children)
- Index availability (affects constraint costs)
- Limit propagation (early termination)
Fan-Out Calculation
Location:packages/zql/src/planner/SQLITE_STAT_FANOUT.md
Fan-out = average number of child rows per parent row
Formula for semi-join selectivity:
sqlite_stat4 and sqlite_stat1 for accurate fan-out statistics.
Storage and State
Location:packages/zql/src/ivm/operator.ts:109
Storage Interface
Operators can store internal state:- Take operator: Stores current result set
- Join operator: Caches relationship data
- Custom operators: Arbitrary state management
Memory Source
Location:packages/zql/src/ivm/memory-source.ts
In-memory source implementation:
- Automatic index creation based on query constraints
- Efficient range queries
- Used for testing and client-side sources
View Factory
Location:packages/zql/src/ivm/view.ts
View Types
View Factory
- Initial materialization
- Incremental updates
- Lifecycle management (cleanup)
- TTL (time-to-live) tracking
Advanced Topics
Constraint Propagation
Constraints flow backwards through the pipeline to enable efficient fetches:SELECT * FROM posts WHERE userId = ? LIMIT 1
See packages/zql/src/planner/README.md:357 for constraint flow details.
Relationship Streams
Relationships are lazy streams, not materialized arrays:- Don’t fetch children unless accessed
- Support infinite/large child collections
- Enable streaming rendering
Debug Delegate
Location:packages/zql/src/builder/debug-delegate.ts
Debug interface for tracing query execution:
- Understanding query execution
- Debugging performance issues
- Testing and validation
Performance Characteristics
Time Complexity
| Operation | Initial Fetch | Push Update |
|---|---|---|
| Source | O(n) | O(1) |
| Filter | O(n) | O(1) |
| Join | O(n × m) | O(k) where k = affected rows |
| Take | O(n) up to limit | O(1) to O(log n) |
| Union | O(n + m) | O(1) |
Space Complexity
- Source: O(n) for all rows
- Filter: O(1) (no state)
- Join: O(n × r) where r = relationships per row
- Take: O(limit) for result buffer
- Union: O(n) for deduplication
Incremental Update Benefits
IVM shines when:- Changes affect small fraction of results (k much less than n)
- Multiple queries over same data
- UI needs real-time updates
- Re-query: O(1M) - scan all users
- IVM push: O(1) - add one node
Implementation Patterns
Dual-State Pattern
Many components separate immutable structure from mutable planning state:Generator-Based Streams
Use generators for lazy evaluation and cleanup:Monomorphic Dispatch
For performance, avoid polymorphic shapes:packages/zql/src/ivm/change.ts:6 for optimization notes.
Next Steps
- Optimize queries using Performance best practices
- Debug issues with Troubleshooting techniques
- Plan upgrades using Migration guides