Your ETL pipeline runs for six hours. The CPU sits at 12% while memory spikes to 90%. You've indexed, partitioned, and tuned garbage collection. Nothing moves the needle.
The Problem Isn't Your Hardware
Most DataFrame libraries execute eagerly. Every filter, join, and groupby materializes intermediate results. With 50GB datasets, you're not computing—you're copying memory. Polars' Lazy API flips this model. You describe the entire transformation as a query plan. The optimizer sees the full DAG before a single row moves.
What the Optimizer Actually Does
Predicate pushdown moves filters to the scan level—Parquet row groups get skipped entirely. Projection pushdown reads only required columns. Join reordering picks the optimal build/probe side based on statistics. Partition pruning eliminates whole files. These aren't theoretical; they compound multiplicatively.
| Optimization | Typical Speedup |
|---|---|
| Predicate pushdown | 3-10x |
| Projection pushdown | 2-5x |
| Join reordering | 2-20x |
| Partition pruning | 5-100x |
Real-World Pattern: Incremental Aggregation
Daily rollups over three years of event data. The naive approach reads everything, groups, writes. The lazy approach:
Debugging the Plan
Before collecting, inspect the optimized plan:
Look for SCAN nodes with pushed predicates and projections. Missing pushdowns usually mean the optimizer lacks statistics—run ANALYZE on your Parquet files or provide schema hints.
Common Pitfalls
Calling .collect() inside a loop defeats lazy evaluation. Each call executes the plan up to that point. Build the full expression chain first. Another trap: mixing eager and lazy objects. Join a lazy frame with an eager DataFrame and Polars must materialize the eager side, losing pushdown opportunities.
"The fastest code is code that never runs. Lazy evaluation lets you write the intent; the engine writes the execution.
— Ritchie Vink, Polars Creator
Your Next Step
Take your slowest ETL job. Wrap the read in scan_parquet, chain every transformation, add .collect(streaming=True) at the end. Measure wall time. Expect 50-80% reduction on I/O-bound workloads, 30-60% on compute-bound. The plan explain() output becomes your new profiling tool—read it like a query plan, because it is one.
✦









