mybatis批量插入对比

本文涉及的产品
云数据库 RDS SQL Server,基础系列 2核4GB
RDS PostgreSQL Serverless,0.5-4RCU 50GB 3个月
推荐场景:
对影评进行热评分析
云原生数据库 PolarDB 分布式版,标准版 2核8GB
简介: 本文介绍了几种在 Spring Boot 项目中使用 MyBatis-Plus 进行批量插入操作的性能对比方法,包括手写循环插入、MyBatis-Plus 的 `saveBatch` 方法、自定义批量插入 SQL 以及开启 MySQL 的 `rewriteBatchedStatements=true` 参数的方式进行saveBatch对比。


测试的项目代码:https://gitee.com/lickq/jenkins-springboot-test/tree/batchSave/

自定义mybatis-plus拦截器,记录拦截次数

package com.springboot.mytest.interceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.util.Collection;
/**
 * 批量保存日志拦截器(实现MyBatis-Plus的InnerInterceptor接口)
 */
public class BatchSaveLoggerInnerInterceptor implements InnerInterceptor {
    static volatile Integer n = 0;
    @Override
    public boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        // 只处理插入操作
        if (ms.getSqlCommandType() == SqlCommandType.INSERT) {
            n++;
            System.out.println("触发次数"+n);
        }
        return true; // 允许继续执行更新操作
    }
    // 其他默认方法可以保持默认实现
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, org.apache.ibatis.mapping.BoundSql boundSql) throws SQLException {
    }
    @Override
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
    }
}

一、示例测试


1、for循环中调用save方法

