当前位置: 代码网 > it编程>数据库>Mysql > MySQL数据归档之从中小表到大表的全场景方案

MySQL数据归档之从中小表到大表的全场景方案

2026年08月20日 Mysql 我要评论
1. 归档要解决什么问题归档不是简单的「把旧数据挪走」,通常要同时满足:目标说明在线表变小热查询只扫近期数据,索引更小、缓存命中率更高冷数据可查客服、审计、纠纷仍能通过 id / 时间查到历史可恢复归

1. 归档要解决什么问题

归档不是简单的「把旧数据挪走」,通常要同时满足:

目标说明
在线表变小热查询只扫近期数据,索引更小、缓存命中率更高
冷数据可查客服、审计、纠纷仍能通过 id / 时间查到历史
可恢复归档失败可重跑,误删可找回
对业务影响小尽量不停写,锁表窗口可控
磁盘真释放逻辑删行 ≠ 文件缩小(后文详述)

常见误区:

  • ❌ 以为 delete 旧数据后在线表文件会变小
  • ❌ 表还在写就要给热表挂永久触发器做归档
  • ❌ 用裸 limit 表示「部分归档」
  • ❌ 先删源表再插归档库

2. 先定策略:四类决策

动手写脚本前,先回答四个问题:

2.1 归档边界(什么数据算「冷」)

-- 典型组合
where status in ('closed', 'cancelled')
  and create_time < @cutoff   -- 如 90 天前
  and keep_flag = 0
  • 任务开始时 固定 cutoff,整次任务不变
  • 优先归档 业务上已封闭 的数据(已关单、已结算),避免冷数据仍被 update

2.2 归档去向(放哪里)

去向适用
归档库(同结构表)仍需 sql 查询历史
备份表(rename 留下的整表)表级瘦身、少查冷数据
文件(csv / parquet)+ 对象存储极少查询、成本优先
按月分表天然按时间隔离

2.3 在线表是否还要保留冷数据

  • 全量迁出:冷数据只存在于归档侧
  • 部分保留:满足冷条件但仍留在线(vip、大额、keep_flag)——见 第 8 节

2.4 可接受的切换窗口

窗口可选方案
无停写分批 delete、分区 drop、gh-ost 类在线重建
秒级~分钟级停写热数据重建 + 追增量 + rename
可维护窗口optimize、整表导出导入

3. 数据量维度:四档场景与选型

数据量 / 特征                    推荐路径
─────────────────────────────────────────────────────────
s  < 500 万、非分区、日增可控      → 分批 delete + 归档库
m  500 万~5000 万、可改表结构     → 时间分区 + exchange/drop
l  单表亿级、短期难分区           → 热数据重建 + 双表 rename
xl 按月暴涨、整月可封闭           → 整表 rename 轮换

3.1 渐进式演进(推荐路径)

很多团队不是一步到位,而是:

阶段 1:s 档 — 脚本分批 delete + 归档库(快速上线)
阶段 2:m 档 — 在线表加分区,新方案切分区归档
阶段 3:l 档 — 历史膨胀后做一次 rename 瘦身,之后走分区

不必等「完美架构」才做归档;先止住在线表增长,再优化手段。

4. 方案一:分批 delete + 归档库(中小表)

4.1 适用

  • 表 < 500 万行,或 delete 一批在可接受时间内完成
  • 尚未分区,改造成本可接受
  • 需要冷数据在归档库可查

4.2 基本流程

定 cutoff → 分批选取 → insert 归档库 → 校验 → delete 源表 → 记日志 → 全局对账

4.3 核心 sql(同事务、同条件)

start transaction;

insert into archive_db.orders_archive
  (id, user_id, amount, status, create_time, archive_batch_id)
select id, user_id, amount, status, create_time, '20240818_001'
from prod_db.orders
where status = 'closed'
  and create_time < '2024-05-01 00:00:00'
  and keep_flag = 0
  and id > 1000000 and id <= 1010000;

delete from prod_db.orders
where status = 'closed'
  and create_time < '2024-05-01 00:00:00'
  and keep_flag = 0
  and id > 1000000 and id <= 1010000;

commit;

4.4 持续写入时为何不需要触发器

  • 新数据 create_time >= cutoff → 不在 where 内,不会被碰
  • 本批 insert 与 delete 条件一致,同事务内 innodb 行锁保证一致
  • 不需要 给热表挂永久触发器

