Spring Boot + Oracle 数据库批量操作性能大比拼:哪种写法效率最高?


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
手打不易,如果转摘,请注明出处!
本文链接:https://zhangxiaofan.blog.csdn.net/article/details/161495781

你是否曾经在项目中纠结过:批量插入/更新数据,到底用哪种写法效率最高?是 foreach 拼SQL?还是循环调用?还是用 BEGIN END 包起来?今天,我用实测数据告诉你答案!

目录


一、前言:为什么要做这个测试?

本篇文章以Oracle为例,MySQL应该类似。

在 Spring Boot + MyBatis + Oracle 的项目中,批量操作数据有多种写法:

  1. 循环单次插入/更新 —— 每次操作一条数据
  2. foreach 批量拼接SQL —— 一条SQL搞定所有数据
  3. BEGIN END —— 用 PL/SQL 块包裹多条语句
  4. JDBC 原生 batch —— 使用 PreparedStatement.addBatch() 批量提交

每种写法在不同数据量下,性能差异巨大。选错了写法,可能让你的接口从几百毫秒变成几十秒!

本文通过实际测试,为你揭示最佳实践。


二、测试环境说明

  • 框架:Spring Boot + MyBatis
  • 数据库:Oracle
  • 时间单位:毫秒(ms)
  • 测试场景:插入数据、更新数据

2.1 实体类定义

public class Student {
    private BigDecimal id;      // 主键ID
    private String name;        // 姓名
    private String name2;       // 姓名2
    private BigDecimal age;     // 年龄
    
    // getter/setter 省略...
}

2.2 MyBatis Executor 类型说明

在开始之前,先了解一下 MyBatis 的三种执行器模式:

模式 说明 适用场景
simple(默认) 每次操作都开启一个 Statement 对象,用完立刻关闭 通用场景
batch 复用 Statement 对象,通过 addBatch() 汇总后统一执行 executeBatch() 大批量操作,无法返回行数
reuse 全局共享 Statement 对象(Map缓存),存在则复用 SQL模板复用场景

💡 配置方式:在 application.yml 中设置 mybatis.configuration.default-executor-type: batch


三、插入操作性能测试

3.1 测试方式对比

方式 说明 SqlSession 创建次数
无事务循环单条插入 每次 insert 都创建新的 SqlSession 多次(性能最差)
有事务循环单条插入 Spring 事务管理,复用 SqlSession 单次
foreach 批量插入 单条 SQL 拼接多条 VALUES 单次
BEGIN END 批量插入 PL/SQL 块包裹多条 insert 单次

3.2 测试结果数据

⏱️ 所有时间单位均为毫秒(ms)

小数据量测试(1000~5000条)
方式 1000条(ms) 2500条(ms) 5000条(ms)
无事务循环单条插入 4493, 3317, 4410 11391, 11088, 13093 -(太慢未测)
有事务循环单条插入 1956, 1344, 1224 3437, 3393, 3460 5967, 5700, 5844
BEGIN END 批量插入 819, 103, 102 3344, 219, 237 18312, 652, 462
foreach 批量插入 453, 45, 39 4063, 396, 81 35399, 174, 195

📊 说明:每组测试执行3次,数据格式为 第一次, 第二次, 第三次。首次执行较慢是因为 Oracle 缓存未命中,后续执行会明显变快。

大数据量测试(1万~10万条)
方式 1万条(ms) 5万条(ms) 10万条(ms)
JDBC 原生 batch-prepared 2811, 297, 299 11321, 657, 735 713758, 886
有事务循环单条插入(Batch模式) 227, 163, 168 1007, 892, 829 200416, 991, 739

3.3 各种写法完整代码示例

❌ 禁止使用:无事务循环单条插入

这是性能最差的写法!每次插入都会创建新的 SqlSession,1000条数据就要耗时 3~4 秒。

/**
 * 【禁止】循环单次单条插入-无Spring事务管理
 * 解析:每次执行都会 Creating new SqlSession, 耗时极大
 * 1000条:4493, 3174, 4109 ms
 * 2500条:11391, 11088, 13093 ms
 */
