DuckDB Internals: Why Is DuckDB Fast? (Part 2 Vectorized Execution)

hornyforsavings1 pts0 comments

DuckDB Internals: Why is DuckDB Fast? (Part 2 Vectorized Execution) | GreybeamNew launchGreybeam's free Snowflake cost observability tool - Greysight<br>Back to BlogKyle Cheung/July 21, 2026/17 min read/DuckDB<br>DuckDB Internals: Why is DuckDB Fast? (Part 2 Vectorized Execution)

DuckDB has become one of those rare pieces of software whose greatness is difficult to capture in words. Part of that is its versatility. It is approachable enough for anyone to install, point at a Parquet file, and begin exploring data on a laptop within minutes, yet sophisticated enough to support serious production systems and an entire ecosystem of companies and tools.

What makes DuckDB especially remarkable is how naturally its ease of use coexists with its technical depth. DuckDB is serious software built on decades of database research, yet DuckLabs, the team behind DuckDB, has somehow kept the project and its broader ecosystem lighthearted and fun.

DuckLabs team being fun 😆Taking a look under the covers though reveals a great deal of careful engineering. Look up why is DuckDB fast and the first few search results often have fancy buzzwords like: in-process, columnar storage, vectorized execution, or parallel processing, but what does that actually mean?

Part 1 of this series followed a query from SQL text through parsing, binding, planning, optimization, and storage. This post picks up where that left off: execution.

More specifically, vectorized execution: how DuckDB processes data in batches instead of one row at a time.

Instructions per cycle#

To understand why batching matters, you need a rough picture of what a CPU core can do.

A CPU core runs on a clock that ticks billions of times a second. A core running at 3 GHz experiences roughly 3 billion ticks each second. Instructions per cycle (IPC) then measures how many instructions the core can complete per tick on average. At 1 IPC it completes one per tick; at 3 IPC, three. IPC is not fixed though, it depends on both the processor and the software running on it.

A CPU can complete more instructions per cycle when the software gives it several useful things to work on at once without frequently forcing it to pause. IPC can fall below one when the CPU has to wait. It could be waiting on data from memory or waiting for one instruction to produce a value the next one needs. An IPC below 1 means some cycles pass without any instruction completing at all.

In 2005, MonetDB/X100 measured how efficiently traditional databases were using the CPU. Running Query 1 in TPCH, MySQL's busiest routines, which operated one row at a time, sustained an IPC of 0.8. A single addition performed through MySQL took roughly 49 CPU cycles. X100, by contrast, could perform a similar operation in about 2.2 cycles by processing many values together in batches.

Much of that difference came from MySQL's query executor: the Volcano model.

The Volcano Model#

For thirty years the Volcano model has been the standard way to build a query executor, and engines like PostgreSQL and MySQL are built on this exact model (SQLite is also row-at-a-time, though it does not use the Volcano model).

A query plan in Volcano is a tree of operators. Take a simple one:

SELECT<br>name<br>FROM customers<br>WHERE<br>country = 'US';<br>Copy

After planning, that becomes three operators: a scan at the bottom that reads rows, a filter that drops rows where country is not 'US', and a projection on top that keeps only name.

Every operator exposes the same three methods: open() to set up, next() to produce one row, and close() to clean up. The engine asks the top operator for a row by calling Project.next(). Project has no rows of its own, so it calls Filter.next(), which calls Scan.next(). The scan reads one row and passes it up. The filter checks the predicate and either passes the row along or discards it and asks the scan for another. Project trims the row to name and returns it. Then the engine asks for the next row and the whole chain runs again.

Data is pulled up through the tree, one row per request, which is why this is called pull-based execution. Every operator implements the same interface, so operators can stack on each other, and adding a new one means writing the same open/next/close contract.

That flexibility comes with a cost. Each row has to travel through the entire chain of next() calls before work on the next row can begin. For a query that processes a million rows, each operator may be called a million times, which repeatedly pays the overhead of function calls, dispatch, etc.

Batches that fit in cache#

MonetDB/X100 explored two ways to escape Volcano's per-row overhead.

The first was to operate on entire columns at once. Load all million values of country into memory, run the filter across the whole array in a tight loop, and pass the resulting array to the next operator. Instead of invoking an operator once per row, the engine invokes it once for the entire column.

This removes most of the per-row overhead,...

duckdb next execution query volcano operator

Related Articles