用 EXPLAIN PIPELINE 加改 max_threads,看同一条聚合查询的处理器流水线是怎么从1条变成8条并行分支的,以及耗时的变化。
还是那张 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 和汇总步骤这些没法无限并行的环节——并行分支数不是越多越快,够用就好。
每一步都是本文上面用EXPLAIN PIPELINE和计时得到的原始结果。
EXPLAIN PIPELINE:展开成处理器图SETTINGS max_threads=N:控制并行分支数