批量INSERT变慢本质是B+树分裂与redo log刷盘争抢资源:每条INSERT触发索引页分裂(尤以UUID等无序键为甚)和默认每次提交刷盘(innodb_flush_log_at_trx_commit=1),导致吞吐退化为逐条执行。

大批量 INSERT 变慢,本质是索引 B+ 树分裂和 redo log 刷盘在抢资源
MySQL 批量插入变慢,不是“写得慢”,而是每条 INSERT 都触发了两件高成本的事:一是二级索引的 B+ 树页分裂(尤其是非递增主键或无序字段建索引时),二是每条语句默认提交后都要刷一次 redo log 到磁盘(受 innodb_flush_log_at_trx_commit=1 控制)。这两者叠加,会让吞吐从“批量写”退化成“逐条扛压”。
为什么加索引会让 INSERT 直接掉速 5–10 倍?
索引不是免费的。每次插入新行,InnoDB 必须定位到对应索引页、检查是否满、不满就插,满了就得分裂——分裂涉及内存拷贝、页重分配、父节点更新,甚至可能触发递归分裂。尤其当插入值在索引中无序(比如用 UUID 或随机字符串做主键/索引字段)时,分裂频率极高。
- 主键是自增整型 +
INSERT按主键顺序来 → 分裂极少,性能接近线性 - 主键是
VARCHAR(36)的 UUID + 无序插入 → 几乎每几条就分裂一次,CPU 和 I/O 都被锁死 - 一个表有 5 个二级索引 → 每次插入要维护 5 棵独立 B+ 树,开销乘以 5
redo log 刷盘怎么偷偷拖慢你的批量导入?
默认配置 innodb_flush_log_at_trx_commit=1 表示每次事务提交都调用 fsync() 把日志刷到磁盘。即使你用 INSERT INTO ... VALUES (...), (...), (...) 一次插多行,只要没显式 BEGIN/COMMIT,MySQL 默认每条语句都是独立事务 —— 等于每插一行就 fsync 一次。机械盘上一次 fsync 耗时 5–15ms,插 10 万行就是近 10 分钟纯等 I/O。
- 临时提速:导入前执行
SET SESSION innodb_flush_log_at_trx_commit = 2(日志只写 OS cache,不强制刷盘) - 更稳做法:用显式事务包裹批量操作,例如
BEGIN; INSERT ...; INSERT ...; COMMIT;,这样整个批次只刷一次 log - 注意:
=0更快但崩溃会丢最近 1 秒事务,生产环境慎用
真正有效的提速组合拳
单改一个参数救不了批量插入。必须同时处理索引 + 日志 + 语句结构:
- 导入前
DROP INDEX掉非必要二级索引,导入完成再CREATE INDEX(比边插边建快 3–8 倍) - 确保主键是
BIGINT AUTO_INCREMENT,避免 UUID 或时间戳倒序插入 - 用
INSERT INTO t VALUES (...),(...),...,(...)单语句最多塞 1000 行(太多会超max_allowed_packet) - 配合事务控制:每 5000–10000 行
COMMIT一次,平衡崩溃恢复成本与锁持有时间 - 确认
innodb_buffer_pool_size足够大(建议设为物理内存 50%–75%),否则索引页频繁换入换出
最容易被忽略的是:很多人关了 autocommit 就以为万事大吉,却忘了应用层 ORM(比如 Django 的 bulk_create)默认仍按单条生成 SQL,或者没设 batch_size —— 最终还是在发 10 万条独立 INSERT。


















