ClickHouse Vectorized Query Processing: 5 Mechanics, Measured in the Lab

ClickHouse internals · Performance engineering · Lab-measured

ClickHouse vectorized query processing is the reason a single CPU core can aggregate billions of values per second. Instead of pushing one row at a time through the query plan, ClickHouse moves blocks of up to 65,409 rows through tight loops over column arrays that the CPU executes with SIMD instructions.

This guide explains how the engine does it, from MergeTree granules to processors to AVX-512 registers, and puts numbers on it. We measured each mechanism in our lab on the ClickHouse 26.7 engine, so every figure below comes with the query that produced it.

Lab at a glance

Engine
ClickHouse 26.7.2.1 (embedded via chDB 4.4.0)
CPU
Intel Xeon 2.8 GHz, AVX2 and AVX-512, 2 vCPU
Memory
7 GB RAM, Linux 6.18
Method
max_threads = 1 to isolate per-core effects; median of 3 to 5 runs

~2,200×Throughput gained by moving from 1-row blocks to the default 65,409-row blocks, single thread
4.27 BUInt8 values summed per second on one core, 200 million rows in memory
1.41×Extra speed-up from the LLVM JIT on an expression-heavy query, on top of vectorization
1 / 20Bytes read when a query touches 1 of 20 columns: column pruning feeds the vectors

Execution models

What ClickHouse vectorized query processing actually means

To see why ClickHouse vectorized query processing matters, start with the alternative. Classic row stores execute plans with the Volcano iterator model: every operator exposes a next() method that returns one row. A scan, a filter and an aggregate over 200 million rows therefore cost around 600 million function calls before any useful arithmetic is done, and each call breaks the CPU pipeline with branches and cache misses.

Vector-at-a-time execution, introduced by the MonetDB/X100 paper by Boncz, Zukowski and Nes (CIDR 2005), changes the unit of work from a row to a batch. ClickHouse adopted the same model: operators produce, pass and consume chunks of rows. The ClickHouse architecture documentation describes it as dispatching operations on arrays rather than on individual values.

With ClickHouse vectorized query processing and the default block size, the same 200 million rows travel as 3,058 blocks, so the three operators make roughly 9,174 calls in total. The per-call overhead becomes noise, and the work inside each call is a simple loop the compiler can vectorize.

The block size is a deliberate compromise. Too small, and the engine pays function-call and bookkeeping costs on every few rows, which is exactly what the Volcano model suffers from. Too large, and the intermediate columns that each operator produces stop fitting in the CPU caches, so every step pays a trip to main memory. A few tens of thousands of rows per block keeps both costs low for typical analytical columns, and ClickHouse additionally caps blocks by bytes through preferred_block_size_bytes so that wide string columns do not blow the cache budget.

ClickHouse vectorized query processing compared with tuple-at-a-time Volcano execution
Figure 1. ClickHouse vectorized query processing versus tuple-at-a-time execution. Same query, same 200 million rows: roughly 600 million iterator calls with tuple-at-a-time execution versus about 9,174 with ClickHouse blocks. Call counts are arithmetic, not measurements.

Inside the engine

Blocks, columns and processors: how the data moves

A Block is ClickHouse’s in-memory unit of work: a set of columns, each an IColumn backed by contiguous arrays, plus their types and names. Numeric columns are plain arrays; strings and arrays use an offsets vector and a data vector. Functions never modify a column in place, they return a new one, which keeps every inner loop simple and branch-light.

  • On disk, MergeTree splits each part into granules of 8,192 rows, and compresses columns in blocks of roughly 64 KB to 1 MB of uncompressed data. See our ClickHouse MergeTree guide for how granules and marks drive index lookups.
  • In memory, readers assemble blocks of up to max_block_size rows, 65,409 by default according to the settings reference. At 8 bytes per value that is about 511 KiB per UInt64 column, small enough to stay close to the core’s caches.
  • In the pipeline, processors such as FilterTransform, ExpressionTransform and AggregatingTransform consume and produce chunks. The pipeline is replicated across max_threads streams and partial aggregation states are merged at the end.

Only the columns a query references are read and decompressed. That is the storage half of the story, and it is covered with measurements in the column pruning section.

ClickHouse vectorized query processing data path from MergeTree granules to blocks, processors and SIMD
Figure 2. The ClickHouse vectorized query processing data path: from MergeTree granules to blocks to processors, with the SIMD register widths ClickHouse can dispatch to at runtime.

Hardware

SIMD and runtime CPU dispatch

SIMD is where ClickHouse vectorized query processing meets the hardware. Once data sits in contiguous arrays, a single instruction can operate on several values at once. The wider the register and the narrower the type, the more values move per instruction:

Instruction setRegister widthFloat64 lanesFloat32 lanesUInt8 lanes
SSE 4.2128-bit2416
AVX2256-bit4832
AVX-512512-bit81664