4.5 注意

  • 单批不宜过大(建议 5k~5w 行),避免长事务、大 binlog
  • delete 后 文件未必缩小——见 第 11 节
  • 归档表对 id唯一键,任务可幂等重跑

5. 方案二:分区表 + exchange / drop partition(大表首选)

5.1 适用

  • 按时间访问明显(订单、日志、流水)
  • 可接受一次 ddl 加分区(或新建分区表迁移)
  • 希望 删冷数据 = 真释放空间

5.2 分区设计示例

create table orders (
  id bigint not null,
  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 p_future values less than maxvalue
);

5.3 归档方式 a:交换分区(冷数据进归档库,可查询)

create table orders_archive_202401 like orders;

alter table orders
  exchange partition p202401
  with table orders_archive_202401;
  • 元数据级交换,秒级
  • orders_archive_202401 获得整月数据;在线表该分区为空

5.4 归档方式 b:直接 drop(不需再查)

alter table orders drop partition p202401;
  • 空间回收最彻底
  • 不可逆,务必先备份或先 exchange 到归档表

5.5 正确性要点

  • 只 drop 已封闭月份(如 2 月 1 日后才 drop 1 月分区)
  • exchange 前校验分区行数;交换后归档表行数应一致
  • 分区表与归档表 结构、索引、约束一致

5.6 与持续写入

当月分区持续写入;历史分区只读封闭 → 天然无 update 同步问题,无需触发器。

6. 方案三:热数据重建 + 双表 rename(超大单表瘦身)

6.1 适用

  • 单表亿级,历史上 未分区,delete 无法让文件变小
  • 在线表只需保留热数据,冷数据整表留存即可
  • 可接受一次 短切换窗口(或迁移期临时触发器)

6.2 思路(你描述的方案)

1. 把「要保留的热数据」复制到 orders_temp
2. rename orders        → orders_backup_20240818   (整表变备份,含全量快照)
3. rename orders_temp   → orders                   (新活动表,仅热数据)

结果:

内容
orders(新)仅热数据,文件小
orders_backup_20240818切换时刻全量(热+冷)

冷数据在备份表中;新活动表不再承载冷行。这是 表级归档,比 delete 更利于 在线表物理变小

6.3 阶段一:不停机复制热数据

create table orders_temp like orders;

-- 分批复制,避免长事务
insert into orders_temp
select * from orders
where create_time >= date_sub(now(), interval 90 day)
   or status not in ('closed', 'cancelled')
   or keep_flag = 1;
-- 按 id 分批 + sleep

记录 copy_start_time 和已复制 max_id

6.4 阶段二:追增量 + 原子切换

选项 a:短暂停写(推荐,简单可靠)

-- 应用停写或只读
lock tables orders write;

insert into orders_temp
select * from orders
where (create_time >= ... or status not in (...) or keep_flag = 1)
  and (id > @max_copied_id or updated_at >= @copy_start_time)
on duplicate key update
  user_id = values(user_id),
  amount = values(amount),
  status = values(status),
  updated_at = values(updated_at);

unlock tables;

rename table
  orders to orders_backup_20240818,
  orders_temp to orders;

-- 校正自增,避免新 id 与备份表冲突
set @next_ai = (select max(id) + 1 from orders_backup_20240818);
set @sql = concat('alter table orders auto_increment = ', @next_ai);
prepare stmt from @sql;
execute stmt;
deallocate prepare stmt;

选项 b:迁移期临时触发器(不能停写时)

在阶段一复制期间,对 orders临时 触发器,把 insert/update/delete 同步到 orders_temp;切换前校验一致、删除触发器、再 rename。详见 第 10 节。

6.5 切换后

  • 不需要 永久触发器——只有一个活动表 orders
  • orders_backup_* 可迁归档库、压缩存储,或确认无查询后 drop
  • 备份表里热数据有 冗余副本(全量快照),属正常

6.6 与 gh-ost / pt-osc

在线改表工具本质也是:建新表 → 复制 + 追增量(触发器或 binlog)→ rename 切换。本方案是同一模式的手动版。

7. 方案四:整表 rename 轮换(按月一张表)

7.1 适用

  • 业务天然按月分表,或表名带月份 orders_202401
  • 每月整表「下线」,无部分行归档

