Compare pull-based (demand-driven) and push-based (data-driven) query execution engines: what changes in how an operator is written, and what does a push-based design buy you?
answer
- data always flows up; control direction differs
- pull: consumer calls next()
- push: producer calls consume()
- push → fused loop, state in registers
- pull → free early stop, easy merge join
basics
~20 sPull: the consumer calls next() on its child and rows come back as return values. Push: the producer drives the loop and hands each tuple or batch to its parent's consume() method. Push fuses operators into one tight loop, suits code generation and multi-consumer plans; pull makes early termination and multi-input operators easier.
solid answer
~50 sIn a **pull** (demand-driven, Volcano) engine control starts at the root: each operator's `next()` calls its child's `next()`. In a **push** (data-driven) engine control starts at the leaves: a scan loops over its data and calls `consume(tuple)` on its parent, which calls `consume()` on its parent, up to the pipeline's end. Operator code changes shape. Pull operators must save their loop state between calls, because they return after each tuple. Push operators keep the loop in one place, so the state stays in local variables and registers — which is exactly why compiling engines (the produce/consume model) prefer push: an entire pipeline collapses into a single generated loop with no per-tuple control transfer. Push also handles plan DAGs naturally (one producer feeding several consumers) and matches morsel-driven parallelism and streaming inputs. What gets harder: early termination needs an explicit stop signal propagated downward rather than just not calling `next()`, and multi-input operators such as merge join, which want to alternate between inputs, are more awkward.
go deeper
Recognize the two directions of control — consumer asks (pull) versus producer delivers (push) — and that classic engines are pull.
Explain how operator code and state differ, and that push fuses adjacent operators into one loop.
Discuss why compiled engines choose push, how pipelines are split by breakers, and the concrete costs: explicit stop signals, awkward merge join, harder profiling.
Treat it as an executor substrate decision entangled with compilation strategy, parallel scheduling (morsel-driven) and DAG plan support, and note it is orthogonal to vectorization.
## Two directions for control flow A query plan is a tree (or DAG) of operators. The data always flows from leaves to root — scans read, the client receives results. What differs between the two designs is where **control** flows. **Pull / demand-driven (the Volcano iterator model).** The client calls `next()` on the root. The root calls `next()` on its child. The chain descends to a scan, which fetches a tuple and returns it; the tuple travels back up as return values. Every operator is a consumer that *asks*. **Push / data-driven.** Execution starts at the leaves. A scan runs its own loop over pages and, for every tuple (or batch), calls a method on its parent — conventionally `consume(tuple)`. The parent processes it and calls `consume()` on *its* parent. Every operator is a producer that *delivers*. ## What changes in the operator code The practical difference is where the loop lives and therefore where state lives. In pull, an operator must return control after each tuple, so anything it was in the middle of — the current position in a hash bucket, the index of the outer row, the count so far — has to be stored in the operator's own object and reloaded on the next call. That is per-tuple bookkeeping the CPU cannot keep in registers. In push, the loop belongs to the producer and never yields. A filter's `consume()` is simply "if the predicate holds, call parent.consume(t)". Local variables stay local; several operators' logic can be inlined into one function body. That is the whole reason data-centric compilation (the produce/consume model popularised by the HyPer system) is built on push: `produce()` walks the plan at code-generation time to emit the loop skeleton, `consume()` emits the per-tuple body, and the result is one machine-code loop per *pipeline* in which tuples never leave registers until a pipeline breaker. ## What push buys - **Operator fusion.** Adjacent non-blocking operators disappear into a single loop; no per-tuple call, no intermediate tuple representation. - **Better locality.** Data stays hot in registers and cache for the whole pipeline instead of being handed back through call frames. - **DAG plans are natural.** If one intermediate feeds two consumers (shared subexpressions, some window and grouping-set plans), a producer just calls two `consume()` targets. In pull, two parents calling `next()` on one child requires a buffering or tee operator. - **Fits push-shaped sources.** Streaming ingest, network row batches and morsel-driven parallel scans all naturally produce data rather than wait to be asked. - **Parallelism.** Morsel-driven scheduling hands each worker a chunk of input and lets it push that chunk through a pipeline — a clean fit. ## What push costs - **Early termination is explicit.** In pull, a limit stops the world by simply not calling `next()` again. In push, the producer is in charge, so the consumer must return or raise a stop signal that the producer checks and honours — easy to get wrong, and it must traverse an already-running loop. - **Multi-input operators are awkward.** Merge join wants to advance whichever side is behind; that is trivial when you can call `next()` on either input and unnatural when both inputs push at you. Engines handle it by materializing one side or by treating each input as a separate pipeline. - **Debuggability.** With compiled push pipelines the stack no longer mirrors the plan, so profiling and per-operator timing need extra instrumentation. ## Pipelines and breakers apply to both Either way, blocking operators cut the plan into pipeline segments: a hash join's build side is one pipeline that ends in a hash table; the probe side is another that starts from that table. Push does not remove blocking; it just makes each segment a single loop. ## How to answer in an interview Say plainly that data flows the same direction in both and only control differs; that pull's virtue is composability and cheap early termination; that push's virtue is fusion, register residency, DAG support and parallel scheduling; and that push is the natural substrate for compiled execution while vectorized engines are found in both flavours (some vectorized engines push batches, others pull them).
- Why do compiling query engines almost always use a push model?Compilation wins by keeping a tuple in registers across several operators. That requires one uninterrupted loop, which is what push gives: the producer owns the loop and each operator contributes inlined body code via consume(). A pull model would force a return after each tuple, spilling state back into operator objects and reintroducing the per-tuple control transfer that compilation is meant to eliminate.
- How does a push-based engine implement a row limit?It needs an explicit back-pressure or stop signal, since the consumer cannot simply stop asking. The limit operator counts rows and, once satisfied, returns a stop indication that propagates down to the driving loop, or sets a flag the producer checks each iteration; the producer then breaks out and closes its pipeline. It is more machinery than pull's "just don't call next() again" and is a common source of bugs where a query keeps scanning after enough rows exist.
saying these in an interview costs you the question
- Saying data flows downward in a pull engine — data always flows leaf-to-root; only control direction differs.
- Equating push with streaming systems only; classic relational engines use push internally for compiled pipelines.
- Claiming push removes blocking operators or spills.
- Treating push versus pull as the same distinction as vectorized versus row-at-a-time — they are independent axes.
- Asserting push is simply faster, without mentioning early termination and multi-input operators as its weak spots.