# Week 11 - Encapsulation of Parallelism in the Volcano Query Processing System

Last edited: 2026-07-28

Paper: Encapsulation of Parallelism in the Volcano Query Processing System — Goetz Graefe, SIGMOD 1990.

The key contribution: all parallelism can be encapsulated in a single operator — the exchange operator — inserted into an existing query tree, leaving every other operator completely unmodified. This is the paper that introduced the iterator model and exchange operator that underpin virtually every modern parallel query engine (Postgres, SQL Server, Spark, DuckDB).

# The Problem: Parallelising Without Breaking Extensibility

Volcano is an extensible query processing engine: operators plug in via a uniform interface. The challenge: parallelise Volcano without requiring operator authors to think about parallelism.

Two approaches existed:

Bracket model (used by GAMMA and Bubba): wrap each operator in a generic process template that handles network I/O. The template code invokes the operator and manages producer–consumer coordination.

Problems with the bracket model:

  • Requires a separate scheduler process for every operator, both initially and whenever a new operator is added.
  • Operators must be coded so that network I/O is their only means of getting input and delivering output — a tight coupling that makes the system non-extensible.
  • Matching two operators’ speeds in a producer–consumer relationship requires IPC system calls even when both run on the same machine.

Operator model (Volcano’s approach): add a single new operator — exchange — that can be inserted at any point in the query tree. All parallelism issues are localised inside exchange. Every other operator is written for single-process execution and never modified.

# Volcano’s Iterator Model

All operators in Volcano implement a uniform open–next–close protocol, identical to conventional file scans:

  • open: initialise state, allocate data structures, recursively open inputs.
  • next: produce the next output record (returns a NEXT_RECORD structure: record identifier + buffer pool address). The record is pinned in the buffer and owned by exactly one operator at a time.
  • close: release resources and recursively close inputs.

Each operator has a state record holding its local state and support functions (comparators, hash functions, predicates) passed in as arguments — operators never inspect data types directly.

Operators declare their inputs as anonymous streams — they don’t know or care whether input comes from a file scan, another operator, or an inter-process channel. This is what makes the exchange operator transparent.

For intermediate results, Volcano uses virtual devices: the buffer manager assigns RIDs to intermediate records and operators treat them as if they came from disk, even though they never leave the buffer pool.

# The Exchange Operator

The exchange operator implements all three forms of parallelism within a single module.

# Vertical Parallelism (Pipelining)

open_exchange creates a port (shared-memory data structure) and then forks a child process using fork(). The child process becomes the producer and runs the query subtree below exchange. The parent process becomes the consumer and proceeds upward in the query tree.

  • The producer uses data-driven (push) dataflow: it calls next on its subtree and pushes records into the port as packets (arrays of NEXT_RECORD structures).
    • When a packet is filled, it is linked into the port’s list and a semaphore signals the consumer.
    • When input is exhausted, the producer marks the last packet end-of-stream and waits for the consumer to allow close.
  • The consumer uses demand-driven (pull) dataflow: next_exchange waits on the port semaphore and returns records one at a time from arriving packets.
Note

The paradigm switch — demand-driven inside a process, data-driven between processes — is intentional. Data-driven dataflow between processes is easier to combine with horizontal parallelism and removes the overhead of request messages. Even using a semaphore for flow control, request messages would still be needed in a demand-driven inter-process scheme.

Packet size is critical: at 1 record/packet, 100,000 records through two process boundaries takes 176 seconds; at 250 records/packet it takes 12.73 seconds. The overhead per record per process boundary is approximately 992 μsec.

Flow control / back pressure: an optional run-time switch enables a second semaphore that limits how many packets the producer can get ahead of the consumer. When a consumer removes a packet from the port, it releases the flow-control semaphore; after inserting a new packet, the producer must acquire it. The initial value (slack) is configurable.

# Bushy Parallelism

When exchange is inserted between two subtrees of a query tree, it forks a child process that runs one subtree independently while the parent runs another. This allows different branches of a query tree — e.g., both inputs to a merge-join — to execute simultaneously on different processors.

     [PRINT]
        |
      [XCHG]          ← vertical parallelism
        |
      [JOIN]
      /    \
   [JOIN]  [XCHG]     ← bushy: XCHG forks a process for the right subtree
   /   \      |
[XCHG][XCHG] [FS]
  |     |
[FS]  [FS]

# Intra-Operator (Horizontal) Parallelism

Intra-operator parallelism runs the same operator on different partitions of a dataset simultaneously.

The port can have multiple input queues, one per producer process. The producer group is managed by a master process: when a subtree is opened in parallel, the master forks slave processes using a propagation tree scheme (master forks 1, both fork 1, all four fork 1, etc.) — this avoids the $O(N)$ fan-out of forking all slaves at once and significantly improves performance.

