观察合并前的重复行,并用 FINAL、argMax 和 CLEANUP 处理
目标
向 ReplacingMergeTree 写入变更记录(更新、注销),用数字确认:合并之前能看到旧版本,如何用 FINAL、argMax 把它们遮住,合并和 CLEANUP 实际删除了什么,以及排序键和重发如何改变结果。
为什么重要
迁移到分析型数据库的生产数据会不断变化。ClickHouse 把更新处理为“写入新版本 INSERT + 之后合并”,而合并何时发生是不确定的。所以同样的查询,有时对、有时错。本实验用 SYSTEM STOP MERGES 固定“合并之前”,亲眼看到那个错误的答案,并学会两种在查询时得出正确答案的方法。评分器根据源数据的生成表达式直接计算期望值,合并是否真的发生过,则通过 system.part_log 的记录来确认。
步骤
- 创建数据库
rmt和表rmt.users——列user_id UInt64, email String, plan LowCardinality(String), score UInt32, ver UInt32, is_deleted UInt8(按此顺序),引擎ReplacingMergeTree(ver, is_deleted),ORDER BY user_id。 - 用
SYSTEM STOP MERGES rmt.users停止这张表的合并后,运行一次/opt/lab/fixtures/replacing/changes.sql(三次 INSERT = 三个数据片段)。 - 创建不加 FINAL 统计全部行数的 /root/ch/replacing/q_raw.sql,以及用
FROM rmt.users FINAL统计的 /root/ch/replacing/q_final.sql,并把两个结果以raw、final写入 /root/ch/replacing/counts.json。 - 创建不使用 FINAL、求出按套餐(plan)的当前用户数的 /root/ch/replacing/q_plan.sql——结果是 plan 和用户数两列。
- 用与
rmt.users相同的列和引擎创建rmt.merged,把rmt.users的行按ver = 1、ver = 2、ver = 3的顺序用三次 INSERT 迁移过去,执行OPTIMIZE TABLE rmt.merged FINAL之后,把rows_after_merge(count())、deleted_rows_kept(is_deleted = 1 的行数)、final_count(用 FINAL 数出的数量)写入 /root/ch/replacing/merged.json。 - 用相同的列和引擎,加上表设置
allow_experimental_replacing_merge_with_cleanup = 1创建rmt.cleaned,用同样的方法写入三次之后,执行OPTIMIZE TABLE rmt.cleaned FINAL CLEANUP,把rows_after_cleanup、deleted_rows_left写入 /root/ch/replacing/cleanup.json。 - 创建列和引擎相同、但排序键为
ORDER BY (user_id, plan)的rmt.bad,用同样的方法写入三次并执行OPTIMIZE TABLE rmt.bad FINAL之后,把good_final(对 rmt.merged 用 FINAL 数出的数量)、bad_final(对 rmt.bad 用 FINAL 数出的数量)、bad_users(rmt.bad FINAL 中不同的 user_id 数)写入 /root/ch/replacing/key.json。 - 模拟迟到的重发——把
rmt.users中ver = 1 AND user_id <= 1000的行分别重新写入rmt.merged和rmt.cleaned,把两张表用 FINAL 数出的值及其差,以merged_final、cleaned_final、resurrected写入 /root/ch/replacing/replay.json。
参考
- 服务器在 Pod 启动时就已经运行。只需输入
clickhouse-client即可连接。如果停了,就运行ch-up(重新启动服务器后 STOP MERGES 会被解除——从第 2 步重新开始)。 SYSTEM STOP MERGES只作用于那张表。第 5–7 步的新表开启了合并,所以可以 OPTIMIZE。对停止了合并的表执行 OPTIMIZE,会以“Cancelled merging parts”被拒绝。- 合并记录会留在
system.part_log中(event_type = 'MergeParts',merged_from中是被合并的数据片段名称)。如果看不到刚发生的事情,请执行SYSTEM FLUSH LOGS。 - 常见错误:在第 5–7 步用一次
INSERT ... SELECT * FROM rmt.users迁移——同一个 INSERT 内的重复会在写入的那一刻被缩减(optimize_on_insert),没有东西可合并了。请按版本分三次写入。 - 想让数字在 JSON 中不带引号,用
--output_format_json_quote_64bit_integers 0。 - 官方文档:ReplacingMergeTree · Working with the ReplacingMergeTree engine · OPTIMIZE · system.part_log
带有版本和删除标记的表
创建数据库 rmt 和表 rmt.users。列依次为 user_id UInt64, email String, plan LowCardinality(String), score UInt32, ver UInt32, is_deleted UInt8,引擎为 ReplacingMergeTree(ver, is_deleted),排序键为 ORDER BY user_id。
引擎的第一个参数是决定哪一行胜出的版本列,第二个参数是告诉你胜出的行是否为删除的列。删除标记列没有版本列就无法使用。排序键就是“同一行”的定义,所以只放不会变化的标识符。
停止合并,写入三批变更记录
用 SYSTEM STOP MERGES rmt.users 停止这张表的合并后,运行一次 /opt/lab/fixtures/replacing/changes.sql。首次加载、更新、注销这三次 INSERT,必须留下三个数据片段。
合并会在后台随时发生,放着不管的话,“合并之前”的状态可能几秒钟就消失了。停止合并必须在写入之前做。如果已经写入了,就在停止之后 TRUNCATE,再重新写入。
不加 FINAL 数与加 FINAL 数
创建不加 FINAL 统计 rmt.users 全部行数的 /root/ch/replacing/q_raw.sql,以及用 FROM rmt.users FINAL 统计的 /root/ch/replacing/q_final.sql,并把两个查询的结果以 raw、final 写入 /root/ch/replacing/counts.json。
FINAL 在查询的同时应用合并规则(排序键相同时只保留 ver 大的行,如果那一行是删除标记就排除)。两个数字的差,就是旧版本行与注销行之和。不要给 q_final.sql 加 WHERE。
不用 FINAL 也能得到相同的答案——argMax
创建不使用 FINAL、求出按套餐(plan)的当前用户数的 /root/ch/replacing/q_plan.sql。结果是 plan 和用户数两列,已注销的用户必须被排除。
直接按 GROUP BY plan 来数,会把旧版本和注销行也数进去。必须先把每个用户缩减为一行——按 user_id 分组,用 argMax(值, ver) 选出版本最大的那一行的 plan 和 is_deleted,再在外层去掉已注销的用户,按 plan 重新分组。
合并之后,注销行也会保留
用与 rmt.users 相同的列和引擎创建 rmt.merged,把 rmt.users 的行按 ver = 1、ver = 2、ver = 3 的顺序用三次 INSERT 迁移过去,执行 OPTIMIZE TABLE rmt.merged FINAL 之后,把 rows_after_merge(count())、deleted_rows_kept(is_deleted = 1 的行数)、final_count(用 FINAL 数出的数量)写入 /root/ch/replacing/merged.json。
CREATE TABLE ... AS 另一张表 会复制列和引擎,但不会复制 STOP MERGES 状态。请看,合并之后行数是否与用户数相同,以及不加 FINAL 数出的值为什么仍然是错的。合并是否真的发生过,会留在 system.part_log 中。
CLEANUP 合并会把注销行也删除
用与 rmt.users 相同的列和引擎,加上表设置 allow_experimental_replacing_merge_with_cleanup = 1 创建 rmt.cleaned,像第 5 步一样迁移三次之后,执行 OPTIMIZE TABLE rmt.cleaned FINAL CLEANUP,把 rows_after_cleanup(count())、deleted_rows_left(is_deleted = 1 的行数)写入 /root/ch/replacing/cleanup.json。
CREATE TABLE 新表 AS 另一张表 之后可以加 SETTINGS。不加设置就执行 CLEANUP,服务器会拒绝——删除了注销行,以后旧版本写入时就无法阻止,所以这是必须有意识地打开的功能。
排序键决定“同一行”
创建列和引擎相同、但 ORDER BY (user_id, plan) 的 rmt.bad,用同样的方法迁移三次并执行 OPTIMIZE TABLE rmt.bad FINAL 之后,把 good_final(对 rmt.merged 用 FINAL 数出的数量)、bad_final(对 rmt.bad 用 FINAL 数出的数量)、bad_users(rmt.bad FINAL 中不同的 user_id 数)写入 /root/ch/replacing/key.json。
如果排序键中有 plan,套餐变了的用户的旧行和新行,键就不同,无法成为一组。注销行也只盖得住注销前那个套餐的组。请看 bad_final 是否比用户数大,bad_users 是否比当前用户数大。
CLEANUP 之后旧行又来了会怎样
把 rmt.users 中 ver = 1 AND user_id <= 1000 的行分别重新写入 rmt.merged 和 rmt.cleaned 之后,把两张表用 FINAL 数出的值及其差,以 merged_final、cleaned_final、resurrected(cleaned_final − merged_final)写入 /root/ch/replacing/replay.json。
这是管道把旧批次重新发送的情形。想一想:在保留了删除标记的表中,是什么胜过旧行;在删除了注销行的表中,旧行在与谁竞争。复活的人,是 1–1000 号中曾经注销的用户。