必须分批处理,SQL Server用TOP+ORDER BY动态推进并建复合索引,MySQL用变量游标配合FOR UPDATE预占,Oracle用BULK COLLECT+LIMIT+FORALL,且须禁用外键、关闭自动统计、监控主从延迟。

直接结论:不分批、不显式提交、不控制事务边界,就等于把日志文件当垃圾桶用——哪怕只跑一条
UPDATE,只要它扫了百万行,
ldf 就可能瞬间涨到 50GB。
SQL Server 必须用 TOP + ORDER BY 分批,别碰 OFFSET/FETCH
OFFSET/FETCH 看似简洁,但每次都要从头扫描前 N 行。10 万行后,单次
OFFSET 99999 ROWS 就可能触发大量逻辑读和排序,日志生成量翻倍。
TOP 推进才是可控解法:
- 起始点必须动态查:
DECLARE @min_id BIGINT = (SELECT MIN(id) FROM t WHERE status = 'pending'),硬编码 @min_id 会导致漏处理
- 更新语句必须带
ORDER BY id,否则 TOP (5000) 返回的行不可预测
- 每批执行后立刻刷新起点:
SELECT @min_id = MIN(id) FROM t WHERE status = 'pending' AND id > @min_id
- 加
IF @@ROWCOUNT = 0 BREAK,否则空结果集会让循环卡死
-
status 和 id 必须建复合索引,比如 CREATE INDEX ix_status_id ON t(status, id),否则每次 WHERE 都全表扫描
MySQL 变量游标必须重置 + FOR UPDATE 预占
MySQL 不支持
UPDATE ... LIMIT 直接关联子查询,靠变量模拟游标时极易出错:
- 每次执行前必须重置:
SET @row_index := -1,否则第二次运行会从上次结束位置继续,漏掉前几批
- 子查询里
ORDER BY id 不可省,否则 @row_index 分配顺序不保,可能跳行或重复
- 典型写法要嵌套两层:
UPDATE t SET status = 'done' WHERE id IN (SELECT id FROM (SELECT id, @row_index := @row_index + 1 AS rn FROM t WHERE status = 'pending' ORDER BY id LIMIT 5000) AS tmp)
- 并发场景下,没加锁就执行,可能漏行或重复处理;建议先
SELECT id FROM t WHERE status = 'pending' ORDER BY id LIMIT 5000 FOR UPDATE 预占,再更新
-
WHERE 条件字段不能是表达式(如 DATE(create_time) = '2026-07-01'),否则索引失效,ORDER BY 触发 filesort
Oracle 19c 必须用 BULK COLLECT + LIMIT + FORALL
传统
WHILE 循环 +
SELECT INTO 在百万级数据下必然失败:
-
LIMIT 值不是越大越好:实测 LIMIT 500 在多数 OLTP 场景下吞吐与内存占用最平衡,LIMIT 5000 容易撑爆 PGA,报 ORA-04030
- 必须搭配
FORALL,否则只是“伪批量”,上下文切换开销照旧
- 每次
FETCH 后检查 v_batch.COUNT = 0 再退出,%NOTFOUND 在最后一批可能不准
- 游标里加提示:
SELECT /*+ INDEX(a idx_status_time) */ ... FROM t_main a WHERE status = 'PENDING',避免走错执行计划
- 别乱加
PARALLEL hint——19c 默认 DOP 可能引发 CPU 争用,除非你明确压测过并调优了 PARALLEL_MAX_SERVERS
所有数据库都绕不开的三个硬约束
再标准的分批逻辑,撞上这三条也会崩,而且错误不直接指向它们:
- 外键约束没禁用:
ALTER TABLE child NOCHECK CONSTRAINT fk_name 必须提前执行,否则每批都校验外键,性能断崖下跌
- 自动统计更新开着:
ALTER DATABASE your_db SET AUTO_UPDATE_STATISTICS OFF,删一半时触发统计更新,锁表+全表扫描
- 主从延迟高:在主库跑分批删,从库可能因单批执行太久而延迟飙升;需监控
replication lag,动态调小 TOP 值(比如从 5000 降到 1000)
真正难的不是写出循环语句,而是判断哪张表该用 3000 行一批,哪张得压到 1000 行——这取决于索引深度、行宽、日志备份频率和当前系统负载。跑之前,先在测试库用
DBCC SQLPERF(logspace) 看一眼日志使用率,删两批后立刻查,比任何理论都准。