应使用top+主键推进分批处理,禁用offset/fetch:因后者每次全扫前n行致性能断崖;需动态查起始id、强制order by id、更新后立即刷新起点、加@@rowcount终止判断,并建(status,id)复合索引。

SQL Server 用 TOP + 主键推进,别碰 OFFSET/FETCH
OFFSET/FETCH 在分批更新中是性能陷阱:每次都要从头扫描前 N 行,处理到第 50 万行时,执行计划里全是 Clustered Index Scan,WRITELOG 和 LCK_M_U 等待飙升。真正可控的做法是记住上一批最后的 id,下一批从它之后开始。
实操要点:
- 起始点必须动态查:
DECLARE @min_id BIGINT = (SELECT MIN(id) FROM orders WHERE status = 'pending'),不能硬编码 - 每次更新必须带
ORDER BY id,否则TOP (5000)行为不可预测 - 更新后立即刷新起点:
SELECT @min_id = MIN(id) FROM orders WHERE status = 'pending' AND id > @min_id - 必须加
IF @@ROWCOUNT = 0 BREAK,否则@min_id变成NULL后循环失控 -
status和id必须建复合索引,例如CREATE INDEX IX_orders_status_id ON orders(status, id)
MySQL 用变量游标模拟分批,绕过 UPDATE + LIMIT 限制
MySQL 不允许 UPDATE ... LIMIT 直接关联子查询源表,但可用变量构造逻辑游标实现等效效果。关键不是“能不能 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预占,或由应用层用分布式锁协调 -
WHERE条件字段必须有索引,且不能是表达式(如DATE(created_at)),否则ORDER BY触发filesort
Oracle 用 BULK COLLECT + LIMIT + FORALL,别写 WHILE 循环
传统 WHILE + SELECT INTO 在百万级下必然失败:全量加载撑爆 PGA、回滚段暴涨、每批都全表排序。BULK COLLECT 是唯一能压住内存和吞吐平衡点的方式。
实操要点:
-
LIMIT推荐值固定为 100~500,Oracle 19c 实测LIMIT 500吞吐与内存占用最平衡 - 必须搭配
FORALL使用,否则只是“伪批量”,上下文切换开销照旧 - 每次
FETCH后检查v_batch.COUNT = 0再退出,不能只靠%NOTFOUND - 游标里加
/*+ INDEX(a idx_status_time) */强制走索引,WHERE条件字段不能包函数(如TRUNC(create_time)) - 每批处理完要
COMMIT,但别无条件 commit——建议每 1000 行一次,避免 I/O 过载
所有数据库共通的事务与错误控制细节
分批不是把大事务切成小事务就完事了。每批是否独立提交、失败后能否续跑、锁是否及时释放,这些才是线上稳定性的命门。
实操要点:
- 每批开头写
BEGIN TRANSACTION,成功后紧跟COMMIT TRANSACTION,不要等循环结束再统一提交 - 在
CATCH块里记录当前@min_id和错误信息,方便人工介入或断点续跑 - 有触发器的表,确认它们不会在每批中重复执行昂贵逻辑;必要时临时禁用:
DISABLE TRIGGER trg_orders_update ON orders - MySQL 存储过程中务必加
CONTINUE HANDLER FOR SQLEXCEPTION捕获死锁,否则一次失败整个过程就停摆 - 别依赖
ROW_COUNT()判断“还有多少没处理”,而要用它判断“本次有没有真删/改到”,这才是真实反馈
READ_COMMITTED_SNAPSHOT 是否开启、innodb_buffer_pool_size 是否足够——这些细节不提前对齐,存储过程一跑就卡在等待事件里。











