2.4 分区表:按时间与大表治理

订单中心跑了一年后,t_order 突破了两亿行。客服查一个三个月前的订单要八秒,对账批处理从两小时涨到七小时。加索引已经没用了,因为索引也跟着表一起膨胀。分库分表动静太大,我们选了一条中间路线:MySQL 分区表。

场景:大表的四种压力

数据量上来后压力来自四个方向。点查变慢:B+ 树层级从三层变四层,回表次数增加。DDL 变难:加一个索引要锁表一小时,大促前不敢做。备份变重:单库快照体积七十多 GB,恢复时间不可接受。归档没出路:按业务来说一年前的数据基本没人查,但删掉又不合规。

分区表恰好对症这四点:数据按分区键物理隔离、查询只扫命中的分区、归档可以直接 drop 分区、DDL 按分区粒度进行。

方案:按月 RANGE 分区

订单数据的访问有强烈的时间局部性,99% 的查询落在最近三个月。我们选 RANGE 按月分区:

分区策略 适用场景 我们的判断
RANGE 按月 时间局部性强 采用
HASH 按用户 按用户散列均衡 需要先分库,放弃
LIST 按渠道 渠道维度隔离 渠道少,不均衡
KEY 按订单号 均衡但无局部性 点查可以,批处理差

DDL 如下,关键字段就是分区表达式与每个分区的范围:

ALTER TABLE t_order PARTITION BY RANGE COLUMNS(create_time) (
    PARTITION p202601 VALUES LESS THAN ('2026-02-01'),
    PARTITION p202602 VALUES LESS THAN ('2026-03-01'),
    PARTITION p202603 VALUES LESS THAN ('2026-04-01'),
    PARTITION pmax VALUES LESS THAN (MAXVALUE)
);

注意 pmax 这个兜底分区很重要。没有它,超出最大范围的写入会直接报错。我们的流程是每月初用定时任务提前建下个月的分区,顺手把最早的分区迁出到归档表。

配置:分区维护脚本

每月一次的维护动作用存储过程或者脚本执行,下面是核心步骤的可读版本:

-- 1. 提前建下月分区
ALTER TABLE t_order ADD PARTITION (
    PARTITION p202605 VALUES LESS THAN ('2026-06-01')
);
-- 2. 归档 18 个月前的分区
ALTER TABLE t_order DROP PARTITION p202409;

drop 分区是秒级操作,比 delete 快几个数量级,而且不产生大量 binlog。归档前先确认该分区没有未完结订单,这一步由前置检查脚本保证。

踩坑:分区表的四个坑

第一个是分区键必须进主键(联合索引)。MySQL 要求分区键属于所有唯一索引的一部分,所以主键从 id 改成 (id, create_time)。这个改动让一些 ORM 框架的 save 方法行为变化,MyBatis 的 selectKey 需要显式带上分区键。

第二个是查询不带分区键会扫所有分区。一条没有 create_time 条件的 SQL,优化器只能扫全部分区,比不分还慢。我们的处理方式是代码层强制:所有 t_order 查询必须带时间范围,用 MyBatis 拦截器在 SQL 上强制追加 create_time 条件。类似的大表查询约束在 MySQL 分库分表实战 里有更系统的做法。

第三个是跨分区 join 的性能陷阱。订单与明细按时间分布在不同分区,join 时如果两边的分区边界对齐,可以分区级别裁剪;如果不对齐(例如明细按季度分区),join 全部打碎。我们的明细表也按月分区,与主表对齐。

第四个是 1000 分区限制。MySQL 单表分区数上限 1024,按月分区能撑七年,但按天就只有三年。月粒度对我们的数据量是甜点:单分区两千万行以内,查询够快、归档够粗。

另外云数据库 MySQL 的分区表维护操作可以做成定时任务,配合监控分区的行数增长,提前发现哪个分区要爆。这些运维操作建议在低峰期执行,虽然 add partition 是在线操作,drop 前的归档查询很重。

