做数据导入的时候,我最怕听到的一句话就是:“就几万条数据,怎么跑了半小时还没跑完?”在MySQL上做批量插入,很多同学第一反应是写个循环,一条INSERT接一条地怼进去。我早些年也是这么干的,直到被线上一个跑了将近一个小时的导入任务教育了,才老老实实把“MySQL 批量插入”这件事从头到尾摸了一遍。
这篇内容不是什么源码级剖析,而是我在实际项目中反复调优、踩坑之后沉淀下来的实战方法。你可以直接照着改,也可以先理解原理再按自己的场景调整。无论你是刚接触MySQL的新手,还是已经在写存储过程、处理大数据导入的开发者,这篇文章的目标只有一条:让你的批量插入不再成为整个数据链路的瓶颈。
1. 为什么业务代码里的批量插入经常不生效:从网络往返说起
1.1 一次INSERT到底有多少往返开销
先说个容易被忽略的事实:你写一个INSERT语句执行成功,哪怕数据只有一行,客户端和服务端之间也不是“把SQL发过去、等结果回来”这么简单。一次典型的单条插入流程包括:SQL文本打包、网络传输、服务端解析、权限检查、执行计划生成、事务与日志处理、结果回包。这个过程中,网络往返(round trip)的时间往往比执行本身还稳定——本地测试大概在0.5到1毫秒,跨机房环境下轻松超过几毫秒。
我见过一个真实的接口,循环往MySQL里插入3万条数据,每条记录大概200字节。按每条1毫秒的网络往返算,光等待网络就是30秒,这还没算SQL解析、索引维护、事务提交这些额外消耗。更麻烦的是,默认自动提交模式下,每条INSERT都自带一个事务,每次都要走一次redo log刷盘,这个开销相当可观。
1.2 很多框架的“批量”是伪批量
很多人觉得自己已经用了批量插入,因为代码里写了addBatch(),或者MyBatis里用了foreach拼SQL。但实测下来没什么提速效果,为什么?大概率是下面两种情况:
- 程序里其实是
for循环逐条执行INSERT,表面封装了批量接口,实际还是单条网络往返; - 用了
addBatch(),但JDBC连接串里没开rewriteBatchedStatements=true,MySQL驱动会“贴心地”帮你把批量拆成逐条发送,服务端收到的还是一条一条的SQL。
还有一类是MyBatis的foreach拼大SQL,这种方式本身有效——它确实把多条记录合并成一条SQL了,但它不是真正的JDBC批次,而是靠拼文本生成的巨型SQL。一旦数据量大,很容易撞上max_allowed_packet的上限,后面我会专门讲这个参数。
1.3 真正的批量INSERT长什么样
核心思路很简单:把多条记录合并到一条INSERT语句里,一次网络往返搞定。
INSERT INTO t_user (name, age, city) VALUES ('张三', 25, '杭州'), ('李四', 30, '上海'), ('王五', 28, '北京');这样网络往返次数从N次降到1次,SQL解析也只有一次。实测在普通SSD机器上,单条插入1万行和批量合并后插入1万行,差距可以达到十倍以上。
那么一次拼接多少条合适?我通常控制在500到1000条一批。太小了网络往返仍然多,太大了容易吃满max_allowed_packet。一条批量SQL的估算体积大约是“单行平均字节数 × 批量行数 + SQL模板本身的长度”,建议控制在max_allowed_packet的80%以内,留出余量。
提示:批量插入不是行数越多越好。我见过有人一次拼5万条VALUES,结果超过包大小限制直接被MySQL拒绝,报错后整个批次全部回滚,白跑一趟。
2. MySQL服务端能扛多少:参数配置是批量导入的第一道天花板
2.1 max_allowed_packet:批量插入的物理上限
max_allowed_packet决定了MySQL最大能接收的单个数据包大小,默认在MySQL 8.0里是64MB,5.7时代是4MB。批量插入拼SQL时,如果整体尺寸超过这个值,会直接报错。这个参数要同时改服务端和客户端两边:MySQL JDBC驱动侧也有一个maxAllowedPacket属性,如果小于服务端配置,客户端发送前就会先失败。
我个人习惯在测试环境先跑一个极小批次,然后逐步扩大批次,卡在哪个值报错就回退一档。比如:
SET GLOBAL max_allowed_packet = 128 * 1024 * 1024; SET SESSION max_allowed_packet = 128 * 1024 * 1024;如果你用的是云数据库或者有专门的DBA团队,记得让他们改配置后重启或热更新,否则你这边改了会话级也没用。
2.2 innodb_buffer_pool_size:批量插入能不能扛住,看它
批量插入的核心瓶颈不只是网络,还在于InnoDB要把这些数据页加载到内存,再通过后台进程把脏页刷到磁盘。innodb_buffer_pool_size如果偏小,批量导入会频繁触发刷盘,导致写入速度忽快忽慢,甚至出现“一开始飞快,越跑越慢”的曲线。
这个参数是全局的,通常建议设为物理内存的60%到75%。比如16GB内存的实例,设个10GB到12GB比较合理。要注意的是,这个参数调整后一般需要重启MySQL才能完全生效(8.0里可以用ALTER INSTANCE SET做在线调整,但仍有诸多限制)。导入前如果条件允许,至少确认一下当前配置:
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';2.3 事务刷盘参数:批量导入期间的“临时暴力优化”
这是优化效果最直观的一组参数。默认情况下innodb_flush_log_at_trx_commit=1,代表每次事务提交都要把redo log刷到磁盘,保证任何时刻宕机数据都不丢。代价是每次提交都有一次磁盘fsync。批量导入时,可以临时把它改成0或2。
2:只把日志写到操作系统缓存,不强制刷盘,速度明显提升,但数据库异常断电可能丢一秒内的数据;0:由后台线程控制刷盘,性能最好,但崩溃时丢失窗口更大。
类似的还有sync_binlog,默认是1,表示每次事务提交都同步binlog到磁盘。导入期间可以临时设置为0,让MySQL自己决定什么时候刷。
这些参数只建议在导入窗口内设为“性能模式”,导入完成后必须恢复默认值。我还是那句话:生产环境务必确认自己能承担极端情况下丢失少量数据的风险,否则别动。我自己在非核心分析库上用过,核心交易库从来没这么干过。
优化的会话级设置可以这样:
SET SESSION autocommit = 0; SET SESSION innodb_flush_log_at_trx_commit = 0; SET SESSION sync_binlog = 0; SET SESSION unique_checks = 0; SET SESSION foreign_key_checks = 0;unique_checks=0的意思是导入期间暂时不做唯一性约束检查,需要你提前保证数据里没有重复的唯一键,否则会出现脏数据。foreign_key_checks=0同理,导入完成后必须重新校验外键关系。
2.4 一个最常见的误操作:ALTER TABLE ... DISABLE KEYS 在InnoDB上无效
很多人从MyISAM时代带过来的习惯:导入数据前先执行ALTER TABLE t DISABLE KEYS,导完再ENABLE KEYS。这个命令在InnoDB上完全无效,InnoDB不支持这样禁用二级索引的维护。
InnoDB的做法是什么?如果你有大量二级索引,且导入的数据规模远大于已有数据,最快的办法是:先DROP INDEX,导入完成后再重新CREATE INDEX。听起来反直觉,但我实际测过:保留索引逐条插入和维护索引的代价,往往比“先删索引、导完再建”要高得多。当然,如果你导入的是增量小数据(几千行以内),保留索引反而更方便,这个要按数据量权衡。
3. JDBC与MyBatis的批处理差异:同样的SQL,差距在驱动和会话
3.1 JDBC开启rewriteBatchedStatements后发生了什么
Java后端连MySQL做批量插入,最常见的坑就是连接串里少了rewriteBatchedStatements=true。没有这个参数时,MySQL驱动收到addBatch()后会一条一条发送SQL,和你单条循环没有本质区别。打开这个参数后,驱动才会把同一条预处理SQL的多组参数值拼成多行VALUES,真正在服务端执行批量插入。
推荐的生产级批量插入连接串长这样:
jdbc:mysql://127.0.0.1:3306/test_db? useUnicode=true&characterEncoding=utf8mb4& rewriteBatchedStatements=true& useServerPrepStmts=true&cachePrepStmts=trueuseServerPrepStmts=true会在服务端创建预处理语句,cachePrepStmts=true会缓存预处理语句,避免反复创建。这两项配合批量插入效果更佳。
对应代码模式如下:
try (Connection conn = DriverManager.getConnection(url, username, password); PreparedStatement ps = conn.prepareStatement( "INSERT INTO t_user (name, age, city) VALUES (?, ?, ?)")) { conn.setAutoCommit(false); int batchSize = 500; for (int i = 0; i < userList.size(); i++) { User u = userList.get(i); ps.setString(1, u.getName()); ps.setInt(2, u.getAge()); ps.setString(3, u.getCity()); ps.addBatch(); if ((i + 1) % batchSize == 0) { ps.executeBatch(); conn.commit(); } } ps.executeBatch(); conn.commit(); }注意setAutoCommit(false)和手动commit()。我见过有人批量接口开了,但没关自动提交,结果还是每条一个事务,性能并没有本质提升。批量插入的核心收益是“攒批+少提交”,两者缺一不可。
3.2 MyBatis里三种“批量”方式的实测区别
MyBatis阵营常见的批量插入有三种写法,效果差别很大:
| 方式 | 原理 | 适用规模 | 风险 |
|---|---|---|---|
foreach拼VALUES | 生成一条巨型INSERT SQL | 几百到几千条 | 拼太大会超max_allowed_packet |
ExecutorType.BATCH | 复用一条预处理SQL并攒批执行 | 几万条以上 | 动态SQL多时无法真正复用 |
手写JDBCbatch | 配合rewriteBatchedStatements | 不限 | 需要控制事务提交粒度 |
foreach写法示例:
<insert id="batchInsert" parameterType="list"> INSERT INTO t_user (name, age, city) VALUES <foreach collection="list" item="u" separator=","> (#{u.name}, #{u.age}, #{u.city}) </foreach> </insert>这个写法前面说过,本质是拼SQL,不是真正的服务器端批量。所以如果你用MyBatis,同时数据量在几万行以上,建议改用ExecutorType.BATCH:
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper = session.getMapper(UserMapper.class); for (int i = 0; i < userList.size(); i++) { mapper.insert(userList.get(i)); if ((i + 1) % 500 == 0) { session.flushStatements(); session.commit(); } } session.commit(); } finally { session.close(); }这里有个问题我在项目里踩过:BATCH模式下,动态SQL里有<if>这类条件片段时,每条记录的SQL结构可能不同,驱动需要重新解析,批效果会大打折扣。所以批量导入场景,我强烈建议先查出来需要插入的数据,然后统一走固定的INSERT模板,别在批量里搞花里胡哨的动态条件。
另外,BATCH模式下MyBatis默认不回填自增主键,因为批量执行时驱动不会逐条返回生成键。如果你后续逻辑需要每行的主键,要么自己事先分配好,要么用SELECT LAST_INSERT_ID()这类手段,别指望批量接口自动帮你把主键set回实体里。
3.3 连接池配置对批量导入的影响
批量导入通常伴随着高并发写入,连接池太小会导致线程互相等待,连接池太大又可能把数据库连接数打满。以HikariCP为例,我的经验是:导入任务如果是单线程跑,maximumPoolSize设5到10就够;如果是并行分片导入,按“分片线程数+少量备用连接”来设,比如8个分片线程就设10到12个连接。
还有个细节:连接池和max_allowed_packet是联动的。如果应用程序连接池用的连接长时间保持,而数据库参数在导入前被调整过,旧连接可能仍然沿用旧的会话参数。所以批量导入前最好重启应用或确保连接池能自动重建连接,否则你改了服务端配置,实际执行还是老样子。
4. 预处理语句的真正价值:不只是防SQL注入
4.1 服务端预处理为什么快
提到PreparedStatement,大部分人的第一反应是防SQL注入,第二反应是代码好写。但在批量插入场景,它还有一个被低估的作用:减少SQL解析开销。
MySQL执行一条SQL文本,要先做词法分析、语法分析、生成执行计划。虽然MySQL 8.0有查询缓存相关的优化(实际上8.0已经移除查询缓存),但解析和优化仍然有成本。服务端预处理通过COM_STMT_PREPARE预先把SQL模板解析好,后面每次执行只需要COM_STMT_EXECUTE传参数,省去了重复解析的环节。
批量插入时,我们用同一个INSERT模板配上不同参数,正是服务端预处理最擅长的工作。所以JDBC连接串里的useServerPrepStmts=true和cachePrepStmts=true不是锦上添花,是实打实的性能因素。
4.2 预处理与BATCH的协同逻辑
一套配合很默契的流程是这样的:
- JDBC驱动发送
PREPARE语句,服务端生成预处理对象并缓存; - 业务代码多次
setString/setInt/setXxx,每次addBatch()往客户端攒一组参数; - 攒到批次大小后,
executeBatch()触发驱动把整批参数发送给服务端; - 服务端拿着预处理模板+整批参数,直接走批量执行路径。
所以我在生产环境推荐开启这三个参数:rewriteBatchedStatements=true、useServerPrepStmts=true、cachePrepStmts=true。尤其是第一条,没开的批量只是心理安慰。
但注意一个边界:如果批量SQL本身每次都不一样(比如动态拼接了不同的表名、不同的字段集合),预处理就失去了模板复用的价值,还可能因为预处理缓存累积导致服务端内存压力。这时不如退回到普通拼接SQL。批量导入最忌讳把“复用模板”和“动态拼SQL”混在一起用。
4.3 占位符的安全与转义问题
批量插入用预处理占位符还有一层好处:参数值里有单引号、反斜杠、换行符这类特殊字符时,驱动会帮你处理好,不用自己在SQL文本里手工转义。如果是foreach拼SQL,你反而要小心字段值里带单引号导致SQL语法错误。
我遇到过一个case:从CSV导入用户备注字段,里面既有双引号又有换行符,用foreach拼SQL时直接把一条记录拆成了两行,报错不说,数据还错了。后来改用PreparedStatement占位符,问题一次解决。千万不要觉得“批量插入就是拼字符串”,数据清洁度和SQL模板的稳定性在这种场景里同样重要。
5. 导入大表时的锁竞争与Binlog放大效应
5.1 InnoDB插入时的锁:超卖问题先放一边,插入锁才是关键
批量插入时,InnoDB并不是无锁一路狂写。每个插入操作都会涉及插入意向锁(Insert Intention Lock),二级索引也要加锁维护。数据量大时,如果多个写事务并发操作同一范围的主键或唯一键,还会出现锁等待和死锁。
自增列还有一个专门的innodb_autoinc_lock_mode参数:
0:每次插入都持有表级自增锁,保证严格连续,但并发差;1:普通插入使用互斥锁批量申请,性能较好,默认值;2:交错模式,插入交错分配自增值,并发最高,但自增值不连续。
批量导入场景,如果你不需要自增ID严格连续,可以对会话临时设置innodb_autoinc_lock_mode=2提升性能。这个参数是全局的,MySQL 8.0里可以在配置文件中改,但要真正生效需要重启。这里有个矛盾:重启代价大,而且生产实例不是你说了算。所以我的做法是:能在建表前设计好批量插入方案就直接用自然主键或业务主键,省得让自增锁成为瓶颈。
5.2 批量INSERT与Binlog的放大关系
很多人对binlog的理解是“主从同步用的日志”,但在大规模导入时,binlog会实实在在拖慢写入。
在binlog_format=ROW格式下,MySQL会把每一行数据的变更记成事件。你一条批量INSERT插了1000行,binlog里就会记录1000行对应的变更事件,日志量随数据量线性放大。如果你同时还有主从复制,从库也要重放这些日志,压力全部在链路上放大。
有一个思路是在导入前确认这个实例不是复制链路的关键节点,然后临时关掉SQL级binlog记录:
SET SESSION SQL_LOG_BIN = 0;这个操作非常危险,一旦实例意外宕机或切换,主从数据就不一致了,之后怎么补救都是麻烦。我只有在独立分析库、无任何从库依赖、且允许重建数据的场景下才用过。生产环境的常规项目,我强烈不建议碰这个开关。更稳妥的做法是:接受binlog开销,在评估导入时间时预留充足余量。
5.3 触发器与外键:导入时别忽略的“隐形参与者”
批量导入时,如果目标表上有触发器,无论你插多少行,触发器都会逐行执行。这会让你的导入时间直接翻倍甚至更多。导入前先检查一下:
SHOW TRIGGERS LIKE 't_user';如果触发器只服务于业务写入(比如记录审计日志),大规模初始化数据时可以先把触发器删掉或停用,导完数据再恢复。外键也一样,foreign_key_checks=0只是部分缓解了子表扫描校验,但如果有复杂的级联操作,性能影响仍然不小。批量导入前的表结构清理,和调参一样重要,甚至更重要。
6. 配套的批量删除与批量更新:导入流程里的两次清扫
6.1 清空旧数据的正确方式:TRUNCATE与DELETE的取舍
导入前要清空目标表,这个步骤看起来不起眼,却决定了整个导入任务的总时长。TRUNCATE TABLE是DDL操作,直接重建表结构并释放数据页,不逐行删除,速度极快,但同时不可回滚;DELETE FROM是DML操作,逐行删,要记redo、binlog,还可能因为大事务拖垮实例。
所以我的规则很简单:
- 整表替换场景,用
TRUNCATE; - 需要清理符合特定条件的部分数据,用分批
DELETE; - 需要回滚保障,宁可慢一点,用
DELETE并保证事务可控。
TRUNCATE在InnoDB上的一个实际表现需要注意:它会隐式提交事务,并且速度虽快,但做表结构重建时对内存和锁资源也有要求。大表TRUNCATE后如果紧接着大批量插入,建议先等缓冲池状态稳定一点,别一口气跑到CPU飙升。
6.2 分批删除:一次DELETE一百万行的教训
有一次我需要清理一个月前的流水数据,数据量大概800万行。一开始图省事,直接一行DELETE FROM flow_log WHERE create_time < '2024-01-01',结果锁表时间太久,业务侧直接报警。后来我改成循环分批删除,每批5000行,重试三次,总耗时反而更短,而且对在线业务几乎无感。
老生常谈的批处理逻辑如下:
DELETE FROM flow_log WHERE create_time < '2024-01-01' LIMIT 5000;配合应用层循环:
while (影响行数 > 0): 执行上述DELETE语句 每次删除后sleep 50ms,避免持续占用IO 如果超时或死锁,稍等后重试这里有个细节:LIMIT配合DELETE在MySQL里是允许的,效果是每次最多删除指定行数。但如果没有合适的索引,即使每次只删5000行,扫描范围仍然很大,必须确保WHERE条件能走索引,否则分批删比一次性删更慢。
6.3 批量UPDATE的惯用技巧:用CASE WHEN省网络往返
批量导入不只有INSERT,还有一个高频场景是“数据已存在,覆盖更新”。如果逐条UPDATE,又会回到网络往返的老问题。常用做法是把多条更新合并到一条SQL里,用CASE WHEN做行级映射:
UPDATE t_user SET age = CASE name WHEN '张三' THEN 26 WHEN '李四' THEN 31 WHEN '王五' THEN 29 END, city = CASE name WHEN '张三' THEN '苏州' WHEN '李四' THEN '南京' WHEN '王五' THEN '深圳' END WHERE name IN ('张三', '李四', '王五');这个写法把N次UPDATE变成一次,收益和批量INSERT一致。但要注意:WHERE name IN (...)列出的行数不能太大,我一般控制在500行以内;同时name字段必须有唯一索引,否则CASE WHEN的映射可能出现多行更新,结果不符合预期。
6.4 导入流程的事务边界设计
整条导入链路通常是:清空旧表(或删除旧数据)→ 批量插入新数据 → 校验数据 → 重建索引或归档。一个常见错误是把“清空+全量导入”包进同一个事务。这样做的本意是保证要么不动,要么全体替换,逻辑上很好看。但数据量大时,一个事务里的undo log会膨胀得极其夸张,事务耗时长还会让间隙锁、插入意向锁范围变大,最后不仅没保住一致性,反而把导入拖垮。
我的做法是拆成多个独立步骤,每个步骤单独提交:清空数据算一个事务,每500到1000条插入算一个事务,校验阶段只读不加锁。如果中途失败,用记录表或文件标记当前进度,下次从断点继续。这是一种工程化的妥协:强一致性的代价是性能和可用性,批量导入这类离线型任务更适合“可重试”而不是“绝对串行”。
7. LOAD DATA INFILE:当批量INSERT还不够快时的终极方案
7.1 为什么LOAD DATA能比INSERT快这么多
如果数据已经躺在文件里(CSV、TSV、定宽文本),或者你做的是跨库迁移、数据倾卸,那么LOAD DATA INFILE是比批量INSERT更合适的选择。MySQL官方文档说它可以达到普通INSERT语句几十倍的速度,我实测也基本符合这个量级。
原理很简单:普通INSERT需要经过完整的SQL解析层、权限检查、执行计划生成,而LOAD DATA INFILE直接把文件内容按约定格式解析后,交给存储引擎做批量加载,中间省掉了大量SQL文本解析和网络协议开销。它甚至比“拼一条大SQL批量INSERT”更快,因为后者仍然要服务端解析一轮SQL文本。
7.2 LOAD DATA INFILE的实用配置
基本用法:
LOAD DATA INFILE '/data/user.csv' INTO TABLE t_user CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (name, age, city);这里几个关键配置项:
FIELDS TERMINATED BY:字段分隔符,CSV就是逗号,TSV就是制表符;ENCLOSED BY:字段包裹符,通常处理带引号的字符串;LINES TERMINATED BY:行分隔符,注意Windows下的CRLF要写'\r\n';IGNORE 1 LINES:跳过首行表头;(name, age, city):按列名映射文件字段,列可以少选,也可以乱序。
如果文件在客户端而不是数据库服务器上,需要加LOCAL关键字:
LOAD DATA LOCAL INFILE '/local/path/user.csv' INTO TABLE ...LOCAL版本意味着文件从客户端上传到服务端,MySQL 8.0默认local_infile是关闭的,需要SET GLOBAL local_infile = 1,并且客户端连接串也要加allowLoadLocalInfile=true。这个开关有安全隐患(用户可能借此读取服务器本地文件),非必要不开启,开了也要收窄文件目录权限。
提示:LOAD DATA默认还会触发外键检查、唯一键检查、binlog日志记录。导入前和批量INSERT一样,先
SET SESSION foreign_key_checks=0; SET SESSION unique_checks=0;,能明显缩短时间。
7.3 文件清洗与字符集是LOAD DATA最容易翻车的地方
用LOAD DATA导入中文数据时,很容易出现乱码。核心原因是源文件编码和表字符集不一致。我建议所有文件统一转成UTF-8,导入时显式指定CHARACTER SET utf8mb4,不要依赖数据库默认字符集。文件里有NULL值时,用\N表示(MySQL的LOAD DATA约定),或者导入前先做预处理替换成空字符串。
另外,如果CSV字段里本身包含逗号和换行符,ENCLOSED BY '"'能解决大部分问题,但如果字段里还有双引号,就需要转义处理。这类文件清洗脚本我通常用Python先跑一遍,搞定编码和转义再交付给LOAD DATA,不要指望MySQL能容错。
8. 实测数据对比:不同批量大小、不同配置下的耗时曲线
8.1 测试条件与方法
为了给一个直观的参考,我在自己的开发机上跑过一组对比测试。环境:MySQL 8.0,8核CPU,16GB内存,SSD盘,InnoDB引擎,目标表无大字段,表里有主键和一个普通二级索引。数据量100万行,每行大约200字节。
测试方式分别覆盖:单条INSERT、批量500条、批量1000条、开启rewriteBatchedStatements后的批量、调优参数后的批量、以及LOAD DATA INFILE。耗时记录从服务端日志角度取运行时间,去掉代码编译和JVM启动等因素干扰。
8.2 耗时结果表
| 方案 | 百万行耗时 | 说明 |
|---|---|---|
| 单条INSERT,自动提交 | 约270秒 | 每条网络往返+事务提交,慢出了天际 |
| JDBC批量500条,未开rewrite | 约230秒 | 驱动逐条发,提升极其有限 |
| JDBC批量500条,开rewrite | 约38秒 | 网络往返大幅减少,核心优化开始生效 |
| JDBC批量1000条,开rewrite,手动每500条提交 | 约31秒 | 批次与提交粒度更匹配 |
| 调参后(关外键检查、unique_checks=0,innodb_flush_log_at_trx_commit=0) | 约19秒 | 减少了大量约束检查与日志刷盘 |
| LOAD DATA INFILE(同样调参) | 约7秒 | 绕开SQL解析层,效率天花板最高 |
同一批数据在不同环境下的绝对时间会有差异,但趋势是一致的:逐条插入是最慢的,开不开rewriteBatchedStatements差别可能小,差别大的是你是否理解了驱动到底做了什么;而参数调优、关闭约束、LOAD DATA带来的倍数差距,无论如何都值得去做。
8.3 完整导入链路的最佳姿势
根据上面这些实测和踩坑,我把一套“最快路径”总结成操作顺序,你遇到大数据量导入时可以直接照这个思路走:
- 第一步:设计表结构,确认主键、唯一键、索引。导入前评估是否需要临时删除二级索引。
- 第二步:执行会话级参数调整:
autocommit=0,innodb_flush_log_at_trx_commit=0,sync_binlog=0,foreign_key_checks=0,unique_checks=0。 - 第三步:如果数据在文件里,优先用
LOAD DATA INFILE;如果数据在应用内存里,用JDBC批量+rewriteBatchedStatements=true,每500到1000条提交一次。 - 第四步:导入完成后,立即恢复参数,重建之前删掉的索引,校验行数和数据完整性。
- 第五步:切流前,做一次主从或时间点备份,确保数据可用归档。
这个链路跑下来,我见过的导入任务从“小时级”降到“分钟级”是常态,个别任务从“跑不完”到“十几分钟搞定”也真实发生过。
最后再分享一个我自己的习惯:批量导入上线之前,我一定会拿生产环境的数据量级缩样先跑一轮压测,注意不是用测试数据随便跑跑,而是用真实字段分布、真实行数和真实索引结构。因为很多性能问题在数据量小的时候完全不存在,一旦数据量上来,参数、锁、binlog、缓冲池全都会暴露问题。提前跑一遍,比你上线后半夜被报警叫醒要舒服得多。