如何删除重复的条目?


92

我必须向现有表添加唯一约束。很好,除了表已经有数百万行,而且许多行违反了我需要添加的唯一约束。

删除有问题的行的最快方法是什么?我有一条SQL语句,该语句查找重复项并将其删除,但要花很长时间才能运行。是否有另一种方法可以解决此问题?也许备份表,然后在添加约束后还原表?

Answers:


101

例如,您可以:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;

2
您能使它与列组不同吗?也许是“ SELECT DISTINCT(ta,tb,tc),* FROM t”?
gjrwebber


36
易于输入:CREATE TABLE tmp AS SELECT ...;。然后,您甚至不需要弄清楚它的布局tmp。:)
Randal Schwartz'2

9
实际上,由于几个原因,这个答案不是很好。@Randal命名了一个。在大多数情况下,尤其是如果您有依赖的对象(如索引,约束,视图等)时,更好的方法是使用实​​际的TEMPORARY TABLE,将原始内容截断并重新插入数据。
Erwin Brandstetter,2012年

7
您对索引是正确的。删除和重新创建要快得多。但是其他依赖的对象将破坏或阻止完全删除该表(OP 创建副本会发现该表),对于“最快的方法”来说是如此之多。不过,您对否决票的看法是正确的。这是没有根据的,因为这不是一个坏答案。只是不是那么好。您可能已经添加了一些有关索引或相关对象的指针,或者像在注释或任何解释中所做的那样,添加了手册的链接。我想我对人们如何投票感到沮丧。删除了downvote。
Erwin Brandstetter,2012年

173

其中一些方法似乎有些复杂,我通常这样做是:

给定table table,要在(field1,field2)上对其进行唯一化,使行保持为最大field3:

DELETE FROM table USING table alias 
  WHERE table.field1 = alias.field1 AND table.field2 = alias.field2 AND
    table.max_field < alias.max_field

例如,我有一个表,user_accounts我想在电子邮件上添加一个唯一约束,但是有一些重复项。还说我想保留最近创建的一个(重复项中的最大id)。

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • 注意- USING不是标准SQL,它是PostgreSQL扩展(但非常有用),但是原始问题专门提到了PostgreSQL。

4
第二种方法在postgres上非常快!谢谢。
埃里克·鲍曼

5
@Tim您能更好地解释USINGpostgresql中的功能吗?
FopaLéonConstantin 2014年

3
到目前为止,这是最好的答案。即使您的表中没有串行列可用于ID比较,还是值得临时添加一个以使用这种简单方法。
Shane

2
我刚刚检查。答案是肯定的。使用小于号(<)仅使您具有最大id,而大于号(>)仅使您具有最小id,删除其余部分。
安德烈C.安德森

1
@Shane可以使用:WHERE table1.ctid<table2.ctid-无需添加串行列
alexkovelsky,2016年

25

除了创建新表外,您还可以在截断后将唯一的行重新插入到同一表中。做这一切在一个事务中。(可选)您可以使用来将临时表自动放在事务末尾ON COMMIT DROP。见下文。

只有在要从表中删除许多行的情况下,此方法才有用。对于一些重复项,请使用Plain DELETE

您提到了数百万行。为了加快操作速度,您需要为会话分配足够的临时缓冲区。必须在当前会话中使用任何临时缓冲区之前调整该设置。找出表的大小:

SELECT pg_size_pretty(pg_relation_size('tbl'));

进行相应设置temp_buffers。由于内存中的表示需要更多的RAM,因此可以进行大量舍入。

SET temp_buffers = 200MB;    -- example value

BEGIN;

-- CREATE TEMPORARY TABLE t_tmp ON COMMIT DROP AS -- drop temp table at commit
CREATE TEMPORARY TABLE t_tmp AS  -- retain temp table after commit
SELECT DISTINCT * FROM tbl;  -- DISTINCT folds duplicates

TRUNCATE tbl;

INSERT INTO tbl
SELECT * FROM t_tmp;
-- ORDER BY id; -- optionally "cluster" data while being at it.

COMMIT;

如果存在依赖的对象,此方法可能优于创建新表。视图,索引,外键或其他引用该表的对象。TRUNCATE让你用干净的石板开始呢(在后台新的文件),并且是快于DELETE FROM tbl大表(DELETE其实是可以用小桌子更快)。

对于大表,删除索引和外键,重新填充表并重新创建这些对象通常更快。至于fk约束,您必须确定新数据当然是有效的,否则在尝试创建fk时会遇到异常。

请注意,这TRUNCATE需要比进行更积极的锁定DELETE。对于具有大量并发负载的表,这可能是一个问题。

如果TRUNCATE不是一种选择,或者通常对于中小型表,则有一种类似的技术,它具有可修改数据的CTE(Postgres 9.1 +):

