必须使用 bulk collect + limit 分批处理并配合显式 commit,否则海量批处理易卡死、锁表、日志暴涨或 oom;传统 while + select into 因全量加载、offset 跳读、无提交等缺陷在百万级数据下必然失败。

直接结论:不用 BULK COLLECT + LIMIT 分批 + 显式 COMMIT,90% 的海量批处理会卡死、锁表、日志暴涨或 OOM。
为什么传统 WHILE + SELECT INTO 一定会失败
很多人写存储过程时习惯先 SELECT COUNT(*) 拿总数,再用 ROWNUM 或 OFFSET 分页循环更新——这在百万级以上数据中就是灾难:
- 每次
OFFSET都要跳过前面所有行,越往后越慢,物理读飙升 -
SELECT ... INTO试图一次性加载全量数据到内存,PL/SQL 变量撑爆 PGA,报ORA-04030 - 整个事务不提交,回滚段持续膨胀,可能触发
ORA-30036(undo tablespace full) - 没索引支撑的
ORDER BY会导致每批都全表排序,CPU 直接拉满
BULK COLLECT LIMIT 的安全取值和必须配合的动作
BULK COLLECT 是 Oracle 批处理的基石,但 LIMIT 值不是越大越好:
- 推荐值固定为
100~500,19c 实测LIMIT 500在多数 OLTP 场景下吞吐与内存占用平衡最佳 - 必须搭配
FORALL使用,否则只是把逐行变成“伪批量”,上下文切换仍频繁 - 每次
FETCH后必须检查v_batch.COUNT = 0再退出,不能只靠%NOTFOUND - 每批处理完必须
COMMIT,但别在循环内无条件COMMIT——要加计数器,比如每 1000 行一次,避免 I/O 过载
游标里必须加的 Hint 和索引约束
光写 BULK COLLECT 不够,游标本身若走错执行计划,批量也白搭:
- 强制走索引扫描:
SELECT /*+ INDEX(a idx_status_time) */ ... FROM t_main a WHERE status = 'PENDING' AND create_time - 避免并行反效果:除非明确有空闲 CPU 且已调优
PARALLEL参数,否则别加PARALLELhint;19c 默认自动 DOP 可能引发争用 - WHERE 条件字段必须有索引,且不能是函数包裹(如
TRUNC(create_time)),否则索引失效 - 如果涉及多表 JOIN,确保驱动表(最左表)有高选择性过滤条件,否则中间结果集爆炸
容易被忽略的自治事务和异常兜底
批处理中途出错时,没人帮你回滚已提交的批次——这是线上事故高发点:
- 日志记录必须用自治事务:
PRAGMA AUTONOMOUS_TRANSACTION,否则主事务一回滚,日志也消失 - 每批处理前记录起始 ID 或时间戳,失败后可从断点续跑,而不是重头来
-
EXCEPTION块里不能只DBMS_OUTPUT.PUT_LINE,要写入日志表并RAISE_APPLICATION_ERROR中断流程 - 别依赖客户端重试——存储过程得自己判断是否还有剩余数据,用
GET DIAGNOSTICS row_count = ROW_COUNT辅助判断
真正卡住人的从来不是语法,而是每批 COMMIT 的时机、游标是否真的走了索引、以及出错后有没有留下可追溯的断点信息。这些细节不写进代码注释,三个月后你自己都看不懂。











