중복 항목을 삭제하는 방법은 무엇입니까?


92

기존 테이블에 고유 한 제약 조건을 추가해야합니다. 테이블에 이미 수백만 개의 행이 있고 많은 행이 추가해야하는 고유 한 제약 조건을 위반한다는 점을 제외하면 괜찮습니다.

문제가되는 행을 제거하는 가장 빠른 방법은 무엇입니까? 중복을 찾아 삭제하는 SQL 문이 있지만 실행하는 데 영원히 걸립니다. 이 문제를 해결할 다른 방법이 있습니까? 테이블을 백업 한 다음 제약 조건이 추가 된 후 복원 할 수 있습니까?

답변:


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

9
이 답변은 실제로 여러 가지 이유로별로 좋지 않습니다. @Randal 이름이 하나입니다. 대부분의 경우, 당신이 특히 등 인덱스, 제약 조건, 뷰 등의 개체를 따라, 우수한 접근 방식은 실제 사용하는 것입니다 TEMPORARY TABLE , TRUNCATE 원본과 재 삽입 데이터를.
어윈 Brandstetter

7
인덱스에 대해 맞습니다. 삭제 및 재생성이 훨씬 빠릅니다. 그러나 다른 종속 객체는 "가장 빠른 접근" 을 위해 OP가 복사본을 만든 알아낼 테이블을 완전히 깨뜨 리거나 떨어 뜨리는 것을 방지합니다 . 그래도 당신은 반대표에 대해 옳습니다. 그것은 나쁜 대답이 아니기 때문에 근거가 없습니다. 그다지 좋지 않습니다. 주석이나 모든 종류의 설명 에서했던 것처럼 인덱스 또는 종속 객체 또는 매뉴얼 링크에 대한 포인터를 추가 할 수 있습니다 . 사람들이 투표하는 방식에 대해 실망한 것 같습니다. 반대표를 제거했습니다.
Erwin Brandstetter 2012 년

173

이러한 접근 방식 중 일부는 약간 복잡해 보이며 일반적으로 다음과 같이 수행합니다.

주어진 table table에서 최대 field3 행을 유지하면서 (field1, field2)에서 고유하고 싶습니다.

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에서 매우 빠릅니다! 감사.
Eric Bowman-abstracto-

5
@ Tim은 USINGpostgresql에서 무엇을하는지 더 잘 설명 할 수 있습니까?
Fopa Léon Constantin 2014

3
이것이 가장 좋은 대답입니다. 테이블에 id 비교에 사용할 직렬 열이 없더라도이 간단한 접근 방식을 사용하기 위해 일시적으로 추가하는 것이 좋습니다.
Shane

2
방금 확인했습니다. 대답은 '예'입니다. 보다 작음 (<)을 사용하면 최대 ID 만 남고,보다 큼 (>)을 사용하면 최소 ID 만 남고 나머지는 삭제됩니다.
André C. Andersen

1
@Shane 하나는 사용할 수 있습니다 : WHERE table1.ctid<table2.ctid-직렬 열을 추가 할 필요 없음
alexkovelsky

25

새 테이블을 생성하는 대신 자른 후 동일한 테이블에 고유 행을 다시 삽입 할 수도 있습니다. 한 번의 트랜잭션으로 모든 작업 을 수행하십시오 . 선택적으로를 사용하여 트랜잭션이 끝날 때 임시 테이블을 자동으로 삭제할 수 있습니다 ON COMMIT DROP. 아래를 참조하십시오.

