3. 范围分区(Range Partitioning):原理、适用场景与典型SQL示例

范围分区,说白了就是按数据值的区间来分桶。你给每个分区划一个范围,比如「1月到3月的数据放这个区,4月到6月放那个区」。这是我最常用的一种分区策略,没有之一。

为什么?因为大部分业务数据都有天然的时间属性。订单、日志、交易流水……这些东西你想想看,查询的时候几乎都是按时间范围来查的。范围分区正好对上了这个模式。

3.1 核心原理

范围分区的逻辑其实很简单:

  • 你选一个分区键(通常是日期、ID、数值)
  • 给每个分区定义一个 VALUES LESS THAN 的上界
  • 数据插入时,数据库根据分区键的值,找到第一个上界大于该值的分区,放进去

举个例子。假设我有张订单表,按月份分区:

CREATE TABLE orders (
    order_id    BIGINT,
    order_date  DATE,
    customer_id INT,
    amount      DECIMAL(10,2)
)
PARTITION BY RANGE (order_date) (
    PARTITION p_2024_01 VALUES LESS THAN ('2024-02-01'),
    PARTITION p_2024_02 VALUES LESS THAN ('2024-03-01'),
    PARTITION p_2024_03 VALUES LESS THAN ('2024-04-01'),
    PARTITION p_future   VALUES LESS THAN (MAXVALUE)
);

注意最后那个 MAXVALUE 分区。我习惯叫它「兜底分区」。没有它,插入一个超出定义范围的数据就会报错。嗯,这里要注意:兜底分区一定要加,否则生产环境会出事故。

关键点:范围分区的核心是「有序」和「连续」。每个分区的范围是连续的,分区之间按顺序排列。这决定了它的优势和局限。

3.2 适用场景

我个人经验里,范围分区最适合下面这几类场景:

场景 为什么适合 我踩过的坑
时间序列数据 查询天然按时间范围,分区裁剪效果好 分区粒度太细(按天),管理成本飙升
数据归档 可以直接 TRUNCATE 旧分区,比 DELETE 快几个数量级 归档前忘了检查外键引用,导致失败
有序ID范围查询 比如按用户ID区间分片,热点可控 ID分布不均匀,导致数据倾斜
报表与OLAP 扫描特定时间范围时,只读相关分区 跨分区聚合时性能反而下降

我曾经接手过一个系统,订单表有3亿行数据,查询慢得要命。一看,没分区。我按月份做了范围分区后,同样的查询从30秒降到了2秒。说白了,就是分区裁剪起了作用——数据库只扫描一个分区,而不是全表。

3.3 典型SQL示例

3.3.1 创建分区表

-- MySQL 语法
CREATE TABLE sales (
    sale_id     INT,
    sale_date   DATE,
    product_id  INT,
    quantity    INT,
    price       DECIMAL(10,2)
)
PARTITION BY RANGE (YEAR(sale_date)) (
    PARTITION p_2022 VALUES LESS THAN (2023),
    PARTITION p_2023 VALUES LESS THAN (2024),
    PARTITION p_2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN (MAXVALUE)
);

-- PostgreSQL 语法(使用表继承或声明式分区)
CREATE TABLE sales (
    sale_id     INT,
    sale_date   DATE,
    product_id  INT,
    quantity    INT,
    price       DECIMAL(10,2)
) PARTITION BY RANGE (sale_date);

CREATE TABLE sales_2022 PARTITION OF sales
    FOR VALUES FROM ('2022-01-01') TO ('2023-01-01');
CREATE TABLE sales_2023 PARTITION OF sales
    FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');
CREATE TABLE sales_2024 PARTITION OF sales
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

3.3.2 查询与分区裁剪

-- 这条查询只会扫描 p_2024 分区
SELECT SUM(amount) 
FROM orders 
WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31';

-- 用 EXPLAIN 可以验证分区裁剪是否生效
EXPLAIN SELECT * FROM orders WHERE order_date = '2024-02-15';
-- 输出应该只显示 p_2024_02 分区被访问

