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


9

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

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_idquestion_id別のレコードの両方を削除します。

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

回答:


15

私の経験では(そして多くのテストのように)NOT INとして@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すべての行に存在し、必ず一意である内部タプル識別子です。参考文献:

テスト

私はあなたの質問と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
このSQLfiddleでは0.5秒で終了します。

代替案

場合は、削除しようとしている行のほとんどを、それは、別のテーブルに生存者を選択し、オリジナルを削除し、遺族のテーブルの名前を変更するために速くなります。オリジナルに定義されたビューまたは外部キー(または他の依存関係)がある場合、注意が必要です。

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

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

存在するソリューションの++。私の提案よりはるかに良いです。
gsiems 2013年

WHERE句のctid比較について説明していただけますか?
ケビンメレディス

1
@KevinMeredith:いくつかの説明を追加しました。
Erwin Brandstetter

6

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

これについてどこでもっと読むことができctidますか?ありがとう。
marcamillion


何のctid略ですか?
marcamillion

@marcamillion-tid == "tuple id"、cの意味がわかりません。
gsiems 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.