@GetMapping(value = "/effective/student/insert/for")
public String insertFori(int num) {
    long start = System.currentTimeMillis();
    long maxId = studentService.getMaxId() + 1;
    for (int i = 0; i < num; i++) {
        long pId = maxId + i;
        Student student = getStudent(pId + "");
        studentService.insert(student);  // 每次都创建新 SqlSession!
    }
    long end = System.currentTimeMillis();
    System.out.println("循环插入-无事务 执行时间:" + (end - start) + " ms");
    return "ok";
}

🔴 问题分析:没有 @Transactional 注解,Spring 不会管理事务,每次 insert() 都会触发 Creating new SqlSession,这是性能杀手!

✅ 推荐:有事务循环单条插入
/**
 * 循环单次单条插入-有事务 Spring事务管理
 * 解析:Spring事务管理后, SqlSession只创建一次, spring 复用 Fetched SqlSession
 * 1000条:1956, 1344, 1224 ms
 * 2500条:3437, 3393, 3460 ms
 * 5000条:5967, 5700, 5844 ms
 */
@Transactional
@GetMapping(value = "/effective/student/insert/fortrans")
public String insertListTrans(int num) {
    long start = System.currentTimeMillis();

    Long maxId = studentService.getMaxId();
    long tempMaxId = (maxId == null) ? 0 : studentService.getMaxId() + 1;

    for (int i = 0; i < num; i++) {
        long pId = tempMaxId + i;
        Student student = getStudent(pId + "");
        studentService.insert(student);
    }
    long end = System.currentTimeMillis();
    logger.info("数据条数:" + num);
    logger.info("循环插入-有事务 执行时间:" + (end - start) + " ms");
    return "ok";
}

🟢 优化点:加了 @Transactional,整个方法只创建一次 SqlSession,循环时复用,性能提升 3~4 倍!

✅ 推荐:foreach 批量插入(<2000条)
/**
 * ★★★<2000条,推荐写法★★★
 * 单次单条insert语句, 批量插入(foreach batch)写法是单条SQL(本身具有事务性)
 * 默认配置下,不受 simple/batch 影响
 * 1000 条:453, 45, 39 ms
 * 2500 条:4063, 396, 81 ms
 * 5000 条:35399, 174, 195 ms
 */
@Transactional  // 事务注解对速度基本无影响
@GetMapping(value = "/effective/student/insert/batch")
public String insertList2(int num) {
    long start = System.currentTimeMillis();
    Long maxId = studentService.getMaxId();
    long tempMaxId = (maxId == null) ? 0 : studentService.getMaxId() + 1;

    List<Student> studentList = new ArrayList<>();
    for (int i = 0; i < num; i++) {
        long pId = tempMaxId + i;
        Student student = getStudent(pId + "");
        studentList.add(student);
    }
    logger.info("Mybatis SQL return :" + studentService.insertListBatch(studentList));
    long end = System.currentTimeMillis();
    logger.info("批量插入 执行时间:" + (end - start) + " ms");
    return "ok";
}

对应的 Mapper XML(foreach batch 写法)

<!-- foreach 批量插入:单条SQL,使用 union all 拼接 -->
<insert id="insertListBatch">
    INSERT INTO STUDENT(
        ID,
        NAME,
        AGE
    )
    <foreach collection="studentList" item="item" index="index" separator="union all">
        (
        SELECT
            #{item.id},
            #{item.name,jdbcType=VARCHAR},
            #{item.age,jdbcType=DECIMAL}
        FROM dual
        )
    </foreach>
</insert>

💡 Oracle 特殊写法:Oracle 不支持 INSERT INTO ... VALUES (...), (...), (...) 语法,需要用 INSERT INTO ... SELECT ... FROM dual UNION ALL SELECT ... FROM dual 的方式实现批量插入。

✅ 可选:BEGIN END 批量插入
/**
 * 单次单条insert语句,批量插入(BEGIN END), 加不加 @Transactional 对效率影响不大
 * 解析:只有一次 SqlSession
 * 1000条:819, 103, 102 ms
 * 2500条:3344, 219, 237 ms
 * 5000条:18312, 652, 462 ms
 */
