最稳方案是用 ROW_NUMBER() + CTE 标序号再删:先用 CTE 计算每组重复数据的行号,再在 DELETE 中引用,确保安全、可控、可读;PARTITION BY 和 ORDER BY 必须严格匹配业务逻辑,避免漏删或误删。
用
+
标序号再删,最稳
PostgreSQL 从 8.4 就支持窗口函数,
是去重删除的核心工具。它不依赖主键,只看你指定的「重复判断字段」和排序逻辑——比如按
分组、按
排,就能精准标出每组最新那条是第 1 名。
关键不是“怎么写”,而是“怎么写才安全”:
必须用
CTE 包一层,不能在
里直接嵌套窗口函数(PostgreSQL 不允许)
的列要严格对应你定义“重复”的业务逻辑,少一个就漏删,多一个就误删
决定留哪条:想留最新的就
,留最早的就
;如果排序字段有
,记得加
或
显式控制
示例(保留每组
中
最小的):
别直接
,性能差还难读
有人图省事用
做物理行标识去删,比如:
这看似不用写排序逻辑,但问题明显:
是物理位置标识,不反映业务意义;同一组重复数据中哪条“更早插入”无法保证,可能随机留一条
+
在大数据量下会触发全表扫描+哈希分组,70 万行实测耗时超 30 秒(见知识库数据)
语句不可读:后续维护者根本看不出你留的是“最早”还是“最新”,也看不出依据哪个字段判重
除非你明确只要“任意留一条且不在乎顺序”,否则别碰
方案。
子查询在有
时会静默失效
这个坑非常隐蔽:如果用来判重的字段(比如
)允许为
,下面这种写法会漏删所有含
的重复组:
原因:
整个表达式结果恒为
,被当作
处理,导致匹配不到任何行。
绕过方法只有两个:
提前过滤掉
:
加到子查询和主查询里
改用
,它对
安全:
大表删重前务必先建索引,不然删着删着就锁表
窗口函数本身不依赖索引,但
的最终执行阶段需要定位并锁定要删的行。如果
字段没索引,PostgreSQL 只能走顺序扫描,70 万行的表可能锁表数分钟——期间所有
都被阻塞。
正确做法:
在去重字段组合上建索引,例如:
如果是多字段判重(如
),建联合索引:
索引建完跑一遍
更新统计信息,让查询计划器选对路径
没索引的删重操作,表面看 SQL 跑得通,实际是拿生产稳定性换来的“省事”。
ROW_NUMBER()CTEROW_NUMBER()create_bycreate_time DESCWITHDELETEPARTITION BYORDER BYDESCASCNULLNULLS LASTNULLS FIRSTemailidWITH ranked AS (
SELECT id, email,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id ASC) AS rn
FROM people
)
DELETE FROM people
WHERE id IN (SELECT id FROM ranked WHERE rn > 1);DELETE FROM ... WHERE ctid IN (...)ctidDELETE FROM people
WHERE ctid NOT IN (
SELECT MIN(ctid)
FROM people
GROUP BY email
);ctidGROUP BYMIN(ctid)ctidNOT INNULLemailNULLNULLDELETE FROM people
WHERE email NOT IN (
SELECT MIN(email) FROM people GROUP BY email
);NOT IN (..., NULL)UNKNOWNFALSENULLWHERE email IS NOT NULLNOT EXISTSNULLDELETE FROM people p1
WHERE EXISTS (
SELECT 1 FROM people p2
WHERE p2.email = p1.email
AND p2.id < p1.id
AND p2.email IS NOT NULL
);DELETEPARTITION BYINSERT/UPDATECREATE INDEX idx_people_email ON people(email);name, emailCREATE INDEX idx_people_name_email ON people(name, email);ANALYZE people;