@Test
    void test1() {
        List<MyTest> list = new ArrayList<>();
        for (Integer i = 0; i < 1000; i++) {
            MyTest test = MyTest.builder().name(i.toString()).age(i).build();
            list.add(test);
        }
        long startTime = System.currentTimeMillis();
        for (MyTest myTest : list) {
            testService.save(myTest);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("耗时:" + (endTime - startTime) / 1000 + "秒");
    }

最终输出:

耗时100秒,触发拦截器1000次。

2、调用mybatis-plus方法:saveBatch

@Test
    void test2() {
        List<MyTest> list = new ArrayList<>();
        for (Integer i = 0; i < 1000; i++) {
            MyTest test = MyTest.builder().name(i.toString()).age(i).build();
            list.add(test);
        }
        long startTime = System.currentTimeMillis();
        testService.saveBatch(list, 200);
        long endTime = System.currentTimeMillis();
        System.out.println("耗时:" + (endTime - startTime) / 1000 + "秒");
    }

最终输出:

一千条,每批次200条,分为5个批次提交

耗时40秒,触发拦截器1000次。

3、自己实现一个batchInsert方法

@Test
    void test3() {
        List<MyTest> list = new ArrayList<>();
        for (Integer i = 0; i < 1000; i++) {
            MyTest test = MyTest.builder().name(i.toString()).age(i).build();
            list.add(test);
        }
        long startTime = System.currentTimeMillis();
        testMapper.batchInsert(list);
        long endTime = System.currentTimeMillis();
        System.out.println("耗时:" + (endTime - startTime) / 1000 + "秒");
    }

batchInsert方法

/**
     * 批量插入数据
     * @param list 要插入的MyTest对象列表
     * @return 插入成功的记录数
     */
    @Insert("<script>" +
            "INSERT INTO test (id, name, age) VALUES " +
            "<foreach collection='list' item='item' separator=','>" +
            "(#{item.id}, #{item.name}, #{item.age})" +
            "</foreach>" +
            "</script>")
    int batchInsert(@Param("list") List<MyTest> list);

最终输出:

耗时1秒,触发拦截器1次。

4、还是用mybatis-plus方法:saveBatch,不过增加了rewriteBatchedStatements=true

@Test
    void test2() {
        List<MyTest> list = new ArrayList<>();
        for (Integer i = 0; i < 1000; i++) {
            MyTest test = MyTest.builder().name(i.toString()).age(i).build();
            list.add(test);
        }
        long startTime = System.currentTimeMillis();
        testService.saveBatch(list, 200);
        long endTime = System.currentTimeMillis();
        System.out.println("耗时:" + (endTime - startTime) / 1000 + "秒");
    }

最终输出:

耗时1秒,触发拦截器1000次。


二、mybatis-plus的saveBatch

底层代码:

@Transactional(
    rollbackFor = {Exception.class}
)
public boolean saveBatch(Collection<T> entityList, int batchSize) {
    String sqlStatement = this.getSqlStatement(SqlMethod.INSERT_ONE);
    return this.executeBatch(entityList, batchSize, (sqlSession, entity) -> {
        sqlSession.insert(sqlStatement, entity);
    });
}

1. 底层执行逻辑

saveBatch 方法本质上是通过 MyBatis 的 Executor.BATCH 执行器实现的,大致流程为:

  • 预编译 SQL:首先对插入语句(如 INSERT INTO table (...) VALUES (...))进行预编译,生成一个预编译 SQL 对象(PreparedStatement)。
  • 循环绑定参数:遍历待插入的实体列表,为预编译 SQL 循环绑定不同的参数(每次绑定一个实体的数据)。
  • 批量提交:当参数绑定到一定数量(或遍历结束)时,调用 executeBatch() 一次性次性提交所有绑定的参数。

2. 与开启 rewriteBatchedStatements 的区别

  • 未开启时
    MySQL 驱动不会重写 SQL,executeBatch() 会将批量操作拆分为多条独立的单条 INSERT 语句,通过网络逐条发送给数据库执行(只是客户端批量发起,数据库仍逐条处理)。
    例如,插入 1000 条数据会生成 1000 条 INSERT INTO ... VALUES (...) 语句,网络交互次数为 1000 次(或按批次大小分几次,但本质仍是单条执行)。
  • 开启时
    MySQL 驱动会将多条单条 INSERT 重写为一条批量 INSERT 语句(如 INSERT INTO ... VALUES (...), (...), (...)),只需一次网络交互即可完成批量插入,效率大幅提升。

3. 性能特点

  • 相比完全手写循环单条插入,saveBatch 未开启 rewriteBatchedStatements 时仍有优化:通过预编译 SQL 减少了 SQL 解析开销,且通过 BATCH 执行器减少了部分客户端与数据库的交互次数(按批次提交)。
  • 但由于未合并 SQL,网络交互次数仍较多,在大数据量(如万级以上)场景下,性能远低于开启 rewriteBatchedStatements 的情况。

总结

未设置 rewriteBatchedStatements=true 时,MyBatis-Plus 的 saveBatch 是通过预编译 SQL + 批量绑定参数 + 分批次提交的方式执行,但最终数据库仍会逐条处理插入语句。因此,对于大量数据的批量插入,建议开启 rewriteBatchedStatements 以利用 MySQL 的批量 SQL 优化。

三、解释

1、为什么示例2比示例1快?

由于示例2中采用了mybatis-plus的saveBatch,通过预编译 SQL 减少了 SQL 解析开销BATCH 执行器减少客户端与数据库的交互次数

1)、预编译 SQL 减少 SQL 解析开销

手写循环单条插入的问题:
for (MyTest myTest : list) {
            testService.save(myTest);
        }
  • 每次循环都会向数据库发送一条完整的 SQL 语句(如INSERT INTO Test(...) VALUES(...))。
  • 数据库收到每条 SQL 后,都需要执行「语法解析→语义分析→生成执行计划」的完整流程(即使 SQL 结构完全一样)。
  • 1000 条数据就会重复执行 1000 次相同的解析过程,这部分属于无意义的性能损耗。
saveBatch 的优化:

saveBatch 会使用 MyBatis 的预编译机制:

  • 首先将 SQL 模板(如INSERT INTO user(name, age) VALUES(?, ?))发送给数据库,数据库只解析一次,生成可复用的执行计划。
  • 之后循环时,只向数据库发送参数(name 和 age 的值),不再重复发送完整 SQL,也不再重复解析。
  • 1000 条数据只需要 1 次 SQL 解析,剩下的都是直接复用执行计划 + 填充参数,大幅减少数据库的解析开销。

从日志中也可以看出来:


2)、BATCH 执行器减少客户端与数据库的交互次数(按批次提交)

手写循环单条插入的问题:

