Wie entferne ich doppelte Datensätze in einer Join-Tabelle in PostgreSQL?


9

Ich habe eine Tabelle mit einem Schema wie diesem:

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"

Ich möchte Datensätze entfernen, die Duplikate sind, dh sie haben beide den gleichen tag_idund einen question_idanderen Datensatz.

Wie sieht SQL dafür aus?

Antworten:


15

Nach meiner Erfahrung (und wie in vielen Tests gezeigt) , NOT INwie durch @gsiems gezeigt ist eher langsam und Skalen schrecklich. Die Umkehrung INist normalerweise schneller (wo Sie wie in diesem Fall auf diese Weise umformulieren können), aber diese Abfrage mit EXISTS(genau das tun, was Sie gefragt haben) sollte noch viel schneller sein - mit großen Tabellen um Größenordnungen :

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

Löscht jede Zeile, in der eine andere Zeile mit derselben (tag_id, question_id)und einer kleineren ctidvorhanden ist . (Hält die erste Instanz effektiv gemäß der physischen Reihenfolge der Tupel.) Verwendenctid In Ermangelung einer besseren Alternative scheint Ihre Tabelle keine PK oder andere eindeutige (Satz von) Spalten zu haben.

ctidist die interne Tupelkennung, die in jeder Zeile vorhanden und notwendigerweise eindeutig ist. Weiterführende Literatur:

Prüfung

Ich habe einen Testfall mit dieser Tabelle ausgeführt, die auf Ihre Frage und 100.000 Zeilen abgestimmt ist:

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;

Indizes helfen in diesem Fall nicht.

Ergebnisse

NOT IN
Die SQLfiddle läuft ab.
Versuchte das gleiche vor Ort, aber ich stornierte es auch nach einigen Minuten.

EXISTS
Beendet diese SQLfiddle in einer halben Sekunde .

Alternativen

Wenn Sie die meisten Zeilen löschen möchten , können Sie die Überlebenden schneller in einer anderen Tabelle auswählen, das Original löschen und die Tabelle der Überlebenden umbenennen. Vorsicht, dies hat Auswirkungen, wenn auf dem Original Ansichts- oder Fremdschlüssel (oder andere Abhängigkeiten) definiert sind.

Wenn Sie Abhängigkeiten haben und diese beibehalten möchten, können Sie:

  • Löschen Sie alle Fremdschlüssel und Indizes - für die Leistung.
  • SELECT Überlebende an einen temporären Tisch.
  • TRUNCATE das Original.
  • Re- INSERTÜberlebenden.
  • Re- CREATEIndizes und Fremdschlüssel. Ansichten können einfach bleiben, sie haben keinen Einfluss auf die Leistung. Mehr hier oder hier .

++ für die vorhandene Lösung. Viel besser als mein Vorschlag.
Gsiems

Könnten Sie bitte den ctid-Vergleich in Ihrer WHERE-Klausel erläutern?
Kevin Meredith

1
@ KevinMeredith: Ich habe eine Erklärung hinzugefügt.
Erwin Brandstetter

6

Sie können die ctid verwenden, um dies zu erreichen. Zum Beispiel:

Erstellen Sie eine Tabelle mit Duplikaten:

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

Wählen Sie die doppelten Daten aus:

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

Löschen Sie die doppelten Daten:

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

In Ihrem Fall sollte Folgendes funktionieren:

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

Wo kann ich mehr darüber lesen ctid? Vielen Dank.
Marcamillion

@marcamillion - Die Dokumentation hat einen kurzen Klappentext zu ctids
gsiems

Wofür steht ctid?
Marcamillion

@marcamillion - tid == "tuple id", nicht sicher, was das c bedeutet.
Gsiems
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.