이 접근 방식은 테이블 전체에서 삭제할 행이 많은 경우에만 유용합니다. 몇 개의 중복에 대해서는 일반 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 (포스트 그레스 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

@xlash : 존재 여부를 확인하고 임시 테이블에 다른 이름을 사용하거나 존재하는 이름을 재사용 할 수 있습니다. .. 내 대답에 약간을 추가했습니다.
Erwin Brandstetter

경고 : @xlash에 +1 조심하세요 TRUNCATE. 이후 임시 테이블이 존재하지 않았기 때문에 데이터를 다시 가져와야 합니다. Erwin이 말했듯이 테이블을 자르기 전에 존재하는지 확인하십시오. codebykat의 대답 @ 참조
요르단 Arseno에게

1
@JordanArseno : ON COMMIT DROP"하나의 트랜잭션으로"쓴 부분을 놓친 사람들이 데이터를 잃지 않도록. 그리고 "하나의 트랜잭션"을 명확히하기 위해 BEGIN / COMMIT를 추가했습니다.
Erwin Brandstetter 2014

1
USING 솔루션은 1,400 만 개의 레코드가있는 테이블에서 3 시간 이상 걸렸습니다. temp_buffers를 사용한이 솔루션은 13 분이 걸렸습니다. 감사.
castt apr

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 2013 년

@ErwinBrandstetter, 제공 한 쿼리를 사용해야 NOT EXISTS합니까?
John

1
@John : 여기 있어야합니다 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);

중복 삭제를 참조하십시오 .


그리고 "id"대신 "ctid"를 사용하면 실제로 완전히 중복 된 행에 대해 작동합니다.
bradw2k

훌륭한 솔루션입니다. 10 억 개의 레코드가있는 테이블에 대해이 작업을 수행해야했습니다. 내부 SELECT에 WHERE를 추가하여 청크로 수행했습니다.

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를 지정 생성 한 점을 누락
Kalanidhi

@Kalanidhi 답변 개선에 대한 귀하의 의견에 감사드립니다.이 점을 고려할 것입니다.
Bhavik Ambani


'oid'에서 오류가 발생하면 시스템 열 'ctid'를 사용할 수 있습니다.
sul4bh

7

중복 삭제를위한 일반화 된 쿼리 :

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

ctid은 모든 테이블에 사용할 수있는 특수 열이지만 특별히 언급하지 않는 한 표시되지 않습니다. ctid열의 값은 테이블의 모든 행에 대해 고유 한 것으로 간주된다.


유일한 보편적 인 대답! self / cartesian JOIN없이 작동합니다. GROUP BY절 을 올바르게 지정하는 것이 필수적이라는 점을 추가 할 가치 가 있습니다. 이것은 현재 위반 된 '고유성 기준'이어야합니다. 또는 키가 중복을 감지하기를 원하는 경우입니다. 잘못 지정된 경우 제대로 작동하지 않습니다
msciwoj

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) $$로 void 반환
알리다
  테이블 이름 ALIAS FOR $ 1;
시작
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_'|| 테이블 이름 || 'AS (SELECT DISTINCT * FROM'|| 테이블 이름 || ');';
  EXECUTE 'DELETE FROM'|| 테이블 이름 || ';';
  'INSERT INTO'실행 || 테이블 이름 || '(SELECT * FROM _DISTINCT_'|| 테이블 이름 || ');';
  EXECUTE '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위에 제안 된대로 "hidden" 열을 LIMIT다음 과 함께 사용할 수 있습니다 .

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

선택한 행 중 첫 번째 행만 삭제됩니다.


나는 그것이 수백만 행에서 많은 것을 복제 한 OP의 문제를 해결하지 못한다는 것을 알고 있지만 어쨌든 도움이 될 수 있습니다.
Skippy le Grand Gourou

이것은 각 중복 행에 대해 한 번씩 실행되어야합니다. shekwi의 대답은 한 번만 실행하면됩니다.
bradw2k

3

먼저, 어떤 "중복"을 유지할 것인지 결정해야합니다. 모든 열이 같으면 그 중 하나를 삭제할 수 있습니다 ...하지만 가장 최근의 기준 만 유지하거나 다른 기준을 유지 하시겠습니까?

가장 빠른 방법은 위의 질문에 대한 답변과 테이블의 중복 비율에 따라 다릅니다. 행의 50 %를 버리면을 수행하는 CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;것이 더 좋고 행의 1 %를 삭제하는 경우 DELETE를 사용하는 것이 좋습니다.

또한 이와 같은 유지 관리 작업의 경우 일반적으로 work_memRAM의 좋은 덩어리 로 설정 하는 것이 좋습니다. 속도면에서 좋습니다. 동시 연결이 하나만있는 한 ...


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;

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 위키 에서 가져 왔습니다.

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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.