bigywater.tech
ClickHouse 内部原理 · 24

ReplacingMergeTree:去重为什么不是立即发生

真实插入同一个k两个版本,对比普通SELECT、SELECT FINAL、OPTIMIZE FINAL三种情况下数据的真实去重状态。

FINAL≠物理去重
SELECT FINAL真实只是查询时临时归并,不改磁盘数据
merge才物理去重
真实只有OPTIMIZE/后台merge才让底层数据变成1行

做的事:同一个k真实插入两次,模拟一条记录被更新

代码CREATE TABLE demo.rmt (k UInt64, v String, ver UInt64) ENGINE=ReplacingMergeTree(ver) ORDER BY k; INSERT INTO demo.rmt VALUES (1, 'v1', 1); INSERT INTO demo.rmt VALUES (1, 'v2', 2); -- 真实模拟同一条记录被更新到v2

验证:merge之前,普通SELECT真实两行都在

实测 · 本机执行输出(merge前)
SELECT * FROM demo.rmt ORDER BY k;
→ 1  v2  2
  1  v1  1     ← 真实两行都在,没有自动去重

SELECT count() FROM system.parts WHERE table='rmt' AND active;
→ 2   ← 真实两个part,各自还没合并

虽然两行的k完全相同、且ver更高的那行(v2)真实应该"覆盖"ver更低的那行(v1),但普通SELECT在merge真正发生之前,真实会把两行原样都返回——ReplacingMergeTree不是在INSERT时刻做去重检查,它的去重逻辑真实只在part合并这个物理动作里执行。

验证:FINAL修饰符能立刻拿到正确结果,但这只是查询时的把戏

实测 · 本机执行输出(不merge,只加FINAL修饰符)
SELECT * FROM demo.rmt FINAL ORDER BY k;
→ 1  v2  2   ← 真实立刻只剩1行,看起来"去重"了

-- 但这时候system.parts真实还是2个part,磁盘上的数据完全没变
-- 真实执行OPTIMIZE TABLE FINAL之后:
OPTIMIZE TABLE demo.rmt FINAL;
SELECT * FROM demo.rmt ORDER BY k;
→ 1  v2  2

SELECT count() FROM system.parts WHERE table='rmt' AND active;
→ 1   ← 真实这时候底层数据才变成了1个part、物理上真的只有1行
FINAL关键字真实做的事情,是在查询执行阶段临时对多个part做一次内存中的归并去重,结果对用户来说是正确的,但底层磁盘上的part数据完全没有被真实修改——下一次查询依然要重新做一遍这个临时归并,如果表很大、part很多,FINAL查询的真实代价会随之上升(这正是它常被建议少用、尤其不要在高频查询里滥用的原因)。真正让数据在磁盘上物理变成1行的,是OPTIMIZE TABLE FINAL这类真实触发part合并的动作——生产环境里,这个合并通常由后台merge线程按ClickHouse自己的调度节奏自动完成,不需要每次都手动OPTIMIZE,但这也意味着如果没有查询用FINAL、也没有等到后台merge真的发生,同一个业务key对应的多个历史版本,会在磁盘上真实并存一段不确定的时间,这段时间里任何不带FINAL的普通查询都可能读到重复的旧数据——这正是"去重为什么不是立即发生"这个问题真实的答案。

分步演示:从两行并存到FINAL临时去重再到物理去重

每一步都是本文上面用真实ReplacingMergeTree跑出来的原始结果。

去重真实只在merge这个时间点物理发生
这一步新验证的状态

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实的 ReplacingMergeTree(ver)
    • 真实对比普通SELECT / SELECT...FINAL / OPTIMIZE TABLE FINAL三种去重时机
    京ICP备2026067502号-1京公网安备11010502063487号