Iterator pipelines and materialization
Follow demand through execution operators, identify blocking boundaries, and compare repeated recomputation with retaining reusable results.
On this page
The question for today
What you’ll understand
- Trace an open-next-close style pull pipeline.
- Distinguish streaming output from blocking input consumption.
- Calculate a materialization tradeoff with explicit production and replay costs.
A plan tree does not imply that every intermediate relation is written out in full. An iterator can ask its child for just enough input to produce its next result, allowing rows to flow through several operators. Other operators need substantial input before yielding. Materialization introduces retained state deliberately, trading memory or temporary I/O for reuse, scheduling independence, or required access behavior.
Pull execution follows a demand chain
In an open-next-close style interface, initialization prepares operator state, next requests another result, and close releases resources. A filter may call its child repeatedly until it finds a qualifying row or reaches end-of-input. A projection can transform that row and pass it upward without assembling a complete intermediate table. This composition is a central idea in the Volcano paper and remains useful for understanding iterator engines. It describes an interface and control flow, not a promise that each next call always performs the same amount of work.
A parent asking for one row might trigger a chain of child requests, including many rejected candidates. Conversely, a LIMIT can stop a compatible streaming pipeline before every source row is visited. Cancellation, end-of-input, and exceptions must release scans, buffers, and temporary state correctly. An execution object also needs well-defined ownership of returned storage: a pointer or batch may remain valid only until a later call. Consuming a row after its producer reused the buffer is a correctness bug regardless of how elegant the pipeline appears.
Blocking depends on the algorithm and required property
A full sort generally must inspect its input before it can guarantee the globally first result. A hash join must build suitable state from one side before normal probing yields results. A hash aggregate may need input exhaustion before final group values are known, while an aggregate over correctly grouped ordered input can emit completed groups earlier. These distinctions locate pipeline boundaries. The word aggregate or join alone is not enough: inspect the physical method, required order, and whether partial or final results are semantically permitted.
Materialization stores an intermediate result so it can be revisited without rerunning its producer. It can help a repeated inner input, decouple a consumer, or support rewind behavior. Storage may be bounded in memory and spill when necessary. That stored result has a lifetime and a scope: a PostgreSQL Materialize node is not automatically a persistent result cache shared across unrelated queries. Nor is every SQL common-table expression necessarily materialized; documented folding rules, reference patterns, volatility, and explicit directives influence the actual plan.
Retention is worthwhile only when reuse is real
Compare the complete costs over the required lifetime. Retaining an expensive small producer result can save work, but materializing a large one-use stream can add copies and delay output. A parameterized subplan may produce different answers for different outer keys, so one stored result cannot be reused indiscriminately. Snapshot and visibility rules still apply. A trustworthy explanation names what is reused, by whom, how often, and which memory or I/O budget retains it. Draw the lifetime overlap when several materialized results coexist.
The cache lab illustrates a small reuse boundary, not materialization inside a database. Reset, set both C=3 and s=4, predict zero hits in the initial sixteen requests, then Play or Step. Separately Reset, set both C=4 and s=4, predict twelve initial hits, then Play or Step. This fixed trace cycles over addresses 0,4,8,12 and starts with an empty fully associative LRU cache before its sixteen initial requests. It neither allocates a SQL spool nor measures iterator overhead. Use it to explain why retaining less than the repeatedly needed set can lose the intended reuse.
Work it through
Retain once or reproduce ten times?
An identical intermediate result costs twelve milliseconds to produce. Ten consumers each need it. In a teaching model, retaining it costs another four milliseconds once and each replay costs two milliseconds. No parameters or snapshot semantics change.
Without retention, ten equivalent productions cost 10×12=120 ms. This assumes each consumer really recomputes the producer rather than benefiting from another cache.
With retention, produce once for 12 ms, retain for 4 ms, and replay ten times for 10×2=20 ms. Total modeled work is 36 ms.
The modeled saving is 120−36=84 ms. Retention wins for r when 12+4+2r<12r, or r>1.6; at least two complete replays suffice under these fixed assumptions.
The benefit follows from actual equivalent reuse. Memory fit, spill, changed parameters, and startup latency can change the tradeoff.
The living notebook
Explore the model
Interactive lab · cache
Retained capacity must cover the reusable set
Reset, set both C=3 and s=4, predict zero hits in the initial sixteen requests, then Play or Step. Separately Reset, set both C=4 and s=4, predict twelve initial hits, then Play or Step. The fixed modulo-sixteen LRU trace is not a query spool, result cache, or iterator benchmark.
- Hits / requests
- 0 / 16
- Hit rate
- 0%
- Address-stream period
- 16 addresses
A fully associative LRU cache starts empty and requests address (i·s) mod 16. Hits move an address to the newest position; misses evict the oldest when full. Playback starts with 16 requests and adds two per second, capped at 4096 explicitly simulated requests. This toy stream is not a processor-cache benchmark. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forRetention helps only if the reused state remains available; a capacity boundary can eliminate the expected benefit.
Pause & reason
Check your understanding
Your turn
Try it yourself
A streaming filter independently retains each row with probability 0.25. Estimate input rows inspected to obtain twenty successes, assuming an effectively unlimited input. Why is the result not an upper bound?
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Use k/s, then consider a run containing many rejected rows.
Compare with the solution
The expected inspected count is 20/0.25=80 rows under the independent constant-probability model. Random variation can produce longer rejection runs, and correlated real input can differ further. A finite table may end before twenty matches appear; a blocking parent can also require more work than this local streaming estimate suggests.
Keep the idea
Take it with you
- Iterator demand can stream results without fully retaining every intermediate.
- Blocking boundaries depend on the algorithm and required output property.
- Materialization trades retention cost and lifetime for genuine equivalent reuse.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.