【问题标题】:Delete duplicate rows from small table从小表中删除重复行
【发布时间】:2011-09-28 20:39:39
【问题描述】:

我在 PostgreSQL 8.3.8 数据库中有一个表,它上面没有键/约束,并且有多行具有完全相同的值。

我想删除所有重复项并只保留每行的 1 个副本。

有一个特别的列(名为“key”)可用于识别重复项,即每个不同的“key”应该只存在一个条目。

我该怎么做? (理想情况下,使用单个 SQL 命令。)
在这种情况下,速度不是问题(只有几行)。

【问题讨论】:

    标签: sql postgresql duplicates


    【解决方案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 代表什么?数?
    • @trthhrtz ctid 指向表中记录的物理位置。与我当时在评论中写的相反,使用小于运算符不一定指向旧版本,因为 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
       );
    

    并考虑:

    对于小型表,索引通常对性能没有帮助。我们不需要再看下去了。

    对于大表少数个重复,(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 BYvirtual 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)
            

            【讨论】:

              猜你喜欢
              • 2010-11-05
              • 2012-10-21
              • 2011-07-18
              • 1970-01-01
              • 1970-01-01
              • 2015-09-01
              相关资源
              最近更新 更多