advanced24 min studyLesson 286 in Engineering

Iterator pipelines and materialization

Follow demand through execution operators, identify blocking boundaries, and compare repeated recomputation with retaining reusable results.

On this page

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.

N^inspected≈ks\widehat N_{\rm inspected}\approx\frac{k}{s}
For k desired successes and independent row survival probability s>0 in a sufficiently long input, the expected inspected-row count is k/s. It is a probabilistic streaming model, not a bound, and does not bypass blocking operators or a finite input that ends first.↔ Scroll wide equations horizontally.

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.

Crepeat=rCp,Cretain=Cp+Cw+rCrC_{\rm repeat}=rC_p,\qquad C_{\rm retain}=C_p+C_w+rC_r
r is the positive number of consumers or replays, C_p is producing one equivalent result, C_w is retention/write cost, and C_r is one replay/read cost, all in consistent units. The model assumes the same reusable result and excludes invalidation, changed parameters, and concurrent memory pressure.↔ Scroll wide equations horizontally.

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.

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.

  1. Without retention, ten equivalent productions cost 10×12=120 ms. This assumes each consumer really recomputes the producer rather than benefiting from another cache.

  2. 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.

  3. 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 result

The benefit follows from actual equivalent reuse. Memory fit, spill, changed parameters, and startup latency can change the tradeoff.

Explore the model

Interactive lab

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.

Retained capacity must cover the reusable setA 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. Current readings: Hits / requests: 0 / 16; Hit rate: 0%; Address-stream period: 16 addresses. All parameters are adjustable with the labeled controls below.Fully associative LRU cache after 16 deterministic requests: 0 hits and 16 misses.Most recent 16 requestsaddress = i × 3 mod 160M3M6M9M12M15M2M5M8M11M14M1M4M7M10M13MCurrent cache order · oldest → newest471013Latest address 13 · miss: inserted as newestToy address stream · period 16 · starts empty
Cached addressMiss / evictionMost recent request
Hits / requests
0 / 16
Hit rate
0%
Address-stream period
16 addresses
t = 0 s

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.

Check your understanding

Which statement about a PostgreSQL Materialize plan node is justified?

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.

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.