ReplacingMergeTree — 用 INSERT 模拟更新与删除
一句话总结
ReplacingMergeTree 是一种在合并时把排序键相同的行缩减为一行的引擎。版本列较大的行胜出,如果胜出的行带有删除标记,就视为没有这个键。合并什么时候发生是不确定的,所以需要准确答案的查询,必须在查询时用 FINAL 或 argMax 应用同样的规则。
为什么需要它
把生产数据库的用户表迁到分析型数据库,很快就会随之而来“套餐变了”“用户注销了”这样的变更。对行式数据库来说,这就是一行 UPDATE、DELETE,但正如前面模块所见,MergeTree 的数据片段一旦写出就不会修改。要改一行,就得把含有这一行的数据片段的列文件重写一遍,如果每次变更都这么做,写入就会暴增。
所以要把思路颠倒过来。不要修改,而是再写入一行新版本。注销也是写入一个“已注销”的版本。官方指南(Working with the ReplacingMergeTree engine)把这称为“用不可变的 INSERT 模拟更新”。清理旧版本的工作,交给本来就会发生的后台合并。写入始终是追加(append),所以很快,代价是“合并之前还能看到旧版本”。
工作原理
引擎声明是 ReplacingMergeTree(ver, is_deleted)。参考文档规定的规则如下。
- “同一行”由
ORDER BY决定。不是 PRIMARY KEY。排序键值相同的行是一组。 - 在一组之内,ver 最大的行会留下。如果 ver 相同,则留下最后写入的行(在实验服务器上,以相同的 ver 先写 'a' 再写 'b',留下的是 'b')。
is_deleted只有在有 ver 时才能使用,胜出的行如果是 1,这个键在查询中就会被排除。但是合并不会删除删除标记行,而是保留它。这是为了之后即使写入更低的版本,也让删除胜出。- 想在合并时连已删除的行也清除,要打开表设置
allow_experimental_replacing_merge_with_cleanup = 1,再执行OPTIMIZE ... FINAL CLEANUP。不设置就执行,会以“Experimental merges with CLEANUP are not allowed”被拒绝。
用本实验的数据(100,000 个用户 · 30,058 行更新 · 5,108 行注销)测出的数字,原样展示了这套规则。停止合并后写入,count() 是 135,166 行,加上 FINAL 是 94,892 个用户。用 OPTIMIZE ... FINAL 合并之后,行仍是 100,000 个——每个用户一行,但其中 5,108 个是删除标记行,所以不加 FINAL 来数,依然是错的。要到 CLEANUP 合并之后,才变成 94,892 行。
FINAL 在查询的同时应用合并规则。同样的答案也可以用 argMax 得到。
SELECT plan, count() FROM (
SELECT user_id, argMax(plan, ver) AS plan, argMax(is_deleted, ver) AS d
FROM rmt.users GROUP BY user_id
) WHERE d = 0 GROUP BY plan;
argMax(x, ver) 是 ver 最大的那一行的 x。应该选什么,由查询自己说明,所以在不能用 FINAL 的地方(其他引擎、合并多张表的结果)也同样适用。
还有一点——同一个 INSERT 内的重复,会在写入的那一刻被缩减。由于默认设置 optimize_on_insert = 1,把三个版本放在一个 INSERT 里写入,数据片段一开始就是 100,000 行。想看“合并之前”,就要分开写入各个版本,并用 SYSTEM STOP MERGES 停止那张表的合并。对停止的表执行 OPTIMIZE,会以“Cancelled merging parts”被拒绝。
在现场相遇的样子
最常见的事故是把会变化的列放进排序键。如果为了查询性能,把它定成 ORDER BY (user_id, plan),套餐变了的用户,排序键就变了,成了“另一行”。在实验中,这样创建的表即使用 FINAL 来数,也得到 115,062 行(95,919 个用户)——旧套餐的行永远不会被合并,删除标记也盖不住旧套餐的行,已注销的用户仍然活着。这就是指南明确要求“ORDER BY 的列不能变化”的原因。
第二种是 CLEANUP 之后的重发。管道把旧批次重新发送是常有的事(重新处理、偏移量回拨)。保留了删除标记的表,版本 3 胜过版本 1,安然无恙,但用 CLEANUP 清掉了删除行的表,没有可比较的对象,旧的行就这样复活了。在实验中,把用户 1–1000 的首次加载行再写入一次,只有做过 CLEANUP 的表中有 56 个用户复活。指南说,只有“确信旧版本不会再次写入”时,才能使用 CLEANUP。
第三种是 FINAL 的成本。FINAL 在查询时做合并,所以过滤条件越是作用在排序键上,就越便宜。如果分区键不会随行而变,指南中还出现了让它按分区分别处理的 do_not_merge_across_partitions_select_final。
下一项实验要做什么
用 ReplacingMergeTree(ver, is_deleted) 创建 rmt.users,在停止合并的情况下写入三批变更记录。比较不加 FINAL 和加 FINAL 数出的行数,并不用 FINAL,用 argMax 求出各套餐的当前用户数。再创建两张新表,按版本迁移数据,一张做普通合并,一张做 CLEANUP 合并,然后数剩下的行,看把 plan 放进排序键的表会让什么复活。最后重新写入旧行,记录两张表的反应有何不同。