bigywater.tech
ClickHouse 内部原理 · 1

ClickHouse 为什么适合 OLAP,不适合高频单行更新

起一个本地 ClickHouse 26.9 单节点,用同一份 200 行数据分别做 200 次单行 INSERT 和 1 次批量 INSERT,再用 ALTER TABLE UPDATE 验证“更新”在 ClickHouse 里到底是什么。

23.641s
200次单行INSERT耗时
0.139s
同样200行批量INSERT耗时
38轮
单行写入触发的后台合并轮数

做的事:200 次单行 INSERT vs 1 次批量 INSERT

同一张 MergeTree 表,写入完全相同的 200 行数据,只是写入方式不同。

本机 clickhouse-client,计时for i in $(seq 1 200); do clickhouse-client --query "INSERT INTO demo.olap_single (id, v) VALUES ($i, $i.5)" done # 耗时: 23.641 s clickhouse-client --query "INSERT INTO demo.olap_batch (id, v) VALUES (1,1.5),(2,2.5),...,(200,200.5)" # 耗时: 0.139 s

数据完全一样,写法不同,耗时差了 170 倍。这不是网络延迟的问题——两次测试都在同一台机器、同一个本地 TCP 连接上跑。真正的原因在 ClickHouse 怎么落盘。

MergeTree 引擎不管一次 INSERT 写了多少行,都会把这次 INSERT 的数据整体写成一个新的 part(一组磁盘文件)。单行 INSERT 200 次,就是在磁盘上新建 200 次 part;200 行一次 INSERT,就是新建 1 次 part。

验证:part 数量、以及后台合并的痕迹

直接查 system.parts,看两张表现在的 part 状态。

SELECT name, level, rows FROM system.parts WHERE table='olap_single' AND activeall_1_194_38 38 194 all_195_199_1 1 5 all_200_200_0 0 1 SELECT name, level, rows FROM system.parts WHERE table='olap_batch' AND activeall_1_1_0 0 200

olap_batch 只有 1 个 part,level=0,说明它从没被合并过——一次 INSERT 直接落盘成 1 个 part,之后再没有其他 part 需要跟它合并。

olap_single 这边,虽然建了 200 个 part,但活跃 part 只剩 3 个:其中 all_1_194_38 这个 part 的 level=38,意味着它是经过 38 轮后台合并才把最初 194 个零散 part 拼成 1 个的。level 每合并一轮 +1,38 这个数字就是后台执行了多少轮 merge 的直接证据。

这就是"不适合高频单行更新"的代价:不是插入慢在 SQL 解析上,而是慢在每次都要新建一组磁盘文件,之后还要靠后台线程不断合并回收,合并本身也要吃 CPU 和磁盘 IO。

验证:UPDATE 在 ClickHouse 里到底是什么

对 olap_batch 执行一次 ALTER TABLE ... UPDATE,看它到底做了什么。

执行并查询 system.mutationsALTER TABLE demo.olap_batch UPDATE v = v * 2 WHERE id % 2 = 0 SELECT table, mutation_id, is_done FROM system.mutations → olap_batch mutation_2.txt 1 SELECT id, v FROM demo.olap_batch WHERE id IN (1,2,3,4) ORDER BY id → 1 1.5 (奇数,未命中 WHERE,原样) → 2 5 (偶数,2.5*2,生效) → 3 3.5 (奇数,原样) → 4 9 (偶数,4.5*2,生效)

UPDATE 语句生效了,偶数 id 的 v 值确实翻了倍。但它的执行方式跟 MySQL 那种"原地改一行"完全不同:ClickHouse 把这次 UPDATE 记录成一条 mutation(mutation_id=mutation_2.txt),后台异步地把受影响的 part 整个重写一遍,不是真的去改磁盘上的某几个字节。数据量小的时候这个过程快到几乎瞬间完成(is_done 立刻变成 1),但如果 part 很大,一次 UPDATE 要重写整个 part 文件。

这也解释了为什么 OLAP 数据库把"批量写入、少量更新"当作设计前提:INSERT 是新建文件,UPDATE 是重写整个 part 文件,两者都是"文件级"操作,天然对高频的单行读写不友好;但对于一次写入几十万行、之后基本只读的分析场景,这套机制反而非常高效。

分步演示:170倍的耗时差,磁盘上发生了什么

每一步都是本文上面起 ClickHouse 单节点跑出来的原始结果。

单行INSERT vs 批量INSERT,差了170倍还留下38轮合并
这一步新验证的结果

    本文用到的工具

    • Homebrew 安装的 ClickHouse 26.9.1,本机单节点,端口 19100(TCP)/19101(HTTP)
    • system.parts:查看 part 的 name / level / rows / active 状态
    • system.mutations:查看 ALTER UPDATE 产生的 mutation 是否完成
    • clickhouse-client 循环 200 次单行 INSERT vs 1 次批量 INSERT,用 Python 计时
    京ICP备2026067502号-1京公网安备11010502063487号