3、分区裁剪原理:查询优化器如何识别分区
分区裁剪(Partition Pruning)这个词,说白了就是让数据库只扫描“有用”的分区,跳过那些跟查询条件无关的分区。我刚开始接触分区表时,以为建了分区就万事大吉,结果发现查询还是全分区扫描——嗯,那时候我才意识到,分区裁剪不是自动生效的,它需要优化器“看懂”你的查询条件。
这一节,我们就把分区裁剪的底层逻辑拆开来看。优化器到底怎么识别分区?执行计划里怎么判断裁剪生效了?谓词下推又跟分区裁剪有什么关系?
核心一句话:分区裁剪的本质是“过滤条件下推 + 元数据过滤”。优化器把 WHERE 条件里的分区键值提前算出来,然后去元数据里匹配分区边界,只保留命中的分区。
3.1 优化器识别分区的三个步骤
我个人习惯把优化器识别分区的过程分成三步:解析分区键 → 计算分区范围 → 裁剪分区列表。每一步都有坑,我们一个个看。
- 解析分区键:优化器先看 WHERE 条件里有没有分区键。比如表按
dt分区,条件里写了dt = '2024-01-15',那优化器就知道“这个条件能用来裁剪”。 - 计算分区范围:如果条件里有
BETWEEN、IN、>=等范围操作,优化器会算出具体落在哪些分区编号上。比如dt BETWEEN '2024-01-01' AND '2024-01-31',优化器会算出这 31 天对应哪几个分区。 - 裁剪分区列表:最后,优化器把不包含目标数据的分区从扫描列表中移除。这一步发生在执行计划生成阶段,不是运行时。
我在项目中遇到过一种情况:条件里用了函数,比如 DATE(dt) = '2024-01-15',但分区键是 dt 本身。优化器没法把 DATE(dt) 反向推导回 dt 的范围,结果分区裁剪失效了。这个坑后面会细讲。
3.2 分区裁剪的执行计划分析
怎么判断分区裁剪有没有生效?看执行计划。不同数据库的展示方式不同,但核心标志是一样的:扫描的分区数量 < 总分区数量。
以 PostgreSQL 为例,执行计划里会显示 Subplans Removed: N。比如:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE order_date = '2024-01-15';
输出可能长这样:
Append
Subplans Removed: 11
-> Seq Scan on orders_20240115 (cost=0.00..35.50 rows=10 width=100)
Filter: (order_date = '2024-01-15'::date)
看到 Subplans Removed: 11 了吗?这说明表有 12 个分区,优化器只保留了 1 个,裁剪掉了 11 个。这就是分区裁剪生效的标志。
在 MySQL 里,用 EXPLAIN 看 partitions 列:
EXPLAIN SELECT * FROM orders
WHERE order_date = '2024-01-15';
输出里 partitions 列会显示 p20240115,而不是 p_all 或 p202401,p202402,...。如果看到多个分区,说明裁剪不彻底。
小技巧:我建议你在测试环境里先跑一次不带 WHERE 条件的执行计划,再跑一次带条件的,对比 partitions 列的变化。这样能直观看到裁剪效果。
3.3 谓词下推与分区裁剪的关系
谓词下推(Predicate Pushdown)和分区裁剪是两回事,但经常一起出现。我简单解释一下:
- 分区裁剪:在表扫描之前,根据分区键条件跳过整个分区。这是“分区级别”的过滤。
- 谓词下推:把 WHERE 条件推到数据源更近的地方执行。比如在分区表上,谓词下推会把条件推到每个分区的扫描节点上,让每个分区只返回符合条件的数据。
两者配合的效果是:分区裁剪先砍掉不需要的分区,谓词下推再在每个剩余分区里做精细过滤。举个例子:
SELECT * FROM orders
WHERE order_date = '2024-01-15'
AND status = 'PAID';
分区裁剪先只扫描 p20240115 这个分区。然后谓词下推把 status = 'PAID' 推到该分区的扫描节点上,只返回已支付的订单。如果没有谓词下推,即使只扫一个分区,也会把该分区所有数据都读出来再过滤——那性能就差远了。
我曾经踩过的坑:有一次我在分区键上用了 YEAR(order_date) = 2024,结果分区裁剪完全失效。因为 YEAR() 函数把分区键包住了,优化器没法反向推导出分区范围。后来改成 order_date BETWEEN '2024-01-01' AND '2024-12-31',裁剪就正常了。记住:分区键上不要套函数,这是分区裁剪的第一条铁律。
3.4 分区裁剪的完整流程
下面这张图展示了从 SQL 解析到分区裁剪完成的完整流程。我把它画成了 SVG,方便你理解每个环节的输入和输出。
3.5 常见分区裁剪失效场景
根据我的经验,分区裁剪失效的场景其实就那么几种。我列了个表格,方便你对照排查:
| 失效场景 | 原因 | 正确写法 |
|---|---|---|
| 分区键套函数 | YEAR(dt) = 2024 无法反向推导 |
dt BETWEEN '2024-01-01' AND '2024-12-31' |
| 隐式类型转换 | dt = '20240115' 与分区键类型不匹配 |
dt = '2024-01-15' 或显式转换 |
| OR 条件包含非分区键 | dt = '2024-01-15' OR status = 'PAID' 导致全分区扫描 |
拆成 UNION 或用 IN 代替 OR |
| 子查询无法提前计算 | dt = (SELECT MAX(dt) FROM orders) 运行时才确定值 |
先查出值再拼入条件 |
| 分区键在 JOIN 条件中 | JOIN 条件里的分区键不会被用于裁剪 | 在 WHERE 里显式加上分区键条件 |
避坑指南:我曾经在一个生产环境里排查慢查询,发现一张 200 个分区的表每次查询都扫全表。最后定位到是 OR 条件的问题——dt = '2024-01-15' OR status = 'PAID' 导致优化器认为必须扫描所有分区才能判断 status 条件。改成 dt = '2024-01-15' AND status = 'PAID' 后,裁剪立刻生效,查询从 12 秒降到 0.3 秒。
3.6 小结
分区裁剪的原理其实不复杂:优化器通过元数据匹配分区边界,提前排除无关分区。但实际应用中,分区键是否裸露、条件是否可计算、执行计划是否显示裁剪信息,这三个点决定了裁剪能否生效。
我个人建议你在建分区表时,先想清楚查询模式。如果查询条件里经常用 BETWEEN 或 IN,那分区裁剪大概率能生效。如果条件里总套函数或隐式转换,那就要考虑改表结构或改写 SQL 了。