批量update实现方案全面解析与最佳实践,带你掌握到底怎么批量更新最快、性能最高
1.概述
在当今应用开发中,批量数据操作是实现实践底层基础,批量更新是全面实际开发中一个常见的操作,同时也是解析一个性能瓶颈点。有多种批量更新的最佳实现方式,但不同的掌底批方案在性能、可维护性和数据库兼容性等方面差异显著。量更本文将基于MyBatis全面剖析各种批量更新方案的新最实现原理、性能表现和适用场景,快性帮助开发者做出合理的批量技术选型,从而实现性能最高的实现实践更新。
2.准备工作
这里我们还是全面以用户表tb_user为示例,并且基于上面总结快速插入了500多万条数据:

当同样更新100条数据时,小表(几千条)和大表(几百万条)使用相同的最佳批量更新方式,执行效率会有差异,掌底批差异程度取决于多个因素
效率不会完全相同,但差异可能不明显,主要因为:
数据定位成本:大表可能需要更多I/O来定位记录索引结构差异:大表的索引层级可能更深内存缓存影响:小表更可能完全缓存在内存中所以我这里为了更能突出区别不同批量更新方案的执行效率,选择了对大表进行批量更新10000条数据来示例。当然了执行效率还与MySQL服务的免费信息发布网配置有关,配置2核2G和4核8G肯定是不一样的。
3.批量更新实现方案
这里我先查出10000条数据,更新user的name,gender,address等字段
复制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; }1.2.3.4.5.6.7.8.9.10.11.12.13.3.1 循环单条更新
这种方式最简单,直接看代码:
复制@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"); }1.2.3.4.5.6.7.8.9.10.执行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: 11.2.3.4.5.6.可以看出是一条一条提交执行的。
执行时长:3846ms
这种方式产生N条独立SQL语句,网络IO次数与数据量成正比,性能很差,在平时开发中几乎不能使用,当然如果是操作小表小批量数据,也问题不大,但最好别这么写,显得代码水平不行,同时这种方式也是代码性能提升方式经常提到一大问题点:for循环里面单条操作SQL语句,这种方式写了就有性能问题~~~
3.2 foreach多条SQL
这种方式需要通过XML写SQL语句实现
复制public interface UserDAO extends BaseMapperX<User> { int batchUpdateByForeach(@Param("userList") List<User> userList); }1.2.3.XML配置如下:
复制<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>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.测试代码:
复制@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"); }1.2.3.4.5.6.7.8.9.10.11.12.这里我只给出了3条数据的更新SQL,500条全给出来太多了。
复制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: 11.2.3.可以看出是单次请求包含多条SQL语句,但本质上每条数据都是单独执行更新的
执行时长:1417ms
3.3 CASE WHEN表达式
直接看XML配置里面写的SQL语句:
复制<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>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.测试代码:
复制@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"); }1.2.3.4.5.6.7.8.9.10.11.12.这里就不给出控制台的输出的SQL语句了,太长了,IT技术网大家自行执行查看
执行时长:988ms
真正的单SQL批量操作,性能很好,但要注意防止SQL语句长度超过限制。
3.4 ON DUPLICATE KEY UPDATE
ON DUPLICATE KEY UPDATE是MySQL特有语法,批量插入,遇到主键/唯一键冲突时转为更新。
复制<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>1.2.3.4.5.6.7.8.测试代码:
复制@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"); }1.2.3.4.5.6.7.8.9.10.11.12.执行时长:1080ms
3.5 REPLACE INTO
replace into与on duplicate key update在一定程度上都能实现无记录时插入,有记录时更新。其判断都是根据主键/唯一键是否存在,但是replace into实现更新的方式是先删除在插入,这就会产生两个binlog,可能导致消费binlog出问题,同时这种更新如果是唯一键冲突,那么先删后插会导致主键变了,如果之前的主键id有在其他表关联使用,这种更新是很危险的。
复制<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>1.2.3.4.5.6.测试代码:
复制@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"); }1.2.3.4.5.6.7.8.9.10.11.12.执行时长:6705ms
3.6 通过MyBatis-Plus批量更新
直接看代码:
复制@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"); }1.2.3.4.5.6.7.8.执行时长: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
N
100-3000
全兼容
除了for循环单条更新不推荐之外,其他方式我个人感觉都可以选择,可以根据具体场景选择具体方式。追求极致性能首选case when
如果存在做更新,没有就插入实现方案首选ON DUPLICATE KEY UPDATE,因为replace into操作可能存在问题,企商汇具体看上面叙述,当然了MyBatis-Plus提供了saveOrUpdateBatch可以操作小批量数据,因为它底层是for循环单条操作实现的,比较慢。
5.总结
批量更新方案的选择需要综合考虑数据库类型、数据量大小、系统架构要求和团队技术栈等因素。对于大多数MySQL应用场景,ON DUPLICATE KEY UPDATE方案提供了最佳的性能和可维护性平衡。而在需要多数据库支持的场景中,CASE WHEN表达式则是更为通用的选择。无论采用哪种方案,都应该结合分批次处理、连接参数优化和适当的监控手段,才能在实际生产环境中获得理想的性能表现。
本文地址:http://www.bhae.cn/html/075c9899826.html
版权声明
本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。