分区 — 先是管理单位,其次才是过滤单位
一句话总结
PARTITION BY 会让行按分区键的值放进不同的数据片段。合并不会跨越分区,所以一个月的行始终只存在于那个月的数据片段里。这样一来,把整个月删除、分离、迁移,都以文件为单位完成。查询变快只是额外的收获,而且这份收获比想象的要小——大部分跳过是排序键完成的。
为什么需要它
日志、订单这类时间序列表,最终都会遇到“把旧的删掉”这个需求。正如上一个模块所见,数据片段不会改变。要删几行,就得把含有这些行的数据片段重新写一遍(变形,mutation),而为了删掉三个月前的数据就把整张表重写,是说不通的。如果行是按月分别放进数据片段的,情况就不同了。把 7 月的数据片段从表里摘掉就行了。
官方文档(Table partitions)把分区称为“主要是数据管理功能”,原因就在这里。同一份文档还指出,跨所有分区的查询,通常比没有分区的表更慢。为了把分区“当作索引”来用而把它分得很细,数据片段数量就会暴增,又回到上一个模块的 TOO_MANY_PARTS。
工作原理
INSERT 会为每个分区写一个数据片段。 一次写入三个月的 150 万行,得到的数据片段是 202607_1_1_0、202608_2_2_0,而 9 月的块被切成两个,所以有两个数据片段。名称前面的 202607 就是 partition_id。OPTIMIZE ... FINAL 会分别合并每个分区,数据片段变成三个——绝不会产生把 7 月和 8 月合并在一起的数据片段。
裁剪有两层。 每个数据片段都记录着分区键所用的列(ts)的最小值和最大值,所以用 8 月的范围过滤,EXPLAIN 中会是这样。
Min-Max
Keys: ts
Parts: 1/3
Granules: 62/183
Partition
Keys: toYYYYMM(ts)
Parts: 1/1
Min-Max 连打开都没打开,就把 7 月和 9 月的数据片段舍弃了。在留下的数据片段内部,前面模块讲过的排序键索引会再次选出颗粒。对于不能转换成分区键的条件(例如 toDayOfWeek(ts) = 1),是 Parts: 3/3——什么也舍弃不了。
但是去掉分区,效果也几乎一样。 在排序键同为 (region, ts)、只是没有分区的表上,同样的 8 月查询读取了 581,632 行(分区表是 505,454 行)。ts 是排序键的第二列,前面的列 region 只有五种取值,所以上一个模块的 generic exclusion search 几乎能把 8 月这个区间都找出来。如果同时按 region 和 8 月过滤,反而是没有分区的表读得更少(106,496 对 114,688)。分区带来的收益,只在边界颗粒的那么几个。
管理操作以数据片段为单位。 ALTER TABLE ... DROP PARTITION 202607 会把 7 月的数据片段从表中摘掉。system.mutations 里什么也不会留下,8 月和 9 月的数据片段连名称都保持原样。如果用 ALTER TABLE ... DELETE WHERE toYYYYMM(ts) = 202607 删除同样的行,会产生一个变形,part_log 里 MutatePart 出现了三次——连一行 7 月数据都没有的 8 月、9 月数据片段,也成了新版本(名称末尾为 _5)。变形本身将在最后一个模块深入讲解。
DETACH PARTITION 会把数据片段移到表的 detached/ 目录中,表就忘了它的存在(在 system.detached_parts 中可以看到)。用 ATTACH PARTITION 放回来,数据片段会得到新的块编号,202609_3_4_1 变回了 202609_5_5_0。ATTACH PARTITION ... FROM 다른표(占位符为另一张表)会从结构相同的表中复制分区——正如文档所说,既不会从源表也不会从目标表中删除,数据片段不经 INSERT 查询就原样过来。这是把一个月的数据分离出来放进归档表的常用方法。
分得太细就会被挡住。 PARTITION BY (toDate(ts), region) 三个月就有 460 个分区。把全部数据写进这张表,会因 Too many partitions for single INSERT block (more than 100) 错误(252),整个 INSERT 被拒绝,一行也没有写进去。上限是设置 max_partitions_per_insert_block(默认 100)。错误信息本身就写着“分区不是为了让 SELECT 变快”。
在现场相遇的样子
最常见的错误是“经常按日期过滤,所以按日期分区”。按天分区,一年就是 365 个,再混入用户、地区,就会有几千个。每个分区都有各自的数据片段,而合并不会跨越分区,于是小数据片段会无止境地堆积。文档(Choosing a partitioning key)要求选择基数低的分区键——通常按月就够了。日期范围过滤,则靠把时间列放进排序键来解决。
反过来,分区大放异彩的地方是保留周期。“删除超过 13 个月的数据”如果用 DELETE,每次变形都要把所有数据片段重写一遍,而如果是月分区,一行 DROP PARTITION 就结束了。把需要重新处理的月份摘下来(DETACH),再用另一张表把修正后的数据补回去(ATTACH ... FROM / REPLACE PARTITION),这样的运维也是同样的原理。
下一项实验要做什么
用月分区创建 ptn.sales,写入三个月的数据,并记录各分区的数据片段。在 8 月查询的 EXPLAIN 中读出 Min-Max 留下的数据片段数,并与没有分区的同排序键表比较 rows_read。用 DROP PARTITION 和 DELETE 删除同样的 7 月,比较变形的数量;把 9 月分离后再附加回去,观察名称的变化;再不经 INSERT 把 7 月复制到另一张表。最后确认(日期, 地区)分区表在 INSERT 时被拒绝。