1. 为什么要做动态分区管理:传统分区方案的痛点
先还原一下我最初遇到这个需求的场景。公司有一套订单流水系统,订单表每天新增几十万条数据,一个月就是千万级。刚开始用单表硬扛,不到三个月查询性能明显下滑,尤其是涉及时间范围的统计报表,全表扫描跑一次要几分钟。于是自然想到分区,按天做RANGE分区,把数据切到不同的物理分区里。
分区做完之后,前两周确实爽,查询快了一个量级。但很快问题就来了:分区是写死的,我需要每个月手动去执行 ALTER TABLE 添加新分区,还要记得删掉过期分区。有一次赶上出差,忘了在月末加分区,结果凌晨的定时任务直接写入报错——"Table has no partition for value ..."。大半夜被电话叫起来处理,那次经历之后我下定决心要把分区管理彻底自动化。
手动管理分区的痛点,总结起来无非这几点:
- 新增分区靠人工:RANGE分区必须提前建好,一旦数据超过最后一个分区边界,写入直接失败,而且这个报错不是在测试环境能提前发现的,总是出现在线上最忙的时候。
- 过期分区不清理:不删旧分区,磁盘空间被历史数据越占越多;想删的时候又得手动算好边界,一个疏忽删错了分区,数据恢复基本不可能。
- 操作窗口尴尬:ALTER TABLE 在数据量大的表上执行,即使是添加分区这种操作,也会对线上产生一定压力。手动操作意味着必须找业务低峰期,深夜加班就成了常态。
- 分区间隔不可控:业务量增长快的时候,按天分区可能太碎,分区数量暴涨;增长慢的时候,按天分区又浪费。没有动态调整机制,分区策略就是死板的。
“动态分区管理”要解决的就是这些问题:让分区的新增、删除、合并、归档都按既定策略自动执行,同时保证执行过程平稳、可回滚、可监控。这篇文章把我实际落地的一套方案完整拆解出来,包含存储过程怎么写、事件调度怎么配、DDL 怎么优化、压测数据长什么样,以及我在生产环境踩过的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分区表设计的前置决策:分区键、分区类型与分区粒度
不要一上来就写自动化脚本。分区管理自动化的前提,是分区表本身设计合理。设计不对,后面所有自动化都是在错误的地基上盖楼。
2.1 分区键和分区类型的选择逻辑
分区键的选择直接决定了查询能否走分区裁剪(partition pruning)。我见过不少表,分区是建了,但查询条件里根本不带分区键,结果每一条 SQL 依然扫全部分区,性能反而更差。
选择分区键,核心就一条原则:业务查询中高频、稳定出现的过滤字段。
订单流水这种场景,绝大多数查询都带时间范围,比如“查某天某用户的订单”“查某个时间段的汇总”,所以 created_time 作为分区键是最自然的。少数场景下,如果查询经常按某个业务维度(比如按租户ID、按区域ID)过滤,且这个维度本身有明确边界,也可以考虑 HASH 或 KEY 分区。但要注意,时间字段做 RANGE 分区是运维上最可控的——因为分区边界是时间,可以按规律生成。
分区类型的选择,我用一张表说明常见场景的适用性:
| 分区类型 | 适用场景 | 典型分区键 | 运维可控性 | 注意事项 |
|---|---|---|---|---|
| RANGE | 时间范围、数值范围 | created_time、id | 高,边界规则明确,易自动生成 | 新数据超出边界会写入失败,所以自动化必须保证“提前建” |
| LIST | 枚举值有限且固定 | status、region_id | 中,新增枚举需手动加分区 | 适合维度相对固定的数据 |
| HASH | 数据均匀分布、无自然范围 | user_id、order_id | 中,分区数固定后可动态增减 | 无法直接删除某个分区中的数据,只能 TRUNCATE PARTITION |
| KEY | 类似 HASH,由 MySQL 内部哈希 | 字符串字段 | 中,同 HASH | 依赖 MySQL 内部实现,不便精确控制分布 |
| RANGE COLUMNS | 多列范围分区 | (created_time, tenant_id) | 高,多维度裁剪 | 边界值定义稍复杂,自动化脚本要处理复合边界 |
我自己主力使用的就是 RANGE COLUMNS(按天或按月,单列或加租户维度),因为订单流水统计场景特别吃范围裁剪,而且分区边界可以写成配置,让脚本按规则推算。
2.2 分区粒度怎么定:天、周还是月
分区粒度是自动化策略里最关键的一步。粒度太小,分区数量膨胀,元数据管理开销增大;粒度太大,单个分区数据量过大,分区裁剪的优势不明显。
经验公式是这样的:
单个分区数据量控制在 500 万到 2000 万行之间是比较合理的区间。低于 500 万说明粒度太小,高于 2000 万说明粒度太大。
以我负责的订单流水表为例,每天新增约 60 万行,那么按天分区单分区 60 万行,略偏小,但结合保留周期(保留 90 天)来看,线上分区总数 90 个左右,MySQL 管理起来完全没问题。如果日增 500 万行,按天分区就是 500 万行一个分区,也还可以;但如果日增 2000 万行以上,按天分区就太碎了,更适合按周甚至按月分区。
还有一个判断维度是查询模式。业务方经常查近 7 天、近 30 天数据,按天分区时 MySQL 可以精准裁剪 7 个或 30 个分区;按月分区则可能查一个自然月要跨 2 个分区(遇到月初月末边界),裁剪效果会略差。反过来,如果业务经常查整月汇总,按月分区反而更优。
我最终选择的方案是:核心大表按天分区 + 90 天保留周期 + 提前创建未来 30 天分区 + 每天自动清理 90 天前的分区。这个组合可以覆盖绝大多数流水型业务。
2.3 分区边界规划:未来分区、当前分区和历史分区的三段式管理
分区管理自动化,本质上是把分区集合划分为三段:
- 未来分区:预创建。脚本按当前日期 + 预创建天数生成分区边界,确保任何时刻都有充足的分区容纳新数据。
- 当前分区:正在写入数据的分区,即今天对应的分区。这个分区不要做任何变更操作,避免锁竞争。
- 历史分区:超过保留周期的分区,由归档或删除任务处理。
这种三段式规划的好处是清晰。自动化脚本只需要维护一个配置表,记录“预创建天数”和“保留天数”,其余全部由存储过程计算。
3. 自动化落地:存储过程 + 事件调度器的完整实现
自动化方案我采用 MySQL 自身的存储过程 + Event Scheduler,而不是外部脚本。核心原因是:不依赖额外的语言环境和调度器,数据库自带的机制最可靠,而且便于在 MySQL 实例内部统一管理。
3.1 自动化分区管理配置表
先建一张配置表,用来存每个分区表的管理参数。这是整个自动化体系的元数据中心。
sql复制CREATE TABLE partition_manage_config (
id INT PRIMARY KEY AUTO_INCREMENT,
table_schema VARCHAR(64) NOT NULL COMMENT '库名',
table_name VARCHAR(64) NOT NULL COMMENT '表名',
partition_column VARCHAR(64) NOT NULL COMMENT '分区字段',
interval_days INT NOT NULL DEFAULT 1 COMMENT '分区间隔(天)',
precreate_days INT NOT NULL DEFAULT 30 COMMENT '预创建未来分区天数',
retain_days INT NOT NULL DEFAULT 90 COMMENT '保留历史分区天数',
last_exec_time DATETIME DEFAULT NULL COMMENT '上次执行时间',
exec_status VARCHAR(20) DEFAULT 'success' COMMENT '上次执行状态',
exec_msg VARCHAR(500) DEFAULT NULL COMMENT '执行信息',
enabled TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_schema_table (table_schema, table_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分区管理配置表';
这张表让每个分区表的自动化参数一目了然,也方便后续做扩展:可以加 last_exec_time 做执行监控,加 exec_msg 记录错误信息。
3.2 动态生成分区 DDL 的存储过程
接下来是核心的存储过程。它做的事情是:根据配置表里的参数,计算缺失的未来分区,逐个生成并执行 ALTER TABLE ADD PARTITION。
sql复制DELIMITER $$
CREATE PROCEDURE sp_auto_add_partition()
BEGIN
DECLARE v_schema VARCHAR(64);
DECLARE v_table VARCHAR(64);
DECLARE v_partition_column VARCHAR(64);
DECLARE v_interval_days INT;
DECLARE v_precreate_days INT;
DECLARE v_retain_days INT;
DECLARE v_max_partition_name VARCHAR(64);
DECLARE v_max_partition_value DATETIME;
DECLARE v_target_date DATETIME;
DECLARE v_partition_name VARCHAR(64);
DECLARE v_partition_value_date DATETIME;
DECLARE v_sql TEXT;
DECLARE v_done INT DEFAULT 0;
DECLARE v_cur_date DATETIME DEFAULT CURDATE();
DECLARE v_end_date DATETIME;
DECLARE v_exec_msg VARCHAR(500);
-- 游标遍历所有启用的配置
DECLARE cur_config CURSOR FOR
SELECT table_schema, table_name, partition_column, interval_days, precreate_days, retain_days
FROM pa