7.2 流程

-- 月初:新表已建好 orders_202402
rename table
  orders_202401 to orders_archive_202401,
  orders_202402 to orders;           -- 或应用改指向新表名
  • 元数据操作,极快
  • 旧表整表保留或 drop

7.3 注意

  • 应用路由或表名策略要统一
  • 跨月查询需扫多表或汇总视图

8. 部分保留:同一条件下只归档一部分

定义: 满足冷数据条件(如 90 天前且已关单)的行里,只搬走一部分,其余继续留在线表。

8.1 条件拆分

归档集合 a = status='closed' and create_time < cutoff
实际搬走   b = a and not 保留规则 r
仍留在线   = a and r
where status = 'closed'
  and create_time < @cutoff
  and keep_flag = 0
  and user_id not in (select user_id from vip_users)  -- 示例
  and amount < 100000

8.2 实现方式

方式说明
keep_flag关单时按 vip/金额写入,脚本只动 keep_flag=0
白名单表not exists (archive_keep_orders)
稳定比例mod(crc32(concat(id,'salt')),100) < 80 约 80% 归档(salt 固定)

8.3 不要用裸 limit 表示「留一部分」

-- ❌ 危险:每批删哪些行不确定,无法对账重跑
delete from orders where ... limit 5000;

限量应用 主键游标 id > @last_id order by id limit n保留语义 仍由 keep_flag 等表达。

8.4 对账

-- 应归档且未保留:任务结束后源表应为 0
select count(*) from orders
where status='closed' and create_time < @cutoff and keep_flag = 0;

-- 应保留:仍在源表
select count(*) from orders
where status='closed' and create_time < @cutoff and keep_flag = 1;

-- 主键无交集
select count(*) from orders o inner join orders_archive a on o.id = a.id;

9. 数据正确性保障体系

9.1 四个维度

维度手段
不丢先 insert 归档再 delete;禁止先删后插
不重归档表 id 唯一;幂等 insert ignore / 按 batch 校验
不错行数 + 校验和(count/sum 或关键字段 crc)
可恢复archive_job_log + 备份表/分区可回导

9.2 每批门控(脚本内)

1. 计算本批 source_count、checksum_source
2. insert 归档
3. 计算 archive_count、checksum_archive
4. 不一致 → rollback / 不 delete、告警
5. 一致 → delete 或 commit
6. 写入 job_log

9.3 全局收尾对账

  • 源表:应归档条件行数 = 0
  • 归档表:同条件行数 = 历史累计
  • 源表与归档表 主键交集 = 0
  • 子表(订单明细)按 order_id 同步归档或对账

9.4 并发 update

场景处理
分批 delete 归档同事务 + 同 where + 可选 for update
热数据 rename 切换迁移窗口 追增量,切换后单表
分区封闭后归档历史分区无写,无问题
冷数据业务禁止修改最强约束,优先采用

10. 迁移窗口内的增量同步:何时需要触发器

10.1 结论一览

阶段是否需要触发器
分批 insert 归档 + delete 源表
分区 exchange / drop
rename 切换 完成之后
热数据复制到 orders_temp 期间(长耗时)视情况:停写追增量 优先;不能停写可用 临时触发器 或 gh-ost

10.2 临时触发器示例(仅迁移期)

create trigger tr_orders_ins_sync
after insert on orders for each row
begin
  if new.create_time >= date_sub(now(), interval 90 day)
     or new.status not in ('closed','cancelled')
     or new.keep_flag = 1 then
    insert into orders_temp values (...);
  end if;
end;

-- update / delete 同理;切换前 drop trigger,再 rename

缺点: 热表每笔写放大;与批量复制叠加时负载高。
替代: 短停写 + on duplicate key update 追增量;或 binlog cdc(长期双写镜像场景)。

10.3 「复制到 staging 后长期双存在」才需要持续同步

若流程是「复制 → rename staging → 很久以后才删源表」,则存在双份且源表仍 update——这是 镜像 问题,应用 cdc 优于永久触发器。
推荐改流程: 复制 → 校验 → 删源(或整表 rename 一切换)→ 结束双存在。

11. 空间回收:delete 之后还要做什么

11.1 innodb 行为

  • delete:逻辑删除,表文件通常不缩小(空洞可给同表 insert 复用)
  • 还给操作系统:需 drop 分区/表重建表

