直接DELETE几百万行会引发UNDO膨胀、锁争用、ORA-01555/01562错误、回滚代价高、HWM不降、索引分裂等问题;应采用ROWID分批删除,即查一批ROWID→删一批→提交→再查下一批,批次建议5000–10000行。
为什么不能直接 DELETE FROM table WHERE ... 删除几百万行
因为 oracle 会为每一行生成 undo 记录,并全程持有表级锁(或至少是大量数据块的 itl 锁),容易触发 ora-01555 快照过旧、ora-01562 无法扩展回滚段,甚至让其他会话长时间阻塞。更糟的是,如果事务中途失败,整个 delete 回滚代价极高。
- UNDO 表空间可能撑爆,尤其在低配环境或高并发时段
- 高水位线(HWM)不会自动下降,后续全表扫描仍扫空块
- 索引维护开销随行数线性增长,DELETE 过程中索引块分裂频繁
- 没有显式提交点,DBA 无法监控进度或安全中断
ROWID 分批 DELETE 的核心逻辑和写法
用 ROWID 是因为它是 Oracle 内部最轻量的定位方式,不依赖索引、不触发函数索引计算,且每个 ROWID 唯一对应一个物理位置。关键不是“按 ROWID 排序删”,而是“查一批 ROWID → 删这批 → 提交 → 查下一批”。
- 必须用子查询先
SELECT ROWID,再用WHERE ROWID IN ( ... ),不能直接在 DELETE 中嵌套复杂条件(否则优化器可能不走预期执行计划) - 批次大小建议 5000–10000 行:太小导致提交太频、日志切换压力大;太大又逼近单事务瓶颈
- 务必加
AND ROWNUM 在子查询里限制数量,否则 CBO 可能全表扫描后才截断 - 示例片段:
DECLARE v_batch_size NUMBER := 5000; BEGIN LOOP DELETE FROM orders WHERE ROWID IN ( SELECT ROWID FROM ( SELECT ROWID FROM orders WHERE status = 'PROCESSED' AND ROWNUM <= v_batch_size ) ); EXIT WHEN SQL%ROWCOUNT = 0; COMMIT; END LOOP; END;
WHERE 条件里含函数或绑定变量时的性能陷阱
如果原始删除条件是 TO_CHAR(create_time, 'YYYYMM') = '202301' 或用 :p_month 绑定变量,直接套进 ROWID 子查询会导致全表扫描 —— 因为 Oracle 无法对函数结果建高效访问路径,ROWID 本身又不带业务语义。
- 正确做法:先用业务字段 + 索引范围查出目标
ROWID集合,再删;比如给create_time建索引,子查询写成WHERE create_time >= DATE '2023-01-01' AND create_time - 避免在子查询中出现
UPPER(name)、TRUNC(dt)等,它们会让索引失效 - 如果必须用函数过滤,考虑建函数索引(如
CREATE INDEX idx_orders_mth ON orders (TO_CHAR(create_time, 'YYYYMM'))),但要注意统计信息是否及时更新 - 绑定变量在 PL/SQL 块中没问题,但别在动态 SQL 里拼接字符串(防 SQL 注入 + 硬解析爆炸)
删完之后 HWM 和索引碎片怎么处理
分批 DELETE 不会降低表的高水位线,也不会自动收缩索引结构 —— 这部分得手动干预,否则后续查询性能依然差。
- 重建表:用
ALTER TABLE t MOVE重置 HWM,但需注意这会失效所有索引,得立刻ALTER INDEX ... REBUILD - 更稳妥:用
DBMS_REDEFINITION在线重定义,适合 24×7 系统,但步骤略多、需额外空间 - 索引别只
REBUILD,优先COALESCE(合并叶块)—— 它不锁表、不占额外空间,对中等程度碎片足够 - 别忘了更新统计信息:
DBMS_STATS.GATHER_TABLE_STATS,否则 CBO 可能继续选错执行计划
UNDO_RETENTION 设置。哪怕分批,每批提交前的 UNDO 仍要保留到事务结束;如果 retention 设得太长,又没开 autoextend,删到一半就卡住是常态。










