bigywater.tech
ClickHouse 内部原理 · 7

MergeTree 家族到底解决了什么

往MergeTree、ReplacingMergeTree、SummingMergeTree里写入同样的重复数据,对比它们在OPTIMIZE FINAL前后的差异,顺带验证SELECT...FINAL和物理合并到底是不是一回事。

同批即去重
同一条INSERT里的重复key立刻折叠
跨part不去重
分批INSERT的重复key要等合并
FINAL≠OPTIMIZE
查询FINAL不改变磁盘,OPTIMIZE才改变

做的事:同样的重复数据,写进3种引擎

同一个 key(k=1)写3次不同的值,同一个 key(k=2)写1次,分别插入普通 MergeTree、ReplacingMergeTree、SummingMergeTree。

插入(每张表用同一份数据)INSERT INTO demo.mt_plain VALUES (1,100,1),(1,200,2),(1,300,3),(2,50,1) INSERT INTO demo.mt_replacing VALUES (1,100,1),(1,200,2),(1,300,3),(2,50,1) INSERT INTO demo.mt_summing VALUES (1,100),(1,200),(1,300),(2,50)

三张表引擎不同,但收到的原始数据完全一样:k=1 被写了3次不同的值,k=2 只写了1次。

验证:OPTIMIZE FINAL 之后,3种引擎给出3种答案

对3张表都执行 OPTIMIZE TABLE ... FINAL,强制触发一次彻底合并,再看结果。

OPTIMIZE TABLE ... FINAL 之后,查询结果-- mt_plain(普通MergeTree,什么都不做) 1 100 1 1 200 2 1 300 3 2 50 1 -- mt_replacing(ReplacingMergeTree(ver),按ver取最大版本) 1 300 3 2 50 1 -- mt_summing(SummingMergeTree(),按主键之外的数值列求和) 1 600 2 50

普通 MergeTree 老老实实保留了全部4行,一行都没少——它压根不做去重。ReplacingMergeTree 只留下了 k=1 里 ver 最大的那一行(300,3),把另外两行丢弃了。SummingMergeTree 则把 k=1 的3个值加总成了600,一行代表一个key。

这就是 MergeTree "家族"真正解决的问题:普通 MergeTree 只管有序存储和范围查询,不管重复数据;家族里的其它引擎在合并(merge)这个本来就要发生的动作上,各自加了一段自定义逻辑——ReplacingMergeTree 加的是"留最大版本",SummingMergeTree 加的是"数值列相加"。

意外发现:去重可能在你执行 OPTIMIZE 之前就发生了

如果3条重复数据是在同一条 INSERT 语句里一起写进去的,会怎样?

同一条INSERT里的3行重复key,立刻查询(还没有OPTIMIZE、没有等待合并)INSERT INTO demo.mt_replacing2 VALUES (1,100,1),(1,200,2),(1,300,3),(2,50,1) SELECT * FROM demo.mt_replacing2 → 立刻只有2行:(1,300,3) 和 (2,50,1) SELECT name, rows, level FROM system.parts → all_1_1_1,level=1(不是刚插入该有的level=0)

同一条 INSERT 语句里的重复 key,在写入的那一刻就已经被去重了,连一次真正的"合并"都没等——因为一次 INSERT 本身在落盘前就要经过跟合并同一套排序、折叠的处理逻辑。但如果把这3行拆成3条独立的 INSERT 语句分别写入,结果完全不同。

3次独立INSERT,同一个key,立刻查询INSERT ... VALUES (1,100,1) INSERT ... VALUES (1,200,2) INSERT ... VALUES (1,300,3) SELECT * FROM demo.mt_replacing3 → 3行全在:100、200、300 都还在 SELECT name, rows, level FROM system.parts → 3个part,各自level=1,谁都没被合并

这次3行都留着——因为它们分散在3个不同的 part 里,还没有发生任何合并,ReplacingMergeTree 的去重逻辑只在合并这个动作里生效,不会跨 part 主动去重。

验证:SELECT ... FINAL 和物理合并不是一回事

对上面那3个还没合并的part,直接在查询里加 FINAL,会怎样?

对比SELECT * FROM demo.mt_replacing3 FINAL → 只返回1行:(1,300,3) -- 但这时候system.parts依然是3个独立part,物理上什么都没变 SELECT name, rows, level FROM system.parts WHERE table='mt_replacing3' → 还是3个part(此时未执行OPTIMIZE) -- 执行 OPTIMIZE TABLE ... FINAL 之后 SELECT name, rows, level FROM system.parts WHERE table='mt_replacing3' → 变成1个part:all_1_3_2,level=2,只有1行

SELECT ... FINAL 是在查询这一刻临时把结果算对(不改变磁盘上的任何东西,每次查询都要重新做一遍这个临时合并,有的额外开销);OPTIMIZE TABLE ... FINAL 才是真正把磁盘上的 part 物理合并成一份,之后不用再临时处理。这是两件完全不同的事,容易被名字里都带着"FINAL"搞混。

分步演示:MergeTree家族的行为差异

每一步都是本文上面建表、插入、查询得到的原始结果。

3种引擎、2种插入方式、2种FINAL,结果各不相同
这一步新验证的场景

    本文用到的工具

    • MergeTree / ReplacingMergeTree(ver) / SummingMergeTree():建表对比
    • OPTIMIZE TABLE ... FINAL:强制物理合并
    • SELECT ... FINAL:查询时临时合并,不改变磁盘
    • system.parts:核对part数量与level
    京ICP备2026067502号-1京公网安备11010502063487号