【问题标题】:How to delete duplicate entries?如何删除重复条目?
【发布时间】:2010-12-17 07:14:54
【问题描述】:

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

删除违规行的最快方法是什么?我有一个 SQL 语句,它可以找到重复项并删除它们,但它需要永远运行。有没有其他方法可以解决这个问题?也许备份表,然后在添加约束后恢复?

【问题讨论】:

    标签: sql postgresql duplicate-removal unique-constraint sql-delete


    【解决方案1】:

    其中一些方法看起来有点复杂,我通常这样做:

    给定表 table,希望在 (field1, field2) 上对其进行唯一化,以保持具有最大字段 3 的行:

    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。

    【讨论】:

    • 第二种方法在 postgres 上非常快!谢谢。
    • @Tim 你能更好地解释一下USING 在 postgresql 中的作用吗?
    • 这是迄今为止最好的答案。即使您的表中没有序列列用于 id 比较,也值得暂时添加一个以使用这种简单的方法。
    • 我刚刚检查过了。答案是肯定的,会的。使用小于 () 只留下最小 id,删除其余部分。
    • @Shane 可以使用:WHERE table1.ctid&lt;table2.ctid - 无需添加序列列
    【解决方案2】:

    例如你可以:

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

    【讨论】:

    • 你能区分一组列吗?也许“SELECT DISTINCT (t.a, t.b, t.c), * FROM t”?
    • 更容易输入:CREATE TABLE tmp AS SELECT ...;。那么你甚至不需要弄清楚tmp 的布局是什么。 :)
    • 这个答案实际上不是很好,有几个原因。 @Randal 命名为一个。在大多数情况下,特别是如果您有索引、约束、视图等依赖对象,最好的方法是使用实​​际的 TEMPORARY TABLETRUNCATE 并重新插入数据。
    • 您对索引是正确的。删除和重新创建要快得多。但是其他依赖对象将破坏或阻止完全删除表 - OP 会在 制作副本后发现 - 对于“最快的方法”来说非常重要。不过,您对否决票是正确的。这是没有根据的,因为这不是一个糟糕的答案。只是不太好。您可以添加一些关于索引或依赖对象的指针或指向手册的链接,就像您在评论或 any 类型的解释中所做的那样。我想我对人们如何投票感到沮丧。删除了反对票。
    【解决方案3】:

    除了创建新表之外,您还可以在截断后将唯一行重新插入到同一个表中。 一次性完成所有操作

    这种方法仅适用于需要从整个表中删除大量行的情况。对于几个重复项,请使用普通的DELETE

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

    SELECT pg_size_pretty(pg_relation_size('tbl'));
    

    设置temp_buffers 至少比它高一点。

    SET temp_buffers = 200MB;   -- example value
    
    BEGIN;
    
    CREATE TEMP TABLE t_tmp AS  -- retains temp for duration of session
    SELECT DISTINCT * FROM tbl  -- DISTINCT folds duplicates
    ORDER  BY id;               -- optionally "cluster" data
    
    TRUNCATE tbl;
    
    INSERT INTO tbl
    SELECT * FROM t_tmp;        -- retains order (implementation detail)
    
    COMMIT;
    

    此方法优于创建新表如果存在依赖对象。引用表的视图、索引、外键或其他对象。 TRUNCATE 无论如何都会让您从一个干净的状态开始(后台中的新文件),并且比使用大桌子的DELETE FROM tbl很多(使用小桌子实际上DELETE 可以更快)。

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

    请注意,TRUNCATEDELETE 需要更积极的锁定。对于具有大量并发负载的表来说,这可能是一个问题。但它仍然没有完全删除和替换表的破坏性。

    如果TRUNCATE 不是一个选项,或者通常用于中小型表,则可以使用data-modifying CTE 的类似技术(Postgres 9.1+): p>

    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 的非常大的表,创建 表会快得多。您必须权衡这与依赖对象可能带来的麻烦/开销。

    【讨论】:

    • 我也使用了这种方法。但是,它可能是个人的,但是我的临时表已被删除,并且在截断后不可用...如果临时表已成功创建并且可用,请小心执行这些步骤。
    • @xlash:您可以检查是否存在以确保,并为临时表使用不同的名称或重用现有的表。我在答案中添加了一些内容。
    • 警告:小心 +1 @xlash -- 我必须重新导入我的数据,因为在 TRUNCATE 之后临时表不存在。正如 Erwin 所说,在截断你的表之前一定要确保它存在。查看@codebykat 的回答
    • @JordanArseno:我切换到没有ON COMMIT DROP 的版本,这样错过我在“一次交易中”写的部分的人就不会丢失数据。我添加了 BEGIN / COMMIT 来澄清“一个事务”。
    • 使用 USING 的解决方案在 1400 万条记录的表上花费了 3 个多小时。这个带有 temp_buffers 的解决方案需要 13 分钟。谢谢。
    【解决方案4】:

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

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

    【讨论】:

    • 对于删除就地NOT EXISTS should be considerably faster:DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid &gt; t.ctid)-- 或使用任何其他列或列集进行排序以选择幸存者。
    • @ErwinBrandstetter,您提供的查询是否应该使用NOT EXISTS
    • @John:这里一定是EXISTS。像这样阅读它:“删除在dist_col 中存在具有相同值但更大ctid 的任何其他行的所有行”。每组骗子唯一的幸存者将是拥有最大 ctid 的那个。
    • 如果您只有几个重复的行,最简单的解决方案。如果您知道重复的数量,可以与LIMIT 一起使用。
    【解决方案5】:

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

    Deleting duplicates

    【讨论】:

    • 并且使用“ctid”而不是“id”,这实际上适用于完全重复的行。
    • 很好的解决方案。我必须为有十亿条记录的表执行此操作。我在内部 SELECT 中添加了一个 WHERE 以分块进行。
    【解决方案6】:

    删除重复项的通用查询:

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

    ctid 列是可用于每个表的特殊列,但除非特别提及,否则不可见。 ctid 列值对于表中的每一行都被认为是唯一的。请参阅PostgreSQL system columns 以了解有关ctid 的更多信息。

    【讨论】:

    • 唯一通用的答案!无需自我/笛卡尔联接即可工作。值得补充的是,正确指定GROUP BY 子句很重要——这应该是现在违反的“唯一性标准”,或者如果您想要检测重复项的关键。如果指定错误,它将无法正常工作
    【解决方案7】:

    来自an old postgresql.org mailing list

    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 感谢您的 cmets 关于改进答案,我会考虑这一点。
    • 如果'oid'报错,你可以使用系统列'ctid'。
    【解决方案8】:

    此函数在不删除索引的情况下删除重复项,并对任何表执行此操作。

    用法:select remove_duplicates('mytable');

    --- --- remove_duplicates(tablename) 从表中删除重复记录(从集合转换为唯一集合) --- 创建或替换函数 remove_duplicates(text) RETURNS void AS $$ 宣布 表名别名为 1 美元; 开始 执行'创建临时表_DISTINCT_' ||表名 || ' AS (SELECT DISTINCT * FROM ' || 表名 || ');'; 执行'删除'||表名 || ';'; 执行“插入”||表名 || ' (SELECT * FROM _DISTINCT_' || 表名 || ');'; 执行“删除表_DISTINCT_”||表名 || ';'; 返回; 结尾; $$ 语言 plpgsql;

    【讨论】:

      【解决方案9】:

      我刚刚成功地使用Erwin Brandstetter's answer 删除了连接表(缺少自己的主 ID 的表)中的重复项,但发现有一个重要的警告。

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

      我刚刚做了CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;,一切正常。

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

      【讨论】:

        【解决方案10】:
        DELETE FROM table
          WHERE something NOT IN
            (SELECT     MAX(s.something)
              FROM      table As s
              GROUP BY  s.this_thing, s.that_thing);
        

        【讨论】:

        • 这是我目前正在做的,但运行时间很长。
        • 如果表中的多行在某列中具有相同的值,这不会失败吗?
        【解决方案11】:

        如果您只有一个或几个重复条目,并且它们确实是重复(即它们出现了两次),您可以使用“隐藏”ctid 列,如上所示,连同LIMIT:

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

        这将只删除所选行中的第一行。

        【讨论】:

        • 我知道它并没有解决 OP 的问题,因为它在数百万行中有许多重复,但无论如何它可能会有所帮助。
        • 这必须为每个重复的行运行一次。 shekwi 的答案只需要运行一次。
        【解决方案12】:

        首先,您需要决定要保留哪些“副本”。如果所有列都相等,好的,您可以删除其中的任何一个...但也许您只想保留最新的,或者其他一些标准?

        最快的方法取决于您对上述问题的回答,以及桌子上重复的百分比。如果你丢弃 50% 的行,你最好使用CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;,如果你删除 1% 的行,使用 DELETE 会更好。

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

        【讨论】:

          【解决方案13】:

          我正在使用 PostgreSQL 8.4。当我运行建议的代码时,我发现它不是 实际上删除重复项。在运行一些测试时,我发现添加 “DISTINCT ON (duplicate_column_name)”和“ORDER BY duplicate_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;
          

          【讨论】:

            【解决方案14】:

            这很好用而且速度很快:

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

            【讨论】:

              【解决方案15】:
              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)
              

              【讨论】:

                【解决方案16】:
                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);
                

                【讨论】:

                • 我测试了它,它有效;为了便于阅读,我对其进行了格式化。它看起来很复杂,但它可以使用一些解释。如何为他/她自己的用例更改此示例?
                猜你喜欢
                • 1970-01-01
                • 2015-03-21
                • 1970-01-01
                • 2021-01-22
                • 2015-06-08
                • 2015-03-12
                • 1970-01-01
                • 2018-10-16
                • 2014-12-28
                相关资源
                最近更新 更多