Why Using INSERT INTO SELECT Can Crash Your MySQL Production – A Real‑World Postmortem
A developer’s costly mistake using MySQL’s INSERT INTO SELECT for large data migration led to OOM, full-table scans, and lost payments, revealing how transaction locks and missing indexes can cause production failures and how to prevent them.
血一般的教训,请慎用 insert into select。同事应用之后,导致公司损失了近10w元,最终被公司开除。
事情的起因
公司交易量大,使用 MySQL,每天增量约百万,未分库分表,需要通过数据迁移维持表性能。
同事李某提出两种方案:
先查询数据,插入历史表,再删除原表。
使用 INSERT INTO SELECT 让数据库 I/O 完成所有操作。
第一方案一次性加载导致 OOM,分批仍然 I/O 多且耗时,最终选择第二方案,测试无误后上线,却被开除。
到底发生了啥,我们复盘一下
先来看第一个方案,先看伪代码
// 1、查询对应需要迁移的数据
List<Object> list = selectData();
// 2、将数据插入历史表
insertData(list);
// 3、删除原表数据
deleteByIds(ids);代码一次性把所有数据加载到内存,导致 OOM。
再来看看第二个方案,到底发生了啥。
为保留最近 10 天数据(约 1 万条),同事在 INSERT INTO SELECT 中加入时间筛选 dateTime < (Ten days ago) ,避免分页查询,消除了 OOM,并简化代码。
在测试环境使用 1 万条模拟数据,定时任务在晚上 8 点执行,测试通过。
但第二天财务对账发现支付流水缺失,排查后发现晚上 8 点后开始出现插入失败,导致大量数据丢失。
最终定位到迁移任务导致的问题,停掉任务后问题消失。
复盘
问题在哪里?
查看 INSERT INTO SELECT 的 EXPLAIN,发现全表扫描。
全表扫描在大表上会导致迁移时间长,白天未出现问题是因为迁移耗时约一小时,夜间任务与业务并发导致锁表。
原因
在默认事务隔离级别下,INSERT INTO SELECT 对目标表 a 加表锁,对源表 b 逐行加锁。逐行加锁时,支付流水表的复合记录被锁定,部分无法获取锁,导致超时或失败。
为什么测试没有问题?
测试使用的是真实数据量,但测试环境并未模拟大批量插入的并发场景,导致未暴露问题。
解决办法
避免全表扫描,对 WHERE 条件建立索引,使 SELECT 能走索引。
insert into 还能用吗?
可以,但必须做好索引和锁控制。
总结
使用 INSERT INTO SELECT 时务必慎重,确保有合适的索引以避免全表扫描和锁表导致的生产事故。
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
