rowid(oracle)和ctid(postgresql)是数据库为每行分配的物理地址标识,天然唯一且不依赖业务字段,可精准定位重复行中需保留或删除的具体记录。

ROWID 和 CTID 是什么,为什么能定位重复行
ROWID(Oracle)和 ctid(PostgreSQL)是数据库为每行分配的物理地址标识,不依赖业务字段、不需索引、天然唯一。它们指向数据在磁盘上的实际位置,哪怕整行内容完全相同,ctid 或 ROWID 也不同——这正是去重时能“区分”重复行的关键。
注意:ctid 在 PostgreSQL 中是临时物理地址,VACUUM 后可能复用;ROWID 在 Oracle 中稳定,但仅对堆表有效(IOT 表用逻辑 ROWID)。二者都不能被索引,也不建议长期存储。
PostgreSQL 用 ctid 删除重复行(无主键场景)
当表没有主键或唯一约束,又必须删重复时,ctid 是最直接的锚点。核心思路:按业务字段分组,取每组中 ctid 最小(或最大)的那条保留,其余删除。
- 必须加事务包裹:
BEGIN;+ROLLBACK;测试成功后再COMMIT; - 不能写成
DELETE WHERE ctid NOT IN (SELECT MIN(ctid) ...)—— 子查询结果为空时整个语句不删任何行,容易误判 - 推荐用
NOT IN配合非空校验,或改用USING关联更安全
示例(保留每组 (name, email) 中 ctid 最小的行):
WITH kept AS ( SELECT MIN(ctid) AS ctid FROM users GROUP BY name, email ) DELETE FROM users WHERE ctid NOT IN (SELECT ctid FROM kept);
Oracle 用 ROWID 去重必须避开的坑
Oracle 的 ROWID 看似更稳定,但仍有隐蔽风险:
-
ROWID不是常量:表移动(如ALTER TABLE ... MOVE)、分区交换、闪回操作后会变化 - 不能直接
GROUP BY ROWID—— 它是伪列,但参与分组没问题;真正危险的是用ROWID排序后误以为“最新插入=最大 ROWID”,其实 ROWID 编码含文件/块信息,不反映时间顺序 - 别在应用层缓存
ROWID做后续更新,过期即失效
安全写法(同样保留每组首行):
DELETE FROM users WHERE ROWID NOT IN ( SELECT MIN(ROWID) FROM users GROUP BY name, email );
跨数据库通用替代方案:用哈希构造确定性锚点
当 ctid/ROWID 不可用(如某些 Greenplum 分布式表、旧版 SQLite),或你想让去重行为可重现(比如测试环境和生产环境排序一致),就得自己造锚点:
- 用
MD5(CONCAT(COALESCE(col1,''), '|', COALESCE(col2,'')))拼接关键字段,避免 NULL 导致整个哈希为 NULL - 不要用
||直接连,MySQL/PostgreSQL 对 NULL 处理不一致;CONCAT()或COALESCE()更稳妥 - 哈希只是辅助排序,不能代替
ctid/ROWID定位——最终删行仍需主键或物理标识,否则无法保证删的是“副本”而非“原件”
真正不可靠的是没物理锚点还硬删:一旦所有字段都相同,数据库根本不知道哪条该留。这种情况下,CREATE TABLE AS SELECT DISTINCT 重建才是唯一安全路径。











