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 命令必须人工执行而非脚本自动。自动化的边界这里需要格外克制:不可逆操作由人来按按钮,系统只负责检查与提示。
