重写分片的修改、只做遮盖的删除、在合并时生效的生命周期
目标
通过 system.mutations、system.part_log、system.parts,确认在不可变的数据片段之上,UPDATE、DELETE、TTL 实际做了什么,并找出停住的变形,把它结束掉。
为什么重要
在 ClickHouse 中,一行 UPDATE 就是把整个数据片段重写一遍,轻量级 DELETE 只是遮盖行,TTL 要等到合并到来。不了解这些差别,一次小小的更正就会变成磁盘 I/O 暴增,以为删掉了的行还留在磁盘上,一个失败的变形就会挡住表的所有修改。本实验的评分器不会相信你写的数字——它会用源数据的生成表达式重新计算应该留下的行,也在关闭掩码(apply_deleted_mask = 0)的情况下统计,并与 system.part_log 中的记录核对。
步骤
- 创建数据库
mut和表mut.events——列event_date Date, ts DateTime, user_id UInt32, email String, amount UInt32, status LowCardinality(String), legal_hold UInt8(按此顺序),MergeTree,ORDER BY (user_id, ts)。用/opt/lab/fixtures/mutation/events.sql一次写入 40 万行,并用OPTIMIZE TABLE mut.events FINAL把数据片段变成一个。 - 用
mutations_sync = 2执行ALTER TABLE mut.events UPDATE status = 'refunded' WHERE status = 'refund_req',把它的mutation_id、前后的活动数据片段名称(part_before、part_after),以及 system.part_log 中那次 MutatePart 所写的行数(rows_rewritten)写入 /root/ch/mutation/mutation.json。 - 用
ALTER TABLE ... DELETE删除user_id = 777的行(mutations_sync = 2)。 - 用轻量级 DELETE(
DELETE FROM)删除user_id = 888的行。把紧接着用普通 SELECT 统计的该用户的行数(visible_rows)、用SETTINGS apply_deleted_mask = 0统计的行数(masked_rows)、system.parts 中活动数据片段的rows(part_rows)写入 /root/ch/mutation/lwd.json,然后执行OPTIMIZE TABLE mut.events FINAL。 - 给
email列加上TTL if(legal_hold = 1, toDate('2100-01-01'), event_date + INTERVAL 90 DAY)(MODIFY COLUMN email String TTL ...),并应用到已有的数据片段。 - 加上表 TTL
event_date + INTERVAL 180 DAY DELETE WHERE status = 'test'(MODIFY TTL),并应用到已有的数据片段。 - 创建表
mut.hits (ts DateTime, user_id UInt32, hits UInt32, max_hits UInt32 DEFAULT hits, sum_hits UInt64 DEFAULT hits),使用MergeTree、PRIMARY KEY (user_id, toStartOfDay(ts), ts)、TTL ts + INTERVAL 30 DAY GROUP BY user_id, toStartOfDay(ts) SET max_hits = max(max_hits), sum_hits = sum(sum_hits),一次写入/opt/lab/fixtures/mutation/hits.sql之后,用OPTIMIZE TABLE mut.hits FINAL应用汇总。 - (不等待地)提交
ALTER TABLE mut.events UPDATE amount = toUInt32(email) WHERE legal_hold = 1,记录到失败之后,把该变形的mutation_id、is_done、parts_to_do、latest_fail_error_code_name(键为error_code_name)写入 /root/ch/mutation/stuck.json,然后用KILL MUTATION结束它。
参考
- 确认变形:
SELECT mutation_id, command, is_done, parts_to_do, latest_fail_reason FROM system.mutations WHERE database = 'mut'。 - 数据片段是如何被重写的:
SYSTEM FLUSH LOGS之后,SELECT event_type, part_name, merged_from, rows FROM system.part_log WHERE database = 'mut' ORDER BY event_time_microseconds。 - TTL 在合并时应用。想立刻应用,用
ALTER TABLE ... MATERIALIZE TTL SETTINGS mutations_sync = 2或OPTIMIZE TABLE ... FINAL。 - 常见错误:在第 4 步先执行 OPTIMIZE,错过了掩码的状态;漏掉第 6 步的 WHERE,把超过 180 天的所有行(=全部)都删掉了;在第 8 步忘了 KILL,导致后面所有变形都被挡住。
- 测试数据是 2024 年的数据。如果弄坏了,最快的办法是
DROP DATABASE mut之后从第 1 步重新来过。 - 官方文档:Avoid mutations · ALTER TABLE ... UPDATE · ALTER TABLE ... DELETE · Lightweight delete · Manage data with TTL · ALTER TABLE ... MODIFY TTL · system.mutations · system.part_log
只有一个数据片段的事件表
创建数据库 mut 和表 mut.events。列依次为 event_date Date, ts DateTime, user_id UInt32, email String, amount UInt32, status LowCardinality(String), legal_hold UInt8,MergeTree,ORDER BY (user_id, ts)。用 /opt/lab/fixtures/mutation/events.sql 一次写入 40 万行,并用 OPTIMIZE TABLE mut.events FINAL 把数据片段变成一个。
把数据片段做成一个,后面每次变形时数据片段名称如何变化,就可以一行一行地跟踪。数据全部是 2024 年的,所以后面要加的 TTL,无论什么时候评分,都会删除同样的行。
为了改 2% 而把整个数据片段重写
执行 ALTER TABLE mut.events UPDATE status = 'refunded' WHERE status = 'refund_req' SETTINGS mutations_sync = 2。把 system.mutations 中它的 mutation_id、执行前后的活动数据片段名称 part_before、part_after,以及 system.part_log 中创建 part_after 的 MutatePart 记录的 rows(作为 rows_rewritten),写入 /root/ch/mutation/mutation.json。
数据片段不能修改,所以变形会把新数据片段整个写出来。新数据片段名称末尾的数字就是变形编号。part_log 每 1 秒清空一次,所以要在 SYSTEM FLUSH LOGS 之后查看,并确认 merged_from 指向原数据片段。请比较改动的行数与重写的行数。
沉重的删除——ALTER TABLE ... DELETE
用 ALTER TABLE mut.events DELETE WHERE user_id = 777 SETTINGS mutations_sync = 2 删除 user_id = 777 的行。
ALTER DELETE 也是变形。它会把含有相关行的数据片段去掉这些行后重写。结束后,即使用关闭掩码的查询(SETTINGS apply_deleted_mask = 0),也不应该再看到这些行。
轻量级 DELETE 只是遮盖
执行 DELETE FROM mut.events WHERE user_id = 888,把紧接着用普通 SELECT 统计的该用户的行数作为 visible_rows,用 SETTINGS apply_deleted_mask = 0 统计的行数作为 masked_rows,system.parts 中活动数据片段的 rows 作为 part_rows,写入 /root/ch/mutation/lwd.json。然后执行 OPTIMIZE TABLE mut.events FINAL。
轻量级 DELETE 会变成往隐藏列 _row_exists 写 0 的变形(请看 system.mutations 的 command)。SELECT 会根据这个标记遮盖行,但数据片段里行仍然存在,所以 rows 不会减少。行真正被去掉,是在合并的时候。
列 TTL——只清除已过期的值
执行 ALTER TABLE mut.events MODIFY COLUMN email String TTL if(legal_hold = 1, toDate('2100-01-01'), event_date + INTERVAL 90 DAY),并应用到已有的数据片段(MATERIALIZE TTL,mutations_sync = 2)。法定保留(legal_hold = 1)的行的 email 必须保留。
列 TTL 会把已过期的值改成该类型的默认值(String 是空字符串)。对于保留的行,用返回遥远未来日期的方式制造例外。请在 system.mutations 中看一看,修改 TTL 时默认是否会同时生成 MATERIALIZE TTL 变形。
行 TTL——只删除符合条件的行
执行 ALTER TABLE mut.events MODIFY TTL event_date + INTERVAL 180 DAY DELETE WHERE status = 'test',并应用到已有的数据片段。不是 test 的行一行都不能消失。
数据全部是 2024 年的,已经超过了 180 天。没有 WHERE 的话,所有行都会过期,整张表被清空。TTL 在合并时应用,所以用 MATERIALIZE TTL 或 OPTIMIZE FINAL 立刻应用。
GROUP BY TTL——把旧行汇总起来
创建表 mut.hits (ts DateTime, user_id UInt32, hits UInt32, max_hits UInt32 DEFAULT hits, sum_hits UInt64 DEFAULT hits),使用 MergeTree、PRIMARY KEY (user_id, toStartOfDay(ts), ts)、TTL ts + INTERVAL 30 DAY GROUP BY user_id, toStartOfDay(ts) SET max_hits = max(max_hits), sum_hits = sum(sum_hits),一次写入 /opt/lab/fixtures/mutation/hits.sql 之后,执行 OPTIMIZE TABLE mut.hits FINAL。
GROUP BY 所用的列必须是主键的前缀,所以把 toStartOfDay(ts) 放进了 PRIMARY KEY。max_hits、sum_hits 的默认值要设成 hits,汇总之前的一行才会以自己的值开始。请比较 INSERT 之后与 OPTIMIZE 之后的行数。
找出停住的变形并结束它
提交 ALTER TABLE mut.events UPDATE amount = toUInt32(email) WHERE legal_hold = 1(不加 mutations_sync)。当 system.mutations 中记录了失败之后,把该变形的 mutation_id、is_done、parts_to_do、latest_fail_error_code_name(键名为 error_code_name)写入 /root/ch/mutation/stuck.json,并用 KILL MUTATION 结束它。
邮箱字符串无法当作数字读取,所以变形在每个数据片段上都会失败,并不断重试。没有回滚,后面的变形全都会被它挡住。等几秒,待 latest_fail_reason 被填上之后再记录,然后用 KILL MUTATION WHERE database = 'mut' AND mutation_id = '…' 结束。