INSERT INTO ... SELECT 直接迁移几十万行会引发锁表、日志爆满、主从延迟等严重问题,因其默认单事务执行;正确做法是用有序字段(如id)分批小事务,每批显式COMMIT,并配合ROW_COUNT()控制循环与错误处理。

直接用 INSERT INTO ... SELECT 迁移几十万行,基本等于给数据库下死命令——锁表、日志爆满、主从延迟飙升,甚至触发 Lock wait timeout exceeded。 根本原因不是语句本身慢,而是它被当作单事务执行,undo/redo 日志和 buffer pool 压力全堆在一起。真正可行的方案,是用带游标逻辑的分批小事务,每批显式 COMMIT,并严格控制定位方式。
为什么不能用 OFFSET 分页做游标
常见错误是写 SELECT ... LIMIT 1000 OFFSET @offset 然后每次递增 @offset。OFFSET 越大,MySQL 就越要扫描前面所有行,百万级数据下第二轮就卡住,第三轮可能直接超时。这不是“慢一点”,而是 O(n²) 级别退化。
- 必须用有序字段(如自增
id或created_at)做范围条件,例如WHERE id > last_id ORDER BY id LIMIT 2000 - 避免
ORDER BY RAND()或无索引字段排序,否则每次都是全表扫描 - 如果源表没有合适索引字段,先加索引再跑迁移,别硬扛
WHILE 循环里怎么安全取数并提交
核心不是“循环多少次”,而是“每次取哪段、取完怎么确认、失败怎么退出”。靠 ROW_COUNT() 判断是否还有数据,比查总数 + 计数更可靠。
- 声明变量:
DECLARE last_id BIGINT DEFAULT 0;和DECLARE batch_size INT DEFAULT 2000; - 循环体里先
INSERT INTO target SELECT ... WHERE id > last_id ORDER BY id LIMIT batch_size - 立刻检查:
IF ROW_COUNT() = 0 THEN LEAVE read_loop; END IF;,防止无限循环 - 更新游标:
SELECT MAX(id) INTO last_id FROM target WHERE id > last_id;(注意:必须从目标表查,否则漏数据) - 每批后必须
COMMIT;—— 不要依赖存储过程结尾自动提交
UPDATE / DELETE 大量数据也得同样分批
批量更新或删除不比插入轻松。InnoDB 行锁会持续到事务结束,单次改 10 万行,其他业务可能集体卡在 Waiting for table metadata lock。
- 优先走主键范围:
UPDATE t SET status='done' WHERE id BETWEEN @start AND @end AND condition=1 - 没连续主键?用
LIMIT+REPEAT循环,但必须加ORDER BY id保证稳定性 - 每次
UPDATE或DELETE后都COMMIT,释放锁和 undo log - 别忘了加
CONTINUE HANDLER FOR SQLEXCEPTION捕获死锁,否则一次失败整个过程就停摆
容易被忽略的配置与边界
分批逻辑写对了,但服务器配置没调,照样崩。比如 max_allowed_packet 默认才 4MB,一批 5000 行宽表数据就超限;又比如 innodb_flush_log_at_trx_commit=1 虽然保证持久性,但在高吞吐场景下会让 COMMIT 成为瓶颈。
- 迁移前确认:
SET autocommit = 0;,否则每条语句自动提交,失去批量意义 - 调整
max_allowed_packet至至少 64M(尤其含 TEXT/BLOB 字段时) - 若允许短暂不一致,可临时设
innodb_flush_log_at_trx_commit = 2加速提交 - 批量大小别写死,作为
IN参数传入,压测时方便快速调成 1000/3000/5000 对比效果


















