直接用INSERT INTO SELECT迁移百万级数据会引发锁表、日志爆满和主从延迟;OFFSET分页因全表扫描导致O(n²)复杂度,应改用有序字段范围查询分批处理。

直接用 INSERT INTO ... SELECT 迁移百万级数据,等于主动触发锁表、日志爆满和主从延迟——这不是慢,是数据库在报警。
为什么不能用 OFFSET 分页做游标
常见错误是写 SELECT ... LIMIT 2000 OFFSET @offset,然后每次递增 @offset。MySQL 执行时必须扫描前面所有行,OFFSET 越大越卡;百万级数据下第二轮就明显延迟,第三轮可能超时。这不是“有点慢”,是时间复杂度退化成 O(n²)。
- 必须用有序字段(如自增
id或带索引的created_at)做范围条件 - 避免
ORDER BY RAND()或无索引字段排序,否则每次都是全表扫描 - 如果源表没有合适索引字段,先加索引再跑迁移,别硬扛
存储过程里怎么安全取数并提交
核心不是“循环多少次”,而是“每次取哪段、取完怎么确认、失败怎么退出”。靠 ROW_COUNT() 判断是否还有数据,比查总数 + 计数更可靠。
- 声明变量:
DECLARE last_id BIGINT DEFAULT 0;和DECLARE batch_size INT DEFAULT 2000; - 循环体里先执行:
INSERT INTO target SELECT ... FROM source 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(id),而不是源表。源表可能有新写入、跳号、或已删数据,用它推进会导致漏迁。另外,COMMIT 必须显式写在循环体内,哪怕开了 AUTOCOMMIT=1,存储过程里仍受事务上下文约束。


















