🚀 数据库插入 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 复制代码
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):

Java 复制代码
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 方法,看起来像是批量操作,但真的快吗?

代码示例:

Java 复制代码
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):

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 批处理。

代码示例:

Java 复制代码
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 官方提供的文件导入命令。

代码示例:

SQL 复制代码
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 天 ☠️ 离职前以此代码交接

💡 总结与建议

  1. 日常开发 (几千/几万条) :直接用 MyBatis foreach,简单方便,性能足够。记得分批(每批 1000 条左右)。
  2. 高性能要求 (几十万/百万条) :使用 JDBC Batch,并开启 rewriteBatchedStatements=true。
  3. 海量数据迁移 (千万/亿级) :别犹豫,生成 CSV 文件,用 LOAD DATA INFILE
  4. 永远不要在循环里写 SQL 插入!

希望这篇文章能帮你避开性能坑,成为团队里的"性能优化大师"!觉得有用点个赞吧 👍


相关推荐
qianshang2332 小时前
SQL注入学习总结
网络·数据库·渗透
what丶k2 小时前
深入解析Redis数据持久化:RBD机制原理、实操与生产最佳实践
数据库·redis·缓存
瀚高PG实验室3 小时前
通过数据库日志获取数据库中的慢SQL
数据库·sql·瀚高数据库
Hgfdsaqwr3 小时前
Python在2024年的主要趋势与发展方向
jvm·数据库·python
invicinble3 小时前
对于Mysql深入理解
数据库·mysql
阳光九叶草LXGZXJ4 小时前
达梦数据库-学习-47-DmDrs控制台命令(LSN、启停、装载)
linux·运维·数据库·sql·学习
Hgfdsaqwr4 小时前
掌握Python魔法方法(Magic Methods)
jvm·数据库·python
s1hiyu4 小时前
使用Scrapy框架构建分布式爬虫
jvm·数据库·python
2301_763472464 小时前
使用Seaborn绘制统计图形:更美更简单
jvm·数据库·python
熊文豪4 小时前
金仓数据库如何以“多模融合“重塑文档数据库新范式
数据库·金仓数据库·电科金仓·mongodb迁移