从小表中删除重复行
Posted
技术标签:
【中文标题】从小表中删除重复行【英文标题】:Delete duplicate rows from small table 【发布时间】:2011-09-28 20:39:39 【问题描述】:我在 PostgreSQL 8.3.8 数据库中有一个表,它上面没有键/约束,并且有多行具有完全相同的值。
我想删除所有重复项并只保留每行的 1 个副本。
有一个特别的列(名为“key”)可用于识别重复项,即每个不同的“key”应该只存在一个条目。
我该怎么做? (理想情况下,使用单个 SQL 命令。) 在这种情况下,速度不是问题(只有几行)。
【问题讨论】:
【参考方案1】:更快的解决方案是
DELETE FROM dups a USING (
SELECT MIN(ctid) as ctid, key
FROM dups
GROUP BY key HAVING COUNT(*) > 1
) b
WHERE a.key = b.key
AND a.ctid <> b.ctid
【讨论】:
为什么比a_horse_with_no_name的方案快? 这更快,因为它只运行 2 个查询。第一个选择所有重复项,然后一个从表中删除所有项目。 @a_horse_with_no_name 的查询执行查询以查看它是否与表中的每个项目匹配。 什么是ctid
?
来自文档:ctid.行版本在其表中的物理位置。请注意,尽管 ctid 可用于非常快速地定位行版本,但行的 ctid 会在每次被 VACUUM FULL 更新或移动时发生变化。因此 ctid 作为长期行标识符是没有用的。
当重复行超过 2 行时,这似乎不起作用,因为它一次只删除一个重复。【参考方案2】:
DELETE FROM dupes a
WHERE a.ctid <> (SELECT min(b.ctid)
FROM dupes b
WHERE a.key = b.key);
【讨论】:
别用了,太慢了! 虽然这个解决方案确实有效,但 @rapimo 的 solution below 执行得更快。我相信这与这里的内部选择语句执行 N 次(对于 dupes 表中的所有 N 行)有关,而不是与其他解决方案中正在进行的分组有关。 对于巨大的表(几百万条记录),这个实际上适合内存,与@rapimo 的解决方案不同。所以在这些情况下,这是更快的一个(没有交换)。 补充说明:它起作用是因为 ctid 是一个特殊的 postgres 列,指示行的物理位置。即使您的表没有唯一 ID,您也可以将其用作唯一 ID。 postgresql.org/docs/8.2/ddl-system-columns.html @PawełMalisak 我在运行后阅读了它。【参考方案3】:这既快速又简洁:
DELETE FROM dupes T1
USING dupes T2
WHERE T1.ctid < T2.ctid -- delete the older versions
AND T1.key = T2.key; -- add more columns if needed
另请参阅我在 How to delete duplicate rows without unique identifier 的回答,其中包含更多信息。
【讨论】:
ct 代表什么?数? @trthhrtzctid
指向表中记录的物理位置。与我当时在评论中写的相反,使用小于运算符不一定指向旧版本,因为 ct 可以环绕,并且具有较低 ctid 的值实际上可能更新。
仅供参考,我尝试了这个解决方案,并在等待 15 分钟后中止了它。尝试了 rapimo 的解决方案,它在大约 10 秒内完成(删除了约 700,000 行)。
@Patrick 无法想象您的数据库是否没有唯一标识符,因为 rapimo 的答案在这种情况下不起作用。
@isapir 我只是好奇,上面的答案,他们在选择min(ctid)
时保留了旧记录吗?而你的保留较新的?谢谢!【参考方案4】:
我试过了:
DELETE FROM tablename
WHERE id IN (SELECT id
FROM (SELECT id,
ROW_NUMBER() OVER (partition BY column1, column2, column3 ORDER BY id) AS rnum
FROM tablename) t
WHERE t.rnum > 1);
由 Postgres wiki 提供:
https://wiki.postgresql.org/wiki/Deleting_duplicates
【讨论】:
与@rapimo 的答案和接受的答案(@a_horse_with_no_name)相比,您对性能有什么看法吗? 如果像问题所述,所有列相同,包括id
,则此列将不起作用。
此查询将删除原始副本和副本。问题是关于保留至少一行。
@pyBomb 错误,它将保留第一个 id
其中 column1...3 重复
从 postgresql 12 开始,这是迄今为止最快的解决方案(针对 3 亿行)。我刚刚测试了这个问题中提出的所有内容,包括接受的答案,这个“官方”解决方案实际上是最快的,并且满足 OP(和我的)的所有要求【参考方案5】:
EXISTS
很简单,对于大多数数据分布来说是最快的:
DELETE FROM dupes d
WHERE EXISTS (
SELECT FROM dupes
WHERE key = d.key
AND ctid < d.ctid
);
从每组重复行(由相同的key
定义)中,这会保留ctid
最少的一行。
结果与currently accepted answer by a_horse 相同。只是更快,因为EXISTS
可以在找到第一个违规行后立即停止评估,而min()
的替代方案必须考虑所有 em> 每组的行数来计算最小值。 这个问题与速度无关,但为什么不考虑呢?
您可能希望在清理后添加UNIQUE
constraint,以防止重复出现:
ALTER TABLE dupes ADD CONSTRAINT constraint_name_here UNIQUE (key);
关于系统专栏ctid
:
如果在表中定义了任何其他列UNIQUE NOT NULL
列(如PRIMARY KEY
),那么请务必使用它而不是ctid
。
如果key
可以是NULL
,而您也只需要其中一个,请使用IS NOT DISTINCT FROM
而不是=
。见:
因为这样比较慢,所以您可以按原样运行上述查询,并且另外:
DELETE FROM dupes d
WHERE key IS NULL
AND EXISTS (
SELECT FROM dupes
WHERE key IS NULL
AND ctid < d.ctid
);
并考虑:
Create unique constraint with null columns对于小型表,索引通常对性能没有帮助。我们不需要再看下去了。
对于大表和少数个重复,(key)
上的现有索引可以提供帮助(很多)。
对于大部分重复,索引可能会增加成本而不是收益,因为它必须同时保持最新。无论如何,查找没有索引的重复项会变得更快,因为有这么多,EXISTS
只需要找到一个。但是,如果您负担得起(即允许并发访问),请考虑一种完全不同的方法:将少数幸存的行写入新表。这也消除了过程中的表(和索引)膨胀。见:
【讨论】:
【参考方案6】:我会使用临时表:
create table tab_temp as
select distinct f1, f2, f3, fn
from tab;
然后,删除tab
并将tab_temp
重命名为tab
。
【讨论】:
这种方法不考虑触发器、索引和统计信息。当然你可以添加它们,但它也增加了很多工作。 不是每个人都需要它。这种方法非常快,并且在 20 万封没有索引的电子邮件 (varchar 250) 上比其他方法效果更好。 完整代码:DROP TABLE IF EXISTS tmp; CREATE TABLE tmp as ( SELECT * from (SELECT DISTINCT * FROM your_table) as t ); DELETE from your_table; INSERT INTO your_table SELECT * from tmp; DROP TABLE tmp;
【参考方案7】:
我必须创建自己的版本。 @a_horse_with_no_name 编写的版本在我的表上太慢了(21M 行)。而且@rapimo 根本不会删除重复。
这是我在 PostgreSQL 9.5 上使用的
DELETE FROM your_table
WHERE ctid IN (
SELECT unnest(array_remove(all_ctids, actid))
FROM (
SELECT
min(b.ctid) AS actid,
array_agg(ctid) AS all_ctids
FROM your_table b
GROUP BY key1, key2, key3, key4
HAVING count(*) > 1) c);
【讨论】:
【参考方案8】:另一种方法(仅当您的表中有任何唯一字段(如 id
)时才有效)按列查找所有唯一 ID 并删除不在唯一列表中的其他 ID
DELETE
FROM users
WHERE users.id NOT IN (SELECT DISTINCT ON (username, email) id FROM users);
【讨论】:
问题是,在我的问题中,这些表没有唯一的 ID; “重复”是多行,所有列上的值完全相同。 对,我加了一些注释【参考方案9】:怎么样:
和 u AS (SELECT DISTINCT * FROM your_table), x AS(从 your_table 中删除) INSERT INTO your_table SELECT * FROM u;我一直担心执行顺序,DELETE 是否会在 SELECT DISTINCT 之前发生,但它对我来说很好。 并且不需要任何关于表结构的知识。
【讨论】:
唯一的缺点是,如果你有不支持相等的数据类型(例如json
),这将不起作用。【参考方案10】:
这是using PARTITION BY
和virtual ctid
column 的解决方案,它的作用类似于主键,至少在单个会话中:
DELETE FROM dups
USING (
SELECT
ctid,
(
ctid != min(ctid) OVER (PARTITION BY key_column1, key_column2 [...])
) AS is_duplicate
FROM dups
) dups_find_duplicates
WHERE dups.ctid == dups_find_duplicates.ctid
AND dups_find_duplicates.is_duplicate
子查询用于将所有行标记为重复行,这取决于它们是否共享相同的“键列”,但与行的“分区”中的“第一个”行是否共享相同的ctid
共享相同的密钥。
换句话说,“第一”被定义为:
min(ctid) OVER (PARTITION BY key_column1, key_column2 [...])
然后,is_duplicate
为 true 的所有行都被它们的ctid
删除。
来自文档,ctid
represents(强调我的):
行版本在其表中的物理位置。请注意,虽然 ctid 可用于非常快速地定位行版本,但如果行的 ctid 被 VACUUM FULL 更新或移动,它会发生变化。因此 ctid 作为长期行标识符是无用的。应该使用主键来标识逻辑行。
【讨论】:
【参考方案11】:Postgresql 有 windows 功能,你可以使用 rank() 来归档你的目标,示例:
WITH ranked as (
SELECT
id, column1,
"rank" () OVER (
PARTITION BY column1
order by column1 asc
) AS r
FROM
table1
)
delete from table1 t1
using ranked
where t1.id = ranked.id and ranked.r > 1
【讨论】:
【参考方案12】:这对我来说效果很好。我有一个包含重复值的表,术语。运行查询以使用所有重复行填充临时表。然后我在临时表中使用这些 id 运行删除语句。 value 是包含重复项的列。
CREATE TEMP TABLE dupids AS
select id from (
select value, id, row_number()
over (partition by value order by value)
as rownum from terms
) tmp
where rownum >= 2;
delete from [table] where id in (select id from dupids)
【讨论】:
以上是关于从小表中删除重复行的主要内容,如果未能解决你的问题,请参考以下文章