小技巧:我每次建完分区表,第一件事就是跑 EXPLAIN 确认分区裁剪生效。别信直觉,信执行计划。

3.3.3 分区管理操作

-- 添加新分区(MySQL)
ALTER TABLE orders ADD PARTITION (
    PARTITION p_2024_04 VALUES LESS THAN ('2024-05-01')
);

-- 删除旧分区(比 DELETE 快得多)
ALTER TABLE orders DROP PARTITION p_2022_01;

-- 拆分分区(MySQL 8.0+)
ALTER TABLE orders REORGANIZE PARTITION p_2024_01 INTO (
    PARTITION p_2024_01_early VALUES LESS THAN ('2024-01-16'),
    PARTITION p_2024_01_late  VALUES LESS THAN ('2024-02-01')
);

-- 合并分区
ALTER TABLE orders REORGANIZE PARTITION p_2024_01_early, p_2024_01_late INTO (
    PARTITION p_2024_01 VALUES LESS THAN ('2024-02-01')
);

警告:我曾经在生产环境直接 DROP 分区,结果发现那个分区还有未归档的数据。从那以后,我定了个规矩:DROP 分区前,先查 COUNT(*),再备份,最后才删。别嫌麻烦,数据丢了更麻烦。

3.4 范围分区的优缺点

优势 劣势
分区裁剪效率极高,范围查询快 数据可能倾斜——某个分区数据量远大于其他分区
分区管理方便,可以滚动删除旧数据 分区键选择不当会导致写入热点
支持分区交换(快速加载数据) 跨分区查询性能不如单分区
对时间序列数据天然友好 分区数量过多时,元数据管理开销大

你想想看,如果分区键选的是「自增ID」,新数据全往最后一个分区写,前面的分区几乎没写入。这就是典型的热点问题。我建议:能用时间就用时间,别用单调递增的数值做范围分区键。

3.5 范围分区与其他分区的对比

特性 范围分区 列表分区 哈希分区
分区依据 连续区间 离散值列表 哈希函数
数据分布 可能不均匀 由业务决定 相对均匀
范围查询 优秀 一般 差
等值查询 一般 优秀 优秀
管理复杂度 低 低 中
典型场景 时间序列、日志 地区、状态枚举 均匀分布、高并发写入

3.6 范围分区核心逻辑图

下面这张图展示了范围分区的工作原理。数据根据分区键的值,被路由到对应的分区中。

范围分区核心逻辑 输入数据 分区键: order_date = '2024-02-15' 分区路由:找到第一个上界 > '2024-02-15' 的分区 分区 p_2024_01 范围: < '2024-02-01' 状态: ❌ 不匹配 分区 p_2024_02 范围: < '2024-03-01' 状态: ✅ 命中! 分区 p_2024_03 范围: < '2024-04-01' 状态: ❌ 不匹配 数据写入该分区

3.7 避坑指南

做了这么多年数据库,范围分区上我栽过的跟头不少。总结几条血的教训:

  • 分区粒度别太细。我曾经按天分区,一年365个分区,管理起来想哭。查询时还要跨几十个分区。建议按月或按季度。
  • 注意MAXVALUE分区。不加的话,新数据超出范围直接报错。加了的话,要定期拆分,否则兜底分区越来越大,失去分区意义。
  • 分区键别用NULL。NULL值在范围分区里怎么处理?不同数据库行为不同。MySQL会把NULL当作最小值,放进第一个分区。我建议:分区键加NOT NULL约束,省得踩坑。
  • 跨分区事务要注意。更新数据导致分区键变化时,数据会跨分区移动。这在某些数据库里可能引发性能问题甚至锁等待。

我的建议:范围分区是入门最简单、效果最明显的分区策略。如果你刚开始做分区设计,从范围分区开始准没错。选好分区键,控制好分区数量,大部分场景都能搞定。