@GetMapping(value = "/effective/student/insert/beginend")
public String insertListBeginEnd(int num) {
    long start = System.currentTimeMillis();

    Long maxId = studentService.getMaxId();
    long tempMaxId = (maxId == null) ? 0 : studentService.getMaxId() + 1;

    List<Student> studentList = new ArrayList<>();

    for (int i = 0; i < num; i++) {
        long pId = tempMaxId + i;
        Student student = getStudent("" + pId);
        studentList.add(student);
    }
    logger.info("Mybatis SQL return :" + studentService.insertListBeginEnd(studentList));
    long end = System.currentTimeMillis();
    logger.info("单次多条insert批量插入(BEGIN END) 执行时间:" + (end - start) + " ms");
    return "ok";
}

对应的 Mapper XML(BEGIN END 写法)

<!-- BEGIN END 批量插入:多条SQL,用 PL/SQL 块包裹 -->
<insert id="insertListBeginEnd">
    <foreach collection="studentList" item="item" index="index" 
             open="begin" close=";end;" separator=";">
        INSERT INTO STUDENT(
            ID,
            NAME,
            AGE
        )VALUES(
            #{item.id},
            #{item.name},
            #{item.age}
        )
    </foreach>
</insert>

📝 说明BEGIN END 是 Oracle 的 PL/SQL 语法块,可以将多条 SQL 语句包裹在一起执行,SQL 本身具有事务性。

3.4 为什么 foreach 大数据量反而慢?

foreach 批量插入在数据量大时,SQL 拼接字段太多,会导致 Oracle 进行硬解析,耗时耗资源。而事务下循环单条操作,会复用模板进行软解析,反而效率更高。

⚠️ 注意:大量的硬解析可能导致 Oracle 报错 ORA-04036 PGA memory 错误。


四、更新操作性能测试

4.1 测试方式对比

方式 说明 限制
foreach 批量更新 单条 SQL 拼接多个 CASE WHEN 不能超过 1000 条
BEGIN END 批量更新 PL/SQL 块包裹多条 update 无限制
事务下循环单条更新 Spring 事务管理,循环调用 无限制

4.2 测试结果数据

⏱️ 所有时间单位均为毫秒(ms)

方式 1000条(ms) 2000条(ms) 5000条(ms)
foreach 批量更新 85, 75, 91 ❌ 报错 ORA-01795 ❌ 报错 ORA-01795
BEGIN END 批量更新 100, 83, 81 2284, 174, 156 17093, 476, 520
事务下循环单条更新 1254, 1346, 1215 2395, 2611, 2655 5907, 6583, 6512

4.3 各种写法完整代码示例

✅ 推荐:foreach 批量更新(<1000条)
/**
 * 推荐写法, batch
 * 1000条:418, 75, 91 ms
 * 
 * 不能超过1000条, 否则报错:ORA-01795
 */
@GetMapping(value = "/effective/student/update/batch")
public String batch(Integer num) {
    long start = System.currentTimeMillis();

    List<Student> list = new ArrayList<>();
    if (null == studentService.getMaxId()) {
        return "no data to delete.";
    }
    long maxId = studentService.getMaxId();
    if (num == null) {
        num = 2;
    }
    for (long i = maxId - num + 1; i <= maxId; i++) {
        Student student = getStudent(i + "");
        student.setAge(BigDecimal.valueOf(num));
        list.add(student);
    }
    int i = 0;
    // 单条件 update batch
    i = studentService.updateListByIdBatch(list);
    logger.info("Mybatis SQL return :" + i);
    long end = System.currentTimeMillis();
    System.out.println("batch 执行时间:" + (end - start) + " ms");
    return i + "";
}

对应的 Mapper XML(foreach batch 更新写法)

