Batched and Vectorized Execution in the PostgreSQL Executor
October 20–23
Columnar and vectorized query engines routinely outperform PostgreSQL by 10-100x on analytical workloads. Three things account for most of that gap.
First, they amortize per-tuple overhead by moving data in batches rather than one row at a time. PostgreSQL's executor re-enters the storage AM, dispatches expression opcodes, and calls through fmgr for every single tuple. A batch of 1024 tuples turns all of that per-row work into per-batch work.
Second, they operate on packed, typed arrays instead of Datum. A column of int32 values in PostgreSQL is an array of 8-byte Datums with per-row bool null flags. In a vectorized engine it's a contiguous int32_t array with a bitmask for nulls. The native type width and contiguous layout let the compiler emit SIMD instructions. The Datum representation structurally prevents this.
Third, they exploit knowing about multiple rows at once. A hash join probe that sees one row at a time stalls on memory latency for every lookup. A probe that sees 1024 rows can issue prefetches for all of them and overlap the latency. Two-phase column fetch can deform only qual-relevant columns for a batch, filter, and skip the rest for non-survivors. None of this is possible when the executor sees one tuple at a time.
This talk presents a design for closing that gap incrementally from inside PostgreSQL -- starting with batch delivery from the table AM through the slot interface, adding a typed columnar intermediate representation, and building toward batch-aware expression evaluation with type-specific opcodes. Each layer is independently useful and leaves the row path intact.
Audience: Core and extension developers working on executor internals, storage access methods, or analytical performance in PostgreSQL.