fivenines
9/25

Guided Problem

MiniPG Build 04: Execute a Bounded Report

Time
25m
Level
intermediate
Artifacts
not specified
Progress0%

Build Your Own PostgreSQL

Executors as Iterator Machines

Time
6 min
Prerequisites
Costs Cardinality And Choosing A Plan

What You Will Learn

  • Trace demand for one result tuple down and back up an executor tree
  • Distinguish streaming nodes from blocking nodes and their resource behavior
  • Connect executor lifecycle, expression memory, visibility, and instrumentation
executorplan nodeiteratortuple slotstreaming nodeblocking nodeexpression contextinstrumentation

The plan starts moving

A bookstore's chosen plan says to scan unpaid orders, look up each customer, and return selected columns. The plan is still inert. The executor makes it move by initializing nodes, requesting tuples, passing them upward, and finishing resources. PostgreSQL's plan tree behaves like an iterator machine: a parent asks a child for the next tuple instead of commanding the whole subtree to run at once.

The iterator model localizes behavior. A sequential scan walks pages and tests visibility. An index scan follows entries and fetches heap tuples. A filter evaluates a predicate. A nested loop repeatedly pulls from an outer child and probes an inner child. Parent nodes need only the common tuple-production contract.

Streaming until it cannot

This streaming shape keeps memory use low for many queries. If a query scans a table, filters rows, and projects two columns, tuples can flow one at a time. The executor does not need to materialize the entire table before returning the first result. That matters for latency and for queries over large relations. Streaming also fits the protocol, where results can be sent incrementally as rows become available.

Tuple flow

Rendering diagram…

Trace demand through the tree

The client requests a result. Project asks the nested-loop join for one tuple. The join asks its outer filter, which asks the sequential scan for an order tuple and rejects it if status does not match. For a surviving order, the join supplies its customer identifier to the inner index scan. When that scan returns a visible customer tuple, the join combines the two, Project selects the output columns, and one result can flow to the client.

Demand then repeats. No node needs to know the entire implementation of its children, and the pipeline can stop early if a limit is satisfied or cancellation arrives. The invariant is lifecycle discipline: every node must honor initialization, tuple production, rescan where supported, and cleanup while preserving the plan's semantics. That uniform interface is what lets very different access and join methods compose.

Blocking points change the behavior

Not every node can be purely streaming. Sort must often read all input before returning the first row. Hash join must build a hash table for one side before probing with the other. Aggregation may need to hold groups in memory. Materialize nodes may intentionally store intermediate results so they can be reread. The executor is therefore a mix of pipelines and blocking points. Knowing where a plan blocks tells you a lot about memory pressure and first-row latency.

Nested loops and hash joins behave differently once execution begins. A nested loop can produce its first joined row quickly if the outer side produces a row and the inner lookup is cheap. It can be slow if the outer side is large and each inner scan repeats heavy work. A hash join delays output while building the hash table, but then probes efficiently for many rows. The planner chose between them using estimates, and execution shows whether those estimates held.

Expression evaluation is another central executor service. Predicates, computed columns, function calls, casts, and aggregate transitions all run inside expression contexts. These contexts manage short-lived memory so per-tuple allocations can be reset cheaply. Without that discipline, a long scan could leak tiny allocations until the backend bloats. The executor's memory model matters as much as its tuple model.

Execution is more than reads

Visibility checks tie execution back to transactions. A heap scan does not simply return every physical row version it finds. It asks whether the tuple is visible under the current snapshot. An update may create a newer version while an older transaction still needs the old one. The executor must apply MVCC rules consistently, or the query result becomes a mix of incompatible times.

Modification statements use the executor too. An `insert` creates tuples, checks constraints, updates indexes, and emits log records through storage layers. An `update` often creates a new tuple version rather than overwriting in place. A `delete` marks a tuple version as no longer visible to future transactions. The plan may have a scan subtree to find affected rows, followed by a modification node that applies changes. Reads and writes share the same framework because both are tuple-producing operations with side effects controlled by transaction state.

Triggers and constraints add controlled interruptions. A row may pass through before-row triggers, constraint checks, index maintenance, and after-row triggers. Foreign keys can require lookups in other tables. The executor coordinates these behaviors without letting them turn the plan tree into arbitrary application code. Each hook has a defined moment in the tuple lifecycle.

Instrumentation often lives here as well. To explain a plan, the system can count rows produced by each node, loops executed, time spent, buffers touched, and memory used. This feedback is how users find out when estimates were wrong or when a plan shape behaves differently than expected. The executor does the work, and it is also the place where planned work becomes observable.

Read a plan as resource behavior

For each node, ask what wakes it, what it retains between calls, and what must happen before it can emit its first tuple. Those questions expose streaming, blocking, rescans, and memory lifetimes more clearly than the node name alone. They also locate cancellation and cleanup obligations: resources acquired while satisfying demand must be released even when demand ends early.

Into storage

The iterator machine ends at storage. Scan nodes need pages. Modification nodes need free space and durable records. Index nodes need ordered structures. The executor asks for tuples, but tuples live inside files arranged as pages. The next layer drops below plans and follows a row into heap storage.

That descent is where the stream of tuples becomes bytes with addresses, headers, and history.

Next step

See what actually stuck.

Take the practice scenarios now.