1. 先把问题问清楚:你在问“条数”,但真正决定性能的是“批量大小”
大概每个写过数据导入脚本、做过接口联调、或者接过报表需求的人,都碰到过这个经典纠结:MySQL批量插入,一次插多少条最快?网上搜一圈,答案五花八门,有人说200条、有人说500条、有人说1000条、还有人拍胸脯说5000条也没问题。我最早干这事时也懵,拿着500条一批的配置跑了半天,换了个环境直接卡死,后来才发现问题根本不是“500”这个数字本身,而是我压根没搞懂批量插入的性能瓶颈到底在哪里。
先说结论:在绝大多数常规场景下,没有全局最优的“条数”,只有最适合你当前环境的“批量大小”。而批量大小由数据量(KB/MB)、事务时长、网络往返、MySQL参数限制共同决定。之所以网上那些“500条/1000条”的建议能流行,是因为这些数值在常见配置(单行几百字节、内网环境、默认max_allowed_packet)下,刚好落在一个“安全区”里,既不会因为批次太小导致网络往返过多,也不会因为批次太大把内存、锁和binlog搞爆。
这篇文章我打算把批量插入这件事拆开讲透:先说底层机制(为什么不是越大越好),再给一组不同场景下的推荐起始值,然后给出一个五分钟就能自测出“你环境里最优值”的方法,最后把实战里常见的报错和坑都列一遍。内容偏向实践,数据库内核原理部分会用生活化类比带过,能落地才是第一位的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 影响“最优批量条数”的底层因素,一次讲透
2.1 网络往返:RTT是第一个隐藏变量
批量插入最直接的好处就是减少客户端和MySQL服务端的交互次数。假设你一条一条插,一条耗时1ms(其中0.5ms是网络往返,0.5ms是执行),一万条就是10秒。如果你每100条合并成一个批次,往返次数从一万次降到一百次,理想状态下时间接近1秒。
这里的核心变量是RTT(往返时延)。内网环境下RTT基本在0.2ms-0.5ms,影响不大;但如果你连的是云数据库,或者客户端和服务端跨地域,RTT可能飙到5ms-20ms,这时候批次数量的优化效果会非常明显。我见过一个真实案例:某系统从本地连阿里云RDS,单条插入平均3ms,性能测试时只能跑到300条/秒,改成每批200条之后,直接跳到3000条/秒,整整十倍提升,原因就是网络往返被压缩了。
所以第一个经验是:网络越差,越要加大批次。但加大批次不意味着无限加大,因为后面的事务、锁、内存会开始拖后腿。
2.2 事务与锁:批量太大,锁持有时间成倍增长
MySQL的InnoDB引擎在批量插入时,如果所有插入在同一个事务里,意味着这批数据在事务提交前,相关的行锁、间隙锁、插入意向锁会一直持有。批越大,锁持有的时间越长,并发写入其他表(或同一张表不同范围)的操作就会被阻塞。
用一个生活类比解释:你一次性买了2000件商品推到收银台,收银员得一件一件扫码,期间后面所有顾客都得排队。如果你分开10次买,每次200件,中间还能让别的顾客插进来。数据库也是一样,事务太大虽然减少了提交次数,却增大了锁冲突的概率。
从实践来看,如果目标表上同时有大量读操作或少量写操作,一批500条-1000条是一个相对稳妥的区间;如果目标表几乎只有写入操作,没有任何并发访问,一批2000条-5000条也可以接受。关键在于你要清楚自己场景的性质,是“独占式批量导入”还是“业务内在线写入”。
2.3 索引维护与binlog:隐藏的CPU和IO开销
批量插入的性能瓶颈除了事务和锁,还有一个更容易被忽视的地方:索引维护和binlog日志。
- 如果一个表有3个二级索引,每插入一行,InnoDB不仅要更新聚簇索引(主键索引),还要同步维护3个二级索引。数据量越大,索引树的节点分裂和页拆分越频繁。
- binlog默认是ROW格式(也是现在MySQL 8.0的默认),每一条被修改的行都会在binlog里记录完整的前后镜像。一次性插入2000条,binlog缓冲里就会堆积2000条的写入量,提交时要同步到binlog文件,这个IO开销和批量大小是线性相关的。
- 如果开了半同步复制,binlog还要等待从库确认,batch越大,主库提交时的等待时间越长。
换句话说,单批数据量太大,相当于把一次性的大规模索引更新和日志落盘塞在一个事务里。短时间看吞吐量还行,但一旦磁盘IO能力一般(比如普通云盘),或者从库延迟本来就高,批量一大就很容易把主库卡住。
2.4 参数限制:max_allowed_packet是硬上限
还有一个硬性约束必须记住:max_allowed_packet。这个参数决定了MySQL服务端能接收的最大单次数据包大小。如果一条批量SQL的总大小超过了这个值,MySQL会直接报错——最常见的错误就是 Packet too large。
需要特别注意的是,这个参数有客户端和服务端两层:
- MySQL服务端:
max_allowed_packet默认值是64MB(8.0版本),但很多云厂商或公司DBA会调小到16MB或8MB。 - MySQL客户端(JDBC driver、mysqldump、命令行):也有自己的
max_allowed_packet限制,比如mysqldump默认可能是24MB。
所以批量插入时,总的数据量不仅要小于服务端限制,还要小于客户端限制,取两者较小值。假设你一行数据大约1KB,那么理论上单批最多能插约16000条(16MB/1KB),但考虑到网络传输和MySQL解析SQL的开销,实际操作中远达不到这个上限,就需要按照经验值来。
3. 没有万能数字,但有一组靠谱的参考值
3.1 不同接入方式下的推荐起始值
先说明:下面这组数值是基于我自己的项目经验和常见的MySQL默认配置(单行500字节-1KB、内网环境、普通SSD、能容忍在线业务少量并发)总结出来的,适合作为起点,不适合直接当作最终答案。
| 接入方式 | 单批推荐条数(起始值) | 单批推荐数据量(估算) | 备注 |
|---|---|---|---|
| JDBC + addBatch,未开启rewriteBatchedStatements | 300-500条 | 300KB-500KB | 注意:这个模式下批处理可能是假的,见3.2 |
| JDBC + addBatch,开启rewriteBatchedStatements | 1000-2000条 | 1MB-2MB | 最推荐的配置组合 |
| MyBatis / MyBatis Plus foreach拼接SQL | 500-1000条 | 500KB-1MB | 受SQL长度和占位符数量限制 |
| Spring JdbcTemplate batchUpdate | 500-1000条 | 500KB-1MB | JdbcTemplate内部也会分包执行 |
| 命令行source执行sql文件 | 无固定限制 | 建议控制在10MB以内 | 用split或分段生成SQL文件 |
| LOAD DATA INFILE | 不需要分条,整文件批量导入 | 建议最大不超过1GB | 性能远高于INSERT语句,优先考虑 |
3.2 JDBC/MyBatis Plus:rewriteBatchedStatements是分水岭
这个点值得单独拿出来说,因为很多人踩坑了都不知道。
JDBC的 addBatch() + executeBatch() 在MySQL驱动下的默认行为比较特殊:如果不开 rewriteBatchedStatements=true,驱动并不会把你攒的1000条合并成一条SQL发给MySQL,而是一条一条地发给MySQL服务端。也就是说,你的批量代码在白忙活,网络往返一点没减少,只是省了客户端到服务端的一些调用开销。
开启方式:
java复制String url = "jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8"
+ "&rewriteBatchedStatements=true";
这个参数一旦打开,MySQL驱动就会把批量插入的SQL重写成 INSERT INTO t (col1, col2) VALUES (...), (...), (...) 这种多值语法,再发给服务端,效果才会真正体现出来。开启前后,同样是1000条一批,耗时差距能达到5-10倍,一点都不夸张。
MyBatis Plus的批量插入有类似的坑:
saveBatch()默认会走JDBC的批量提交,此时必须确认连接URL上配置了rewriteBatchedStatements=true,否则性能提升有限。- 有些人手写
<foreach>拼接VALUES,这种方式每条SQL的占位符数量受MySQLmax_allowed_packet和数据库预处理语句的占位符上限(65535个)限制。一个INSERT多行语句,有10个字段,那一行就要占10个占位符,超过65535个占位符就会报错。所以这类SQL在做MySQL批量插入时,条数上限往往是占位符决定的,而不是你的意愿决定的。
3.3 命令行/LOAD DATA:另类的“批量插入”
如果数据量特别大,比如一次要导入几百万行,普通的INSERT方式(即使分批)都算不上最优解。MySQL提供一个专门干这事的命令:
sql复制LOAD DATA INFILE '/tmp/data.csv'
INTO TABLE your_table
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES;
LOAD DATA的效率比逐条INSERT高很多,原因是它走的是服务端直接读文件 + InnoDB的专用导入路径,跨过了客户端解析SQL的环节。数据源是文本文件,且有相当规模(几十万行以上)时,优先选它。
需要注意两点:
- 出于安全考虑,
LOAD DATA LOCAL INFILE(客户端文件)和LOAD DATA INFILE(服务端文件)都需要相关权限,部分云数据库默认关闭。 - 导入大量数据前,建议先
ALTER TABLE ... DISABLE KEYS关闭非唯一索引的更新,导入完再ENABLE KEYS,避免每插一行都同步更新索引树。这个操作对于插入性能的提升非常明显,有次我在本地测试一张有4个二级索引的表,关闭索引导入比不关闭快了接近3倍。
4. 五分钟自测法:找到你环境里的最优批量条数
前面给了很多参考值,但“别人环境里的最优值”不一定是“你的最优值”。与其到处问,不如花五分钟自己测一遍。方法很简单,用MySQL自带的信息和一段多批次的插入脚本就能判断。
4.1 测试脚本设计与前置准备
第一步,准备一张测试表:
sql复制CREATE TABLE perf_test (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
第二步,分别在批大小=100、200、500、1000、2000、5000、10000这几个档位下,分别插入相同总量(比如10万条)的数据,记录每个档位的耗时。用存储过程跑最方便:
sql复制DELIMITER $$
CREATE PROCEDURE batch_insert_test(IN total INT, IN batch_size INT)
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE batch_count INT;
SET batch_count = total / batch_size;
START TRANSACTION;
WHILE i < batch_count DO
INSERT INTO perf_test (name, status)
SELECT CONCAT('user_', i + 1), i % 2 FROM (
SELECT 1 UNION SELECT 2 UNION SELECT 3 ... -- 根据batch_size扩展,这里用数字表技巧
) t;
SET i = i + 1;
END WHILE;
COMMIT;
END$$
DELIMITER ;
存储过程主要用来控制批大小和事务边界。如果不想写存储过程,也可以用Python/Node脚本在应用层测试,只要能清晰控制每批条数即可。
注意几个前置条件:
- 清空测试表(
TRUNCATE),避免索引缓存、缓冲池状态影响结果。 - 把buffer pool设置得稍微大一点(如果权限允许),减少因数据量超过缓冲池导致的磁盘读写差异。
- 每测完一个档位,重启一下数据库连接(如果是脚本),让结果尽量公平。
4.2 测试结果怎么读:吞吐量与延迟的取舍
测试完,把数据整理成表格,比如10万条数据在不同批量下耗时大概长这样:
| 单批条数 | 总耗时(秒) | 吞吐量(条/秒) | 单批平均耗时(毫秒) |
|---|---|---|---|
| 100 | 6.32 | 15823 | 6.3 |
| 500 | 2.55 | 39216 | 12.8 |
| 1000 | 2.21 | 45249 | 22.1 |
| 2000 | 2.06 | 48544 | 41.2 |
| 5000 | 2.68 | 37313 | 134.0 |
| 10000 | 4.10 | 24390 | 410.0 |
从这个模拟数据能看到一个典型规律:吞吐量先升后降,存在一个峰值区间。本例中2000条/批附近表现最好,5000条以后明显劣化。虽然没有哪种情况是“唯一正确答案”,但从我的经验来看,曲线拐点通常出现在单批数据量达到1MB-4MB的区间,大家可以用自己的实际数据去验证。
判断最优值时不能只看吞吐量,还要考虑延迟稳定性。批量过大时,事务耗时会成倍增加。如果你的业务是交易链路的一部分,200ms-400ms的单批耗时往往是不可接受的;如果是晚间跑批任务,100ms-400ms则完全没问题。
4.3 数据初始化:首插和累计插入要区分
再补充一个容易忽略的细节:分批测试时建议把“首次全量插入”和“增量插入”分开看。如果测试表是空的,批量插入走的是顺序写,效率很高;但真实业务里,表里可能已经有上百万条数据,这时批量插入会面临页分裂、索引树节点重新平衡等问题,性能比空表插入差不少。
自测时,可以在表里预置50万条数据,再用同样的档位插入10万条,看看结果和空表时的差异。两条曲线一对比,你就能知道你环境的“真实最优批量值”大概是哪个档位了。我在实际项目中测过,预置数据后,最优批量值会比空表时小一些(比如从2000降到1000),原因是索引维护的开销开始占主导。
5. 实战中常见的批量插入问题与排查
5.1 报错排查速查表
批量插入的报错通常非常直接,但原因往往隐藏在配置里。我把线上线下遇到的典型错误整理成一个速查表:
| 报错信息 | 常见原因 | 解决方案 |
|---|---|---|
| Packet too large for buffer size | 单条SQL超过max_allowed_packet限制 | 调小batch_size,或调大max_allowed_packet(客户端+服务端都调) |
| The total number of locks exceeds the lock table size | 批量过大导致InnoDB锁内存不足 | 增大innodb_buffer_pool_size;或者降低单批次大小 |
| Lock wait timeout exceeded | 大批量事务持有锁太久,其他事务等待超时 | 缩小批量大小,缩短事务时间;检查是否有其他长事务 |
| MySQL server has gone away | 批量过大会导致连接超时或数据包超限 | 调小batch_size;检查连接超时参数;增加socketTimeout |
| PreparedStatement contains too many placeholders | mybatis foreach等拼接导致占位符超过65535 | 减小单批次条数,或改用JDBC批量接口 |
| Duplicate entry for key | 批量插入的数据中有主键/唯一键冲突 | 检查源数据去重,或使用INSERT IGNORE / ON DUPLICATE KEY |
这些错误里,Packet too large 和 Lock wait timeout exceeded 是出现频率最高的两个。前者侧面说明你单批太大了,后者说明你把事务拖得太长了。
5.2 批量插入导致主从延迟怎么处理
一批插入5000条以上,主库执行时间可能只要几百毫秒,但binlog传到从库执行时,如果从库配置弱或者单线程复制,延迟会瞬间飙升。表现就是主库写入后,从库查询数据迟迟看不到。
如果项目用了主从架构,并且对数据实时性有要求,有两个常规思路:
- 适当下调单批大小,控制主库事务的峰值执行时间,避免单次复制积压过大。
- 如果业务可以容忍,在低峰期跑大批量导入,比如凌晨执行。
- 同时可以观察从库的
seconds_behind_master(或MySQL 8.0里的replica_status),确认延迟是否恢复。
5.3 一个小技巧:监控批量插入的实时进度
大批量导入时,最怕的不是慢,而是看起来像卡住了。这种事我也碰到过,一个2000万行的导入,跑了二十分钟进度还没挪动,忍不住就Ctrl+C了,后来发现它其实在正常跑,只是没给进度反馈。
有一个轻量级的监控方法:批量导入过程中,隔几十秒执行一次SQL,观察进度:
sql复制SELECT COUNT(*) FROM perf_test WHERE id > 0;
或者在应用里按批次输出日志。如果是MySQL 5.7及以上,还可以在另一个会话里查:
sql复制SELECT ROWS_EXAMINED, ROWS_AFFECTED, LAST_EXECUTION_TIME, CURRENT_EXECUTION_TIME
FROM performance_schema.events_statements_current WHERE THREAD_ID = (SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_ID = CONNECTION_ID());
说实话,这些查询都不复杂,关键是要有“先确认在动,再决定是不是卡了”的意识。导入程序把日志打全了,操作员才能真正安心。
6. 最终的几条结论
聊了这么多,最后放几条我用了很多年也没出过问题的经验:
一次性大批量插入,直接看业务需求分情况处理。如果是几十万乃至几百万条的大文件导入,优先考虑 LOAD DATA INFILE,不要纠结INSERT批量条数。如果只能在应用层用JDBC/ORM拼接SQL,那么确认开启 rewriteBatchedStatements=true,然后把单批控制在1000-2000条(内网)或300-500条(跨地域网络),这个区间在绝大多数场景下不会翻车。
自测永远值得做,花五分钟跑一组对比,你就是自己环境里的专家。遇到性能骤降时,先看批量大小是不是已经超过了 max_allowed_packet,再看是不是把大批量事务和在线业务混在了一起。理论上能跑满不代表实际上稳定,事务时间、锁粒度、从库延迟才是生产环境最需要关注的三个指标。
