结论:禁用单条INSERT...SELECT+DELETE大事务,必须分批、带校验、有熔断;推荐pt-archiver或SQL Server分区SWITCH,自建脚本须严守时间窗口、行数校验与底线条件。

直接说结论:别用单条 INSERT ... SELECT + DELETE 大事务干这事,90% 的锁表、超时、主从延迟都源于此。必须分批、带校验、有熔断。
为什么不能用大事务一次性迁移删除
MySQL 的 DELETE 和 INSERT ... SELECT 在无索引时间字段上会全表扫描;即使有索引,单次操作百万行也会撑爆 innodb_buffer_pool、触发长事务等待、拖垮 binlog 同步。更危险的是:一旦中间失败(比如磁盘满、网络抖动),你既没迁完也没删干净,状态不可逆。
-
EXPLAIN必须确认WHERE条件走了索引,否则先加索引再动手 - 单次操作行数超过 5 万,
max_allowed_packet和innodb_lock_wait_timeout极易被击穿 - 主从架构下,大事务在从库回放时会卡住整个 SQL 线程,延迟飙升
用 pt-archiver 分批归档最省心
它本质是 Perl 脚本封装的“查→插→删→休眠→重试”闭环,天然规避大事务风险,且支持跨实例归档。生产环境首选,不用自己写存储过程踩坑。
- 命令示例:
pt-archiver --source h=prod-db,P=3306,u=dba,p=xxx,D=orders,t=order_log --dest h=archive-db,P=3307,u=dba,p=xxx,D=archive,t=order_log_archive --where "create_time < '2024-01-01'" --limit 10000 --sleep 0.1 --progress 10000 --statistics -
--limit控制每批处理行数,--sleep防止 IO 冲击,--progress实时打点 - 失败自动重试,默认最多 3 次;加
--dry-run可预演不真删 - 注意:源库和归档库不能是主从关系,否则归档写入会干扰复制流
自定义脚本必须带原子校验和边界防护
如果因合规或权限限制不能用第三方工具,自己写脚本也得守住底线:每次只搬固定时间窗口(如一天)、插入后立刻比对行数、删前再查一遍、保留底线条件。
- 先查源表待归档量:
SELECT COUNT(*) FROM orders WHERE create_time < '2024-01-01' AND create_time >= '2023-12-01' - 插入时加
LIMIT和时间范围:INSERT INTO archive.orders SELECT * FROM orders WHERE create_time < '2024-01-01' AND create_time >= '2023-12-01' LIMIT 10000 - 插入后立刻
SELECT ROW_COUNT(),若为 0 则退出;若不等于预期,中止后续DELETE - 删之前再执行一次
SELECT COUNT(*),确保数据未被业务新写入;DELETE语句必须含相同时间范围 +LIMIT - 硬编码保留底线:
AND create_time > '2022-01-01',防止误删核心历史
SQL Server 分区切换是唯一真正秒级方案
如果你用的是 SQL Server 且表已分区,SWITCH 是唯一能避开锁表、日志爆炸、事务膨胀的归档方式——它只是改指针,不碰数据本身。
- 前提严苛:源表与归档表结构、索引、约束、填充因子、文件组必须完全一致
- 切换前务必验证分区边界:
SELECT $PARTITION.pf_DateRange(create_time) AS pnum, COUNT(*) FROM orders GROUP BY $PARTITION.pf_DateRange(create_time) - 典型操作链:
CREATE TABLE orders_archive_2023 (...) ON [FG_ARCHIVE]→ALTER TABLE orders SWITCH PARTITION 3 TO orders_archive_2023→DROP TABLE orders_archive_2023 - 注意:
SWITCH不触发触发器、不记完整日志、无法回滚,操作前必须备份该分区
最常被忽略的一点:归档不是迁移完就结束。归档表要立刻设为只读,原表对应字段补上 COMMENT 'archived after 2024-06',且每月必须检查归档任务日志里单次耗时是否突增——那往往意味着索引失效或数据倾斜开始发生。

