ClickHouse ships several compute kernels for hot functions, written with intrinsics or left to compiler auto-vectorization, and picks the fastest one at runtime from the cpuid instruction. According to the ClickHouse VLDB 2024 paper, this lets one binary run on hardware as old as 15 years, with SSE 4.2 as the minimum on x86-64, while still using AVX-512 where it exists. The same paper notes more than 30 hash table variants selected per key type, which is the same idea applied to aggregation.

Want to know why the whole engine is built this way? Our overview of why ClickHouse is so fast and the comparison of columnar and row-based databases cover the broader design.

ChistaDATA lab

ClickHouse vectorized query processing in numbers

Theory is cheap, so we measured ClickHouse vectorized query processing directly. We ran every test on the ClickHouse 26.7.2.1 engine embedded in chDB 4.4.0, on a 2-vCPU Intel Xeon at 2.8 GHz with AVX-512 and 7 GB of RAM. Every query pins max_threads = 1, so the numbers describe one core. They are real measurements on a small virtual machine, not a benchmark of your hardware.

Test 1: block size is the vectorization dial

If ClickHouse vectorized query processing matters, making the vectors tiny should hurt. The first test keeps the query fixed and changes only max_block_size. Shrinking the block to one row effectively turns ClickHouse back into a row-at-a-time engine.

SELECT sum(number * 3 + 1)
FROM numbers(200000000)
WHERE number % 7 != 3
SETTINGS max_block_size = 65409, max_threads = 1;
max_block_sizeRows processedMedian timeThroughput
12,000,00018.66 s0.11 million rows/s
1620,000,00011.37 s1.76 million rows/s
256200,000,0009.02 s22.2 million rows/s
4,096200,000,0001.35 s148.3 million rows/s
65,409 (default)200,000,0000.85 s235.8 million rows/s
ClickHouse block size versus throughput from ChistaDATA lab measurements
Figure 3. ClickHouse vectorized query processing depends on block size: throughput rises by roughly 2,200× between one-row blocks and the default block size. Smaller row counts were used for the two smallest blocks to keep run times reasonable; throughput is normalised per second.

Test 2: narrower types, more values per register

The second test sums one column of a 200-million-row Memory table, so disk and decompression are out of the picture.

SELECT sum(u8) FROM mem SETTINGS max_threads = 1;   -- repeated for u32, u64, f32, f64
Column typeBytes per valueMedian timeThroughput
UInt810.047 s4,272 million rows/s
UInt3240.111 s1,810 million rows/s
Float3240.131 s1,522 million rows/s
UInt6480.203 s986 million rows/s
Float6480.211 s949 million rows/s
ClickHouse sum throughput by data type, UInt8 at 4.27 billion rows per second on one core
Figure 4. ClickHouse vectorized query processing by data type: a single core sums 4.27 billion UInt8 values per second. Narrow types fit more lanes per SIMD instruction and move fewer bytes through memory; both effects contribute.
How to read these numbers: the one-row case also slows the numbers() generator, and the type test is partly bound by memory bandwidth. The trend is what matters. Always test on your own workload and hardware before changing production settings, and keep a verified backup and DR posture in place.

Code generation

LLVM JIT on top of vectorization

ClickHouse vectorized query processing has one known cost: each function writes a temporary column that the next function reads back. For long arithmetic expressions, ClickHouse can fuse the chain into one native function with LLVM. The VLDB paper explains that compilation starts only after the same expression has been seen a configurable number of times, controlled by min_count_to_compile_expression, so short one-off queries do not pay the compile cost.

SELECT sum(((number * 7 + 3) % 1000) * 2 + (number % 13) * 5
           - (number % 11) * 3 + ((number * 17) % 97))
FROM numbers(100000000)
SETTINGS max_threads = 1,
         compile_expressions = 1,
         min_count_to_compile_expression = 0;
106.9 Mrows per second with compile_expressions = 0
151.0 Mrows per second with compile_expressions = 1, a 1.41× gain on the same core

Compiled code is cached, so the compile cost is paid once per expression shape rather than once per query, and the cache can be inspected and dropped like any other ClickHouse cache. Where compilation fails or is not supported for a function, the engine silently falls back to the regular vectorized path, which makes the setting safe to leave enabled.

Both runs return the same result. The gain depends on how long the expression chain is: simple aggregates barely change, while wide arithmetic and conditional expressions benefit most.

Storage

Column pruning: the I/O half of vectorized processing

ClickHouse vectorized query processing is only fast if the right data reaches the vectors. We loaded 20 million rows into a 20-column MergeTree table of UInt64 values and compared a query that touches one column with one that touches all 20.

MeasurementOne column (c7)All 20 columns
Compressed bytes on disk76.9 MiB1.50 GiB
Uncompressed bytes152.6 MiB2.98 GiB
Median query time, one thread0.26 s5.59 s

Reading one column touched about one twentieth of the table’s bytes. The all-columns query also does 20 times more arithmetic, so the time ratio mixes I/O and compute, but the lesson is the same: SELECT * on a wide table throws away most of what a column store gives you. Our SQL antipatterns in ClickHouse guide lists the other common ways to fall off the fast path.

Honest limits

Where ClickHouse vectorized query processing stops helping

