第3章:分区裁剪原理

分区裁剪,说白了就是数据库在执行查询时,只扫描必要的分区,跳过那些无关的分区。我刚开始接触这个特性时,觉得它就是个简单的“过滤”动作。后来在项目中踩过几次坑,才发现里面的门道不少。

举个例子。一张订单表按月份分了12个分区。你查1月份的数据,数据库如果还去扫2月到12月的分区,那就是傻扫描。分区裁剪就是让数据库变聪明——只扫1月那个分区。

核心定义:分区裁剪(Partition Pruning)是查询优化器在执行计划生成阶段,根据查询条件中的分区键,提前排除不需要访问的分区,从而减少 I/O 和 CPU 消耗的优化技术。

3.1 裁剪的决策过程

分区裁剪不是查询执行时才发生的。它发生在优化器生成执行计划的阶段。我习惯把这个过程拆成三步:

  1. 解析查询条件——优化器先看 WHERE 子句里有没有分区键的条件
  2. 计算分区范围——根据条件值,算出需要访问哪些分区
  3. 裁剪执行计划——在计划中只保留目标分区,去掉无关分区

你想想看,如果查询条件里没有分区键,那优化器也没办法裁剪。这就是为什么我总跟团队说:分区键一定要出现在 WHERE 条件里,否则分区等于白分。

个人经验:我曾经接手过一个慢查询优化项目。一张10亿行的日志表按天分区,但开发人员写查询时只用了 log_type 做过滤条件,没带日期。结果每次查询都扫全部分区,性能惨不忍睹。加上日期条件后,查询时间从30秒降到了0.5秒。

3.2 裁剪的两种模式

根据分区键条件的类型,裁剪分为两种模式:

模式 触发条件 示例 裁剪效果
静态裁剪 分区键条件为常量 WHERE dt = '2024-01-15' 精确匹配单个分区
动态裁剪 分区键条件为子查询或参数 WHERE dt IN (SELECT max_date FROM ...) 运行时确定分区范围

静态裁剪是最理想的。优化器在编译阶段就能确定分区范围,执行计划里直接写死。动态裁剪稍微复杂一点,需要等到执行时才能确定具体分区。不过,能裁剪总比不裁剪好。

注意:动态裁剪虽然有用,但有些数据库(比如早期版本的 MySQL)对子查询的裁剪支持不完善。我建议尽量用静态条件,或者用 JOIN 替代子查询。

3.3 执行计划中的分区裁剪标识

怎么知道数据库有没有做分区裁剪?看执行计划。不同数据库的标识方式不同,但思路是一样的。

PostgreSQL 示例

EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM orders 
WHERE order_date = '2024-01-15';

-- 输出片段
Append
  Subplans Removed: 11   ← 这就是裁剪标识
  ->  Seq Scan on orders_202401  
        Filter: (order_date = '2024-01-15'::date)

看到 Subplans Removed: 11 了吗?这说明原本有12个分区,优化器裁剪掉了11个,只留了1个。我个人习惯先看这个数字,如果它等于0,那就要警惕了——查询可能没走分区裁剪。

MySQL 示例

EXPLAIN PARTITIONS 
SELECT * FROM orders 
WHERE order_date = '2024-01-15';

-- 输出片段
+----+-------------+--------+------------+------+...
| id | select_type | table  | partitions | type |...
+----+-------------+--------+------------+------+...
|  1 | SIMPLE      | orders | p202401    | ref  |...
+----+-------------+--------+------------+------++

MySQL 的 partitions 列会显示实际访问的分区名。如果这里显示 p202401,说明只扫了1月分区。如果显示 p202401,p202402,...,p202412,那就是全扫了。

避坑指南:我曾经遇到一个案例,执行计划里显示 Subplans Removed: 0,但查询条件明明带了分区键。排查后发现是分区键的数据类型和条件值不匹配——分区键是 DATE 类型,条件传的是字符串,隐式转换导致索引失效,裁剪也没生效。所以,类型一致是分区裁剪的前提。

3.4 分区裁剪的决策流程图

下面这张图展示了优化器做分区裁剪的完整决策流程。我把它画出来,方便你理解。

分区裁剪决策流程图 分区裁剪决策流程 收到查询请求 WHERE 中是否包含分区键? (静态常量 / 子查询 / 参数) 否 全分区扫描 是 条件类型判断 常量 / 子查询 / 参数 常量 静态裁剪(编译期) 子查询/参数 动态裁剪(运行时) 生成裁剪后的执行计划 ⚠ 裁剪失败常见原因 类型不匹配 / 函数包裹分区键 隐式转换 / 无分区键条件

3.5 裁剪失效的常见场景

分区裁剪不是万能的。我总结了几种常见的失效场景,你写查询时要注意避开:

  • 分区键被函数包裹——比如 WHERE DATE(partition_col) = '2024-01-15',优化器无法识别
  • 隐式类型转换——分区键是 INT,条件传字符串 '123'
  • 分区键不在 WHERE 中——这个最基础,但也最容易犯
  • 使用 OR 连接非分区键条件——比如 WHERE partition_key = 'A' OR other_col = 'B'
  • 子查询嵌套过深——某些数据库对多层子查询的裁剪支持有限

重要提醒:我曾经在生产环境见过一个案例,开发人员在分区键上用了 TO_CHAR(partition_col, 'YYYY-MM-DD') 做格式化,然后跟字符串比较。结果分区裁剪完全失效,查询跑了20分钟。改成 partition_col = TO_DATE('2024-01-15', 'YYYY-MM-DD') 后,查询秒回。

3.6 如何验证裁剪是否生效

我个人的工作习惯是,每次写完分区表查询,都会做三步验证:

  1. 看执行计划——检查是否有 Subplans Removed 或 partitions 列
  2. 看扫描行数——如果扫描行数远小于总行数,说明裁剪生效
  3. 看执行时间——裁剪后的查询应该比全表扫描快一个数量级

这三步走完,基本就能确认分区裁剪有没有正常工作。如果发现裁剪没生效,优先检查分区键的条件写法。

小技巧:在 PostgreSQL 中,我经常用 EXPLAIN (ANALYZE, BUFFERS, SUMMARY) 来查看分区裁剪的详细信息。如果 Buffers: shared hit= 的值很小,说明只访问了少量数据块,裁剪大概率是生效的。