SQL 如何批量插入百万级数据不卡死事务(分批 + 临时表)

15次阅读

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

SQL 如何批量插入百万级数据不卡死事务(分批 + 临时表)

为什么 直接 INSERT … VALUES (…) 会卡死?

因为单条 INSERT 带上百万行值,SQL 解析器要先完整解析整个语句、构建执行计划、锁住目标表、预分配事务日志空间——还没开始写数据,内存和日志就可能爆了。MySQL 甚至会直接报 Packet too largeOut 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 IGNOREON DUPLICATE KEY UPDATE 避免唯一冲突中断流程

不同数据库的分批写法差异

语法细节决定成败。比如 PostgreSQL 没有 LIMIT 在子查询里的限制,但 MySQL 8.0+ 才支持 INSERT …… SELECTLIMIT;SQL Server 要用 TOPOFFSET/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 是兼顾吞吐与回滚成本的经验平衡点。

text=ZqhQzanResources