手写循环插入时,默认是「执行一条 SQL 就立即提交一次」:

  • 每条 INSERT 语句都会触发一次客户端(应用程序)与数据库的网络交互(发送 SQL + 参数→数据库执行→返回结果)。
  • 1000 条数据就会产生 1000 次网络往返,而网络通信(哪怕是本地数据库)是相对耗时的操作(涉及 TCP 握手、数据包传输等)。
  • 同时,每次提交都会触发数据库的事务日志刷新(如 MySQL 的 binlog),频繁提交会加剧性能损耗。
saveBatch 的优化:

saveBatch 使用 MyBatis 的BATCH执行器,本质是「累积一定数量的 SQL 后再批量提交」:

  • 执行流程是:循环绑定参数→暂存到本地缓存→达到批次大小(默认 1000 条,可配置)→一次性发送所有参数到数据库执行→一次提交。
  • 例如插入 1000 条数据,可能只需要 1 次网络交互(或按配置的批次大小分几次,比如 200 条一批,只需 5 次),而非 1000 次。
  • 减少了网络往返次数,同时合并了事务提交,降低了数据库的日志刷新频率。

总结对比

操作方式

SQL 解析次数

网络交互次数

事务提交次数

手写循环单条插入

1000 次(每条都解析)

1000 次(每条一次)

1000 次(默认自动提交)

saveBatch(未开 rewrite)

1 次(只解析模板)

约 1 次(按批次提交)

约 1 次(按批次提交)



2、为什么示例3比示例2快?

(1)batchInsert 方法(用 <foreach> 标签)

  • 执行原理:通过 MyBatis 的 <foreach> 标签,直接在 SQL 层面拼接出一条合并后的批量插入语句
    例如,插入 3 条数据时,生成的 SQL 是:sql