Producers use a support function to choose which queue (partition) each output record belongs to — round-robin, key-range, or hash. The consumer reads from all queues.

The result: $N$ processes each scan $1/N$ of a partitioned file and produce results that are repartitioned or merged by the consumer.

# The Example Walkthrough

Consider a query with operators $A$, $B$, $C$, $D$ to be run in three process groups $A_0$, $BC_{0..2}$, $D_{0..3}$, with exchange $X$ between $A$ and $BC$, and exchange $Y$ between $BC$ and $D$:

  1. $A_0$ calls open on $X$, which forks $BC_0$.
  2. $BC_0$ (master of the BC group) calls open on $Y$, which forks $D_0$ (master of D group).
  3. $D_0$ forks $D_1$, $D_2$, $D_3$ via propagation tree.
  4. All $D$ processes run concurrently, pushing packets to $Y$’s port (4 input queues).
  5. $BC_0$ forks $BC_1$ and $BC_2$; all three $BC$ processes consume from $Y$, process through $B$ and $C$, and push to $X$’s port.
  6. $A_0$ consumes from $X$.
  7. When $D$ processes exhaust input, they send end-of-stream packets. After 4 such packets, $Y$ propagates end-of-stream to $BC$ processes.
  8. After $BC$ processes close, $BC_0$ releases the semaphore for $D$ processes to terminate; $A_0$ receives end-of-stream and closes.

The entire shutdown is self-scheduling — no external coordinator needed.

# Exchange Operator Variants

Replicate/broadcast: a switch in the exchange state record causes the operator to pin each record and send it to all consumer queues. Used by hash-division (the divisor relation must be present at every partition of the dividend) and by parallel join algorithms where one input is not moved at all.

Merge: a merge iterator derived from Volcano’s sort module reads from multiple sorted input streams (one per producer) and merges them into one sorted output stream. Unlike other operators, merge must distinguish which producer each record came from to correctly merge sorted runs — records from producer 0 must be merged in order against records from producer 1.

Process operator tree (exchange in the middle of a query tree without forking): the exchange establishes a port for data exchange but its next operation requests records from its input tree and routes them to whichever process in the group needs a record for its partition. This mode makes flow control obsolete (no asynchrony) and allows a single process to multiplex between producer and consumer roles — effectively implementing application-specific co-routines.

# Why the Operator Model Beats the Bracket Model

PropertyBracket modelOperator model (Volcano)
Existing operators modified?Yes — must use IPC for I/ONo — unchanged
Scheduler needed?Separate process per operatorNone — self-scheduling
Adding new operatorsMust update template + schedulerNo change needed
Placement flexibilityTop or bottom of operator treeAnywhere in the tree
Number of inputsLimited to 1 or 2Unlimited
System portabilityTwo modules (template + scheduler) must be portedOnly exchange needs porting

The operator model allows the query optimiser to insert exchange operators freely — parallelism becomes a query optimisation decision, not an architectural constraint.

# Performance

Measured on a Sequent Symmetry with 12 × Intel 80386 (16 MHz), 64 KB cache per CPU.

Exchange overhead (no new processes): adding three exchange operators to a single-process program that creates 100,000 records adds 7.72 seconds, or 25.73 μsec per record per exchange operator.

Pipeline speedup: creating a 4-process pipeline (one exchange operator spawning a child) ran 100,000 records in 16.21 seconds — faster than the single-process baseline of 20.28 seconds — confirming that pipelined multi-process execution is net positive even for small workloads.

Packet size sensitivity (100,000 records through 2 intermediate process groups):

Packet size (records)Elapsed time (seconds)
1176.4
297.6
1027.67
5015.71
25012.73

Regression gives approximately 992 μsec overhead per record per process boundary (intercept 12.18 s, slope 0.001654 s/packet). The dominant cost at small packet sizes is data exchange overhead; amortising IPC over large packets collapses this.

# Key Takeaways

ConceptLesson
Iterator modelopen/next/close + anonymous inputs = operators compose without parallelism knowledge
Exchange operatorSingle module encapsulates vertical, bushy, and intra-operator parallelism
Paradigm switchDemand-driven within a process; data-driven (push) between processes
Flow controlBack-pressure semaphore prevents fast producers from swamping slow consumers
Self-schedulingNo separate scheduler; shutdown propagates automatically via end-of-stream
Packet sizeAmortise IPC overhead over batches; ~1000 μsec per record per boundary at packet size 1

The exchange operator as described here is the direct ancestor of the exchange/redistribute/repartition operators in every modern parallel query engine. Volcano’s iterator (open/next/close) became the standard execution model used verbatim in PostgreSQL, SQL Server, DuckDB, and most other database systems.