3、Range分区实战:基于时间范围的分区设计(如按年/月/日分区),MySQL与PostgreSQL实现对比。

时间范围分区,是我在实际项目中用得最多的分区策略。说白了,就是把数据按时间切块——1月的数据放一个区,2月的数据放另一个区。为什么这么流行?因为大部分业务数据都有时间属性,而且查询也经常带着时间范围。

我记得刚入行时接手过一个日志系统,单表2亿条记录,按时间查一个月的数据要跑40秒。后来改成按月分区,同样的查询降到0.5秒。嗯,这就是Range分区的魅力。

3.1 核心概念:Range分区到底在做什么?

Range分区的逻辑很简单:根据列值的范围,把数据分配到不同的物理分区。每个分区就是一个独立的存储单元,可以单独管理。

举个例子,你有一张订单表,按月份分区:

  • 2024-01-01到2024-01-31的数据 → 分区p202401
  • 2024-02-01到2024-02-29的数据 → 分区p202402
  • 以此类推...

查询时,数据库会自动裁剪掉不相关的分区。你查1月份的数据,它只扫描p202401这一个分区,而不是全表。

核心收益:

  • 查询性能提升:只扫描相关分区,减少IO
  • 管理更灵活:可以单独归档、备份、压缩旧分区
  • 清理数据方便:直接DROP分区,比DELETE快几个数量级

3.2 MySQL实现:按年/月/日分区实战

MySQL从5.1开始支持分区。我个人习惯用RANGE COLUMNS,因为它支持更多数据类型,而且分区表达式更灵活。

3.2.1 按年分区

适合数据量增长稳定、查询跨度大的场景。比如历史归档表。

