用rowid删重最可靠,但必须验证、备份、分步执行;因rowid是物理唯一标识,可处理null、clob等场景,且能控制保留行;常见错误包括子查询未关联、not in遇null失效、大表性能差;正确做法是显式关联或用临时表+not exists;存储过程需校验表存在、捕获异常、打印影响行数、避免自动commit。
直接说结论:用 rowid 删除重复数据是 oracle 里最可靠、最不依赖业务逻辑的方案,但「安全」的前提是必须验证、备份、分步执行——漏掉任何一步都可能丢数据。
为什么非得用 ROWID,而不是 DISTINCT 或 GROUP BY
DISTINCT 和 GROUP BY 只能查,不能删;而 ROWID 是每行物理地址的唯一标识,哪怕所有字段全为 NULL,ROWID 也不同。这意味着:
- 不用怕判重字段含
NULL(NOT IN遇NULL直接失效) - 不需要建索引或改表结构
- 不依赖字段是否可比较(比如
CLOB字段无法参与GROUP BY) - 保留哪条由你控制:用
MAX(ROWID)留最新插入的,用MIN(ROWID)留最早的
DELETE ... WHERE ROWID NOT IN (SELECT MIN(ROWID) ...) 为什么常出错
这个写法看着简洁,但实际线上踩坑最多。常见错误包括:
- 子查询没加
WHERE关联条件,导致返回单个MIN(ROWID),外层把整张表删剩一行 - 判重字段含
NULL,NOT IN返回空结果集,DELETE不生效却以为成功 - 大数据量下子查询触发笛卡尔积或全表扫描 × 全表扫描,I/O 爆炸,会话卡死
- 没加
/*+ parallel(4) */hint,小表没问题,千万级表跑一小时还没结束
正确写法必须显式关联,例如按 name 和 email 判重:
DELETE FROM users a
WHERE a.ROWID NOT IN (
SELECT MIN(b.ROWID)
FROM users b
WHERE b.name = a.name
AND b.email = a.email
GROUP BY b.name, b.email
);
大表(>100万行)必须拆成临时表 + NOT EXISTS
当重复率高、总行数大时,上面那种带子查询的 DELETE 很容易被 CBO 选错执行计划。更稳的路径是两步走:
- 先建临时表存要保留的
ROWID:CREATE TABLE tmp_keep AS SELECT name, email, MAX(ROWID) keep_rid FROM users GROUP BY name, email;
- 再删非保留行,且必须用
NOT EXISTS(不是NOT IN),避免NULL干扰:DELETE FROM users a WHERE NOT EXISTS ( SELECT 1 FROM tmp_keep b WHERE b.name = a.name AND b.email = a.email AND b.keep_rid = a.ROWID );
注意:临时表建议加 NOLOGGING 和 PCTFREE 0 加速,但前提是允许丢失归档日志。
存储过程封装去重逻辑时,最容易忽略的三件事
写成存储过程看似自动化了,反而更容易出事:
- 没做
EXECUTE IMMEDIATE 'CREATE TABLE ... AS SELECT ...'的异常捕获,建表失败后继续往下跑,删的是空表 - 没检查输入参数(如判重字段列表)是否真实存在于目标表中,拼 SQL 时字段名错位,删错列
- 没加
DBMS_OUTPUT.PUT_LINE打印实际影响行数,只看SQL%ROWCOUNT,但并行 DML 下它可能不准 - 最致命:过程里直接
COMMIT,没留回滚机会——应该让调用方决定是否提交
真正安全的存储过程,开头第一句应该是:
IF NOT EXISTS (SELECT 1 FROM user_tables WHERE table_name = UPPER(p_table_name)) THEN RAISE_APPLICATION_ERROR(-20001, '表不存在:' || p_table_name); END IF;
ROWID 方案本身很干净,但「安全」不在语法里,而在操作链路上:先 SELECT 验证、再备份建表、再删、再 COUNT 检查 HAVING COUNT(*) > 1 是否为空——这四步少一步,就不是安全删重,只是碰运气。











