跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

PostgreSQL如何实现根据非主键字段去重删除_利用窗口函数分析

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

相关文章