第 12 章 · Mutation:UPDATE / DELETE 的真相

四个交互演示:① Mutation 重写 Part 动画 · ② 轻量级 DELETE 墓碑机制 · ③ UPDATE 替代方案对照 · ④ Mutation 进度监控

「修改一本书 = 重新印一本」 — Mutation 重写整 Part

下面是一个 Part 的 5 列内容。点 「ALTER UPDATE score = score+1」,看 ClickHouse 怎么生成新 Part:

  • ① 解析 WHERE → 用稀疏索引找命中 Part
  • ② 读出 Part 所有列(不只是要改的那一列)
  • ③ 按 UPDATE 表达式重写需要改的列
  • ④ 其他列 hardlink 复制到新 Part 目录
  • ⑤ 旧 Part 标记 inactive
  • ⑥ 后台慢慢回收旧 Part 物理文件

📦 旧 Part: 202604_1_3_2 [active]

大小:50.2 GB 行数:1.2 亿

📦 新 Part: 202604_1_3_2_5 [未生成]

大小: 行数:
注意写放大:UPDATE 一行 → 整 Part 重写。50GB Part 即使只动一列,也要写 50GB 的新 Part(其他列 hardlink,但元数据 + 校验仍然要写)。
性能数据(参考): 单 Part 1 亿行 / 50GB 的 UPDATE 一列大概 30~120s;同一个表上同时挂 5 个以上 Mutation 通常会拖垮 Merge 通道。

轻量级 DELETE:写墓碑列 _row_exists,不重写 Part

events 有 8 行。执行 DELETE FROM events WHERE event_type='view',看 ClickHouse 内部做了什么:

iduser_idevent_typepayload_row_exists (隐藏)

用户视角:SELECT 自动跳过墓碑

SELECT count() FROM events;
-- 引擎自动加 WHERE _row_exists = 1
-- 用户看不到 'view' 行
可见行:8 查询耗时:12ms

系统视角:物理上行还在

SELECT sum(rows) FROM system.parts
WHERE table='events' AND active;
-- 物理行数 > 可见行数 = 墓碑数量
物理行:8 墓碑数:0
轻量级 DELETE 的代价: ① 墓碑列 _row_exists 写入快(小 IO) ② 但每次 SELECT 都要带 WHERE _row_exists = 1 过滤 ③ 墓碑积累 → 查询慢慢退化 → 需要后续 Merge 物理清理
不要滥用:每秒一条 DELETE 会让墓碑列爆炸。轻量级 DELETE 适合"每天几次的批量删",不适合"每秒一条"。

UPDATE / DELETE 替代方案选型

选择业务场景,看 ClickHouse 推荐用哪种姿势"修改"数据:

⚡ 毫秒
ReplacingMergeTree
维度表"软更新" — 追加新版本,按 (key, version) 自动去重
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY user_id;

INSERT 新版本即可
SELECT argMax(name, updated_at) ...
⚡ 毫秒
CollapsingMergeTree
状态翻转 — 用 +1/-1 的 sign 列表示新增/作废,后台抵消
ENGINE = CollapsingMergeTree(sign)
ORDER BY order_id;

INSERT (... , -1) 作废
INSERT (... ,  1) 新版本
⚡ 自动
TTL DELETE
数据生命周期 — 引擎自动按时间删除过期数据
ALTER TABLE t MODIFY TTL
  event_date + INTERVAL 90 DAY DELETE;
⚡ 秒级
REPLACE PARTITION
整批"修改" — 在 staging 表算好新版本,整块替换
ALTER TABLE prod
REPLACE PARTITION '202604'
  FROM staging.t_2026_04;
⚠ 几秒
轻量级 DELETE (22.8+)
合规删除 / 偶发删除 — 写墓碑列,不重写 Part
DELETE FROM events
WHERE user_id = 12345;
🐢 分钟~小时
重型 Mutation
万不得已 — 逐 Part 重写,IO 巨大,频繁用必崩
ALTER TABLE t UPDATE col = ...
WHERE ...;
-- 实际是 ALTER, 异步
口诀:
  • 能用 INSERT 解决的, 不要用 UPDATE
  • 能按分区操作的, 不要按 WHERE 过滤
  • 能让 TTL 自动做的, 不要手动 DELETE
  • 真要单条改, 控制频率, 别每秒一条

system.mutations 监控模拟器

真实环境用 SELECT * FROM system.mutations 跟踪。下面模拟:随时发起 Mutation,可观察进度、并发数、KILL 行为。

mutation_idcommandparts_to_do进度状态create_time
常用查询:
SELECT mutation_id, command, parts_to_do, is_done, latest_fail_reason
FROM system.mutations
WHERE database = 'learn_ck' AND NOT is_done
ORDER BY create_time;

KILL MUTATION WHERE mutation_id = 'mutation_42.txt';