<!-- foreach 批量更新:使用 CASE WHEN 语法 -->
<update id="updateListByIdBatch">
    UPDATE STUDENT
    <trim prefix="set" suffixOverrides=",">
        <trim prefix="AGE =case" suffix="end,">
            <foreach collection="studentList" item="item" index="index">
                <choose>
                    <when test="item.age != null">
                        WHEN ID=#{item.id} THEN #{item.age,jdbcType=DECIMAL}
                    </when>
                    <otherwise>
                        <!-- 字段为null, 用原值更新(保留原值), 否则会被重置为null -->
                        WHEN ID=#{item.id,jdbcType=DECIMAL} THEN age
                    </otherwise>
                </choose>
            </foreach>
        </trim>
        <trim prefix="NAME =case" suffix="end">
            <foreach collection="studentList" item="item" index="index">
                <choose>
                    <when test="item.name != null">
                        WHEN ID=#{item.id,jdbcType=DECIMAL} THEN #{item.name,jdbcType=VARCHAR}
                    </when>
                    <otherwise>
                        <!-- 字段为null, 用原值更新(保留原值), 否则会被重置为null -->
                        WHEN ID=#{item.id,jdbcType=DECIMAL} THEN name
                    </otherwise>
                </choose>
            </foreach>
        </trim>
    </trim>
    WHERE ID IN
    <foreach collection="studentList" item="item" index="index" 
             separator="," open="(" close=")">
        #{item.id,jdbcType=DECIMAL}
    </foreach>
</update>

⚠️ 重要提示WHERE ID IN (...) 中的 IN 子句不能超过 1000 个元素,否则会报错 ORA-01795

✅ 可选:BEGIN END 批量更新
/**
 * 1000条:801, 83, 81 ms
 * 2000条:2284, 174, 156 ms
 * 5000条:17093, 476, 520 ms
 */
@GetMapping(value = "/effective/student/update/beginend")
public String updateBeginEnd(Integer num) {
    long start = System.currentTimeMillis();

    List<Student> list = new ArrayList<>();
    if (null == studentService.getMaxId()) {
        return "no data to delete.";
    }
    long maxId = studentService.getMaxId();
    if (num == null) {
        num = 2;
    }
    for (long i = maxId - num + 1; i <= maxId; i++) {
        Student student = getStudent(i + "");
        student.setAge(BigDecimal.valueOf(num));
        list.add(student);
    }
    int i = studentService.updateListByIdBeginEnd(list);
    logger.info("Mybatis SQL return :" + i);
    long end = System.currentTimeMillis();
    System.out.println("beginend 执行时间:" + (end - start) + " ms");
    return i + "";
}

对应的 Mapper XML(BEGIN END 更新写法)

<!-- BEGIN END 批量更新:多条SQL,用 PL/SQL 块包裹 -->
<update id="updateListByIdBeginEnd">
    <foreach collection="studentList" item="item" index="index" 
             open="begin" close=";end;" separator=";">
        UPDATE STUDENT
        <set>
            <if test="item.name != null">
                NAME = #{item.name,jdbcType=VARCHAR},
            </if>
            <if test="item.name2 != null">
                NAME2 = #{item.name2,jdbcType=VARCHAR},
            </if>
            <if test="item.age != null">
                AGE = #{item.age,jdbcType=DECIMAL},
            </if>
        </set>
        WHERE ID=#{item.id,jdbcType=VARCHAR}
    </foreach>
</update>
✅ 推荐:事务下循环单条更新(>1000条)
/**
 * 1000条:1254, 1346, 1215 ms
 * 2000条:2395, 2611, 2655 ms
 * 5000条:5907, 6583, 6512 ms
 */
@Transactional
@GetMapping(value = "/effective/student/update/fori/trans")
public String fori(Integer num) {
    long start = System.currentTimeMillis();

    if (null == studentService.getMaxId()) {
        return "no data to delete.";
    }
    long maxId = studentService.getMaxId();
    if (num == null) {
        num = 2;
    }
    for (long i = maxId - num + 1; i <= maxId; i++) {
        Student student = getStudent(i + "");
        student.setAge(BigDecimal.valueOf(num));
        studentService.update(student);
    }
    long end = System.currentTimeMillis();
    System.out.println("batch 执行时间:" + (end - start) + " ms");
    return "ok";
}

