批量update实现方案全面解析与最佳实践,带你掌握到底怎么批量更新最快、性能最高

1.概述

在当今应用开发中,数据操作是底层基础,批量更新是实际开发中一个常见的操作,同时也是一个性能瓶颈点。有多种批量更新的实现方式,但不同的方案在性能、可维护性和数据库兼容性等方面差异显著。本文将基于MyBatis全面剖析各种批量更新方案的实现原理、性能表现和适用场景,帮助开发者做出合理的技术选型,从而实现性能最高的更新。

2.准备工作

之前我总结分享过关于批量插入的知识点:最近做百万级数据性能压测,来看看人家如何使用MyBatis 优雅批量插入数据,从80s优化到1s!!!可以让我们快速插入大批量数据

也总结了SQL语句的执行流程:一条SQL语句到底是怎么执行的?可以看看更新语句是怎么执行的。

这里我们还是以用户表tb_user为示例,并且基于上面总结快速插入了500多万条数据:

sql 复制代码
CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_no` varchar(255) NOT NULL COMMENT '编号',
  `name` varchar(255) DEFAULT NULL COMMENT '昵称',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(255) NOT NULL COMMENT '手机号',
  `gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别  0:男生   1:女生',
  `birthday` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '出生日期',
  `is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标志 0:否  1:是',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `creator` bigint(20) DEFAULT NULL COMMENT '创建人',
  `updater` bigint(20) DEFAULT NULL COMMENT '更新人',
  `address` varchar(1024) DEFAULT NULL COMMENT '地址',
  `role_id` varchar(100) DEFAULT NULL COMMENT '角色id',
  `hobby` varchar(255) DEFAULT NULL COMMENT '爱好',
  `remark` varchar(255) DEFAULT NULL COMMENT '个人说明',
  `org_id` bigint(20) NOT NULL COMMENT '公司id',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uk_user_no` (`user_no`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5201026 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

当同样更新100条数据时,小表(几千条)和大表(几百万条)使用相同的批量更新方式,执行效率会有差异,差异程度取决于多个因素

效率不会完全相同,但差异可能不明显,主要因为:

  1. 数据定位成本:大表可能需要更多I/O来定位记录
  2. 索引结构差异:大表的索引层级可能更深
  3. 内存缓存影响:小表更可能完全缓存在内存中

所以我这里为了更能突出区别不同批量更新方案的执行效率,选择了对大表进行批量更新10000条数据来示例。当然了执行效率还与MySQL服务的配置有关,配置2核2G和4核8G肯定是不一样的。

3.批量更新实现方案

这里我先查出10000条数据,更新user的name,gender,address等字段

ini 复制代码
    public List<User> listUsers() {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.select(User::getId, User::getName);
        queryWrapper.ge(User::getId, 10000L).lt(User::getId, 20000L);
        List<User> users = userDAO.selectList(queryWrapper);
        users.forEach(user -> {
            user.setName(user.getName() + "1");
            user.setAddress("杭州" + user.getId());
            user.setGender(user.getId() % 2 == 0 ? 1 : 0);
            user.setUpdateTime(new Date());
        });
        return users;
    }

3.1 循环单条更新

这种方式最简单,直接看代码:

ini 复制代码
  @Test
  public void testBatchUpdateByFor() {
      List<User> users = listUsers();
      long start = System.currentTimeMillis();
      users.forEach(user -> {
          userDAO.updateById(user);
      });
      long end = System.currentTimeMillis();
      System.out.println("执行时长:" + (end - start) + "ms");
  }

执行SQL部分如下:

sql 复制代码
c.p.b.e.mybatis.dao.UserDAO.updateById   : ==>  Preparing: UPDATE tb_user SET gender=?, address=?, name=?, update_time=?, updater=? WHERE id=?
c.p.b.e.mybatis.dao.UserDAO.updateById   : ==> Parameters: 1(Integer), 杭州19998(String), 罗百夜1(String), 2025-07-08 11:08:23.588(Timestamp), null, 19998(Long)
c.p.b.e.mybatis.dao.UserDAO.updateById   : <==    Updates: 1
 c.p.b.e.mybatis.dao.UserDAO.updateById   : ==>  Preparing: UPDATE tb_user SET gender=?, address=?, name=?, update_time=?, updater=? WHERE id=?
 c.p.b.e.mybatis.dao.UserDAO.updateById   : ==> Parameters: 0(Integer), 杭州19999(String), 张七土1(String), 2025-07-08 11:08:23.588(Timestamp), null, 19999(Long)
c.p.b.e.mybatis.dao.UserDAO.updateById   : <==    Updates: 1

可以看出是一条一条提交执行的。

执行时长:3846ms

这种方式产生N条独立SQL语句,网络IO次数与数据量成正比,性能很差,在平时开发中几乎不能使用,当然如果是操作小表小批量数据,也问题不大,但最好别这么写,显得代码水平不行,同时这种方式也是代码性能提升方式经常提到一大问题点:for循环里面单条操作SQL语句,这种方式写了就有性能问题~~~

3.2 foreach多条SQL

这种方式需要通过XML写SQL语句实现

less 复制代码
public interface UserDAO extends BaseMapperX<User> {
    int batchUpdateByForeach(@Param("userList") List<User> userList);
}

XML配置如下:

ini 复制代码
  <update id="batchUpdateByForeach">
        <foreach collection="userList" item="u" separator=";">
            UPDATE tb_user
            SET
            update_time = now()
            <if test="u.name != null">
                ,name = #{u.name}
            </if>
            <if test="u.address != null">
                ,address = #{u.address}
            </if>
            <if test="u.gender != null">
                ,gender = #{u.gender}
            </if>
            WHERE id = #{u.id}
        </foreach>
    </update>

测试代码:

ini 复制代码
  @Test
  public void testBatchUpdateByForeach() {
      List<User> users = listUsers();
      long start = System.currentTimeMillis();
      // 分批处理
      List<List<User>> splitList = CollUtil.split(users, 500);
      splitList.forEach(userList -> {
          userDAO.batchUpdateByForeach(userList);
      });
      long end = System.currentTimeMillis();
      System.out.println("执行时长:" + (end - start) + "ms");
  }
​

这里我只给出了3条数据的更新SQL,500条全给出来太多了。

vbnet 复制代码
c.p.b.e.m.d.U.batchUpdateByForeach       : ==>  Preparing: UPDATE tb_user SET update_time = now() ,name = ? ,address = ? ,gender = ? WHERE id = ? ; UPDATE tb_user SET update_time = now() ,name = ? ,address = ? ,gender = ? WHERE id = ? ; UPDATE tb_user SET update_time = now() ,name = ? ,address = ? ,gender = ? WHERE id = ? ; 
c.p.b.e.m.d.U.batchUpdateByForeach       : ==> Parameters: 王十金1111(String), 杭州13000(String), 1(Integer), 13000(Long), 杨一月1111(String), 杭州13001(String), 0(Integer), 13001(Long), 周六云1111(String), 杭州13002(String), 1(Integer), 13002(Long)
2025-07-08T13:55:41.618+08:00 DEBUG 53878 --- [plasticene-boot-mybatis-example] [           main] c.p.b.e.m.d.U.batchUpdateByForeach       : <==    Updates: 1

可以看出是单次请求包含多条SQL语句,但本质上每条数据都是单独执行更新的

执行时长:1417ms

3.3 CASE WHEN表达式

直接看XML配置里面写的SQL语句:

ini 复制代码
    <update id="batchUpdateByCaseWhen">
        UPDATE tb_user
        SET
        update_time=now(),
        name = CASE
        <foreach collection="userList" item="item">
            WHEN id = #{item.id} AND #{item.name} IS NOT NULL THEN #{item.name}
        </foreach>
        ELSE name
        END,
        address = CASE
        <foreach collection="userList" item="item">
            WHEN id = #{item.id} AND #{item.address} IS NOT NULL THEN #{item.address}
        </foreach>
        ELSE address
        END,
        gender = CASE
        <foreach collection="userList" item="item">
            WHEN id = #{item.id} AND #{item.gender} IS NOT NULL THEN #{item.gender}
        </foreach>
        ELSE gender
        END
        WHERE id IN
        <foreach collection="userList" item="item" open="(" separator="," close=")">
            #{item.id}
        </foreach>
    </update>

测试代码:

ini 复制代码
  @Test
  public void testBatchUpdateByCaseWhen() {
      List<User> users = listUsers();
      long start = System.currentTimeMillis();
      // 分批处理
      List<List<User>> splitList = CollUtil.split(users, 500);
      for (List<User> userList : splitList) {
          userDAO.batchUpdateByCaseWhen(userList);
      }
      long end = System.currentTimeMillis();
      System.out.println("执行时长:" + (end - start) + "ms");
  }

这里就不给出控制台的输出的SQL语句了,太长了,大家自行执行查看

执行时长:988ms

真正的单SQL批量操作,性能很好,但要注意防止SQL语句长度超过限制。

3.4 ON DUPLICATE KEY UPDATE

ON DUPLICATE KEY UPDATE是MySQL特有语法,批量插入,遇到主键/唯一键冲突时转为更新。

sql 复制代码
<insert id="batchUpdateOnDuplicate">
    INSERT INTO tb_user(user_no, name, phone, address, gender, org_id) VALUES
    <foreach collection="userList" item="item" separator=",">
        (#{item.userNo}, #{item.name}, #{item.phone}, #{item.address}, #{item.gender}, #{item.orgId})
    </foreach>
    ON DUPLICATE KEY UPDATE
    name=VALUES(name), org_id=VALUES(org_id)
</insert>

测试代码:

ini 复制代码
  @Test
  public void testBatchUpdateOnDuplicate() {
      List<User> users = listUsers();
      long start = System.currentTimeMillis();
      // 分批处理
      List<List<User>> splitList = CollUtil.split(users, 500);
      for (List<User> userList : splitList) {
          userDAO.batchUpdateOnDuplicate(userList);
      }
      long end = System.currentTimeMillis();
      System.out.println("执行时长:" + (end - start) + "ms");
  }

执行时长:1080ms

3.5 REPLACE INTO

replace intoon duplicate key update在一定程度上都能实现无记录时插入,有记录时更新。其判断都是根据主键/唯一键是否存在,但是replace into实现更新的方式是先删除在插入,这就会产生两个binlog,可能导致消费binlog出问题,同时这种更新如果是唯一键冲突,那么先删后插会导致主键变了,如果之前的主键id有在其他表关联使用,这种更新是很危险的。

ini 复制代码
<insert id="batchUpdateReplace">
    REPLACE INTO tb_user(user_no, name, phone, address, gender, org_id) VALUES
    <foreach collection="userList" item="item" separator=",">
        (#{item.userNo}, #{item.name}, #{item.phone}, #{item.address}, #{item.gender}, #{item.orgId})
    </foreach>
</insert>

测试代码:

ini 复制代码
 @Test
  public void testBatchUpdateReplace() {
      List<User> users = listUsers();
      long start = System.currentTimeMillis();
      // 分批处理
      List<List<User>> splitList = CollUtil.split(users, 500);
      for (List<User> userList : splitList) {
          userDAO.batchUpdateReplace(userList);
      }
      long end = System.currentTimeMillis();
      System.out.println("执行时长:" + (end - start) + "ms");
  }

执行时长:6705ms

3.6 通过MyBatis-Plus批量更新

直接看代码:

ini 复制代码
    @Test
    public void testBatchUpdateByMybatisPlus() {
        List<User> users = listUsers();
        long start = System.currentTimeMillis();
        userDAO.updateById(users, 500);
        long end = System.currentTimeMillis();
        System.out.println("执行时长:" + (end - start) + "ms");
    }

执行时长:1730ms

4.性能对比表格

方案 1万条耗时 网络IO次数 SQL解析次数 适用数据量 数据库兼容性
for循环单条更新 3.-4.s N N <100 全兼容
foreach多条SQL 1-2s 1 N 100-5000 需配置
mybaits-plus 1-2s 1 1 100-5000 全兼容
CASE WHEN 0.5-1s 1 1 >1000 全兼容
ON DUPLICATE KEY UPDATE 0.5-1s 1 1 >1000 MySQL only
replace into 4-7s 1 1 100-3000 全兼容

除了for循环单条更新不推荐之外,其他方式我个人感觉都可以选择,可以根据具体场景选择具体方式。追求极致性能首选case when

如果存在做更新,没有就插入实现方案首选ON DUPLICATE KEY UPDATE,因为replace into操作可能存在问题,具体看上面叙述,当然了MyBatis-Plus提供了saveOrUpdateBatch可以操作小批量数据,因为它底层是for循环单条操作实现的,比较慢。

5.总结

批量更新方案的选择需要综合考虑数据库类型、数据量大小、系统架构要求和团队技术栈等因素。对于大多数MySQL应用场景,ON DUPLICATE KEY UPDATE方案提供了最佳的性能和可维护性平衡。而在需要多数据库支持的场景中,CASE WHEN表达式则是更为通用的选择。无论采用哪种方案,都应该结合分批次处理、连接参数优化和适当的监控手段,才能在实际生产环境中获得理想的性能表现。

相关推荐
sun0077002 小时前
mysql索引底层原理
数据库·mysql
一只叫煤球的猫2 小时前
【🤣离谱整活】我写了一篇程序员掉进 Java 异世界的短篇小说
java·后端·程序员
程序员秘密基地2 小时前
基于html,css,vue,vscode,idea,,java,springboot,mysql数据库,在线旅游,景点管理系统
java·spring boot·mysql·spring·web3
你的人类朋友3 小时前
🫏光速入门cURL
前端·后端·程序员
叁沐4 小时前
MySQL 11 怎么给字符串字段加索引?
mysql
aramae5 小时前
C++ -- STL -- vector
开发语言·c++·笔记·后端·visual studio
lifallen5 小时前
Paimon 原子提交实现
java·大数据·数据结构·数据库·后端·算法
舒一笑6 小时前
PandaCoder重大产品更新-引入Jenkinsfile文件支持
后端·程序员·intellij idea
PetterHillWater6 小时前
AI编程之CodeBuddy的小试
后端·aigc
codervibe6 小时前
如何用 Spring Security 构建无状态权限控制系统(含角色菜单控制)
java·后端