sql server百万行分批更新必须用主键推进而非offset/fetch,mysql需用变量模拟分页;均须建复合索引、显式事务控制、合理批次大小(推荐2000),禁用真实游标。

SQL Server 用 TOP + 主键推进分批更新
直接 UPDATE 百万行,基本等于触发锁升级、日志暴涨、连接超时。必须分批,且不能用 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行为无定义 - 更新完立刻刷新:
SELECT @min_id = MIN(id) FROM orders WHERE status = 'pending' AND id > @min_id - 加
IF @@ROWCOUNT = 0 BREAK,防止空结果集导致无限循环 -
status和id必须建复合索引,否则每次WHERE都全表扫
MySQL 用变量游标模拟分批,绕过 UPDATE LIMIT 限制
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行又容易触发锁等待超时,推荐从2000起调优
别用真实游标(Cursor)处理百万级更新
真实游标逐行 fetch + update,在百万级场景下就是性能黑洞。它本质是把服务器端压力转成大量 round-trip 和锁持有时间,还容易被阻塞、超时、内存溢出。
除非你真需要对每一行做独立判断(比如调外部 API、复杂分支逻辑),否则一律避免:
-
DECLARE ... CURSOR FOR SELECT ...→OPEN→FETCH NEXT→ 循环体 →CLOSE→DEALLOCATE这套流程,吞吐量极低 - 即使加了
FAST_FORWARD,也改变不了它单行锁定、无法批量提交的本质 - 如果业务逻辑真绕不开逐行处理,优先考虑把数据导出到应用层(如 Python/Java),用连接池+批量语句回写,比 T-SQL 游标快一个数量级
status 字段被高频更新时,MIN(id) 查询可能被阻塞,这时得考虑用 READPAST 或降级为时间范围切片。