ClickHouse vectorized query processing removes interpretation overhead and feeds SIMD units. It does not remove every cost, and knowing the boundaries saves a lot of tuning time.

  • Point lookups and tiny queries. A query that returns a handful of rows spends most of its time parsing, planning and locating granules. Block size barely matters there; the primary key and sort order decide the latency.
  • Random memory access. Hash aggregation and hash joins probe tables at unpredictable addresses. ClickHouse chooses a specialised hash table per key type to soften this, but high-cardinality GROUP BY and large joins remain bound by cache misses and memory, not by SIMD width.
  • Row-shaped logic. Executable UDFs, per-row string parsing and deeply nested conditionals leave less for the vector units to do. Rewriting them with built-in array and string functions usually pays back quickly.
  • Merges and small parts. Thousands of tiny parts mean thousands of small reads and short vectors. Batching inserts and keeping part counts healthy is part of query performance; see monitoring merge queues in ClickHouse.
  • Too many threads on too few cores. Parallel streams multiply throughput only while cores and memory bandwidth are free. On shared hosts, raising max_threads past the physical cores adds contention rather than speed.

In practice, ClickHouse vectorized query processing is rarely the bottleneck in a slow ClickHouse estate. The usual culprits are schema choices, sort keys that do not prune, and ingestion patterns. That is why every ChistaDATA engagement starts with evidence from system.query_log and system.parts rather than with settings changes.

Practice

Keeping queries on the ClickHouse vectorized fast path

PracticeWhy it mattersHow to verify
Leave max_block_size at its default for analytical queriesSmaller blocks multiply per-block overhead, as Test 1 showsSELECT getSetting('max_block_size')
Use the narrowest correct type, and LowCardinality(String) for repetitive stringsMore values per register and fewer bytes through memorysystem.columns compressed and uncompressed sizes
Use Nullable only where NULL carries meaningEach Nullable column carries an extra null-map column to read and checksystem.columns type review
Select only the columns you needColumn pruning keeps I/O and decompression proportional to the queryread_bytes in system.query_log
Prefer built-in functions to executable UDFsExecutable UDFs serialize blocks to an external processQuery profile and query_duration_ms
Keep compile_expressions enabled for repeated heavy expressionsFuses chains of functions into one native loopBefore-and-after timing, as in the JIT test

Two queries show whether ClickHouse vectorized query processing is working for you. EXPLAIN PIPELINE shows which processors run and how many parallel streams they use:

EXPLAIN PIPELINE
SELECT event_type, count()
FROM analytics.events
WHERE event_date >= today() - 7
GROUP BY event_type;

And system.query_log shows how much data each query shape read and how much CPU it burned:

SELECT
    normalized_query_hash,
    count()                                             AS runs,
    quantile(0.99)(query_duration_ms)                   AS p99_ms,
    formatReadableSize(avg(read_bytes))                 AS avg_read,
    round(avg(ProfileEvents['UserTimeMicroseconds']) / 1e6, 2) AS avg_cpu_s
FROM system.query_log
WHERE type = 'QueryFinish'
  AND event_date >= today() - 1
GROUP BY normalized_query_hash
ORDER BY p99_ms DESC
LIMIT 20;

If this is the kind of analysis you want done on your cluster, our ClickHouse performance tuning and optimization engagements start from exactly these system tables.

FAQ

ClickHouse vectorized query processing: frequently asked questions

Is vectorized execution the same as SIMD?

No. ClickHouse vectorized query processing is the engine design of processing blocks of values per operator call. SIMD is the CPU feature that executes one instruction on several values. Vectorized execution makes SIMD possible, because the inner loops run over contiguous arrays.

Should max_block_size ever be lowered?

Rarely for analytical queries. Lowering it can reduce peak memory for very wide rows, but Test 1 shows how quickly throughput falls. Change it per query, measure, and keep the default otherwise.

Does ClickHouse need AVX-512?

No. On x86-64 the minimum is SSE 4.2. ClickHouse detects AVX2 and AVX-512 at runtime and uses faster kernels when they are available, so the same binary runs everywhere.

Does the JIT replace vectorization?

No. ClickHouse vectorized query processing remains the execution model. The LLVM JIT fuses repeated expressions into one native function and runs inside the same block-based pipeline.

Why are our queries slow if ClickHouse is vectorized?

Usually because of what reaches the vectors: too many columns, a sort key that does not prune granules, too many small parts, or joins that spill. A ClickHouse performance audit finds which one from your query log.

Next step

Put these numbers to work on your own cluster

ClickHouse vectorized query processing only pays off when queries stay on the fast path. ChistaDATA engineers profile your real workload, find where queries leave the vectorized fast path, and fix the schema and settings behind it, with a measured before-and-after report.

About Shiv Iyer 262 Articles
Open Source Database Systems Engineer with a deep understanding of Optimizer Internals, Performance Engineering, Scalability and Data SRE. Shiv currently is the Founder, Investor, Board Member and CEO of multiple Database Systems Infrastructure Operations companies in the Transaction Processing Computing and ColumnStores ecosystem. He is also a frequent speaker in open source software conferences globally.