4.4 关键发现

⚠️ foreach 批量更新的坑

Oracle 对 IN 子句有限制,foreach 批量更新不能超过 1000 条,否则会报错:

ORA-01795:列表中的最大表达式数为 1000
💡 null 值处理的坑

foreach 批量更新中,如果某个字段为 null,需要用 <otherwise> 处理,否则会把数据库中的值也更新为 null

<choose>
    <when test="item.age != null">
        WHEN ID=#{item.id} THEN #{item.age,jdbcType=DECIMAL}
    </when>
    <otherwise>
        <!-- 字段为null时, 用原值更新(保留原值) -->
        WHEN ID=#{item.id,jdbcType=DECIMAL} THEN age
    </otherwise>
</choose>

五、JDBC 原生批量操作

当数据量超过 5 万条时,JDBC 原生 batch 是性能最优的选择。它绕过了 MyBatis 的封装层,直接使用 JDBC 的 PreparedStatement.addBatch()executeBatch() 方法。

5.1 测试结果数据

⏱️ 所有时间单位均为毫秒(ms)

数据量 执行时间(ms)
1000条 198, 206, 178
5000条 248, 289, 227
1万条 2811, 297, 299
5万条 11321, 657, 735
10万条 713758, 886

5.2 JDBC 原生 Batch 完整代码示例

/**
 * JDBC 原生 batch-prepared 批量插入
 * 测试结果:
 * 1000条:198, 206, 178 ms
 * 5000条:248, 289, 227 ms
 * 10000条:2811, 297, 299 ms
 * 50000条:11321, 657, 735 ms
 * 100000条:713758, 886 ms
 */
public class JdbcBatchInsert implements Runnable {

    private ConnectionPoolService oracleConnect;
    private int num;

