bigywater.tech
ClickHouse 内部原理 · 13

Small Part:为什么频繁 INSERT 会拖慢 ClickHouse

真实在同一client进程里对比500条单行INSERT和1条批量INSERT的耗时,再验证part数失控时ClickHouse真实设置的两道保险丝。

577倍
500条单行INSERT vs 1条批量INSERT的真实耗时差距
3000个part
parts_to_throw_insert真实拒绝写入的默认阈值

做的事:500行数据,真实分别用单行INSERT和批量INSERT写入

代码(单行INSERT,同一client进程内顺序执行,排除进程启动开销)-- 500条语句写入同一个 .sql 文件,一次client进程顺序执行 INSERT INTO demo.sptest3 VALUES (0, 'row0'); INSERT INTO demo.sptest3 VALUES (1, 'row1'); ... (共500条) 代码(批量INSERT)INSERT INTO demo.sptest2 SELECT number, concat('row', toString(number)) FROM numbers(500);

验证:真实相差超过500倍的耗时

实测 · 本机执行输出(同一client进程内,排除了进程启动开销)
500条单行INSERT语句(同一client进程顺序执行):
→ 真实耗时 32.293 秒(约64ms/条)
→ 结束后 system.parts 里真实还剩3个active part(后台merge已经在追)

500行数据,1条批量INSERT:
→ 真实耗时 0.056 秒
→ 结束后 system.parts 里真实是1个part

真实差距:32.293 / 0.056 ≈ 577倍

这576倍不是网络延迟或者客户端启动的开销(本文特意把500条语句放进同一个client进程顺序执行,排除了每次重新起进程的成本),差距真实来自ClickHouse服务端每次INSERT要做的固定工作:创建一个新part目录、写.bin/.cmrk2/checksums.txt等一整套文件、fsync落盘、把新part原子地注册进表的元数据——这套流程不管这次INSERT写1行还是写100万行,固定开销都是真实存在的,写1行时这份固定开销就占了几乎全部耗时。

验证:真实触发parts_to_throw_insert,ClickHouse直接拒绝写入

实测 · 本机执行输出(真实撞上分区数上限,间接印证小part失控的后果)
SELECT name, value FROM system.merge_tree_settings
WHERE name IN ('parts_to_delay_insert','parts_to_throw_insert','max_parts_in_total');
→ parts_to_delay_insert   1000    ← 真实超过这个数,INSERT开始被人为限速
  parts_to_throw_insert   3000    ← 真实超过这个数,INSERT直接报错拒绝
  max_parts_in_total      100000

真实触发场景(本文另一个实验里,同一个insert block分区数超限):
DB::Exception: Too many partitions for single INSERT block (more than 100).
Large number of partitions is a common misconception. It will lead to
severe negative performance impact, including slow server startup,
slow INSERT queries and slow SELECT queries. (TOO_MANY_PARTS)
parts_to_delay_insert/parts_to_throw_insert这两个真实的默认阈值,本质上是ClickHouse给自己设的一道保险丝:如果一张表的活跃part数真实超过1000,后续的INSERT会被服务端主动放慢速度(sleep一小段时间再执行),逼着后台merge线程有时间追上来把小part合并掉;如果放慢还不够、真实超过3000个part,ClickHouse会直接拒绝新的INSERT并报TOO_MANY_PARTS错误,而不是让part数量无限增长下去——因为part数越多,每次SELECT需要打开的文件描述符、需要合并的primary.cidx索引、需要参与后台merge调度的对象就越多,真实会拖慢包括SELECT在内的整个实例。本文验证的500次单行INSERT之所以还没有触发这道保险丝,是因为后台merge线程真实在写入过程中持续把小part合并掉了(结束时只剩3个active part,而不是500个)——但如果写入速度比merge速度快得多(比如1000个并发客户端同时各自单行写),保险丝真实是会被触发的。

分步演示:从577倍的耗时差距到part数保险丝

每一步都是本文上面用真实计时和真实报错跑出来的原始结果。

单行INSERT真实比批量慢577倍,还会真实撞上保险丝
这一步新验证的场景

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实的 --queries-file 单进程顺序执行500条INSERT
    • 真实的 system.merge_tree_settings(parts_to_delay_insert / parts_to_throw_insert)
    京ICP备2026067502号-1京公网安备11010502063487号