CREATE TABLE orders_yearly (
    order_id INT NOT NULL,
    order_date DATE NOT NULL,
    amount DECIMAL(10,2),
    PRIMARY KEY (order_id, order_date)
)
PARTITION BY RANGE COLUMNS(order_date) (
    PARTITION p2022 VALUES LESS THAN ('2023-01-01'),
    PARTITION p2023 VALUES LESS THAN ('2024-01-01'),
    PARTITION p2024 VALUES LESS THAN ('2025-01-01'),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

这里有个坑——主键必须包含分区列。MySQL要求分区列必须是主键或唯一键的一部分。我曾经在这个问题上卡了半天,后来才反应过来。

3.2.2 按月分区

这是最常见的粒度。适合月活数据几百万到几千万的系统。

CREATE TABLE orders_monthly (
    order_id INT NOT NULL,
    order_date DATE NOT NULL,
    amount DECIMAL(10,2),
    PRIMARY KEY (order_id, order_date)
)
PARTITION BY RANGE COLUMNS(order_date) (
    PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
    PARTITION p202402 VALUES LESS THAN ('2024-03-01'),
    PARTITION p202403 VALUES LESS THAN ('2024-04-01'),
    PARTITION p202404 VALUES LESS THAN ('2024-05-01'),
    PARTITION p202405 VALUES LESS THAN ('2024-06-01'),
    PARTITION p202406 VALUES LESS THAN ('2024-07-01'),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

我的习惯:每次创建分区时,多预留一个p_future分区。这样即使忘记提前加分区,数据也不会报错。等下次维护时再拆分。

3.2.3 按日分区

日分区适合高频写入、查询粒度细的场景。比如实时日志、交易流水。

CREATE TABLE logs_daily (
    log_id BIGINT NOT NULL,
    log_time DATETIME NOT NULL,
    message TEXT,
    PRIMARY KEY (log_id, log_time)
)
PARTITION BY RANGE COLUMNS(log_time) (
    PARTITION p20240101 VALUES LESS THAN ('2024-01-02 00:00:00'),
    PARTITION p20240102 VALUES LESS THAN ('2024-01-03 00:00:00'),
    PARTITION p20240103 VALUES LESS THAN ('2024-01-04 00:00:00'),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

注意:日分区数量会快速增长。一年就是365个分区。MySQL官方建议分区数不超过8192个,但实际超过1000个时,DDL操作就会变慢。我个人建议日分区只保留最近30天,旧数据归档到按月分区的历史表。

3.3 PostgreSQL实现:声明式分区更优雅

PostgreSQL从10开始支持声明式分区。说实话,我第一次用的时候觉得比MySQL顺手多了——不需要操心主键约束,分区裁剪也更智能。

3.3.1 按年分区

CREATE TABLE orders_yearly (
    order_id INT NOT NULL,
    order_date DATE NOT NULL,
    amount DECIMAL(10,2)
) PARTITION BY RANGE (order_date);

CREATE TABLE orders_yearly_2022 PARTITION OF orders_yearly
    FOR VALUES FROM ('2022-01-01') TO ('2023-01-01');

CREATE TABLE orders_yearly_2023 PARTITION OF orders_yearly
    FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

CREATE TABLE orders_yearly_2024 PARTITION OF orders_yearly
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

PostgreSQL的分区定义更直观——FROM ... TO ...,左闭右开。而且分区表本身不需要主键包含分区列,自由度更高。

3.3.2 按月分区

CREATE TABLE orders_monthly (
    order_id INT NOT NULL,
    order_date DATE NOT NULL,
    amount DECIMAL(10,2)
) PARTITION BY RANGE (order_date);

CREATE TABLE orders_monthly_202401 PARTITION OF orders_monthly
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE orders_monthly_202402 PARTITION OF orders_monthly
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

CREATE TABLE orders_monthly_202403 PARTITION OF orders_monthly
    FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');

3.3.3 按日分区

CREATE TABLE logs_daily (
    log_id BIGINT NOT NULL,
    log_time TIMESTAMP NOT NULL,
    message TEXT
) PARTITION BY RANGE (log_time);

CREATE TABLE logs_daily_20240101 PARTITION OF logs_daily
    FOR VALUES FROM ('2024-01-01') TO ('2024-01-02');

CREATE TABLE logs_daily_20240102 PARTITION OF logs_daily
    FOR VALUES FROM ('2024-01-02') TO ('2024-01-03');

注意:PostgreSQL的分区名不能重复。如果按月分区,建议命名格式为表名_年月,避免混淆。

3.4 MySQL vs PostgreSQL:Range分区核心差异对比

对比维度 MySQL PostgreSQL
分区语法 PARTITION BY RANGE COLUMNS PARTITION BY RANGE
主键约束 分区列必须在主键/唯一键中 无此限制
分区定义 VALUES LESS THAN FOR VALUES FROM ... TO ...
默认分区 支持MAXVALUE 不支持,需手动创建
分区裁剪 较成熟,但复杂表达式可能失效 更智能,支持多列分区裁剪
DDL灵活性 分区数多时DDL慢 分区数多时性能下降不明显
子分区 支持 支持(通过复合分区)

我个人更倾向PostgreSQL的分区设计,尤其是它的分区裁剪能力。但MySQL胜在生态成熟、运维工具多。选哪个,看你的团队技术栈。

3.5 避坑指南:我踩过的那些坑

做Range分区这么多年,我总结了几条血泪教训:

  • 分区列不要用函数包裹:比如PARTITION BY RANGE (YEAR(order_date)),MySQL会失效分区裁剪。直接用原始列。
  • 预留分区很重要:我曾经忘记给MySQL加MAXVALUE分区,结果数据插入时报错,线上事故。现在我的模板里永远有一个兜底分区。
  • 分区不是越多越好:分区数超过1000后,MySQL的DDL操作会变得很慢。PostgreSQL稍好,但也不建议超过2000个。
  • 注意时区问题:如果业务跨时区,分区列建议用UTC时间,避免夏令时导致数据错乱。

3.6 一张图看懂Range分区流程

下面这张图展示了Range分区的核心流程:数据写入时如何路由到正确的分区,查询时如何裁剪不相关的分区。

Range分区核心流程 数据写入 分区路由判断 分区 p202401 2024-01-01 ~ 2024-02-01 分区 p202402 2024-02-01 ~ 2024-03-01 分区 p202403 2024-03-01 ~ 2024-04-01 查询:WHERE order_date BETWEEN '2024-02-01' AND '2024-02-15' 只扫描 p202402 其他分区跳过 分区裁剪:只扫描匹配的分区 大幅减少IO,提升查询性能

流程很简单:数据写入时,数据库根据分区列的值判断应该进入哪个分区;查询时,数据库自动裁剪掉不匹配的分区,只扫描相关分区。这就是Range分区性能提升的核心原理。

3.7 小结

Range分区是时间序列数据的首选方案。MySQL和PostgreSQL的实现各有千秋——MySQL胜在生态成熟,PostgreSQL胜在灵活智能。我个人建议:如果团队以MySQL为主,就用MySQL的分区;如果追求更优雅的架构,PostgreSQL是更好的选择。