WITH del AS (DELETE FROM tbl RETURNING *)
INSERT INTO tbl
SELECT DISTINCT * FROM del;
-- ORDER BY id; -- optionally "cluster" data while being at it.

大桌子比较慢,因为TRUNCATE那里更快。但是对于小桌子可能会更快(更简单!)。

如果根本没有依赖对象,则可以创建一个新表并删除旧表,但是通过这种通用方法几乎没有任何收获。

对于无法容纳到可用RAM中的非常大的表,创建表的速度将大大提高。您必须权衡此问题与依赖对象可能带来的麻烦/开销。


2
我也使用这种方法。但是,它可能是个人的,但是我的临时表已删除,并且在截断之后不可用...如果临时表已成功创建并且可用,请谨慎执行这些步骤。
xlash 2012年

@xlash:您可以检查是否存在以确保存在,并且可以为临时表使用其他名称,或者重用现有的名称..我在回答中添加了一些内容。
Erwin Brandstetter,2012年

警告:小心+1到@xlash-我必须重新导入我的数据,因为之后的临时表不存在TRUNCATE。正如Erwin所说,在删除表之前,请确保它存在。见@ codebykat的答案
乔丹砷

1
@JordanArseno:我切换到了不带的版本ON COMMIT DROP,这样错过了我在“一次交易中”写过部分的人们就不会丢失数据。并且我添加了BEGIN / COMMIT来阐明“一项交易”。
Erwin Brandstetter

1
使用USING解决方案花费了3个多小时的时间,记录了1400万条记录。使用temp_buffers的解决方案花费了13分钟。谢谢。
Castt 2015年

20

您可以使用oid或ctid,它们通常是表中的“不可见”列:

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);

4
对于就地删除,NOT EXISTS应该快得多DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)-或使用其他任何列或一组列进行排序以选择幸存者。
Erwin Brandstetter

@ErwinBrandstetter,您提供的查询应该使用NOT EXISTS吗?
约翰

1
@约翰:一定在EXISTS这里。像这样阅读:“删除存在其他任何行的所有行,并以相同的值添加dist_col一个更大的行ctid”。每组骗子中唯一的幸存者将是最大的幸存者ctid
Erwin Brandstetter 2014年

如果只有几个重复的行,这是最简单的解决方案。LIMIT如果您知道重复的数量,可以与之配合使用。
Skippy le Grand Gourou

19

PostgreSQL窗口函数对于解决这个问题很方便。

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);

请参阅删除重复项


使用“ ctid”而不是“ id”,这实际上适用于完全重复的行。
bradw2k 2015年

很好的解决方案。我必须对具有十亿条记录的表执行此操作。我在内部SELECT中添加了WHERE以大块方式进行。
1

7

旧的postgresql.org邮件列表中

create table test ( a text, b text );

独特价值

insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );

值重复

insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );

再重复一遍

insert into test values ( 'x', 'y');

select oid, a, b from test;

选择重复的行

select o.oid, o.a, o.b from test o
    where exists ( select 'x'
                   from test i
                   where     i.a = o.a
                         and i.b = o.b
                         and i.oid < o.oid
                 );

删除重复的行

注意:PostgreSQL在from删除子句中提到的表上不支持别名。

delete from test
    where exists ( select 'x'
                   from test i
                   where     i.a = test.a
                         and i.b = test.b
                         and i.oid < test.oid
             );

您的解释很聪明,但您遗漏了一点,在创建表中指定oid,然后仅访问oid else错误消息显示
Kalanidhi 2014年

@Kalanidhi感谢您对改进答案的意见,我将考虑这一点。
Bhavik Ambani'3


如果'oid'给您一个错误,则可以使用系统列'ctid'。
sul4bh 2015年

7

通用查询删除重复项:

DELETE FROM table_name
WHERE ctid NOT IN (
  SELECT max(ctid) FROM table_name
  GROUP BY column1, [column 2, ...]
);

该列ctid是可用于每个表的特殊列,但除非特别说明,否则不可见。该ctid列的值被认为是表中的每一行都是唯一的。


唯一的普遍答案!在没有自我/笛卡尔联接的情况下工作。值得补充的是,正确指定GROUP BY子句是必不可少的-这应该是现在违反的“唯一性标准”,或者如果您想要检测重复项的键。如果指定错误,它将无法正常工作
msciwoj 2016年

4

我只是成功地使用了Erwin Brandstetter的答案来删除联接表(缺少自己的主ID的表)中的重复项,但发现有一个重要警告。

包括ON COMMIT DROP意味着临时表将在事务结束时被删除。对我来说,这意味着我要插入临时表时该临时表不再可用

