WHILE循环逐条INSERT是设计反模式,因每行触发redo刷盘、SQL解析、锁校验及自动提交,吞吐常低于300行/秒;应改用START TRANSACTION显式开启事务,按1000行/批分片插入,每批后COMMIT,末尾补一次COMMIT,将百万次日志刷盘降至千次。

存储过程里写循环插入百万级数据,不是语法错,是设计反模式——它默认每行都触发 redo 刷盘、解析、锁校验和自动事务提交,实际吞吐常低于 300 行/秒。真正提速的关键,是绕开单行瓶颈,用批量 + 显式事务 + 合理分片。
为什么WHILE循环逐条INSERT特别慢
MySQL 存储过程默认 autocommit = ON,没显式事务时,每条 INSERT 都是独立事务:
- 每次都要写
redo log(innodb_flush_log_at_trx_commit = 1时强制刷盘) - 每次都要解析 SQL、做权限检查、更新索引 B+ 树节点
- 目标表有唯一索引或外键时,每行都触发全量约束校验
-
RAND()、NOW()这类函数在循环内反复求值,无法预编译优化
必须用START TRANSACTION + 批量提交
把 100 万行拆成 1000 行/批,每批包在一个事务里,日志刷盘次数从 100 万次降到 1000 次。这是最简单也最有效的动作:
- 在
BEGIN后立即写START TRANSACTION - 用
IF i % 1000 = 0 THEN COMMIT; START TRANSACTION;控制节奏 - 循环结束后补一次
COMMIT(否则最后不足 1000 行的批次会丢失) - 避免在循环里嵌套
SELECT ... FOR UPDATE或复杂子查询
示例片段:
START TRANSACTION;
WHILE i <= total DO
INSERT INTO usertest (name, mobile) VALUES (CONCAT('u', i), CONCAT('138', LPAD(i, 9,'0')));
IF i % 1000 = 0 THEN
COMMIT;
START TRANSACTION;
END IF;
SET i = i + 1;
END WHILE;
COMMIT;
比循环更优的替代路径
真要插百万级,优先考虑非循环方案。存储过程只是“能做”,不代表“该做”:
- 用
LOAD DATA INFILE(MySQL)或COPY(PostgreSQL)——跳过 SQL 解析层,快 5–10 倍 - 先
INSERT INTO #staging SELECT ...把源数据卸到临时表,再用WHILE+TOP N分批插入目标表(SQL Server) - 应用层用
PreparedStatement.addBatch()+executeBatch()(Java)或executemany()(Python),由驱动自动拼多值INSERT - 导入初期可临时禁用约束:
SET FOREIGN_KEY_CHECKS = 0、ALTER TABLE t DISABLE KEYS,完成后恢复
容易被忽略的细节
性能卡点往往藏在配置和边界条件里:
-
max_allowed_packet默认 4MB,拼 1000 行VALUES很容易超限,得提前调大 - 临时表
#staging在动态 SQL 中创建后,外部无法引用——作用域隔离,必须在存储过程开头建好 - 没递增 ID 列时,用
ORDER BY (SELECT NULL)分片不保证幂等,重试可能重复插入 -
@@ROWCOUNT不能用来判断是否还有数据——最后一批可能不足批次大小,但循环仍需继续

















