真正快的批量插入往往绕开INSERT语句本身,优先用INSERT...SELECT或LOAD DATA INFILE直通存储引擎,跳过SQL解析层;同时需关闭autocommit、避免触发器、控制单批5000行以内。

大批量 INSERT 慢,90% 不是 SQL 写得不对,而是数据库在“边写边整理”——每插一行,它就在更新索引、检查触发器、刷日志、等锁。
为什么单条 INSERT 拼成 VALUES() 还是慢?
你以为把 1000 行合并成一条 INSERT INTO t (a,b) VALUES (),(),... 就万事大吉?不一定。MySQL 仍要为每一行执行完整的插入流程:校验约束、定位 B+ 树位置、分裂页、写 redo log、触发器逐行执行……它不是“批量执行”,而是“批量提交”。
- 如果表上有 3 个二级索引,插入 1 行 = 写主键索引 + 写 3 个二级索引,共 4 次随机写入
- 若某索引字段基数极低(如
status TINYINT),B+ 树频繁页分裂,IO 放大更明显 -
max_allowed_packet默认 4MB,拼太多行会直接报错Packets larger than max_allowed_packet are not allowed,别硬堆到 5000 行
触发器让 INSERT 变成 N×慢,而不是慢一点
哪怕是个空触发器,也会强制事务串行化;而含 SELECT 或 JSON 解析的触发器,在批量插入时开销直接乘以 N。
-
INSERT INTO t SELECT * FROM s LIMIT 10000→ 触发器执行 10000 次,不是 1 次 - 触发器里写
json_extract(NEW.payload, '$.user_id')?10 万行 = 10 万次 JSON 解析,无缓存、不复用 - 用
IF EXISTS(SELECT 1 FROM ref WHERE id = NEW.ref_id)做校验?批量下只查第一行,其余静默通过,逻辑错误
autocommit=ON 是隐形性能杀手
默认开启时,每条 INSERT 都是独立事务:写 redo、刷盘、释放锁、清理事务状态——光日志刷盘就可能卡住整条链路。
- 查当前状态:
SHOW VARIABLES LIKE 'autocommit',结果为ON就是隐患 - 正确做法:显式
BEGIN/COMMIT包裹批量操作,或连接串加?useSSL=false&autoReconnect=true&rewriteBatchedStatements=true(JDBC 场景) - 注意:长事务不能太长,否则阻塞 purge 线程、拖慢 MVCC,建议单批控制在 5000 行以内
真正快的批量插入,往往绕开 INSERT 语句本身
当数据来自文件或 staging 表,INSERT ... SELECT 和 LOAD DATA INFILE 能跳过 SQL 解析层,直通存储引擎。
-
INSERT INTO t SELECT * FROM staging WHERE batch_id = 123:数据不出库,无网络传输,但需确保staging.batch_id有索引 -
LOAD DATA INFILE '/tmp/data.csv':比等量INSERT快 5–10 倍,但要求文件在 MySQL 服务端,且需FILE权限 - 如果用 MyBatis,务必确认连接参数含
rewriteBatchedStatements=true,否则addBatch()仍是逐条发送
最易被忽略的一点:慢 INSERT 很少单独发生。它常和 Page cleaner took XXXms 错误日志、Lock wait timeout exceeded 或 query end 状态一起出现——这时候问题已经不在 SQL,而在磁盘吞吐、buffer pool 大小或上游大事务积压。先看 error.log,再动 SQL。










