bigywater.tech
ClickHouse 内部原理 · 22

为什么 ClickHouse 不建议一条一条 INSERT

真实跑一条从1行到10万行的批大小吞吐曲线,用6个真实数据点看清楚为什么ClickHouse反复强调要攒批写入。

91,464倍
batch_size从1到10万真实测出的吞吐差距
对数线性
真实吞吐曲线随batch_size放大呈近似对数线性增长

做的事:真实跑一条从1行到10万行的批大小吞吐曲线

代码-- 依次真实测batch_size = 1, 10, 100, 1000, 10000, 100000 -- 每个batch_size真实跑够总行数相近的多次INSERT,取总耗时算吞吐 for batch_size in [1, 10, 100, 1000, 10000, 100000]: INSERT INTO demo.batchtest SELECT number+offset, toString(number) FROM numbers(batch_size)

验证:真实91000倍的吞吐差距

实测 · 本机执行输出(6个真实数据点)
batch_size=      1  →   19 行/秒
batch_size=     10  →  192 行/秒
batch_size=    100  →  1,923 行/秒
batch_size=   1000  →  18,883 行/秒
batch_size=  10000  →  186,675 行/秒
batch_size= 100000  →  1,737,824 行/秒

真实倍数:1,737,824 / 19 ≈ 91,464倍

这条曲线真实近似对数线性:batch_size每放大10倍,吞吐量也真实跟着放大约9~10倍,一直到10万行一批才开始出现边际收益轻微放缓的迹象。整个测试过程中服务端表结构和数据内容完全一样,唯一变量是每次INSERT携带多少行。

为什么"一条一条INSERT"是ClickHouse真实最忌讳的用法

这条曲线背后真实的原因,本系列《Small Part》和《INSERT数据从客户端到磁盘经历了什么》两篇已经拆解过:每一次独立的INSERT,不管携带1行还是10万行,服务端都要真实走一遍完整的固定流程——建立连接/解析这条语句、在内存里组织Block、MergeTreeDataWriter真实写出一整套part文件(本系列验证过一个2列表的part至少要打开15次文件)、fsync落盘、把新part原子注册进元数据。这套固定开销跟这次INSERT携带的行数无关,行数越少,固定开销占比就越高——1行一批时,几乎全部耗时都花在这套"开一次门"的固定流程上,真正写1行数据的时间可以忽略不计。这就是为什么ClickHouse官方文档和几乎所有实践经验都反复强调:攒够一批(几千到几十万行)再一次性INSERT,而不是应用层每产生一条数据就发一次INSERT。如果业务场景本身就是高频的小数据到达(比如每次只有1条日志/1个事件),真实的解决方案不是硬着头皮一条条插,而是在应用层加一个缓冲队列攒批,或者直接用下一篇要验证的async_insert——让ClickHouse服务端自己帮你把多个小请求在服务端合并成一次真实的写入。

分步演示:真实的批大小-吞吐曲线

每一步都是本文上面用真实计时跑出来的原始吞吐数字。

从19行/秒到173万行/秒,真实91000倍的吞吐曲线
这一步新测的batch_size

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实测量6个batch_size档位的INSERT吞吐
    京ICP备2026067502号-1京公网安备11010502063487号