去年做订单归档,一条 insert into history select 加个时间条件,直接把生产库拖住了四十分钟,从库延迟报警,业务方电话打爆。语句本身没错,错在没算清它要锁多少行、写多少日志。

锁和事务范围先想清楚

InnoDB 在可重复读隔离级别下,insert into select 会对源表扫描到的行加共享锁,没走索引就是全表扫描加锁。想避免就改成读已提交,或者按主键分段,每批几千行提交一次。别在一个事务里塞几百万行,回滚段和 undo 会撑爆。

字段顺序和目标表结构

不写列名最危险,源表加一列、目标表顺序不同,数据就错位,而且不报错。养成写全列名的习惯:insert into t (a,b,c) select a,b,c from s。目标表的自增主键、唯一索引、非空默认值都要提前确认,主键冲突会让整条语句回滚。

别把自己也读进去

insert into t select 同表操作,MySQL 会报 1093 错误或者偷偷建临时表,数据量大时磁盘直接满。要么先落中间表,要么用主键范围把新写入的部分排除掉。where 条件漏写更是常见事故,先 select count 验证行数再执行。

一段能跑的分批写法

insert into t_order_his (id,uid,amount) select id,uid,amount from t_order where id > 0 and id <= 100000 order by id;

每批记下最大 id,循环往前推,批间 sleep 几十毫秒让主从追上。开了 binlog 的话,大事务还会让从库回放卡住,分批同样能缓解。

执行前的检查清单

  • 行数:先 count,超过十万就分批。
  • 索引:where 字段必须有索引,explain 的 type 不能是 ALL。
  • 空间:目标表空间和 binlog 目录留足余量。
  • 时间:避开业务高峰,准备好 kill 的语句。

源表和目标表不在同一实例时,老老实实导出 CSV 再 load data,比硬跑一条跨库语句稳得多。