bigywater.tech
ClickHouse 内部原理 · 4

Vectorized Execution:为什么 ClickHouse 一批一批处理数据

在2000万行表上,把 max_block_size 从1一路调到默认的65505,看同一条SUM聚合的墙钟耗时怎么从32.8秒掉到0.09秒。

361倍
block_size=1 vs 默认值的耗时差距
32.8s→0.09s
同一条SUM聚合耗时区间
~305次
默认设置下2000万行被切成的批次数

做的事:同一条聚合,只改 max_block_size

插入 2000 万行数据,跑同一条 SUM 聚合,唯一变量是每次处理多少行为一批。

插入INSERT INTO demo.big SELECT number, sin(number) FROM numbers(20000000) SELECT count() FROM demo.big → 20000000 同一条查询,只改 max_block_sizeSELECT sum(v) FROM demo.big SETTINGS max_block_size = N

ClickHouse 执行查询时不是一行一行地处理,而是把数据切成一批一批的"块"(Block),每批固定装 max_block_size 行,函数和聚合器每次调用处理一整批。这条设置直接决定了"一批"有多大。

验证:从 1 行一批,加到 65505 行一批

同一条 SUM 查询,测出的墙钟耗时。

计时(同一台机器,同一份2000万行数据)max_block_size=1 → 32.836 s max_block_size=100 → 0.446 s max_block_size=1000 → 0.121 s max_block_size=8192 → 0.097 s max_block_size=65505 → 0.091 s(ClickHouse 默认值)

从 1 行一批加到 100 行一批,耗时就从 32.8 秒降到 0.45 秒,快了 73 倍;再加到默认的 65505 行一批,总共快了 约 361 倍。而从 8192 加到 65505 几乎没有区别(0.097s → 0.091s)——说明批量大到一定程度之后,"每批处理的开销均摊"这件事的收益就趋于饱和了。

这就是"向量化执行":不是靠 SIMD 指令这些底层优化才叫向量化,而是先把数据攒成一批,让同一段代码(比如 sum 累加)在一次函数调用里循环处理一整批数据。批处理次数越少,摊到每一批头上的函数调用开销、虚函数分发开销就越小;批太小(比如 max_block_size=1)就退化成了几乎逐行调用,2000 万次函数调用的固定开销累加起来就是 32.8 秒里的大头。

为什么这个开销这么大

2000 万行、每行都要单独走一次聚合器和表达式求值的完整流程,开销出在哪。

把 max_block_size 设成 1,聚合器 AggregatingTransform、表达式计算 ExpressionTransform 每次只处理 1 行,但每次调用仍然要走完整的一套:检查数据类型、可能触发一次虚函数调用、更新内部状态……这些"每次调用固定要做的事"跟本次真正要做的计算(一次浮点数加法)比起来,开销反而占了大头。批量处理把"调用次数"从 2000 万次压到了 20000000/65505 ≈ 305 次,固定开销总量直接压缩了 65505 倍,剩下的浮点加法本身在现代 CPU 上几乎不算什么。

分步演示:从逐行到65505行一批,的361倍

每一步都是本文上面在2000万行表上跑出来的原始耗时。

同一条聚合,批越大越快,但收益会饱和
这一步新验证的批大小

    本文用到的工具

    • 本机 ClickHouse 26.9.1 单节点,2000万行 MergeTree 表(numbers() 表函数生成)
    • SETTINGS max_block_size=N:控制每批处理多少行
    • Python 计时同一条 SQL 在不同设置下的墙钟耗时
    京ICP备2026067502号-1京公网安备11010502063487号