Vectorized execution — process batches, not tuples

Pulling one row at a time through a chain of virtual function calls (Volcano) is interpreter overhead; processing 8192 column values per call lets the compiler emit SIMD inner loops and amortizes dispatch cost across the batch.

Previously

The bytes on disk are now tiny. But pulling tiny bytes through the CPU one row at a time still leaves a 50x performance win on the table — it depends on how the executor walks those bytes.

Scene 04

Vectorized execution: process batches, not tuples

  1. Watch
  2. Try it
  3. Predict
  4. Capture
Volcano (tuple-at-a-time)ScanoperatorFilteroperatorAggregateoperatornext()next()CPU SIMD1/8 litvirtual calls: 00.1M rows/secVectorized (batch = 8192)ScanoperatorFilteroperatorAggregateoperator8192-vec8192-vecCPU SIMD8/8 litvirtual calls: 31.0M rows/secROWS PROCESSED · target 1,000,000Volcano0%Vectorized0%8192 column values per call — one dispatch, one SIMD inner loop, ~10x throughput.
What to watch for

Same query on both panels: scan a column, filter, aggregate over 1M rows. The top panel walks one tuple at a time; the bottom panel walks 8192-row batches. Watch the race bar — and the virtual-call counter on each side.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Volcano.executeQuery()
tuple-at-a-time: one virtual next() per row per operator
def execute(plan):
op = plan.root # Aggregate -> Filter -> Scan
while True:
row = op.next() # virtual dispatch, every row
if row is EOF:
break
# 3M virtual calls for 1M rows (3 operators deep)
# SIMD lanes idle — no contiguous array to vectorize
accumulate(row)
Vectorized.executeQuery()
batch-at-a-time: one dispatch per 8192 values
def execute(plan):
op = plan.root # operators consume + produce Blocks
while True:
block = op.nextBlock() # 8192 column values per call
if block is EOF:
break
# one virtual call per BATCH, not per row
# block.col is a contiguous array -> SIMD kernel
accumulateBlock(block)
Filter.apply(block)
the SIMD kernel — or the per-row escape hatch
def apply(block):
if predicate.isNativeKernel():
# tight loop -> AVX2 (8 int32) / AVX-512 (16)
for i in range(block.n):
out[i] = block.col[i] > threshold
return block.select(out)
# opaque UDF: planner can't prove what it does
for i in range(block.n):
out[i] = python_udf(block.col[i]) # FFI per row
return block.select(out)

Where this sits in Build a columnar OLAP store (ClickHouse / Druid style)

Scene 04 of 13, in the Speedups act — Compression and vectorized execution — where the orders of magnitude live.. Tuple-at-a-time Volcano is interpreter overhead; processing 1024–8192 column values per call lets the CPU emit SIMD inner loops.

Up next. Vectorized execution wants thousands of values per call. That sets a hard rule for writes too — they have to arrive in batches, not one row at a time, or the per-column overhead crushes the engine.

All 13 scenes in Build a columnar OLAP store (ClickHouse / Druid style) · Every curriculum

Built with Arqly
Every scene in Build a columnar OLAP store (ClickHouse / Druid style) builds on the one before it.All 13 Build a columnar OLAP store (ClickHouse / Druid style) scenes