INSERT INTO test (id, name, age) 
VALUES 
(#{item1.id}, #{item1.name}, #{item1.age}),
(#{item2.id}, #{item2.name}, #{item2.age}),
(#{item3.id}, #{item3.name}, #{item3.age})
  • 数据库执行方式:数据库会将这条 SQL 当作一个完整的批量操作处理,只需一次解析、一次执行,效率极高。

(2)未开启 rewriteBatchedStatements=true saveBatch

  • 执行原理:依赖 MyBatis 的 BATCH 执行器,虽然会预编译 SQL 并批量提交,但生成的仍是多条独立的单条插入语句(只是客户端一次性发送给数据库)。
    例如,插入 3 条数据时,实际执行的是 3 条独立 SQL:sql
INSERT INTO test (id, name, age) VALUES (#{item1.id}, #{item1.name}, #{item1.age});
INSERT INTO test (id, name, age) VALUES (#{item2.id}, #{item2.name}, #{item2.age});
INSERT INTO test (id, name, age) VALUES (#{item3.id}, #{item3.name}, #{item3.age});
  • 数据库执行方式:数据库会逐条解析并执行这 3 条 SQL(即使客户端一次性发送),本质仍是多次单条操作的累积,效率低于单条批量 SQL。

(3)性能差异的核心原因

对比维度

<foreach>

标签的批量插入

未开 rewriteBatchedStatements

saveBatch

SQL 数量

1 条(合并后的批量 SQL)

N 条(与数据量相同的单条 SQL)

数据库解析次数

1 次(仅解析一次批量 SQL)

N 次(每条 SQL 都需解析,即使结构相同)

数据库执行逻辑

一次批量写入操作

N 次单条写入操作的累积

网络交互次数

1 次(发送一条 SQL)

1 次(客户端批量发送 N 条 SQL,与前者相同)

可见,<foreach> 标签的批量插入在数据库层面的执行效率上占据绝对优势 —— 它将多条插入逻辑合并为数据库原生支持的批量操作,避免了数据库对多条单条 SQL 的重复解析和执行开销。

(4)注意事项

  • 这种方式的局限性是:当数据量极大(如超过 1 万条)时,拼接出的 SQL 语句会非常长,可能导致数据库接收的 SQL 长度超过限制(需调整数据库配置,如 MySQL 的 max_allowed_packet)。
  • 与开启 rewriteBatchedStatements=truesaveBatch 相比,两者最终效果类似(都生成 VALUES(...),(...) 形式的批量 SQL),但 <foreach> 是在应用层拼接,而 rewriteBatchedStatements 是在 MySQL 驱动层自动重写。



3、为什么示例4比示例2快?

rewriteBatchedStatements 是 MySQL JDBC 驱动(com.mysql.cj.jdbc.Driver)的一个连接参数,它在批量操作场景中与 MyBatis-Plus 的批量处理功能结合使用时能显著提升性能。

其核心作用是:开启 MySQL 的批量 SQL 重写功能,将多条相同结构的 SQL 语句合并为一条批量语句执行。

例如:

INSERT INTO user (name) VALUES ('a');
INSERT INTO user (name) VALUES ('b');

重写为

INSERT INTO user (name) VALUES ('a'), ('b');

从而减少网络交互次数,大幅提升批量操作的效率(尤其是数据量较大时)。


4、示例2和示例4谁更快

1. 两者的底层执行逻辑对比

(1)<foreach> 标签的批量插入
  • SQL 生成时机:在应用层(MyBatis 动态 SQL 阶段)直接拼接出一条完整的批量 SQL,例如:sql
INSERT INTO test (id, name, age) VALUES (1,'a',20), (2,'b',21), (3,'c',22)
  • 数据库执行:通过一次网络请求发送这条 SQL,数据库直接执行一次批量插入。
(2)开启 rewriteBatchedStatements=truesaveBatch
  • SQL 生成时机:MyBatis 的 BATCH 执行器先预编译单条 SQL(INSERT INTO test (...) VALUES (?)),然后循环绑定参数;最终由 MySQL 驱动在底层将多条单条 SQL 重写为上述批量 SQL 格式。
  • 数据库执行:同样通过一次网络请求发送重写后的批量 SQL,数据库执行一次批量插入。

2. 性能差异的关键场景

(1)数据量较小时(如 1000 条以内)

两者性能几乎无差别,因为最终执行的都是同一种格式的批量 SQL(VALUES(...),(...)),数据库处理逻辑完全一致。

(2)数据量较大时(如 1 万条以上)
  • <foreach> 标签可能略慢:因为 SQL 拼接在应用层完成,当数据量极大时,拼接出的 SQL 字符串会非常长(可能达几十 KB 甚至更大),这会消耗应用服务器的内存和 CPU(字符串拼接开销),且传输长 SQL 本身也会占用更多网络带宽。
  • saveBatch 可能更优:MySQL 驱动在重写 SQL 时采用更高效的二进制协议处理参数,避免了应用层拼接长字符串的开销,对内存和网络的压力更小。

3. 其他影响因素

  • 数据库配置:若 MySQL 的 max_allowed_packet 配置较小,<foreach> 拼接的超长 SQL 可能触发「数据包过大」错误,而 saveBatch 由驱动处理,更不容易触发此问题。
  • 代码维护性saveBatch 是 MyBatis-Plus 封装的方法,无需手动编写动态 SQL,代码更简洁;而 <foreach> 标签需要手动维护 SQL 模板,扩展性较差(如新增字段时需同步修改 SQL)。

结论

  • 中小数据量(推荐 saveBatch:两者性能接近,但 saveBatch 代码更简洁,且避免了手动拼接 SQL 的潜在问题(如 SQL 过长)。
  • 超大数据量(倾向 saveBatchsaveBatch 由驱动处理 SQL 重写,内存和网络效率更高,稳定性更好。
  • 极端场景验证:若对性能有极致要求,建议针对具体业务数据量进行压测(如 1 万、10 万条数据对比),不同数据库版本和服务器配置可能出现细微差异。


总体而言,开启 rewriteBatchedStatements=truesaveBatch 是更优的选择 —— 它兼顾了性能、稳定性和开发效率。

目录
相关文章
|
3月前
|
存储 SQL 关系型数据库
RDS DuckDB技术解析一:当 MySQL遇见列式存储引擎
RDS MySQL DuckDB分析实例以​列式存储与向量化计算​为核心,实现​复杂分析查询性能百倍跃升​,为企业在海量数据规模场景下提供​实时分析能力​,加速企业数据驱动型决策效能。​​
|
3月前
|
消息中间件 OLAP Kafka
Apache Doris 实时更新技术揭秘:为何在 OLAP 领域表现卓越?
Apache Doris 为何在 OLAP 领域表现卓越?凭借其主键模型、数据延迟、查询性能、并发处理、易用性等多方面特性的表现,在分析领域展现了独特的实时更新能力。
357 9
|
3月前
|
消息中间件 Java Kafka
Java 事件驱动架构设计实战与 Kafka 生态系统组件实操全流程指南
本指南详解Java事件驱动架构与Kafka生态实操,涵盖环境搭建、事件模型定义、生产者与消费者实现、事件测试及高级特性,助你快速构建高可扩展分布式系统。
232 7
|
4月前
|
JSON 关系型数据库 Apache
十亿 JSON 秒级响应:Apache Doris vs ClickHouse,Elasticsearch,PostgreSQL
JSONBench 是一个为 JSON 数据而生的数据分析 Benchmark,在默认设置下,Doris 的性能表现是 Elasticsearch 的 2 倍,是 PostgreSQL 的 80 倍。调优后,Doris 查询整体耗时降低了 74%,对比原榜单第一的 ClickHouse 产品实现了 39% 的领先优势。本文详细描述了调优思路与 Doris 调优前后的性能表现,欢迎阅读了解~
705 0
十亿 JSON 秒级响应:Apache Doris vs ClickHouse,Elasticsearch,PostgreSQL
|
3月前
|
人工智能 缓存 JavaScript
SpringBoot 公共字段自动填充的6种方法
本文深入探讨了在Java开发中如何高效维护公共字段的多种解决方案。首先分析了手动设置公共字段带来的代码重复、维护成本高和易遗漏等问题,接着介绍了使用MyBatis-Plus自动填充、AOP统一处理等基础与进阶方案,实现字段自动赋值。文章还涵盖了多数据源适配、分布式ID生成、空指针防护、字段覆盖问题解决、性能优化以及操作日志追踪等生产环境中的最佳实践与避坑指南。最终通过方案组合使用,显著提升了开发效率与系统稳定性,为构建高质量企业级应用提供了有力支撑。
219 0
|
Arthas 缓存 Java
在 Windows 下的 Arthas 快速安装 | 学习笔记
快速学习在 Windows 下的 Arthas 快速安装
在 Windows 下的 Arthas 快速安装 | 学习笔记
|
3月前
|
存储 关系型数据库 数据库
【赵渝强老师】PostgreSQL数据库的WAL日志与数据写入的过程
PostgreSQL中的WAL(预写日志)是保证数据完整性的关键技术。在数据修改前,系统会先将日志写入WAL,确保宕机时可通过日志恢复数据。它减少了磁盘I/O,提升了性能,并支持手动切换日志文件。WAL文件默认存储在pg_wal目录下,采用16进制命名规则。此外,PostgreSQL提供pg_waldump工具解析日志内容。
316 0
|
3月前
|
关系型数据库 MySQL 程序员
从自建MySQL到阿里云RDS:程序员的数据库减负革命
如果你正在为自建MySQL数据库的高成本运维发愁,为凌晨三点的主从同步故障告警而崩溃,为开发团队频繁索要新测试库的要求感到窒息——是时候开启一场数据库的自我救赎了。 程序员更需构建"技术敏锐度+工程落地能力+跨域协作"的三维竞争力,通过创建技术组合形成差异化优势。企业应建立持续学习机制,提供AI沙盒环境促进技术转化。
|
3月前
|
存储 人工智能 NoSQL
基于PolarDB-PG一站式AI Agent长记忆方案
本文介绍了基于PolarDB-PG的AI Agent长记忆方案,结合Mem0框架,提供向量与图数据库一站式支持,解决LLM跨会话、跨应用“失忆”问题。方案具备跨会话记忆、个性化服务、高效检索等能力,适用于各类AI应用场景。
|
6月前
|
缓存 JSON 关系型数据库
MySQL 查询优化分析 - 常用分析方法
本文介绍了MySQL查询优化分析的常用方法EXPLAIN、Optimizer Trace、Profiling和常用监控指标。