web-dev-qa-db-ja.com

PostgreSQLの結合テーブルで重複するレコードを削除するにはどうすればよいですか?

次のようなスキーマを持つテーブルがあります。

create_table "questions_tags", :id => false, :force => true do |t|
        t.integer "question_id"
        t.integer "tag_id"
      end

      add_index "questions_tags", ["question_id"], :name => "index_questions_tags_on_question_id"
      add_index "questions_tags", ["tag_id"], :name => "index_questions_tags_on_tag_id"

重複しているレコードを削除したい、つまり、両方に同じtag_idおよびquestion_idを別のレコードとして。

そのためにSQLはどのように見えますか?

9
marcamillion

私の経験では(多くのテストで示されているように)NOT IN as @ gsiemsによって示される はかなり遅く、ひどくスケーリングします。逆INは通常は高速です(この場合のように、そのように再定式化できます)が、このクエリは EXISTS (要求されたとおりに行う)である必要がありますはるかに速くなる-大きなテーブルの場合桁違い

DELETE FROM questions_tags q
WHERE  EXISTS (
   SELECT FROM questions_tags q1
   WHERE  q1.ctid < q.ctid
   AND    q1.question_id = q.question_id
   AND    q1.tag_id = q.tag_id
   );

_(tag_id, question_id)が同じでctidが小さい別の行が存在するすべての行を削除します。 (タプルの物理的な順序に従って最初のインスタンスを効果的に保持します。)よりよい代替がない場合にctidを使用すると、テーブルにPKまたは他の一意の(セットの)列がないように見えます)。

ctidは、 every 行に存在し、必ず一意である内部タプル識別子です。参考文献:

テスト

私はあなたの質問と10万行に一致するこのテーブルでテストケースを実行しました:

CREATE TABLE questions_tags(
  question_id integer NOT NULL
, tag_id      integer NOT NULL
);

INSERT INTO questions_tags (question_id, tag_id)
SELECT (random()* 100)::int, (random()* 100)::int
FROM   generate_series(1, 100000);

ANALYZE questions_tags;

この場合、インデックスは役に立ちません。

結果

NOT IN
SQLfiddle がタイムアウトします。
同じことをローカルで試しましたが、数分後にキャンセルしました。

EXISTS
これで0.5秒で終了します SQLfiddle

代替案

Ifほとんどの行を削除する場合、生存者を別のテーブルに選択し、元のテーブルを削除して生存者のテーブルの名前を変更する方が高速です。元のビューまたは外部キー(または他の依存関係)が定義されている場合、注意が必要です。

依存関係があり、それらを保持したい場合は、次のことができます。

  • パフォーマンスのために、すべての外部キーとインデックスを削除します。
  • SELECTサバイバーを一時テーブルに。
  • TRUNCATEオリジナル。
  • Re -INSERT生存者。
  • Re -CREATEインデックスと外部キー。ビューはそのまま使用でき、パフォーマンスに影響を与えません。詳細 ここ または ここ
15

Ctidを使用してそれを実現できます。例えば:

重複するテーブルを作成します。

=# create table foo (id1 integer, id2 integer);
CREATE TABLE

=# insert into foo values (1,1), (1, 2), (1, 2), (1, 3);
INSERT 0 4

=# select * from foo;
 id1 | id2 
-----+-----
   1 |   1
   1 |   2
   1 |   2
   1 |   3
(4 rows)

重複するデータを選択します。

=# select foo.ctid, foo.id1, foo.id2, foo2.min_ctid
-#  from foo
-#  join (
-#      select id1, id2, min(ctid) as min_ctid 
-#          from foo 
-#          group by id1, id2 
-#          having count (*) > 1
-#      ) foo2 
-#      on foo.id1 = foo2.id1 and foo.id2 = foo2.id2
-#  where foo.ctid <> foo2.min_ctid ;
 ctid  | id1 | id2 | min_ctid 
-------+-----+-----+----------
 (0,3) |   1 |   2 | (0,2)
(1 row)

重複するデータを削除します。

=# delete from foo
-# where ctid not in (select min (ctid) as min_ctid from foo group by id1, id2);
DELETE 1

=# select * from foo;
 id1 | id2 
-----+-----
   1 |   1
   1 |   2
   1 |   3
(3 rows)

あなたの場合、以下がうまくいくはずです:

delete from questions_tags
    where ctid not in (
        select min (ctid) as min_ctid 
            from questions_tags 
            group by question_id, tag_id
        );
6
gsiems