对月分区进行裁剪、删除、分离与复制
目标
通过 EXPLAIN 读出月分区表中裁剪(Min-Max)舍弃了什么,并与没有分区的表比较读取的行数。通过 system.mutations、part_log 确认 DROP/DETACH/ATTACH PARTITION 与 DELETE 的区别,并观察分得太细的分区如何挡住 INSERT。
为什么重要
分区键一旦确定就很难修改,选错了(分得太细),数据片段会暴增,INSERT 会被挡住。反过来,选得好,保留周期的管理就以文件为单位完成。本实验的评分器不会相信你写的数字——它会给你保存的 SELECT 加上 EXPLAIN,以只读方式重新运行,分区操作则用表的行哈希和 system.mutations、part_log、query_log 来核对。
步骤
- 创建数据库
ptn和表ptn.sales——列ts DateTime, region LowCardinality(String), order_id UInt64, amount UInt32(按此顺序),引擎MergeTree,PARTITION BY toYYYYMM(ts),ORDER BY (region, ts)。 - 运行一次
/opt/lab/fixtures/partition/sales.sql,写入 150 万行,并用OPTIMIZE TABLE ptn.sales FINAL合并之后,把包含每个分区的partition_id、part(数据片段名称)、rows的 JSON 数组保存到 /root/ch/partition/partitions.json。 - 创建求 2026 年 8 月(
ts >= '2026-08-01 00:00:00' AND ts < '2026-09-01 00:00:00')amount之和的 /root/ch/partition/q_aug.sql,把 EXPLAIN indexes = 1 的输出保存到 /root/ch/partition/explain_aug.txt,然后把 Min-Max 阶段剩余的/全部数据片段和这个查询的rows_read,以parts_selected、parts_total、rows_read写入 /root/ch/partition/prune.json。 - 创建没有分区、列和
ORDER BY (region, ts)相同的ptn.sales_flat,把ptn.sales的行迁移过去并合并。创建以同样的 8 月条件读取该表的 /root/ch/partition/q_flat.sql,并把两个查询的rows_read以partitioned_rows_read、flat_rows_read写入 /root/ch/partition/compare.json。 - 创建
ptn.sales的副本ptn.s_drop、ptn.s_delete(CREATE TABLE ... AS ptn.sales+ 复制行 + 合并),并删除 7 月——s_drop用DROP PARTITION 202607,s_delete用ALTER TABLE ... DELETE WHERE toYYYYMM(ts) = 202607 SETTINGS mutations_sync = 1。把两张表在 system.mutations 中的行数,以及s_delete在 part_log 中的MutatePart事件数,以drop_mutations、delete_mutations、delete_mutated_parts写入 /root/ch/partition/dropdel.json。 - 用同样的方法创建副本
ptn.s_detach,用DETACH PARTITION 202609分离,确认 system.detached_parts 中可见的数据片段名称,再用ATTACH PARTITION 202609附加回去。把分离时的名称和重新附加之后的活动数据片段名称,以detached_part、attached_part写入 /root/ch/partition/detach.json。 - 用
CREATE TABLE ptn.sales_jul AS ptn.sales创建一张空表,不经 INSERT,用ALTER TABLE ptn.sales_jul ATTACH PARTITION 202607 FROM ptn.sales把 7 月复制过来。 - 创建列相同、
PARTITION BY (toDate(ts), region)、ORDER BY ts的ptn.sales_fine,尝试把ptn.sales全部 INSERT 进去,并把出现的错误保存到 /root/ch/partition/toofine.txt。数一数ptn.sales中(日期, 地区)的组合有多少个,以partitions_needed写入 /root/ch/partition/fine.json。
参考
- 查看分区:
SELECT partition_id, name, rows FROM system.parts WHERE database = 'ptn' AND table = '...' AND active。 - EXPLAIN 中会依次出现 Min-Max · Partition · PrimaryKey 阶段。本实验要的是 Min-Max 阶段的
Parts: 남은/전체(占位符依次为剩余的数据片段数与总数)。 - 读取的行数用
clickhouse-client --use_query_condition_cache 0 --queries-file q_aug.sql --format JSON | jq .statistics.rows_read测量(关闭查询条件缓存)。 CREATE TABLE 새표 AS ptn.sales(占位符为新表名)连 PARTITION BY 也会复制。像第 4 步那样没有分区的表,请直接写出列来创建。- 常见错误:把第 6 步重新附加之后的名称写成分离时的名称(重新附加会得到新的块编号)。在第 8 步中调高
max_partitions_per_insert_block让它通过——这一步是要看它被拒绝。 - 官方文档:Table partitions · Choosing a partitioning key · Custom partitioning key · ALTER ... PARTITION · ALTER ... DELETE · EXPLAIN
创建月分区表
创建数据库 ptn 和表 ptn.sales。列依次为 ts DateTime, region LowCardinality(String), order_id UInt64, amount UInt32,引擎 MergeTree,PARTITION BY toYYYYMM(ts),ORDER BY (region, ts)。
PARTITION BY 写在 ORDER BY 前面或后面都可以。toYYYYMM(ts) 会返回 202607 这样的整数,这个值就是 partition_id。排序键中不需要放入分区键——在一个数据片段内,反正都是同一个月。
写入三个月的数据,并记录各分区的数据片段
运行一次 /opt/lab/fixtures/partition/sales.sql,写入 1,500,000 行,并用 OPTIMIZE TABLE ptn.sales FINAL 合并。把每个分区包含 partition_id、part(活动数据片段名称)、rows 的对象组成的 JSON 数组,保存到 /root/ch/partition/partitions.json。
即使只有一次 INSERT,只要有三个分区,数据片段也至少是三个。FINAL 会分别合并每个分区,所以合并之后,数据片段仍然有分区数那么多。把以 JSONEachRow 取出的各行用 jq -s . 合起来,就成了数组。
只过滤 8 月——Min-Max 裁剪
创建求 2026 年 8 月(ts >= '2026-08-01 00:00:00' AND ts < '2026-09-01 00:00:00')amount 之和的 /root/ch/partition/q_aug.sql,把给该 SELECT 加上 EXPLAIN indexes = 1 的输出保存到 /root/ch/partition/explain_aug.txt。把 Min-Max 阶段的 Parts: 남은/전체(占位符依次为剩余的数据片段数与总数)和这个查询的 rows_read(关闭查询条件缓存)以 parts_selected、parts_total、rows_read 写入 /root/ch/partition/prune.json。
每个数据片段都记录着分区键所用列(ts)的最小值和最大值,范围不重叠的数据片段不会被打开。Min-Max 之后的 Partition 阶段只会再查看剩下的数据片段。请把读取的行数与 8 月分区的行数比一比。
去掉分区,会多读多少
创建没有分区、列和 ORDER BY (region, ts) 相同的 ptn.sales_flat,把 ptn.sales 的行迁移过去,并用 OPTIMIZE ... FINAL 合并。创建以同样的 8 月条件求该表 amount 之和的 /root/ch/partition/q_flat.sql,并把 q_aug.sql 和 q_flat.sql 的 rows_read 以 partitioned_rows_read、flat_rows_read 写入 /root/ch/partition/compare.json。
CREATE TABLE ... AS ptn.sales 连 PARTITION BY 也会复制,所以要直接写出列来创建。即使没有分区,ts 也是排序键的第二列,而前面的列 region 只有五种取值——想一想上一个模块的 generic exclusion search 做了什么。
用两种方法删除同一个 7 月
创建 ptn.sales 的副本 ptn.s_drop、ptn.s_delete(CREATE TABLE ... AS ptn.sales、复制行、OPTIMIZE ... FINAL),并删除 7 月——ptn.s_drop 用 ALTER TABLE ptn.s_drop DROP PARTITION 202607,ptn.s_delete 用 ALTER TABLE ptn.s_delete DELETE WHERE toYYYYMM(ts) = 202607 SETTINGS mutations_sync = 1。把两张表在 system.mutations 中的行数,以及 s_delete 在 part_log 中的 MutatePart 事件数,以 drop_mutations、delete_mutations、delete_mutated_parts 写入 /root/ch/partition/dropdel.json。
DROP PARTITION 是把数据片段从表中摘掉,既不读行,也不写行。ALTER DELETE 会成为变形,把数据片段重写成新版本——请用 part_log 数一数,没有要删除的行的数据片段是否也变成了新版本。mutations_sync = 1 会让它等到变形结束。
分离再附加,名称会变
用与第 5 步相同的方法创建副本 ptn.s_detach,并用 ALTER TABLE ptn.s_detach DETACH PARTITION 202609 分离。确认 system.detached_parts 中可见的数据片段名称之后,用 ATTACH PARTITION 202609 附加回去,并把分离时的名称和重新附加之后的 202609 活动数据片段名称,以 detached_part、attached_part 写入 /root/ch/partition/detach.json。
DETACH 不会删除,而是把数据片段移到 detached/ 目录,在此期间,表会忘记那个数据片段(count() 会减少)。重新附加的数据片段会从表中得到新的块编号,所以名称中间的数字会不同。分离之后,请立刻把名称存进变量。
不经 INSERT,把一个月复制过来
用 CREATE TABLE ptn.sales_jul AS ptn.sales 创建一张结构相同的空表,并用 ALTER TABLE ptn.sales_jul ATTACH PARTITION 202607 FROM ptn.sales 把 7 月分区复制过来。ptn.sales 必须仍是 150 万行,不要对 ptn.sales_jul 做 INSERT。
ATTACH PARTITION ... FROM 是复制分区,既不会从源表删除,也不会从目标表删除。两张表的结构和分区键必须相同。评分器还会在 query_log 中查看是否有写入这张表的 INSERT——用 INSERT ... SELECT 填充的话,即使行相同也不合格。
分得太细的分区
创建列相同、PARTITION BY (toDate(ts), region)、ORDER BY ts 的 ptn.sales_fine,运行 INSERT INTO ptn.sales_fine SELECT * FROM ptn.sales,并把出现的错误(stderr)保存到 /root/ch/partition/toofine.txt。数一数 ptn.sales 中(日期, 地区)的组合有多少个,以 partitions_needed 写入 /root/ch/partition/fine.json。
一个 INSERT 块可以创建的分区数有上限(max_partitions_per_insert_block),超过的话,整个块都会被拒绝。三个月 × 五个地区是多少个呢?组合数用 uniqExact(toDate(ts), region) 来数。不要通过调高上限来让它通过。