bigywater.tech
ClickHouse 内部原理 · 5

Query Pipeline:查询如何拆成多个处理器

用 EXPLAIN PIPELINE 加改 max_threads,看同一条聚合查询的处理器流水线是怎么从1条变成8条并行分支的,以及耗时的变化。

×N
读表处理器数量等于max_threads
0.120s→0.087s
1线程到8线程的耗时区间
饱和于4线程
再加线程数收益趋近于0

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

还是那张 2000 万行的表,同一条 SUM 聚合,唯一变量是并行线程数。

EXPLAIN PIPELINE SELECT sum(v) FROM demo.big SETTINGS max_threads=N

Query Pipeline 是执行计划真正落地的样子:计划里的一个逻辑步骤(比如"读表"、"聚合")会被拆成一组物理上并行跑的处理器(Processor),处理器之间靠一条条"端口"(port)连接数据流。max_threads 直接决定了这组处理器要并行复制成几份。

验证:处理器数量随线程数变化

同一条 SQL,只改 max_threads,看 EXPLAIN PIPELINE 里处理器数量的变化。

max_threads=1(输出)MergeTreeSelect(pool: ReadPoolInOrder, algorithm: InOrder) × 3 max_threads=2(输出)Resize 2 → 1 MergeTreeSelect(pool: ReadPool, algorithm: Thread) × 2 max_threads=4(输出)Resize 4 → 1 MergeTreeSelect(pool: ReadPool, algorithm: Thread) × 4 max_threads=8(输出)Resize 8 → 1 MergeTreeSelect(pool: ReadPool, algorithm: Thread) × 8

看到读表这一步的处理器数量精确跟着 max_threads 走:2 线程就是 2 个并行的 MergeTreeSelect 处理器,8 线程就是 8 个。每个处理器各自读一部分数据、各自算出一份局部聚合结果,最后靠一个 Resize N → 1 处理器把 N 条并行分支重新汇合成 1 条,再往后面传给最终的合并步骤。

验证:并行分支变多之后,耗时怎么变

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

计时max_threads=1 → 0.120 s max_threads=2 → 0.095 s max_threads=4 → 0.089 s max_threads=8 → 0.087 s

从 1 线程到 2 线程有明显提升(0.120s → 0.095s),但从 4 线程到 8 线程几乎没有变化(0.089s → 0.087s)。这条查询本身很轻(对 2000 万个 Float64 求和),瓶颈很快就不在"能并行切成几份",而在读盘 IO 和汇总步骤这些没法无限并行的环节——并行分支数不是越多越快,够用就好。

Pipeline 的处理器数量是"计划层面能并行到什么程度"的体现,但实际收益要看数据量和瓶颈在哪:数据量小、IO 是瓶颈时,加线程数边际收益很快归零,这一点从 EXPLAIN PIPELINE 里的处理器数量看不出来,必须跑一遍计时才知道。

分步演示:Pipeline处理器数量是怎么随线程数变化的

每一步都是本文上面用EXPLAIN PIPELINE和计时得到的原始结果。

处理器数量随max_threads线性增长,但耗时收益会饱和
这一步新验证的线程数

    本文用到的工具

    • EXPLAIN PIPELINE:展开成处理器图
    • SETTINGS max_threads=N:控制并行分支数
    • Python 计时同一条 SQL 在不同线程数下的墙钟耗时
    京ICP备2026067502号-1京公网安备11010502063487号