数据库插入 1000 万数据?别再傻傻用 for 循环了!实测 5 种方式效率对比

张开发
2026/4/21 14:52:56 15 分钟阅读

分享文章

数据库插入 1000 万数据?别再傻傻用 for 循环了!实测 5 种方式效率对比
在日常的后端开发中我们经常会遇到数据迁移、初始化、或者日志归档等场景需要向数据库中导入海量数据。老板让我往数据库插 1000 万条数据我写了个 for 循环跑了一晚上还没跑完...如果你还在用 for 循环单条插入那这篇通过实测数据说话的文章绝对能帮你打开新世界的大门。今天我们就以MySQL为例实测对比5 种常见的插入方式看看谁才是真正的“性能之王”。️ 测试环境与准备为了保证测试的公平性我们统一测试环境数据库MySQL 8.0 (Docker 部署)ORM 框架Spring Data JPA (Hibernate) / MyBatis / JDBC测试数据量1000 万条 (分批次测试)表结构一张简单的用户表 user (id, username, password, email, create_time)sql体验AI代码助手代码解读复制代码CREATE TABLE user (id bigint(20) NOT NULL AUTO_INCREMENT,username varchar(255) DEFAULT NULL,password varchar(255) DEFAULT NULL,email varchar(255) DEFAULT NULL,create_time datetime DEFAULT NULL,PRIMARY KEY (id)) ENGINEInnoDB DEFAULT CHARSETutf8mb4;1. 青铜选手For 循环单条 Insert这是最直观、最容易想到的方式也是性能最差的方式。代码示例 (JPA):Java体验AI代码助手代码解读复制代码public void insertOneByOne(ListUser users) {for (User user : users) {userRepository.save(user);}}原理分析每一次 save 操作都会建立一次数据库连接发送 SQL执行提交事务关闭连接。1000 万次网络 I/O 1000 万次事务开销 灾难。实测结果插入 1 万条数据耗时约50 秒。推算插入 1000 万条数据需要138 小时(约 5.7 天)。评价除非你是在写 Hello World否则严禁在生产环境使用。2. 白银选手JPA 的 saveAll (伪批量)Spring Data JPA 提供了 saveAll 方法看起来像是批量操作但真的快吗代码示例:Java体验AI代码助手代码解读复制代码public void saveAll(ListUser users) {userRepository.saveAll(users);}原理分析默认配置下Hibernate 的 saveAll 其实还是循环调用 save。虽然它在一个事务中执行减少了事务提交的次数但 SQL 依然是一条一条发的。INSERT INTO user ...INSERT INTO user ...实测结果插入 10 万条数据耗时约12 秒。推算 1000 万条数据需要20 分钟。评价比单条快了不少但依然不够看。 优化 Tip可以通过配置 spring.jpa.properties.hibernate.jdbc.batch_size1000 开启 Hibernate 的批量插入支持性能会有所提升但依然受限于 Hibernate 的一级缓存机制内存占用较高。3. 黄金选手MyBatis 的 foreach 拼接 SQL这是 MyBatis 用户最常用的批量插入方式。代码示例 (XML):Xml体验AI代码助手代码解读复制代码insert idbatchInsertINSERT INTO user (username, password, email, create_time) VALUESforeach collectionlist itemitem separator,(#{item.username}, #{item.password}, #{item.email}, #{item.createTime})/foreach/insert原理分析这种方式会生成一条巨长的 SQLINSERT INTO user (...) VALUES (...), (...), (...);数据库只需要解析一次 SQL构建一次执行计划大大减少了网络 I/O 和数据库解析开销。实测结果插入 10 万条数据耗时约2-3 秒。推算 1000 万条数据需要3-5 分钟。评价性能非常不错是日常开发的首选。⚠️ 注意SQL 长度限制MySQL 对 SQL 语句长度有限制 (max_allowed_packet)默认 4MB。如果一次拼接太多数据会报错。建议分批每批 1000-5000 条。解析成本MyBatis 解析动态 SQL 也需要时间数据量过大时解析会变慢。4. 钻石选手原生 JDBC Batch回归本质使用最底层的 JDBC 批处理。代码示例:Java体验AI代码助手代码解读复制代码public void jdbcBatchInsert(ListUser users) {String sql INSERT INTO user (username, password, email, create_time) VALUES (?, ?, ?, ?);try (Connection conn dataSource.getConnection();PreparedStatement ps conn.prepareStatement(sql)) {conn.setAutoCommit(false); // 开启事务for (int i 0; i users.size(); i) {User user users.get(i);ps.setString(1, user.getUsername());// ... 设置其他参数ps.addBatch();if ((i 1) % 1000 0) {ps.executeBatch(); // 执行批处理ps.clearBatch();}}ps.executeBatch(); // 处理剩余数据conn.commit();} catch (Exception e) {e.printStackTrace();}}关键配置连接字符串必须加上 rewriteBatchedStatementstrue否则 executeBatch 依然是一条条发送jdbc:mysql://localhost:3306/test?rewriteBatchedStatementstrue原理分析开启 rewriteBatchedStatements 后MySQL 驱动会在客户端将多条 INSERT 语句重写为 INSERT ... VALUES (...), (...) 的形式。相比 MyBatis它省去了框架解析 XML 和映射对象的开销。实测结果插入 10 万条数据耗时约1.5 秒。推算 1000 万条数据需要2.5 分钟。评价性能极致内存占用低适合对性能有极高要求的场景。5. 王者选手MySQL LOAD DATA INFILE如果说前面的都是在“写代码”那这个就是在“开挂”。这是 MySQL 官方提供的文件导入命令。代码示例:SQL体验AI代码助手代码解读复制代码LOAD DATA INFILE /data/users.csvINTO TABLE userFIELDS TERMINATED BY ,LINES TERMINATED BY \n(username, password, email, create_time);原理分析直接读取文件流绕过了 SQL 解析层直接操作存储引擎。这是数据库导入数据的最快方式没有之一。实测结果插入 1000 万条数据耗时约1-2 分钟(取决于磁盘 IO)。评价降维打击。缺点需要先生成文件CSV/TXT。需要数据库服务器的文件读取权限。逻辑较死板不适合复杂的业务校验。 最终排行榜 (1000 万数据估算)排名方式耗时估算复杂度推荐指数适用场景1LOAD DATA INFILE~1 分钟高 (需文件)⭐⭐⭐离线数据迁移、初始化2JDBC Batch~2.5 分钟中⭐⭐⭐⭐⭐高性能业务代码3MyBatis Foreach~4 分钟低⭐⭐⭐⭐日常批量操作 (中小数据量)4JPA saveAll~20 分钟极低⭐⭐少量数据偷懒专用5For 循环单插~5.7 天低☠️离职前以此代码交接 总结与建议日常开发 (几千/几万条)直接用MyBatis foreach简单方便性能足够。记得分批每批 1000 条左右。高性能要求 (几十万/百万条)使用JDBC Batch并开启 rewriteBatchedStatementstrue。海量数据迁移 (千万/亿级)别犹豫生成 CSV 文件用LOAD DATA INFILE。永远不要在循环里写 SQL 插入希望这篇文章能帮你避开性能坑成为团队里的“性能优化大师”觉得有用点个赞吧 原文链接https://juejin.cn/post/7592451588849057811

更多文章