进阶实战:归档冷热分离

分区表解决了一部分问题,另一部分要靠归档。我们的数据冷热非常明显:最近三个月的订单承载了 96% 的查询,一年前的订单只有客服偶尔翻。归档策略经过三次调整才定型。

归档阶段 策略 查询入口
0 至 3 月 热分区 业务库直查
3 至 12 月 分区迁出 + 归档表 归档库
12 月以上 冷存储导出 客服后台

归档表与热表结构完全一致,迁移走「create table as select 加 rename」的影子表方式,全程不锁线上表。归档库配小规格实例,成本很低,只有客服主动搜索时才被访问。冷存储导出的文件保留在对象存储里,满足合规审计要求。

归档窗口的选择也有讲究。我们定在每月 5 号凌晨,原因有二:月底是对账高峰,机器吃紧;5 号是发版窗口的低峰期。运维操作与业务高峰错开是基本纪律。

另一个实战经验是分区数量与查询计划的统计信息。某个分区的统计信息陈旧,优化器选错执行计划,查询突然变慢。云数据库 MySQL 的统计信息可以手动收集,分区数多的时候按分区收集比全表收集便宜,我们把收集任务与分区维护放在同一天执行。

速查清单

分区表维护的月度流程:

步骤 动作 时机
建分区 提前建下月分区 每月 1 号
看容量 分区行数监控 每日
归档 迁出 18 月前分区 每月 5 号
收集统计 分区级收集 每月 1 号

小结

分区表是大表治理的中间路线:物理隔离、查询裁剪、归档 drop。代价是分区键约束和查询必须带时间范围。数据量到了亿级、查询以时间维度为主,分区表是性价比最高的方案。下一章进入库存的深水区:一致性。

常见误解:分区表的三个陷阱

第一个陷阱是「分区表等于分库分表」。同事一度以为上了分区表就获得了水平扩展能力,其实分区表仍是单机存储:总数据量受单实例磁盘限制、写入压力仍在单实例上。它解决的是大表治理与归档,不是容量瓶颈。真到了单实例写不动,就必须走分库分表。两者的区别在 MySQL 分库分表实战 里有详细对比。

陷阱 真相
分区等于分库 分区仍在单实例
查询自动快 不带分区键更慢
分区免维护 需定期建分区
分区即归档 归档还需导出冷存

第二个陷阱是「分区表自动提速」。前面说过,不带分区键的查询扫所有分区,比普通表还慢。提速的前提是查询条件命中分区键。我们的所有订单查询都强制带 create_time 范围,用 MyBatis 拦截器做 SQL 改写,从机制上杜绝裸查。

第三个陷阱是「分区表免维护」。分区不是建一次就完事:每月要建新分区、老分区要归档、统计信息要收集,这些都是运维任务。我们把整套维护做成了定时任务加日报,分区维护失败会告警。每季度还做一次「分区体检」,查看各分区的行数与空间占用,异常增长提前预警。分区表是把大表的运维成本从「一次性大手术」变成「每月小保养」,而这个保养本身需要纪律。

团队协作:分区维护的值班制度

分区维护最终落在值班同学的日常工作里。每月的维护清单被做成了工单:建分区、体检、归档、收集统计,每一步有勾选项。完成后的工单附维护截图归档,新人跟着旧工单就能学会整套流程。把运维知识沉淀进工单而不是存在某个人脑子里,是团队能持续运转的前提。

维护动作 执行人 验收标准
建下月分区 值班同学 新分区在表中存在
分区体检 值班同学 无异常增长分区
归档冷分区 DBA 归档表行数对齐
统计收集 DBA 执行计划无回退

有一次归档操作前检查脚本漏跑了,直接 drop 掉一个还有活跃订单的分区,花了三小时从备份恢复。复盘后归档之前必须跑「活跃检查」这步,并且 drop 命令必须人工执行而非脚本自动。自动化的边界这里需要格外克制:不可逆操作由人来按按钮,系统只负责检查与提示。

延伸阅读