重複するエントリを削除する方法は?


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 2009年


36
入力しやすい:CREATE TABLE tmp AS SELECT ...;。そうすれば、レイアウトが何であるかを理解する必要さえありませんtmp。:)
ランダルシュワルツ

9
この答えは実際にはいくつかの理由であまり良くありません。@Randalが名前を付けました。ほとんどの場合、あなたは、インデックス、制約、ビューなどのようなオブジェクトに依存している場合は特に、優れたアプローチは、実際に使用することですTEMPORARY TABLEをTRUNCATE元とデータを再挿入します。
Erwin Brandstetter、2012年

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の最大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
その2番目のアプローチはpostgresでは非常に高速です。ありがとう。
エリック・ボーマン-アブストラクト-

5
@Tim USINGは、postgresqlで何ができるかをよりよく説明できますか?
FopaLéonConstantin 2014

3
これは断然最良の答えです。IDの比較に使用するシリアル列がテーブルにない場合でも、この単純なアプローチを使用するために一時的に1つ追加する価値があります。
シェーン

2
チェックしたところです。答えは「はい」です。小なり(<)を使用すると最大IDのみが残り、大なり(>)を使用すると最小IDのみが残り、残りは削除されます。
アンドレ・C.アンデルセン

1
@Shaneが使用できるもの:WHERE table1.ctid<table2.ctid-シリアル列を追加する必要はありません
alexkovelsky 2016年

25

新しいテーブルを作成する代わりに、一意の行を切り捨てた後で同じテーブルに再挿入することもできます。すべてを1つのトランザクションで実行します。オプションで、トランザクションの終了時に一時テーブルをで自動的に削除できます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 tblDELETE実際には小さなテーブルの方が高速です)。

大きなテーブルの場合、インデックスと外部キーを削除し、テーブルにデータを再入力してこれらのオブジェクトを再作成するが通常は高速です。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に収まらない非常に大きなテーブルの場合、新しいテーブルの作成はかなり高速になります。これを、依存するオブジェクトで発生する可能性のあるトラブル/オーバーヘッドと比較検討する必要があります。


2
私もこのアプローチを使用しました。ただし、それは個人的なものである可能性がありますが、一時テーブルが削除され、切り捨て後に使用できなくなりました...一時テーブルが正常に作成されて使用可能になっている場合は、これらの手順を実行するように注意してください。
xlash、

@xlash:存在を確認して確認し、一時テーブルに別の名前を使用するか、存在する名前を再利用することができます..私は私の答えに少し追加しました。
Erwin Brandstetter、2012年

警告:+1から@xlashに注意してください-一時テーブルが後に存在しなくなったため、データを再インポートする必要がありますTRUNCATE。アーウィンが言ったように、テーブルを切り捨てる前に、それが存在することを確認してください。@codebykatの回答を参照してください
ジョーダンアルセ

1
@JordanArseno:のないバージョンに切り替えたON COMMIT DROPので、「1つのトランザクションで」書いた部分を見逃した人がデータを失うことはありません。そして、「1つのトランザクション」を明確にするためにBEGIN / COMMITを追加しました。
Erwin Brandstetter 2014

1
USINGを使用したソリューションは、1400万レコードのテーブルで3時間以上かかりました。temp_buffersを使用したこのソリューションには13分かかりました。ありがとう。
キャスト2015

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
@ジョン:それはEXISTSここにあるに違いない。次のように読みます:「他の行が同じ値であるdist_colがより大きい行が存在するすべての行を削除しますctid」。群れのグループあたりの唯一の生存者は、最大のものctidです。
Erwin Brandstetter 2014年

重複する行が少ない場合は、最も簡単な解決策です。LIMIT重複の数がわかっている場合に使用できます。
Skippy le Grand Gourou 2014

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を追加しました。
Jan

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

もう1つの二重複製

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

あなたの説明は非常にスマートですが、1つのポイントがありません。テーブルの作成でoidを指定してから、oidにのみアクセスしてください。エラーメッセージの表示
Kalanidhi

@Kalanidhi回答の改善に関するコメントをありがとう、私はこの点を考慮します。
Bhavik Ambani 2014


「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カラムの値はテーブルのすべての行に対して一意であると考えられます。


唯一の普遍的な答え!自己/デカルトのJOINなしで動作します。ただし、GROUP BY句を正しく指定することは不可欠です。これは、現在違反している、または重複を検出するためのキーが必要な場合に「一意性の基準」になるはずです。指定が間違っていると、正しく機能しません
msciwoj

4

Erwin Brandstetterの回答を使用しましたして結合テーブル(独自のプライマリIDがないテーブル)の重複を削除したところ、重要な注意点が1つあることがわかりました。

含めるON COMMIT DROPと、トランザクションの終了時に一時テーブルが削除されます。私にとっては、一時テーブルが利用できなくなったことを意味します挿入しようとした時点でていました。

今やりました CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;、すべてがうまく。

一時テーブルは、セッションの終了時に削除されます。


3

この関数は、インデックスを削除せずに重複を削除し、任意のテーブルに対して実行します。

使用法: select remove_duplicates('mytable');

---
--- remove_duplicates(tablename)はテーブルから重複レコードを削除します(セットから一意のセットに変換します)
---
CREATE OR REPLACE FUNCTION remove_duplicates(text)RETURNS void AS $$
宣言する
  $ 1のテーブル名エイリアス。
ベギン
  「一時テーブルの作成_DISTINCT_」を実行|| テーブル名|| 'AS(SELECT DISTINCT * FROM' ||テーブル名|| ');';
  'DELETE FROM'を実行する|| テーブル名|| ';';
  'INSERT INTO'を実行する|| テーブル名|| '(SELECT * FROM _DISTINCT_' || tablename || ');';
  'DROP TABLE _DISTINCT_'を実行する|| テーブル名|| ';';
  戻る;
終わり;
$$言語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 2009年

1
テーブルの複数の行が何かの列に同じ値を持っている場合、これは失敗しませんか?
shreedhar 2013

3

重複するエントリが1つまたは少数しかなく、実際に重複している(つまり、2回表示される)場合はctid、上記のように、「非表示」列を次のように使用できますLIMIT

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

これにより、選択した行の最初の行のみが削除されます。


何百万行もの多くが重複しているOPの問題には対応していませんが、とにかく役立つかもしれません。
Skippy le Grand Gourou 2014

これは、重複する行ごとに1回実行する必要があります。シェクウィの答えは一度だけ実行する必要があります。
bradw2k

3

まず、どの「複製」を保持するかを決定する必要があります。すべての列が等しい場合は、OK、それらのいずれかを削除できます...しかし、おそらく最新の基準または他の基準のみを保持したいですか?

最速の方法は、上記の質問への回答と、テーブル上の重複の割合によって異なります。行の50%を破棄する場合は、を実行した方CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;がよいでしょう。行の1%を削除する場合は、DELETEを使用する方が適切です。

また、このようなメンテナンス操作の場合、一般的にwork_memは、RAMの適切なチャンクに設定することをお勧めします。EXPLAINを実行し、並べ替え/ハッシュの数Nを確認して、work_memをRAM / 2 / Nに設定します。RAMを大量に使用します。スピードにいいです。同時接続が1つしかない限り...


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

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

私はそれをテストしましたが、うまくいきました。読みやすいようにフォーマットしました。かなり洗練されているように見えますが、説明が必要な場合があります。この例を自分の使用例に合わせてどのように変更しますか?
トビアス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.