TT Lab
开始
学习 学习路径 课程

ClickHouse — 从内部理解列式分析数据库

重写分片的修改、只做遮盖的删除、在合并时生效的生命周期

在 TT Lab 中继续学习

目标

通过 system.mutations、system.part_log、system.parts,确认在不可变的数据片段之上,UPDATE、DELETE、TTL 实际做了什么,并找出停住的变形,把它结束掉。

为什么重要

在 ClickHouse 中,一行 UPDATE 就是把整个数据片段重写一遍,轻量级 DELETE 只是遮盖行,TTL 要等到合并到来。不了解这些差别,一次小小的更正就会变成磁盘 I/O 暴增,以为删掉了的行还留在磁盘上,一个失败的变形就会挡住表的所有修改。本实验的评分器不会相信你写的数字——它会用源数据的生成表达式重新计算应该留下的行,也在关闭掩码(apply_deleted_mask = 0)的情况下统计,并与 system.part_log 中的记录核对。

步骤

  1. 创建数据库 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 把数据片段变成一个。
  2. 用 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。
  3. 用 ALTER TABLE ... DELETE 删除 user_id = 777 的行(mutations_sync = 2)。
  4. 用轻量级 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。
  5. 给 email 列加上 TTL if(legal_hold = 1, toDate('2100-01-01'), event_date + INTERVAL 90 DAY)(MODIFY COLUMN email String TTL ...),并应用到已有的数据片段。
  6. 加上表 TTL event_date + INTERVAL 180 DAY DELETE WHERE status = 'test'(MODIFY TTL),并应用到已有的数据片段。
  7. 创建表 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 应用汇总。
  8. (不等待地)提交 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 结束它。

参考

只有一个数据片段的事件表

创建数据库 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 = '…' 结束。