11.2 各方案的空间效果

方案在线表文件
分批 delete往往不变,需 optimize / 换表
drop partition明显缩小
热数据 rename 换新表新表小文件
整表 rename 轮换在线表始终新文件

11.3 非分区表 delete 后的回收

-- 低峰执行,大表会锁表或耗时很长
optimize table orders;
-- 或 alter table orders engine=innodb;

大表可用 gh-ost 做在线重建。分区表优先 drop partition,避免全表 optimize。

12. 运维配套:任务表、监控、回滚

12.1 任务日志表

create table archive_job_log (
  id bigint auto_increment primary key,
  job_name varchar(64) not null,
  batch_id varchar(32) not null,
  cutoff_time datetime not null,
  id_range_start bigint,
  id_range_end bigint,
  source_count int,
  archive_count int,
  checksum_source varchar(64),
  checksum_archive varchar(64),
  status enum('running','success','failed') not null,
  error_msg text,
  started_at datetime not null,
  finished_at datetime,
  unique key uk_batch (job_name, batch_id)
);

12.2 监控告警

  • 单批 source_count != archive_count
  • 全局对账:源表遗漏、主键交集 > 0
  • 任务超时、连续 0 行(条件错误)
  • 主从延迟超阈值时暂停 delete

12.3 回滚思路

方案回滚
归档库 + delete从归档表 insert 回源表(注意幂等)
exchange再 exchange 回去
rename 切换再 rename 换回(需未对新表写入或先冻结)
drop partition不可回滚,必须先备份或 exchange 到归档表

12.4 从库策略

  • 读压力大的校验可在从库做 count/checksum
  • delete 仍在主库执行,关注 binlog 与复制延迟

13. 方案选型速查表

场景数据量推荐方案停写空间回收触发器
历史可查、表不大< 500 万分批 delete + 归档库需 optimize
时间明显、可分区500 万+exchange / drop partition
亿级单表、delete 不缩表亿级热数据 rename 切换短窗口很好仅迁移期可选
按月整表封闭任意整表 rename 轮换很好
冷数据极少查drop partition 或导出 oss最好
部分保留(vip 等)任意在上述方案上加 keep 条件同左同左同左

14. 总结

  1. 归档边界 先于脚本:cutoff、状态、keep_flag,任务内固定 cutoff。
  2. 按数据量选型:中小表分批搬迁;大表分区;超大单表热数据 rename;按月整表轮换。
  3. 正确性 靠同事务同条件、每批门控、全局对账、幂等设计,而不是永久触发器。
  4. delete 不缩表;要真释放空间用 drop partition、新表 rename 或 optimize。
  5. 热数据重建 + 双表 rename 是亿级单表瘦身的利器;触发器只用于 迁移窗口追增量,切换完即删。
  6. 部分保留keep_flag / 白名单表达,不用裸 limit。

归档没有银弹,但路径清晰:先止住在线表膨胀,再按量级升级到分区或 rename。把边界、校验、日志做扎实,比追求一次性完美架构更重要。

附录:伪代码 — 分批归档任务

def archive_orders(cutoff: str, batch_size: int = 5000):
    if job_already_running("orders_archive"):
        return

    last_id = 0
    batch_no = 0

    while true:
        batch_no += 1
        batch_id = f"{date.today()}_{batch_no:03d}"

        with db.transaction():
            rows = db.query(
                "select id, ... from orders where ... and id > %s order by id limit %s",
                (last_id, batch_size)
            )
            if not rows:
                break

            ids = [r.id for r in rows]
            checksum_src = checksum(rows)

            db.execute("insert into archive.orders_archive ...", rows)
            checksum_arc = db.query_one(
                "select count(*), sum(...) from archive.orders_archive where batch_id = %s",
                (batch_id,)
            )

            if not verify(len(rows), checksum_src, checksum_arc):
                raise archiveerror("batch verify failed")

            db.execute(
                "delete from orders where id in (%s)",
                (ids,)
            )

            log_success(batch_id, len(rows), checksum_src)
            last_id = max(ids)

        sleep(0.1)  # 降低主库压力

    reconcile_global(cutoff)

以上就是mysql数据归档之从中小表到大表的全场景方案的详细内容,更多关于mysql数据归档的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com