做后端这些年,MySQL 的 UPDATE 操作我写过无数次,但真正让我觉得“这玩意儿得认真对待”的,是第一次在业务高峰期跑一个批量更新任务。当时有一批订单状态要改成已取消,大概两万行。我图省事,直接在脚本里 SELECT 出 id,然后 for 循环一条条 UPDATE。结果跑了三分多钟还没结束,连接数飙红,数据库 CPU 直接上去,差点把线上业务拖停。后面复盘了几遍才发现,MySQL 批量 UPDATE 的靠谱姿势其实就两种:一种是用 CASE WHEN 表达式把多行更新压进同一句 SQL,另一种是借助 JOIN 关联一张数据表让 MySQL 自己去匹配。这篇文章把这两种方式的原理、写法、实测结果和坑一次讲清楚,适合正在写数据订正脚本、功能开发中需要批量改状态的后端开发,也适合 DBA 随手翻一翻。
1. 先看场景:为什么“一条条 UPDATE”会被我拉进黑名单
1.1 逐条更新的成本明细
很多人一听到“批量更新”,第一反应是查出来然后 for 循环一条条 UPDATE。数据量只有几十条时没毛病,但一旦变成几千、几万,这条路基本走不通。问题不在于 UPDATE 语句本身,而在于“逐条”两个字叠加出的综合成本。
第一条成本是网络往返。应用和 MySQL 之间每执行一条 SQL,都是一次完整的请求响应。不要小看这个往返,局域网内一次往返可能 0.1ms,跨机房或者用了连接池且连接繁忙时,可能到几毫秒甚至几十毫秒。一万条 UPDATE 就是一万次来回,光网络开销就够喝一壶。
第二条成本是 SQL 解析。MySQL 收到一条 UPDATE,要先做语法解析、权限检查、执行计划生成。这些动作虽然单次只要零点几毫秒,但一万条累加起来就是肉眼可见的耗时。
第三条成本在 InnoDB 层。每一条 UPDATE 都要开启事务、写 undo log、写 redo log、加行锁、提交事务、写 binlog。哪怕更新的是同一行,这些动作一个都少不了。最后还有应用层等待——每一条都是同步调用,延迟线性叠加。
我算过一笔账:假设单条 UPDATE 平均执行 10ms,一万条就是 100 秒;如果数据库繁忙、连接池排队,单条延迟到 50ms,那就是 500 秒。这种任务放业务高峰期跑,基本等于自杀。
1.2 批量更新的核心思路:把 N 次交互压成 1 次
批量 UPDATE 的本质,就是减少客户端和数据库之间的交互次数,把 N 条 SQL 执行合并成一条或少数几条。一条 SQL 更新 5000 行,和 5000 条 SQL 各更新一行,在 InnoDB 内部差异非常明显。
前者只需要一次 SQL 解析、一次事务开启、一次提交;行锁在一条语句内部由 MySQL 统一调度,锁的获取顺序整体可控。后者要频繁切换事务状态,每次提交都要刷一次磁盘,日志写放大非常严重。
当然,这不代表“一条 SQL 更新越多越好”。单条巨型 UPDATE 会带来大事务、锁范围扩大、binlog 体积暴涨等问题,后面会专门讲。批量更新的正确姿态是:合并到一个合理的粒度,比如每批 5000 行,而不是走两个极端。
1.3 适合上批量 UPDATE 的典型场景
从我实际接触的业务看,批量 UPDATE 主要出现在三类场景。第一类是数据订正:历史数据因为 bug 或者需求变更,需要按某个映射关系批量改字段。比如把一批订单的 shop_id 从旧值迁移到新值,这时候新值往往来自另一张表或配置文件。
第二类是业务状态流转:订单超时批量取消、优惠券过期、会员积分清零、用户等级批量调整。这类任务通常是定时任务触发,一跑就是几万行。
第三类是外部数据同步:运营给了一个 Excel 名单,要求更新一批用户的标签;或者说接口批量返回了新的状态,需要回写到业务表。这类场景的数据源在外部,天然适合导入临时表后 JOIN 更新。
有一类场景不适合硬套批量 UPDATE:每次更新都依赖用户实时操作、需要逐行做复杂业务校验的接口。强行合并成一条 SQL 没问题,但业务逻辑会变得极其拧巴,出了问题也难定位。批量更新更应该在离线任务、后台脚本、数据订正这种场景里发光发热。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 方式一:CASE WHEN 表达式,把多个更新条件塞进一条 SQL
2.1 最基本的写法
CASE WHEN 方式应该是很多后端开发最早接触的批量更新写法。它的核心思路是:在一条 UPDATE 语句里,通过 CASE 表达式对不同的行分别计算要写入的值。举个例子:
sql复制UPDATE users
SET level = CASE id
WHEN 10001 THEN 'vip1'
WHEN 10002 THEN 'vip2'
WHEN 10003 THEN 'vip3'
END
WHERE id IN (10001, 10002, 10003);
这条 SQL 的含义是:只更新 id 为 10001、10002、10003 的三行,每一行根据自身 id 匹配对应的 WHEN 分支,把 level 分别改成 vip1、vip2、vip3。
MySQL 执行时会先用 WHERE id IN (...) 定位到目标行,然后逐行计算 CASE 表达式,再把结果写回。注意这里 WHERE 条件非常重要,如果不带 WHERE,MySQL 会对全表每一行都计算 CASE,性能不可控,而且可能出现非常严重的副作用。
什么副作用?如果某行的 id 没有出现在任何一个 WHEN 分支里,CASE 表达式的结果就是 NULL,UPDATE
