Performance Optimization
Zero is designed for high performance with its incremental view maintenance (IVM) engine and intelligent query planning. This guide covers optimization strategies to get the most out of your Zero applications.Query Optimization
Understanding the Query Planner
Zero uses a cost-based query planner that optimizesWHERE EXISTS (correlated subquery) statements by choosing optimal join execution strategies.
The planner evaluates two join strategies:
-
Semi-join (default): Parent is outer loop, child is inner
-
Flipped join: Child is outer loop, parent is inner
packages/zql/src/planner/README.md for detailed planner documentation.
Index Optimization
Create indexes on join columns to enable efficient lookups:- Enable fast constraint lookups during joins
- Improve query planner cost estimates
- Critical for flipped join performance
Running ANALYZE
Zero’s planner uses SQLite statistics to estimate query costs. RunANALYZE periodically to keep statistics fresh:
sqlite_stat1: Basic table and index statisticssqlite_stat4: Histogram data for accurate selectivity estimates- Fan-out factors for join operations
- After initial data load
- After bulk imports or updates
- When query performance degrades
- Periodically (e.g., daily or weekly)
Avoid NULL in Unique Constraints
NULL values in OR queries can cause SQLite to abandon index optimization and perform full table scans:- NULL + OR can cause 320x slowdowns
- A query that should take less than 1ms can take 320ms
- SQLite won’t use MULTI-INDEX OR optimization
Query Structure Best Practices
Use Limits Wisely
Limits propagate through the query pipeline and can significantly reduce work:- Semi-joins: Limit propagates to parent scan
- Flipped joins: Limits are removed (must scan all children)
- The planner considers this when choosing join strategy
Avoid Deep Nesting
Deeply nested queries can be expensive. Consider restructuring:Filter Early and Often
Apply filters as early as possible in the query:Memory and Resource Management
Understanding IVM Memory Usage
The IVM engine maintains materialized views in memory. Each query creates an operator pipeline that:- Fetches initial results from the source
- Maintains incremental state for push updates
- Stores relationship streams for hierarchical data
- Queries with large result sets consume more memory
- Related data creates additional relationship storage
- Multiple queries multiply memory usage
Cleanup and Lifecycle
Destroy queries when no longer needed:- Calls
destroy()on all operators in the pipeline - Removes subscriptions to source changes
- Frees memory used by materialized views
Operator Pipeline Efficiency
Each operator in the pipeline processes changes incrementally:Filter: Evaluates conditions, may split edit changesJoin: Manages relationship streams, fetches child dataTake: Limits output, short-circuits when limit reachedUnionFanIn/Out: Manages OR branches
packages/zql/src/ivm/operator.ts for operator interfaces.
Client-Side Optimization
Debounce Rapid Updates
If your app generates rapid mutations, consider debouncing:Batch Mutations
Group related mutations when possible:Use Selective Subscriptions
Only subscribe to data you need:Server-Side Optimization
Connection Pooling
Zero Cache uses connection pooling for PostgreSQL. Configure appropriately:Monitor Query Performance
Use OpenTelemetry integration to track query metrics:packages/zql/src/query/metrics-delegate.ts for metric types.
Schema Design
Optimize your database schema:- Use appropriate data types (e.g.,
INTEGERvsTEXTfor IDs) - Add foreign key constraints to help the query optimizer
- Denormalize selectively for read-heavy workloads
- Partition large tables if supported by your database
Benchmarking and Profiling
Enable Debug Logging
Use the planner debugger to analyze query plans:attempt-start: New flip pattern being evaluatednode-constraint: Constraint propagated to a nodenode-cost: Cost computed for a nodebest-plan-selected: Best plan chosen
Measure Query Materialization Time
Track how long queries take to materialize:Test with Realistic Data
Performance characteristics change with data scale:- Test with production-like data volumes
- Include realistic data distributions (skewed data, NULLs, etc.)
- Measure both initial fetch and incremental updates
Common Performance Pitfalls
1. Missing Indexes
Problem: Joins without indexes cause full table scans Solution: Create indexes on all foreign key columns2. Stale Statistics
Problem: Planner makes poor decisions with outdated statistics Solution: Run ANALYZE regularly, especially after data changes3. Too Many Active Queries
Problem: Each query maintains state, consuming memory Solution: Destroy queries when components unmount, use query consolidation4. Inefficient Filters
Problem: Low-selectivity filters don’t reduce work much Solution: Add more selective filters, use compound conditions5. Large Result Sets
Problem: Fetching thousands of rows is slow and memory-intensive Solution: Use pagination, virtual scrolling, or limit result sizesPerformance Checklist
- Indexes created on all join columns
- ANALYZE run on database
- Query limits used where appropriate
- Queries cleaned up when no longer needed
- Filters applied early in query pipeline
- OpenTelemetry metrics monitored
- Tested with production-like data volumes
- No NULL values in OR conditions
- Connection pool sized appropriately
- Mutations batched when possible
Next Steps
- Learn about the ZQL Engine internals
- Review Troubleshooting for debugging tips
- Understand Migration strategies for version upgrades