从表中删除没有唯一键的重复行

时间:2015-04-02 09:13:03

标签: sql postgresql duplicates duplicate-removal

如何删除Postgres 9表中的重复行,这些行在每个字段上都是完全重复的,并且没有单独的字段可以用作唯一键,所以我不能只使用GROUP BY列并使用{ {1}}陈述。

我正在寻找一个单独的SQL语句,而不是一个需要我创建临时表并将记录插入其中的解决方案。我知道如何做到这一点,但需要更多的工作来适应我的自动化过程。

表格定义:

NOT IN

示例数据:

jthinksearch=> \d releases_labels;
Unlogged table "discogs.releases_labels"
   Column   |  Type   | Modifiers
------------+---------+-----------
 label      | text    |
 release_id | integer |
 catno      | text    |
Indexes:
    "releases_labels_catno_idx" btree (catno)
    "releases_labels_name_idx" btree (label)
Foreign-key constraints:
    "foreign_did" FOREIGN KEY (release_id) REFERENCES release(id)

5 个答案:

答案 0 :(得分:7)

如果你有能力重写整个表格,这可能是最简单的方法:

WITH Deleted AS (
  DELETE FROM discogs.releases_labels
  RETURNING *
)
INSERT INTO discogs.releases_labels
SELECT DISTINCT * FROM Deleted

如果您需要专门定位重复记录,则可以使用唯一标识行的内部ctid字段:

DELETE FROM discogs.releases_labels
WHERE ctid NOT IN (
  SELECT MIN(ctid)
  FROM discogs.releases_labels
  GROUP BY label, release_id, catno
)

ctid要非常小心;它随着时间而变化。但是你可以依赖它在单一陈述的范围内保持不变。

答案 1 :(得分:4)

单个SQL语句

这是一个删除重复项的解决方案:

DELETE FROM releases_labels r
WHERE  EXISTS (
   SELECT 1
   FROM   releases_labels r1
   WHERE  r1 = r
   AND    r1.ctid < r.ctid
   );

由于没有唯一键,因此我(ab)使用元组ID ctid。第一排在第一排中存活下来。

ctid是一个不属于关联行类型的系统列,因此当在表达式r1 = r中引用包含表别名的整行时,只有可见列比较(不是ctid或其他)。这就是为什么整行可以相等,一个ctid仍然比另一行小。

只有少数重复项,这也是所有解决方案中最快的 使用 lot 重复项,其他解决方案更快。

然后我建议:

ALTER TABLE discogs.releases_labels ADD COLUMN releases_labels_id serial PRIMARY KEY;

为什么它适用于NULL值?

这有点令人惊讶。原因在chapter Composite Type Comparison in the manual

中解释
  

SQL规范要求按行进行比较以返回NULL   结果取决于比较两个NULL值或NULL和a   非NULL。 PostgreSQL仅在比较两者的结果时才这样做   行构造函数(如第9.23.5节)或比较行构造函数   到子查询的输出(如第9.22节)。在其他情况下   比较两个复合类型值,两个NULL字段值    被认为是等于,并且认为NULL大于非NULL。   这是必要的,以便进行一致的排序和索引   复合类型的行为。

大胆强调我的。

第二张表的替代品

我删除了该部分,因为solution with a data-modifying CTE provided by @Nick更好。

答案 2 :(得分:0)

您可以尝试这样:

CREATE TABLE temp 
INSERT INTO temp SELECT DISTINCT * FROM discogs.releases_labels;
DROP TABLE discogs.releases_labels;
ALTER TABLE temp RENAME TO discogs.releases_labels;

答案 3 :(得分:0)

由于您没有主键,因此没有简单的方法可以将一个重复的行与任何其他行区分开来。这就是为什么强烈建议任何表都有主键(*)的原因之一。

所以你只剩下2个解决方案:

  • 使用Rahul建议的临时表(恕我直言,更简单,更清洁)(**)
  • 使用过程SQL和游标来自过程语言,如Python或[放在这里你的首选语言]或PL / pgSQL。像(小心未经测试)的东西:

    CREATE OR REPLACE FUNCTION deduplicate() RETURNS integer AS $$
    DECLARE
     curs CURSOR FOR SELECT * FROM releases_labels ORDER BY label, release_id, catno;
     r releases_labels%ROWTYPE;
     old releases_labels%ROWTYPE;
     n integer;
    BEGIN
     n := 0;
     old := NULL;
     FOR rec IN curs LOOP
      r := rec;
      IF r = old THEN
       DELETE FROM releases_labels WHERE CURRENT OF curs;
       n := n + 1;
      END IF;
      old := rec;
     END LOOP;
     RETURN n;
    END;
    $$ LANGUAGE plpgsql;
    
    SELECT deduplicate();
    

    应删除重复的行并返回实际删除的行数。它不一定是最有效的方式,但您只需触摸需要删除的行,这样您就不必锁定整个表。

(*)希望PostgreSQL提供可用作密钥的ctid伪列。如果您的表格包含oid列,您也可以使用它,因为它永远不会改变。

(**)PostgreSQL WITH允许您在单个SQL语句中执行此操作

这两点来自Nick Barnes的回答

答案 4 :(得分:0)

由于您将来还需要避免重复,因此您可以在重复数据删除时添加代理键和唯一约束:


-- add surrogate key
ALTER TABLE releases_labels
        ADD column id SERIAL NOT NULL PRIMARY KEY
        ;

-- verify
SELECT * FROM releases_labels;

DELETE FROM releases_labels dd
WHERE EXISTS (SELECT *
        FROM releases_labels x
        WHERE x.label = dd.label
        AND x.release_id = dd.release_id
        AND x.catno = dd.catno
        AND x.id < dd.id
        );

-- verify
SELECT * FROM releases_labels;

-- add unique constraint for the natural key
ALTER TABLE releases_labels
        ADD UNIQUE (label,release_id,catno)
        ;

-- verify
SELECT * FROM releases_labels;