真正有效的解法是分段+批量+显式事务控制:SQL Server用TOP+主键推进并建复合索引,MySQL用变量游标模拟分批并加FOR UPDATE,所有场景均需每批显式事务、错误处理及合理批次大小(推荐2000起调优)。

直接在存储过程中对百万级数据执行单次 UPDATE,基本等于给数据库“上刑”——锁升级、日志爆满、超时断连、SSMS 卡死都是常态。真正能跑通的方案,必须用分段 + 批量 + 显式事务控制把大操作切成可控小块。
SQL Server 中用 TOP + 主键推进实现安全分段更新
别碰 OFFSET/FETCH 做分页更新,它每次都要扫描前 N 行,10 万行后性能断崖式下跌。稳定做法是基于主键或时间戳推进:
-
DECLARE @min_id BIGINT = (SELECT MIN(id) FROM orders WHERE status = 'pending')—— 起始点必须查一次,不能硬编码 - 循环内用
UPDATE TOP (5000) orders SET status = 'processed' WHERE id >= @min_id AND status = 'pending' ORDER BY id——ORDER BY不可省,否则TOP行为无定义 - 更新完立刻刷新
@min_id:SELECT @min_id = MIN(id) FROM orders WHERE status = 'pending' AND id > @min_id - 加
IF @@ROWCOUNT = 0 BREAK,防止空结果集导致无限循环 -
status和id字段必须有复合索引,否则每次WHERE都全表扫
MySQL 中用变量游标模拟分批,避开 LIMIT 直接 UPDATE 的限制
MySQL 不允许 UPDATE ... LIMIT 在子查询中直接关联源表,但可以用变量构造逻辑游标:
- 每次执行前重置变量:
SET @row_index := -1,否则第二次运行会从上次位置继续,漏数据 - 子查询必须带
ORDER BY id,否则@row_index分配顺序不可控 - 写法示例:
UPDATE orders SET status = 'processed' WHERE id IN (SELECT id FROM (SELECT id, @row_index := @row_index + 1 AS row_num FROM orders WHERE status = 'pending' ORDER BY id LIMIT 5000) AS t) - 并发场景下可能漏行或重复,建议加
SELECT ... FOR UPDATE预占,或由应用层用分布式锁协调
所有数据库都必须处理的事务边界与错误陷阱
分批不等于安全。没加事务控制或错误处理,失败时根本不知道卡在哪一批:
- 每批必须显式
BEGIN TRANSACTION→ 执行 →COMMIT或ROLLBACK,不能依赖自动提交 - SQL Server 必须包在
TRY...CATCH块里,捕获死锁(错误 1205)后记录并重试,而不是让整个过程崩掉 - MySQL 存储过程中,
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION是刚需,否则异常直接中断流程 - 批次大小不是越小越稳:
500行太碎,网络和事务开销反升;10000行又容易触发锁等待超时
最容易被忽略的不是怎么切批次,而是每次循环后是否真正释放了锁、日志是否被截断、以及 ORDER BY 是否真的生效——这三个点一错,整个分批就退化成慢速全表扫。

















