直接 INSERT ... VALUES (...) 卡死因解析大语句、锁表、预分配日志致内存/WAL爆;应改用临时表+分批INSERT SELECT,每批5000–10000行、独立事务、主键范围切片。

为什么直接 INSERT ... VALUES (...) 会卡死?
因为单条 INSERT 带上百万行值,SQL 解析器要先完整解析整个语句、构建执行计划、锁住目标表、预分配事务日志空间——还没开始写数据,内存和日志就可能爆了。MySQL 甚至会直接报 Packet too large 或 Out of memory;PostgreSQL 可能卡在 WAL 写入阶段;SQL Server 容易触发锁升级成表锁。
- 单次插入超 1000 行值,性能断崖式下降
- 事务日志(WAL / LDF)膨胀严重,尤其在 FULL 恢复模式下
- 主从延迟陡增,binlog 写入成为瓶颈
- 锁持有时间过长,阻塞其他读写请求
用临时表 + 分批 INSERT 的实操要点
核心思路是:把原始数据先灌进一个本地临时表(CREATE TEMPORARY TABLE),再按 5000–10000 行一批,用 INSERT INTO ... SELECT 拆着写进目标表。这样避免大 SQL 解析,也控制事务粒度。
- 临时表必须与目标表结构一致(含索引、约束可省略,但字段类型、NULL 性、字符集要对齐)
- 分批时用
LIMIT+OFFSET不推荐(深度分页慢),改用主键/自增 ID 范围切片,例如:WHERE id BETWEEN ? AND ? - 每批单独开启事务(
BEGIN; INSERT ...; COMMIT;),别包在一个大事务里 - MySQL 下可加
INSERT IGNORE或ON DUPLICATE KEY UPDATE避免唯一冲突中断流程
不同数据库的分批写法差异
语法细节决定成败。比如 PostgreSQL 没有 LIMIT 在子查询里的限制,但 MySQL 8.0+ 才支持 INSERT ... SELECT 带 LIMIT;SQL Server 要用 TOP 和 OFFSET/FETCH 组合。
- MySQL:
INSERT INTO target SELECT * FROM temp_table WHERE id >= 10000 AND id - PostgreSQL:
INSERT INTO target SELECT * FROM temp_table ORDER BY id LIMIT 10000 OFFSET 20000; - SQL Server:
INSERT INTO target SELECT * FROM temp_table ORDER BY id OFFSET 20000 ROWS FETCH NEXT 10000 ROWS ONLY; - 所有数据库都建议关闭自动提交(
SET autocommit = 0),手动COMMIT控制时机
容易被忽略的三个性能开关
光分批还不够,不调底层参数,照样慢得像在等编译完成。
- 禁用目标表非必要索引:批量导入前
DROP INDEX,导入完重建(尤其是唯一索引、全文索引) - 调整日志刷盘策略:MySQL 设
innodb_flush_log_at_trx_commit = 2(仅限导入期间);PostgreSQL 临时设synchronous_commit = off - 增大事务日志缓冲:MySQL 调
innodb_log_file_size,SQL Server 扩ldf文件,避免频繁日志切换
临时表本身不走 WAL(多数引擎),但目标表的每次 INSERT 仍记日志——所以分批大小不是越小越好,5000~10000 是兼顾吞吐与回滚成本的经验平衡点。










