在日常的后端开发中,我们经常会遇到数据迁移、初始化、或者日志归档等场景,需要向数据库中导入海量数据。
"老板让我往数据库插 1000 万条数据,我写了个 for 循环,跑了一晚上还没跑完..."
如果你还在用 for 循环单条插入,那这篇通过实测数据说话的文章,绝对能帮你打开新世界的大门。今天我们就以 MySQL 为例,实测对比 5 种 常见的插入方式,看看谁才是真正的“性能之王”。
🛠️ 测试环境与准备
为了保证测试的公平性,我们统一测试环境:
- 数据库:MySQL 8.0 (Docker 部署)
- ORM 框架:Spring Data JPA (Hibernate) / MyBatis / JDBC
- 测试数据量:1000 万条 (分批次测试)
- 表结构:一张简单的用户表
user(id, username, password, email, create_time)
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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
1. 🐢 青铜选手:For 循环单条 Insert
这是最直观、最容易想到的方式,也是性能最差的方式。
代码示例 (JPA):
public void insertOneByOne(List<User> 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 方法,看起来像是批量操作,但真的快吗?
代码示例:
public void saveAll(List<User> 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_size=1000 开启 Hibernate 的批量插入支持,性能会有所提升,但依然受限于 Hibernate 的一级缓存机制,内存占用较高。
3. 🥇 黄金选手:MyBatis 的 foreach 拼接 SQL
这是 MyBatis 用户最常用的批量插入方式。
代码示例 (XML):
<insert id="batchInsert">
INSERT INTO user (username, password, email, create_time) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.username}, #{item.password}, #{item.email}, #{item.createTime})
</foreach>
</insert>
原理分析:
这种方式会生成一条巨长的 SQL:
INSERT 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 批处理。
代码示例:
public void jdbcBatchInsert(List<User> 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();
}
}
关键配置:
连接字符串必须加上 rewriteBatchedStatements=true,否则 executeBatch 依然是一条条发送!
jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
原理分析:
开启 rewriteBatchedStatements 后,MySQL 驱动会在客户端将多条 INSERT 语句重写为 INSERT ... VALUES (...), (...) 的形式。相比 MyBatis,它省去了框架解析 XML 和映射对象的开销。
实测结果:
插入 10 万条数据耗时约 1.5 秒。
推算 1000 万条数据需要 2.5 分钟。
评价:性能极致,内存占用低,适合对性能有极高要求的场景。
5. 👑 王者选手:MySQL LOAD DATA INFILE
如果说前面的都是在“写代码”,那这个就是在“开挂”。这是 MySQL 官方提供的文件导入命令。
代码示例:
LOAD DATA INFILE '/data/users.csv'
INTO TABLE user
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
(username, password, email, create_time);
原理分析:
直接读取文件流,绕过了 SQL 解析层,直接操作存储引擎。这是数据库导入数据的最快方式,没有之一。
实测结果:
插入 1000 万条数据耗时约 1-2 分钟 (取决于磁盘 IO)。
评价:降维打击。
缺点:
- 需要先生成文件(CSV/TXT)。
- 需要数据库服务器的文件读取权限。
- 逻辑较死板,不适合复杂的业务校验。
📊 最终排行榜 (1000 万数据估算)
| 排名 | 方式 | 耗时估算 | 复杂度 | 推荐指数 | 适用场景 |
|---|---|---|---|---|---|
| 1 | LOAD DATA INFILE | ~1 分钟 | 高 (需文件) | ⭐⭐⭐ | 离线数据迁移、初始化 |
| 2 | JDBC Batch | ~2.5 分钟 | 中 | ⭐⭐⭐⭐⭐ | 高性能业务代码 |
| 3 | MyBatis Foreach | ~4 分钟 | 低 | ⭐⭐⭐⭐ | 日常批量操作 (中小数据量) |
| 4 | JPA saveAll | ~20 分钟 | 极低 | ⭐⭐ | 少量数据,偷懒专用 |
| 5 | For 循环单插 | ~5.7 天 | 低 | ☠️ | 离职前以此代码交接 |
💡 总结与建议
- 日常开发 (几千/几万条) :直接用 MyBatis foreach,简单方便,性能足够。记得分批(每批 1000 条左右)。
- 高性能要求 (几十万/百万条) :使用 JDBC Batch,并开启 rewriteBatchedStatements=true。
- 海量数据迁移 (千万/亿级) :别犹豫,生成 CSV 文件,用 LOAD DATA INFILE。
- 永远不要在循环里写 SQL 插入!
希望这篇文章能帮你避开性能坑,成为团队里的“性能优化大师”!觉得有用点个赞吧 👍