跳到主内容

千万级大表瘦身实录:冷热分离的分区表设计与归档流水线落地 ​

订单表滚到三亿行之后,团队发现三件事同时恶化:B+ 树变高导致点查也变慢,缓冲池命中率下降引发周期性抖动,每天六小时的备份窗口已经快要覆盖到白天。这些问题的共同根源是热数据被海量冷数据稀释——一年前的订单几乎没有查询,却占据着索引、缓冲池和备份窗口。解法是冷热分离:热数据留在主表保持精瘦,冷数据按策略归档到专属存储。这篇实录把承载形态选择、归档流水线设计与限速实操一次讲清。

先回答「哪些表值得做」。冷热分离不是所有大表的万能药:访问模式高度均匀、任何时间段都有等概率查询的表,分离后反而要多一跳联合查询。判断标准很朴素——统计近三个月的查询时间分布,如果九成以上查询集中在最近三个月的数据,剩余体量只贡献不到一成的访问,这张表就值得分离;反之则先优化索引与缓冲池,不必急着动存储结构。

承载形态先想清楚:分区表、归档表与分工 ​

推荐的双层结构是:主表按时间 RANGE 分区,近月分区承载热数据;归档库建同构的非分区表承接历史数据。先看主表设计:

sql
-- 按月 RANGE 分区:查询携带 create_time 才能命中分区裁剪
CREATE TABLE order_info (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  order_no    VARCHAR(32)     NOT NULL COMMENT '业务订单号',
  user_id     BIGINT UNSIGNED NOT NULL,
  amount      DECIMAL(10, 2)  NOT NULL,
  status      TINYINT         NOT NULL DEFAULT 0,
  create_time DATETIME        NOT NULL COMMENT '创建时间,分区键',
  PRIMARY KEY (id, create_time),                -- 唯一键必须包含分区键,这是分区表的硬约束
  UNIQUE KEY uk_order_no (order_no, create_time),
  KEY idx_user (user_id)
) ENGINE = InnoDB
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)
);

三个硬约束写进设计文档,别到上线才发现:所有唯一键(含主键)必须包含分区列,业务唯一键 order_no 也要被迫带上 create_time;InnoDB 分区表不支持外键,关联完整性要靠应用层保证;查询不携带分区键时优化器会扫描所有分区,分区裁剪完全依赖 SQL 写法。此外要建好分区滚动机制:定期 ALTER TABLE 新增下月分区,避免数据涌进 pmax 导致分区失去意义。分区数量也要有节制——单表几百上千个分区会让优化器与文件句柄的开销显著上升,按月分区保留两到三年、配合归档流水线持续输出,通常比无限堆分区更健康。

归档流水线的五个环节:扫描、迁移、删除、对账、清理 ​

归档不是「一条 DELETE 挂后台跑」,而是一条带安全措施的流水线:扫描环节用主键区间把存量切成小批次;迁移环节分批写入归档库并保证幂等;删除环节用与迁移完全一致的主键集合小批量删除;对账环节在删除前校验两边行数一致;清理环节维护分区滚动与归档侧生命周期。核心 SQL 骨架如下:

sql
-- 迁移:按主键区间分批写入归档库,INSERT IGNORE 保证任务重跑幂等
INSERT IGNORE INTO shop_archive.order_info
SELECT * FROM shop.order_info
WHERE create_time < '2026-01-01'
  AND id BETWEEN 1 AND 50000;

-- 对账:删除前按同一集合比对行数,不一致立即终止流水线人工介入
SELECT COUNT(*) AS src_rows FROM shop.order_info
WHERE create_time < '2026-01-01' AND id BETWEEN 1 AND 50000;
SELECT COUNT(*) AS dst_rows FROM shop_archive.order_info
WHERE create_time < '2026-01-01' AND id BETWEEN 1 AND 50000;

-- 删除:与迁移条件严格一致,单批删除再 LIMIT 限流,循环到影响行数为 0
DELETE FROM shop.order_info
WHERE create_time < '2026-01-01'
  AND id BETWEEN 1 AND 50000
LIMIT 5000;

调度程序要承担并发控制:每批一个独立事务,批间 sleep 50 到 100 毫秒,同时轮询主从延迟,超过阈值自动暂停——归档任务引发主从延迟雪崩是这套方案最常见的翻车点。「先写后删、两步用完全相同的主键集合」是安全底线:只按时间条件先后两次执行,两次之间新落入区间或被修改的行会造成两边数据不一致。

归档库自身的生命周期也要管起来。归档不是终点:按月分文件的归档表要在保留期到达后压缩转储到对象存储,再从在线归档库下线;对账报表要按月自动生成并留痕,作为数据一致性的长期凭证。这套机制一次搭好,之后每年都省下数不清的人工核对时间。

秒级搬走一整个月:EXCHANGE PARTITION 与 pt-archiver ​

对按月分区的表,还有一条远快于逐行搬运的路:分区交换。把整个旧分区与一张结构完全一致的归档表做原子交换,本质是元数据操作,秒级完成:

sql
-- 前提:归档表结构与分区定义完全一致(含索引),且为非分区表
ALTER TABLE shop.order_info EXCHANGE PARTITION p202601
WITH TABLE shop_archive.order_info_202601;

交换完成后 p202601 分区变成空壳,直接 DROP 即可;被换出的归档表再按需转储到对象存储或数据仓库。存量已超大的表不想一次性改造分区,可以用 pt-archiver 做持续归档,它自带限速与主从延迟感知:

bash
# pt-archiver 持续归档:小批量搬运、批间限速、联动从库延迟自动暂停
# 密码通过环境变量或配置文件注入,不要写进脚本
pt-archiver \
  --source h=127.0.0.1,D=shop,t=order_info \
  --dest h=10.0.0.8,D=shop_archive,t=order_info \
  --where "create_time < '2026-01-01'" \
  --limit 1000 --commit-each \
  --max-lag 1 --check-slave-lag h=10.0.0.9 \
  --progress 10000 --statistics

避坑总结 ​

  1. 分区键与唯一键的冲突要在建表期解决。 存量普通表改造成分区表等于整表重建,业务唯一键还必须带上分区列,索引设计与查询写法都要适配,所以分区方案优先在新表新规则上落地,改造存量要单独立项评估。
  2. 批删必须小批量、限速、盯延迟三件套齐活。 单批控制在千行级别,批间 sleep,主从延迟超阈值自动熔断;归档任务跑挂业务写入,比归档慢一天严重得多。
  3. 先写后删,且两步共用完全相同的主键集合。 只按时间条件先后两次执行,两次之间数据变动会造成源与归档不一致;用主键区间或固化快照把「迁移集合」与「删除集合」锁成同一个。
  4. 归档库要有查询出口。 客诉回查、法务取证、财务对账都可能需要冷数据,归档库的只读授权与查询工具提前就位,别等要用时才发现数据躺在没人能查的库。
  5. DROP PARTITION 与 EXCHANGE PARTITION 也是 DDL。 同样需要获取元数据锁,同样会被长事务卡住并拖垮全表访问,执行前清场长事务、放低峰窗口,与文章 303 里的在线 DDL 纪律完全一致。

写在最后 ​

大表瘦身的技术动作并不复杂:分区定边界、流水线保安全、分区交换提速度。真正拉开差距的是工程纪律——小批量、限速、对账、幂等这些枯燥的约束,才是归档任务能长期无人值守运行的底气。热数据精瘦之后,查询、备份、DDL 会同时受益,这是性价比最高的一次存储治理。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。