skip to content

Walk through how the classic Volcano (iterator) execution model runs a query plan — what open(), next() and close() do on each operator — and explain where its CPU overhead comes from.

level: middleimportance: must knowfreq 52%

answer

  1. open / next / close
  2. demand-driven: root pulls, tuple returns
  3. one virtual call per row per level
  4. pipelining = bounded memory, early first row
  5. overhead invisible in OLTP, fatal in OLAP

basics

~20 s

Every plan operator implements open/next/close. The root's next() pulls one row from its child, which pulls from its child, down to the scans. It is simple and streams rows early, but costs an indirect call per row per operator, so big scans spend most cycles on machinery rather than data.

solid answer

~50 s

A physical plan is a tree of operators (scan, filter, join, sort, limit). In the Volcano or iterator model every operator exposes the same three methods: `open()` allocates state and opens its children, `next()` returns exactly one tuple or end-of-stream, `close()` releases resources. Execution is demand-driven: the client calls `next()` on the root, which calls `next()` on its child, down to a scan; the tuple then travels back up. The payoff is pipelining — no intermediate result is materialized, memory is bounded by operator state, the first row can come back before the scan finishes, and a row limit stops work simply by nobody calling `next()` again. Any operator composes above any other. The cost is per-row overhead: one indirect call per operator level per row, expression trees interpreted per row, loop state pushed into operator fields instead of CPU registers, and instruction-cache thrash that defeats branch prediction and SIMD. Irrelevant for OLTP; dominant on a 50-million-row scan.

code

text · 11 lines
text
Filter.next():
    while true:
        t = child.next()
        if t == EOF: return EOF
        if eval(predicate, t): return t

Limit.next():
    if emitted >= n: return EOF
    t = child.next()
    if t != EOF: emitted += 1
    return t

go deeper

for a junior

Be able to say a plan is a tree of operators and that rows are pulled one at a time from the top down via next().

for a middle

Name open/next/close, explain demand-driven pull and pipelining, and locate the overhead in per-row virtual calls and interpreted expressions.

for a senior

Quantify: calls per row per operator level, cache and branch-prediction effects, and why the same model is excellent for OLTP and poor for large scans.

for a principal

Frame it as an interface choice — uniform per-tuple dispatch buys composability and low latency and sells throughput; explain what vectorization or compilation changes about that trade.

## What a plan is before we talk about execution After parsing and optimization, a SQL statement becomes a *physical plan*: a tree of operators. Leaves read data (sequential scan, index scan); interior nodes transform it (filter, project, join, sort, aggregate, limit); the root hands rows to the client. Nothing in the tree is SQL any more — each node is a small piece of executable machinery with a fixed interface. ## The iterator interface The Volcano model (from the Volcano research system; also called the *iterator model*) gives every operator the same three methods: - **open()** — allocate state and recursively open children. A scan positions itself at the start of the heap or index; a hash join may build its hash table here. - **next()** — return exactly one tuple, or an end-of-stream marker. To produce it, the operator calls `next()` on its child (or children) as often as needed. - **close()** — free buffers, latches and temp files, and recursively close children. Because every operator both *implements* and *calls* this interface, any operator plugs above any other. That uniformity is why one executor runs both a two-table plan and a thirty-table plan. ## Demand-driven control flow Control flows top-down and *pulls*: the client asks the root for a row, the root asks its child, and so on until a scan finally touches a page. The tuple then returns back up as a return value. Producing one row at the root costs one call per operator level. A filter is essentially: ``` next(): loop: t = child.next() if t is EOF: return EOF if predicate(t): return t ``` ## Why this design won and still ships - **Pipelining.** Rows flow through without materializing intermediate results, so memory is O(operator state), not O(rows). - **Early first rows.** The root can return row 1 before the scan has read page 2. - **Free early termination.** A row limit simply stops calling `next()`; everything below unwinds. - **Composability.** New operators need no knowledge of their neighbours. - **Blocking operators still fit.** Sort or a hash build just consume their entire input inside the first `next()` (or in `open()`), then stream results out afterwards. ## Where the overhead comes from For an OLTP statement touching a handful of rows this bookkeeping is invisible. On a 50-million-row scan it dominates: 1. **One indirect (virtual) call per row per operator level.** A five-deep plan over 50M rows is 250M calls the CPU cannot inline and often mispredicts. 2. **Interpreted expressions.** A predicate like `a + b > 10` is usually a small expression tree walked per row, with more dispatch and generic typed values. 3. **State save/restore.** Because `next()` returns after one row, cursors and counters live in operator fields rather than registers. 4. **Poor locality.** Running operator A's code, then B's, then C's for every single row thrashes the instruction cache and blocks loop unrolling, SIMD and effective branch prediction. 5. **Tuple handling.** Per-row copying or deforming into a generic tuple representation, plus null bookkeeping. The vectorized-execution literature measures roughly an order of magnitude of overhead versus a hand-written loop for simple scan-and-aggregate work: most cycles go to the machinery, not the data. ## What engines do about it Two families of fixes, both keeping the operator tree: - **Vectorized execution** — `next()` returns a *batch* (say 1024 rows, column-wise) instead of one row, amortizing the call and letting inner loops be tight and SIMD-friendly. - **Query compilation** — generate machine code that fuses adjacent operators into one loop so tuples stay in registers and there is no per-row call at all. Row-at-a-time iteration survives where it is genuinely right: OLTP plans, index nested-loop lookups, and any plan whose row counts are small enough that latency and simplicity beat raw throughput.

  • Why is the iterator model still the right choice for OLTP engines?
    OLTP plans touch few rows, so per-row dispatch never accumulates into a visible cost. What OLTP does need is exactly what the model gives cheaply: minimal memory per query, low time-to-first-row, and free early termination on point lookups and small limits. It is also far simpler to debug and to extend with new operators than a code-generating engine.
  • How do blocking operators such as sort fit a model built around returning one row at a time?
    They keep the same interface but break the pipeline internally. On the first call to next() (or during open()) the sort drains its child completely, sorts the buffered rows, possibly spilling to disk, and only then returns the first row; subsequent next() calls just walk the sorted result. So the interface stays uniform while the timing changes: nothing comes out until everything has gone in.

A bucket brigade where each person hands up exactly one bucket when the person above shouts "next" — perfect when you need the first bucket immediately, wasteful when you have to move a lake.

saying these in an interview costs you the question

  • Saying the iterator model materializes each intermediate result — the whole point is that it does not.
  • Claiming next() returns a set or batch of rows; in the classic model it returns exactly one tuple.
  • Believing the overhead is I/O — it is CPU dispatch and interpretation, and it shows up even when everything is in the buffer pool.
  • Assuming Volcano's per-row cost makes it obsolete; it is still the right model for OLTP.
  • Confusing the iterator model with parallelism (Volcano's exchange operator is a separate idea).

context