Bagaimana cara menghapus rekaman duplikat di tabel bergabung di PostgreSQL?

9

Saya punya tabel yang memiliki skema seperti ini:

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"

Saya ingin menghapus catatan yang merupakan duplikat, yaitu mereka memiliki catatan yang sama tag_iddan yang question_idlain.

Seperti apa tampilan SQL untuk itu?

marcamillion
sumber

Jawaban:

15

Dalam pengalaman saya (dan seperti yang ditunjukkan dalam banyak tes) NOT INseperti yang ditunjukkan oleh @ gsiems agak lambat dan skala sangat. Kebalikannya INbiasanya lebih cepat (di mana Anda dapat memformulasikan ulang seperti itu, seperti dalam kasus ini), tetapi kueri dengan EXISTS(melakukan persis seperti yang Anda tanyakan) harus lebih cepat lagi - dengan tabel besar berdasarkan pesanan besarnya :

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

Menghapus setiap baris di mana baris lain dengan yang sama (tag_id, question_id)dan lebih kecil ctidada . (Secara efektif menyimpan instance pertama sesuai dengan urutan fisik tupel.) Dengan ctidtidak adanya alternatif yang lebih baik, meja Anda tampaknya tidak memiliki PK atau kolom (set) unik lainnya.

ctidadalah pengenal tuple internal yang hadir di setiap baris dan tentu saja unik. Bacaan lebih lanjut:

Uji

Saya menjalankan test case dengan tabel ini yang cocok dengan pertanyaan Anda dan 100 ribu baris:

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;

Indeks tidak membantu dalam kasus ini.

Hasil

NOT IN
Waktu SQLfiddle habis.
Mencoba yang sama secara lokal tetapi saya membatalkannya juga, setelah beberapa menit.

EXISTS
Selesai dalam setengah detik dalam SQLfiddle ini .

Alternatif

Jika Anda akan menghapus sebagian besar baris , akan lebih cepat untuk memilih yang selamat ke tabel lain, jatuhkan yang asli dan ganti nama tabel yang selamat. Hati-hati, ini berimplikasi jika Anda memiliki pandangan atau kunci asing (atau dependensi lainnya) yang ditentukan pada aslinya.

Jika Anda memiliki dependensi dan ingin mempertahankannya, Anda dapat:

  • Jatuhkan semua kunci dan indeks asing - untuk kinerja.
  • SELECT selamat ke meja sementara.
  • TRUNCATE asli.
  • Re- INSERTselamat.
  • Mengindeks ulang CREATEdan kunci asing. Tampilan bisa tetap, mereka tidak berdampak pada kinerja. Lebih banyak di sini atau di sini .
Erwin Brandstetter
sumber
++ untuk solusi yang ada. Jauh lebih baik dari saran saya.
gsiems
Bisakah Anda menjelaskan perbandingan ctid dalam klausa WHERE Anda?
Kevin Meredith
1
@KevinMeredith: Saya menambahkan beberapa penjelasan.
Erwin Brandstetter
6

Anda dapat menggunakan ctid untuk mencapai itu. Sebagai contoh:

Buat tabel dengan duplikat:

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

Pilih data duplikat:

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

Hapus data duplikat:

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

Dalam kasus Anda, yang berikut ini harus berfungsi:

delete from questions_tags
    where ctid not in (
        select min (ctid) as min_ctid 
            from questions_tags 
            group by question_id, tag_id
        );
gsiems
sumber
Di mana saya bisa membaca lebih lanjut tentang ini ctid? Terima kasih.
marcamillion
@marcamillion - Dokumentasi memiliki uraian singkat tentang ctids di postgresql.org/docs/current/static/ddl-system-columns.html
gsiems
Apa artinya ctid?
marcamillion
@marcamillion - tid == "tuple id", tidak yakin apa artinya c.
gsiems