92

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

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

4

16 回答 16

174

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

给定 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。
于 2010-12-14T18:34:14.307 回答
102

例如,您可以:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;
于 2009-11-17T02:36:47.063 回答
26

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

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

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

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 tblDELETE

对于大表,删除索引和外键 (FK)、重新填充表并重新创建这些对象通常更快。当然,就 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的非常大的表,创建表会快得多。您必须权衡依赖对象可能带来的麻烦/开销。

于 2012-01-11T21:26:36.490 回答
20

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

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);
于 2011-05-12T11:05:16.557 回答
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);

请参阅删除重复项

于 2013-04-04T10:33:35.613 回答
9

删除重复项的通用查询:

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

该列ctid是可用于每个表的特殊列,但除非特别提及,否则不可见。列ctid值对于表中的每一行都被认为是唯一的。请参阅PostgreSQL 系统专栏以了解有关ctid.

于 2016-04-11T23:01:54.010 回答
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
             );
于 2012-02-13T12:57:12.777 回答
4

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

用法:select remove_duplicates('mytable');

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

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

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

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

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

于 2013-11-03T18:27:19.723 回答
3

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

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

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

于 2009-11-22T01:31:12.387 回答
3
DELETE FROM table
  WHERE something NOT IN
    (SELECT     MAX(s.something)
      FROM      table As s
      GROUP BY  s.this_thing, s.that_thing);
于 2009-11-17T02:38:59.263 回答
3

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

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

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

于 2014-09-15T16:48:00.920 回答
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);
于 2013-11-21T08:55:21.043 回答
1

我正在使用 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;
于 2010-02-15T23:16:39.127 回答
1

这非常好用并且非常快:

CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;
于 2013-10-28T05:35:50.680 回答
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)
于 2017-02-18T10:07:24.627 回答