    @Override
    public void run() {
        Connection connection = null;
        PreparedStatement insertStmt = null;
        
        try {
            // 1. 获取连接
            connection = oracleConnect.getConnection();
            
            // 2. 关闭自动提交,开启手动事务
            connection.setAutoCommit(false);
            
            // 3. 查询最大ID(用于生成新ID)
            Long maxId = queryMaxId(connection);
            long tempMaxId = (maxId == null) ? 0 : maxId + 1;
            
            // 4. 创建 PreparedStatement,使用参数化SQL
            String sqlTemplate = "INSERT INTO STUDENT(ID, NAME, AGE) VALUES (?,?,?)";
            insertStmt = connection.prepareStatement(sqlTemplate);
            
            // 5. 循环设置参数,添加到批处理队列
            for (int i = 0; i < this.num; i++) {
                long pId = tempMaxId + i;
                Student student = getStudent(pId + "");
                
                // 设置参数
                insertStmt.setBigDecimal(1, student.getId());
                insertStmt.setString(2, student.getName());
                insertStmt.setBigDecimal(3, student.getAge());
                
                // 添加到批处理队列(不立即执行)
                insertStmt.addBatch();
            }
            
            // 6. 统一执行批处理
            int[] result = insertStmt.executeBatch();
            
            // 7. 提交事务
            connection.commit();
            
            System.out.println("批量插入完成,影响行数:" + result.length);
            
        } catch (SQLException e) {
            // 异常时回滚事务
            if (connection != null) {
                try {
                    connection.rollback();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            e.printStackTrace();
            throw new RuntimeException("SQL执行错误");
        } finally {
            // 8. 关闭 Statement
            if (insertStmt != null) {
                try {
                    insertStmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            // 9. 归还连接到连接池
            if (connection != null) {
                oracleConnect.returnConnection(connection);
            }
        }
    }

    /**
     * 查询最大ID
     */
    private Long queryMaxId(Connection connection) throws SQLException {
        String selectSql = "SELECT MAX(id) FROM STUDENT";
        Statement selectStmt = connection.createStatement();
        ResultSet resultSet = selectStmt.executeQuery(selectSql);
        
        Long maxId = null;
        while (resultSet.next()) {
            maxId = resultSet.getLong("MAX(id)");
        }
        
        resultSet.close();
        selectStmt.close();
        return maxId;
    }

    /**
     * 构造Student对象
     */
    private Student getStudent(String id) {
        Student student = new Student();
        student.setId(new BigDecimal(id));
        student.setName(UUID.randomUUID().toString().substring(0, 3));
        student.setAge(new BigDecimal("" + new Random().nextInt(9999)));
        return student;
    }
}

5.3 JDBC Batch 核心要点

executeBatch() 返回值说明
int[] result = insertStmt.executeBatch();

返回的 int[] 数组与批处理中的 SQL 一一对应,每个元素代表影响的行数:

返回值 含义
>= 0 成功执行,影响的具体行数
-2 (EXECUTE_FAILED) 执行成功,但无法获取影响行数
-3 (SUCCESS_NO_INFO) 执行失败
JDBC Batch 的优势
优势 说明
性能最优 直接操作 JDBC,无 MyBatis 封装开销
可返回行数 executeBatch() 返回每条 SQL 的影响行数
SQL 模板复用 PreparedStatement 参数化 SQL,Oracle 软解析
内存可控 可以分批提交,避免内存溢出
JDBC Batch 的注意事项
// ✅ 正确:分批提交,避免内存溢出
int batchSize = 1000;  // 每1000条提交一次
for (int i = 0; i < totalNum; i++) {
    insertStmt.setBigDecimal(1, student.getId());
    insertStmt.setString(2, student.getName());
    insertStmt.setBigDecimal(3, student.getAge());
    insertStmt.addBatch();
    
    // 每 batchSize 条提交一次
    if (i > 0 && i % batchSize == 0) {
        insertStmt.executeBatch();
        connection.commit();
    }
}
// 最后提交剩余的
insertStmt.executeBatch();
connection.commit();

// ❌ 错误:一次性添加太多,可能导致内存溢出
for (int i = 0; i < 100000; i++) {
    insertStmt.addBatch();  // 10万条全加进去,可能OOM
}
insertStmt.executeBatch();  // 一次性执行

5.4 JDBC Batch vs MyBatis Batch Executor

对比项 JDBC 原生 Batch MyBatis Batch Executor
性能 最优(无封装开销) 较优(有 MyBatis 封装)
返回行数 ✅ 可以返回 ❌ 无法返回(返回常量)
代码复杂度 较高(手动管理连接) 较低(Spring 管理)
适用场景 >5万条数据 2万~5万条数据

六、原理分析:为什么性能差异这么大?

6.1 SqlSession 的生命周期

SqlSession 是 MyBatis 的核心对象,负责执行 SQL、管理事务。它的创建和销毁是有成本的:

SqlSession 创建流程:
1. 从 SqlSessionFactory 获取 SqlSession
2. 创建 Executor(Simple/Batch/Reuse)
3. 创建 Connection(从 DataSource 获取)
4. 创建 Statement(PreparedStatement)
5. 执行 SQL
6. 关闭 Statement
7. 关闭 Connection(归还给 DataSource)

无事务时:每次 insert() 都会走完上述流程,创建和销毁的开销巨大。

有事务时:Spring 通过 TransactionInterceptor 拦截方法,整个方法只创建一次 SqlSession,循环时复用:

@Transactional 流程:
1. 方法开始 → 创建 SqlSession(只一次)
2. 循环 insert() → 复用 SqlSession
3. 方法结束 → 提交事务 → 关闭 SqlSession

6.2 Statement 对象的复用

MyBatis 的三种 Executor 模式,核心区别在于 Statement 的管理:

Executor Statement 管理 性能影响
Simple 每次创建,用完关闭 开销最大
Batch 复用 Statement,批量提交 开销最小,但无法返回行数
Reuse Map 缓存 Statement,全局复用 中等开销

Batch 模式的工作原理

// BatchExecutor 内部实现
public int doUpdate(MappedStatement ms, Object parameter) {
    // 1. 获取或复用 Statement
    Statement stmt = getStatement(ms);
    // 2. 添加到批处理队列
    stmt.addBatch(sql);
    // 3. 不立即执行,等 executeBatch()
    return BATCH_UPDATE_RETURN_VALUE;  // 无法返回真实行数!
}

public List<BatchResult> doFlushStatements() {
    // 统一执行所有批处理
    return stmt.executeBatch();
}

⚠️ 注意:Batch 模式下,insert() 返回值永远是 BATCH_UPDATE_RETURN_VALUE(一个常量),无法获取真实的插入行数!

6.3 Oracle 的硬解析与软解析

当 Oracle 收到一条 SQL 时,会进行解析:

硬解析(Hard Parse)

  1. 语法检查
  2. 语义检查
  3. 生成执行计划
  4. 缓存执行计划(存入 Library Cache)

软解析(Soft Parse)

  1. 在 Library Cache 中找到相同的 SQL
  2. 直接使用缓存的执行计划

💡 关键点:Oracle 判断 SQL 是否"相同",是看 SQL 文本的完全匹配!

-- 这两条 SQL 对 Oracle 来说是不同的,会硬解析两次!
INSERT INTO STUDENT(ID, NAME, AGE) VALUES(1, '张三', 20);
INSERT INTO STUDENT(ID, NAME, AGE) VALUES(2, '李四', 21);

-- 使用 PreparedStatement 后,SQL 模板相同,Oracle 会软解析
INSERT INTO STUDENT(ID, NAME, AGE) VALUES(?, ?, ?);

这就是为什么:

  • foreach 大数据量慢:拼接的 SQL 文本太长,每次都是新的 SQL,触发硬解析
  • 循环单条 + PreparedStatement 快:SQL 模板相同,Oracle 软解析复用执行计划

6.4 Oracle 缓存的影响

测试数据中,首次执行和后续执行时间差异巨大,这是因为 Oracle 的缓存机制:

缓存类型 作用 影响因素
Library Cache 缓存 SQL 执行计划 SQL 文本相同才能复用
Buffer Cache 缓存数据块 频繁访问的数据会缓存
PGA 程序全局区,存储排序、哈希连接等 大数据量可能耗尽

首次执行慢的原因

  1. SQL 硬解析,生成执行计划
  2. 数据块从磁盘加载到 Buffer Cache
  3. 绑定变量解析

后续执行快的原因

  1. SQL 软解析,复用执行计划
  2. 数据块已在 Buffer Cache 中
  3. 绑定变量已缓存

七、终极结论

7.1 插入操作最佳实践

数据量 推荐方式 原因 预估耗时(ms)
< 2000 条 foreach 批量插入 单条 SQL,效率最高 40~500
2000 ~ 5万条 事务下循环单条插入 避免 SQL 硬解析 200~1000
> 5万条 JDBC 原生 batch-prepared 批量提交,性能最优 300~800

7.2 更新操作最佳实践

数据量 推荐方式 原因 预估耗时(ms)
< 1000 条 foreach 批量更新 单条 SQL,效率最高 80~100
> 1000 条 事务下循环单条更新 避免 ORA-01795 错误 1200~6000

7.3 核心要点总结

要点 说明
🔴 永远不要用无事务的循环单条操作 每次都创建 SqlSession,性能极差!
🟡 foreach 批量操作有数据量限制 插入无限制(但大数据量会变慢),更新限制 1000 条
🟢 事务是你的好朋友 Spring 事务管理可以复用 SqlSession,大幅提升性能
🔵 Oracle 数据库缓存会影响首次执行 foreachBEGIN END 首次执行可能较慢,后续会变快
🟣 null 值处理要小心 foreach 批量更新时,null 字段要用 <otherwise> 保留原值
🟤 超大数据量用 JDBC 原生 batch 绕过 MyBatis 封装,性能最优,且可返回行数

八、写在最后

性能优化没有银弹,不同的数据量、不同的数据库,最佳实践也不同。希望这篇测试文章能帮助你在实际项目中做出正确的选择!

一句话总结

  • 小数据量用 foreach,大数据量用事务循环,超大数据量用 JDBC 原生 batch。

如果你觉得有用,欢迎点赞收藏~ 有问题欢迎评论区讨论!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