Photo by Domaintechnik on Unsplash
Column-Oriented Query Execution: Why Databases Process Data Vertically
Most developers learn to think about databases in terms of rows. You insert a row, update a row, fetch rows that match certain conditions. The mental model aligns with how we conceptualize records: each user, transaction, or log entry is a discrete thing with multiple attributes.
But modern analytical databases and increasingly even transactional systems execute queries in a fundamentally different way. Instead of processing one complete row at a time, they operate on chunks of a single column, applying the same operation to thousands of values in tight loops. This shift from row-oriented to column-oriented execution unlocks dramatic performance improvements by better matching how CPUs actually work.
The Row-at-a-Time Model
Traditional query execution follows what’s called the iterator or volcano model. Each operator in the query plan (scan, filter, join, aggregate) implements a next() function that returns one row at a time. The filter operator calls next() on the scan below it, evaluates the predicate, and either returns that row or asks for another.
This approach is conceptually clean and makes query plan composition straightforward. But it comes with overhead. Every row transition involves a function call, conditional branches based on row content, and scattered memory access patterns as the execution engine jumps between different parts of each row’s data. For analytical queries that touch millions of rows, these costs accumulate brutally.
Columnar Execution Basics
Column-oriented execution inverts the model. Instead of next() returning a single row, operators process batches of typically 1,000 to 10,000 values from a single column at a time. A filter operation receives a batch array of values and a batch array representing the filter column, applies the predicate to all values, and produces a selection vector indicating which positions passed.
The advantages emerge from how this maps to CPU execution. Modern processors excel at repetitive operations on contiguous memory. When you apply the same comparison to 1,000 consecutive integers, the CPU can:
- Keep the tight loop’s instructions in the instruction cache
- Predict branches more accurately since the loop structure is consistent
- Prefetch data into cache because access patterns are sequential
- Apply SIMD (Single Instruction Multiple Data) instructions to process multiple values per cycle
A single AVX-512 instruction can compare eight 64-bit integers simultaneously. Column-oriented execution makes it feasible for query engines to emit or use these instructions, either through explicit vectorization or by writing code patterns that compilers can auto-vectorize.
Storage vs Execution
It’s worth distinguishing column-oriented storage from column-oriented execution. Columnar storage formats like Parquet or ORC physically store all values from a column together on disk, which improves compression and allows queries to read only relevant columns. Column-oriented execution is about how the query engine processes data in memory during computation.
These concepts are complementary and often used together, but they’re independent. You can have a row-oriented storage format (traditional database heap files) with column-oriented execution by decomposing rows into column batches after reading. Conversely, you could read from columnar storage but process queries row-at-a-time, though that would waste the format’s advantages.
Compilation and Vectorization
Many modern systems combine columnar execution with just-in-time compilation. Instead of interpreting a generic filter function for each batch, the query engine generates specialized machine code for the specific predicate in your query. This eliminates interpreter overhead and gives the compiler optimization opportunities it wouldn’t have with a generic implementation.
DuckDB, for instance, generates C++ code for query pipelines and compiles it using LLVM. The combination of batched columnar processing and compiled code produces execution speeds that can exceed hand-written C loops for data processing.
Where Row-Oriented Execution Persists
Column-oriented execution shines for analytical workloads that scan large portions of tables and apply uniform operations. But it’s not universally superior. Transactional workloads that fetch or update single rows by primary key don’t benefit much from batch processing. The overhead of decomposing rows into columnar batches and recomposing them can exceed the savings.
Most OLTP databases still use row-oriented execution for this reason, though some hybrid systems switch execution models based on query characteristics. PostgreSQL’s recent development of JIT compilation and vectorized execution for aggregate operations reflects growing recognition that even transactional databases encounter analytical query patterns.
The Bigger Pattern
Column-oriented execution represents a broader principle in systems design: shape your data flow to match your hardware’s strengths. CPUs are fast at predictable, repetitive operations on contiguous memory. They struggle with unpredictable branches and scattered memory access. By processing columns in batches, query engines transform the inherently unpredictable work of filtering and aggregating diverse data into predictable, vectorizable loops that modern processors handle efficiently.
The performance difference isn’t incremental. Well-implemented columnar execution can be 10-100x faster than row-at-a-time processing for analytical queries, not because the algorithm is fundamentally different, but because it allows the hardware to run at closer to its theoretical throughput.