🚀 数据库插入 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 插入!

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


相关推荐
lllsure1 天前
【MySQL】数据分片
数据库·mysql
语落心生1 天前
深入doris查询计划以及io调度(五)列式存储结构 - 分析Segment格式、列数据编码
数据库
DBA小马哥1 天前
金仓数据库 vs 达梦:MySQL迁移谁更胜一筹?
数据库·mysql·金仓数据库·kes
技术小泽1 天前
MQTT从入门到实战
java·后端·kafka·消息队列·嵌入式
Luna-player1 天前
那个在DG数据库中将多行指定字段的文本替换操作
数据库
それども1 天前
MySQL 执行计划中 filtered = 100 是什么意思
数据库·mysql
半夏知半秋1 天前
rust学习-Option与Result
开发语言·笔记·后端·学习·rust
独自破碎E1 天前
Spring Boot支持哪些嵌入Web容器?
前端·spring boot·后端
疯狂成瘾者1 天前
后端Spring Boot 核心知识点
java·spring boot·后端