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

0 阅读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)
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 万数据估算)

排名方式耗时估算复杂度推荐指数适用场景
1LOAD DATA INFILE~1 分钟高 (需文件)⭐⭐⭐离线数据迁移、初始化
2JDBC Batch~2.5 分钟⭐⭐⭐⭐⭐高性能业务代码
3MyBatis Foreach~4 分钟⭐⭐⭐⭐日常批量操作 (中小数据量)
4JPA saveAll~20 分钟极低⭐⭐少量数据,偷懒专用
5For 循环单插~5.7 天☠️离职前以此代码交接

💡 总结与建议

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

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