skip to content

After optimization, the executor is handed a plan tree. Describe what that tree contains and how a typical relational executor consumes it to produce rows, including what changes when an operator cannot stream its output.

level: seniorimportance: should knowfreq 34%

answer

  1. operators expose open / next / close
  2. demand-driven pull from the root down
  3. streaming vs blocking (pipeline breakers)
  4. sort and hash build must see all input, may spill
  5. vectorized = batches, same tree, same breakers

basics

~20 s

The tree is physical operators - scans at the leaves, joins, aggregates and sorts above. The classic executor is demand-driven: each operator exposes open/next/close and pulls rows from its children. Streaming operators pass rows through; blocking ones like sort or hash build must consume their whole input first.

solid answer

~60 s

The executor receives a tree of **physical operators**: leaves are access paths (table scan, index scan, index-only scan), interior nodes are joins, aggregations, sorts, limits, each with its algorithm and parameters already fixed. The classic model is the **iterator (Volcano) model**: every operator implements `open`, `next`, `close`. The top asks for a row, that call recurses down, and rows are pulled upward one at a time. Nothing is materialized unless an operator requires it, so a `LIMIT` can stop the whole tree early and the client can receive the first row before the last one is computed. The key distinction is **streaming versus blocking (pipeline breaker)** operators. Nested loop join, filter and projection stream. Sort, hash aggregation and the build side of a hash join must consume their entire input before emitting anything - they hold state, consume memory, and spill to disk when they exceed their budget. That is why a plan with an early sort has poor time-to-first-row and unstable memory behaviour. Modern engines vary this by pushing **batches of rows** (vectorized) or compiling the tree to machine code, but the tree shape and blocking semantics are the same.

code

text · 6 lines
text
Limit (10)                       <- streaming, stops early
  Sort (order by total desc)     <- BLOCKING: needs all input
    HashAggregate (group by c.id)<- BLOCKING build
      HashJoin (build customers) <- build blocks, probe streams
        SeqScan customers
        IndexScan orders_customer_id

go deeper

for a junior

Say the plan is a tree of operators and that rows are pulled from the leaves upward; naming one blocking operator such as sort is enough.

for a middle

Describe open/next/close, pipelining and early termination, and name which operators block versus stream.

for a senior

Reason about memory budgets, spilling, time-to-first-row, and how misestimates at a hash build turn into an operational incident; mention vectorized or compiled execution as a variation.

for a principal

Discuss execution-model tradeoffs - pull versus push, interpretation versus compilation, parallel exchange - and how breaker placement drives latency SLOs and memory admission control at the cluster level.

## What the executor receives The optimizer's output is not code and not SQL - it is a **tree of physical operators**. Leaves produce rows from storage; interior nodes consume rows from their children and produce rows for their parent; the root delivers rows to the client protocol. Typical leaves: sequential/table scan, index range scan, index-only scan, or a lookup by key. Typical interior nodes: filter, projection/expression evaluation, nested loop join, hash join, merge join, hash or sorted aggregation, sort, limit, set operations, and materialization nodes. Each node carries parameters chosen at planning time - which index, which predicate, which join keys, which sort keys, how much memory it may use, and its estimated row count. ## The iterator (Volcano) model The long-standing execution model is demand-driven and uniform: every operator exposes the same three-call interface. - `open()` - initialize state, open children. - `next()` - return the next row or an end-of-stream marker. - `close()` - release resources. The root's `next()` recurses down the tree; each operator calls `next()` on its children as many times as it needs to produce one output row. A filter calls its child repeatedly until a row passes. A nested loop join calls the outer child once, then drives the inner child for each outer row. Three consequences follow directly: 1. **Pipelining.** Rows flow through without materializing the whole intermediate result, so memory use stays bounded for streaming plans. 2. **Early termination.** A `LIMIT` at the root simply stops calling `next()`, and the entire subtree stops working - which is why plans differ so much when a small limit is present. 3. **Incremental delivery.** A client can begin reading rows before the query has finished. ## Streaming versus blocking operators The most important operational property of a plan tree is where its **pipeline breakers** are. **Streaming operators** produce output as they consume input: filter, projection, nested loop join, merge join (given sorted inputs), the probe phase of a hash join, limit. **Blocking operators** must consume their *entire* input before producing the first output row: - **Sort** - cannot know the first row until it has seen the last. - **Hash aggregation and hash join build side** - the hash table must be complete before probing or emitting groups. - **Materialization / spooling nodes** - deliberately buffer a subtree for reuse. Blocking operators hold state proportional to their input, take a memory budget, and **spill to disk** when they exceed it, which turns a CPU-bound operator into an I/O-bound one and is a common source of cliff-edge slowdowns. They also destroy time-to-first-row: with a sort near the root, `LIMIT 10` still requires the full input to be sorted. ## Where transactional semantics enter The executor, not the optimizer, applies visibility: as scans read rows, they check whether each version is visible to this statement's snapshot or acquire the appropriate locks, depending on the concurrency model. It also enforces constraints on writes, fires triggers, and maintains indexes for modified rows. The plan says which rows to touch; the executor decides which of them this transaction may see. ## Modern variations on the same shape - **Vectorized execution.** Instead of one row per `next()`, operators exchange **batches** of hundreds or thousands of rows in columnar form. This amortizes the per-call overhead and enables CPU-friendly tight loops. The tree and its blocking semantics are unchanged. - **Compiled execution.** The plan tree is translated into native code that fuses pipelines between breakers, eliminating interpretation overhead entirely. - **Push-based execution.** Instead of parents pulling, producers push rows into consumers; this maps better to pipeline fusion and parallelism, but the pipeline segments are still delimited by the same blocking operators. - **Parallelism.** A subtree can be executed by several workers with an exchange/gather operator merging their outputs; degree of parallelism is decided at planning time and materializes as extra nodes in the tree. ## Reading a plan tree operationally When a plan is slow, the tree tells you where to look: - Follow the leaves: is a huge scan feeding a selective filter that should have been an index lookup? - Find the breakers: how much data reaches each sort or hash build, and does the memory budget cover it? - Check estimate versus reality per node: a badly under-estimated build side is the usual reason a hash operation spills or a nested loop is chosen where a hash join was needed. - Ask about time-to-first-row: for interactive queries, a breaker high in the tree is often the actual complaint even when total runtime is acceptable. The unifying point for an interview: the plan tree is not a script executed top to bottom - it is a dataflow graph consumed on demand, and its performance character is set by where data must stop and pile up.

  • Why can a query with LIMIT 10 be dramatically faster in one plan than another that returns the same rows?
    Because early termination only helps if the path between the root and the leaves is streaming. If the plan pipelines an index scan in the required order straight into the limit, it stops after ten rows. If a sort or hash aggregation sits in between, that breaker must consume the full input before emitting anything, so the limit saves almost nothing.

A pull-based assembly line: each station asks the one before it for the next part. Most stations pass parts along immediately; a sorting station must receive every part before it can hand over the first one.

saying these in an interview costs you the question

  • Describing execution as running the plan top-to-bottom like a program instead of pulling rows on demand.
  • Claiming every intermediate result is fully materialized between operators.
  • Not knowing that sorts and hash builds are blocking and can spill.
  • Thinking vectorized execution removes pipeline breakers.
  • Believing the executor re-chooses join algorithms based on what it sees at runtime.

context