数据清理是数据库运维中绕不开的话题。订单表、日志表这类按时间不断增长的业务表,如果不定期清理过期数据,表体积会越来越庞大,查询和写入性能都会持续下降。很多同学的第一反应是写一条DELETE FROM log WHERE create_time < '某个日期'定时执行,但数据量一大,这条语句就会变成性能杀手:执行时间长、锁范围大、undo日志暴涨,稍有不慎还会把主从延迟拉爆。本文就来聊聊如何用Range分区来解决这个问题。

传统DELETE方案到底慢在哪
假设有一张操作日志表,每天新增千万级数据,保留策略是只留最近90天。用DELETE按时间删除时,MySQL需要逐行定位、逐行标记删除,被删除的行并不会立即释放磁盘空间,而是形成空洞等待后续复用。删除三千万行数据,意味着三千万次的行级操作,事务日志量可能达到几十GB,期间主从复制延迟会不断累积,业务高峰期甚至会造成连接堆积。
另一个容易被忽视的问题是锁与MVCC。一个大的DELETE事务会持有大量的行锁和间隙锁,同时为了维护一致性读,undo链会一直拉长,其他查询需要沿着undo链回溯版本,导致普通SELECT也变慢。即使把DELETE拆成小批量循环执行,本质上仍然是逐行删除,只是把压力摊薄了,清理一次数据可能要跑几个小时,维护窗口根本不够用。
总结一下DELETE方案的三个痛点:一是逐行删除效率低,二是空间无法即时回收,三是事务日志压力大。这些问题的根源在于DELETE是逻辑层面的行级操作,而如果我们换一个思路,把数据按时间物理隔离,删除时直接丢弃整块存储空间,效率就会完全不同,这正是Range分区的价值所在。
Range分区的原理与建表实践
Range分区的核心思想是按照某个列的取值范围把一张逻辑表的数据分散到多个物理分区中,每个分区对应独立的存储文件。对于按时间增长的数据,通常以时间列作为分区键,每个月或每个季度一个分区。删除过期数据时不再逐行DELETE,而是执行ALTER TABLE ... DROP PARTITION,数据库直接删除该分区对应的数据文件,几百GB的数据也能在毫秒到秒级完成,并且空间立即释放,不需要 OPTIMIZE TABLE 之类的重建操作。
下面是一个完整的建表示例,以操作日志表为例,按月创建分区,注意分区键必须包含在主键和唯一键中,这是新手最常踩的坑:
CREATE TABLE op_log (
id BIGINT NOT NULL AUTO_INCREMENT,
biz_id BIGINT NOT NULL,
content VARCHAR(512),
create_time DATETIME NOT NULL,
PRIMARY KEY (id, create_time)
)
PARTITION BY RANGE (TO_DAYS(create_time)) (
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
PARTITION p202404 VALUES LESS THAN (TO_DAYS('2024-05-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
);建好分区表后,日常维护只需要两个动作:月初新增下一个月的分区,月底删除过期的分区。新增分区用ALTER TABLE op_log ADD PARTITION,但要注意如果已经存在pmax这个兜底分区,直接ADD会报错,需要先REORGANIZE拆分pmax,或者干脆不建pmax,改用下面的写法一次性完成分区轮转:
-- 拆分最大分区,同时新增下月分区
ALTER TABLE op_log REORGANIZE PARTITION pmax INTO (
PARTITION p202405 VALUES LESS THAN (TO_DAYS('2024-06-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- 直接删除90天前的整个分区,耗时毫秒级
ALTER TABLE op_log DROP PARTITION p202401;除了删除快,分区还能带来查询收益,也就是常说的分区裁剪。当查询条件中带上分区键create_time时,优化器会自动跳过不相关的分区,只扫描命中的那几个分区。比如按月查询某个时间段的日志,原本要全表扫描几亿行,现在只需要扫描一两个分区的几百万行,配合时间索引,响应时间可以从秒级降到毫秒级。可以用EXPLAIN PARTITIONS查看某条SQL实际访问了哪些分区,确认裁剪是否生效。
存量表如何平滑改造为分区表
线上业务很少有从第一天空白建表的机会,大多数场景是存量表已经跑了一两年,数据几十上百GB,这时候怎么改造?主流做法有两种。第一种是原表直接执行ALTER TABLE ... PARTITION BY RANGE,MySQL会重建整张表并把数据按规则搬到各分区。这种方式语法最简单,但重建期间会锁表,而且需要临时磁盘空间大约等于表大小的1到2倍,只适合停机窗口充足的场景。
第二种是新建分区表加双写迁移,适合不能长时间停服的核心表。流程是:先创建一张结构相同的新分区表,通过触发器或业务代码双写保证新旧表数据一致,再用INSERT INTO ... SELECT分批把存量数据搬过去,搬迁完成后在低峰期做一次短暂切换。整个过程对线上业务几乎无感,代价是开发和协调成本更高,需要处理搬迁期间的数据校验与补偿。
最后提醒几个使用上的注意点。第一,分区键一旦确定就不能修改,除非重建整张表,所以分区粒度要提前规划好,日志类表按月、超大表可以按周。第二,所有唯一键必须包含分区键,这意味着像唯一订单号这类约束需要额外想办法,比如把时间冗余进唯一索引。第三,DROP PARTITION是不可恢复的物理删除,执行前务必确认备份策略,对于需要归档的数据,可以先用EXCHANGE PARTITION把分区交换到一张归档表再删除,既快又留了后路。把这些细节处理好,分区方案才能在清理过期数据的场景里真正发挥秒级删除的优势。