我只是做了CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;,一切都很好。

临时表确实在会话结束时被删除。


3

此函数删除重复项而不删除索引,并将其复制到任何表。

用法: select remove_duplicates('mytable');

---
-remove_duplicates(tablename)从表中删除重复记录(从集合转换为唯一集合)
---
创建或替换功能remove_duplicates(text)返回无效的$$
宣布
  表名ALIAS FOR $ 1;
开始
  执行“创建临时表_DISTINCT_” || 表名|| 'AS(SELECT DISTINCT * FROM'||表名||');';
  执行'DELETE FROM'|| 表名|| ';';
  执行'INSERT INTO'|| 表名|| '(SELECT * FROM _DISTINCT_'||表名||');';
  执行'DROP TABLE _DISTINCT_'|| 表名|| ';';
  返回;
结束;
$$ LANGUAGE plpgsql;

3
DELETE FROM table
  WHERE something NOT IN
    (SELECT     MAX(s.something)
      FROM      table As s
      GROUP BY  s.this_thing, s.that_thing);

这是我目前正在做的事情,但是运行需要很长时间。
gjrwebber

1
如果表中的多行某列中的值相同,这不会失败吗?
shreedhar

3

如果您只有一个或几个重复的条目,并且确实是重复的(即,它们出现了两次),则可以使用ctid上面建议的“隐藏” 列,以及LIMIT

DELETE FROM mytable WHERE ctid=(SELECT ctid FROM mytable WHERE […] LIMIT 1);

这将仅删除所选行的第一行。


我知道它不能解决OP的问题,因为OP的问题在数百万行中有很多重复,但是无论如何它可能会有所帮助。
Skippy le Grand Gourou 2014年

每个重复行都必须运行一次。shekwi的答案只需运行一次。
bradw2k 2015年

3

首先,您需要确定要保留哪些“重复项”。如果所有列都相等,那么可以删除其中的任何一个。。。但是也许您只想保留最新列或其他条件?

最快的方法取决于您对上述问题的回答,还取决于表格中重复项的百分比。如果丢弃50%的行,则最好这样做CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;,如果删除1%的行,则使用DELETE更好。

同样对于这样的维护操作,通常最好将work_memRAM 设置为一个很大的块:运行EXPLAIN,检查排序/哈希数N,然后将work_mem设置为RAM / 2 / N。对速度有好处。只要您只有一个并发连接...


1

我正在使用PostgreSQL 8.4。当我运行建议的代码时,我发现它实际上并没有删除重复项。在运行一些测试时,我发现添加“ DISTINCT ON(duplicate_column_name)”和“ ORDER BYplicate_column_name”可以解决问题。我不是SQL专家,我在PostgreSQL 8.4 SELECT ... DISTINCT文档中找到了它。

CREATE OR REPLACE FUNCTION remove_duplicates(text, text) RETURNS void AS $$
DECLARE
  tablename ALIAS FOR $1;
  duplicate_column ALIAS FOR $2;
BEGIN
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || ' AS (SELECT DISTINCT ON (' || duplicate_column || ') * FROM ' || tablename || ' ORDER BY ' || duplicate_column || ' ASC);';
  EXECUTE 'DELETE FROM ' || tablename || ';';
  EXECUTE 'INSERT INTO ' || tablename || ' (SELECT * FROM _DISTINCT_' || tablename || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
  RETURN;
END;
$$ LANGUAGE plpgsql;

1

这非常有效并且非常快速:

CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;

1
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);

按列删除重复项,并保留ID最低的行。该模式取自postgres Wiki

使用CTE,您可以通过以下方式获得上述内容的更具可读性的版本

WITH duplicate_ids as (
    SELECT id, rnum 
    FROM num_of_rows
    WHERE rnum > 1
),
num_of_rows as (
    SELECT id, 
        ROW_NUMBER() over (partition BY column1, 
                                        column2, 
                                        column3 ORDER BY id) AS rnum
        FROM tablename
)
DELETE FROM tablename 
WHERE id IN (SELECT id from duplicate_ids)

1
CREATE TABLE test (col text);
INSERT INTO test VALUES
 ('1'),
 ('2'), ('2'),
 ('3'),
 ('4'), ('4'),
 ('5'),
 ('6'), ('6');
DELETE FROM test
 WHERE ctid in (
   SELECT t.ctid FROM (
     SELECT row_number() over (
               partition BY col
               ORDER BY col
               ) AS rnum,
            ctid FROM test
       ORDER BY col
     ) t
    WHERE t.rnum >1);

我对其进行了测试,并且效果良好;我将其格式化以提高可读性。它看起来相当复杂,但可以使用一些解释。一个人如何改变自己的用例呢?
Tobias
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.