Come rimuovo i record duplicati in una tabella di join in PostgreSQL?


9

Ho una tabella che ha uno schema come questo:

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"

Vorrei rimuovere i record che sono duplicati, cioè hanno entrambi la stessa tag_ide question_iddi un altro record.

Che aspetto ha l'SQL?

Risposte:


15

Nella mia esperienza (e come mostrato in molti test) NOT INcome dimostrato da @gsiems è piuttosto lento e si ridimensiona terribilmente. L'inverso INè in genere più veloce (dove puoi riformulare in quel modo, come in questo caso), ma questa query con EXISTS(facendo esattamente quello che hai chiesto) dovrebbe essere ancora molto più veloce - con grandi tabelle per ordini di grandezza :

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

Elimina ogni riga in cui esiste un'altra riga con la stessa (tag_id, question_id)e una più piccolactid . (Mantiene efficacemente la prima istanza in base all'ordine fisico delle tuple.) Usando ctidin assenza di un'alternativa migliore, la tua tabella non sembra avere un PK o qualsiasi altra colonna (e) unica (serie).

ctidè l'identificatore di tupla interno presente in ogni riga e necessariamente unico. Ulteriori letture:

Test

Ho eseguito un caso di test con questa tabella corrispondente alla tua domanda e 100.000 righe:

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;

Gli indici non aiutano in questo caso.

risultati

NOT IN
Il timeout di SQLfiddle .
Ho provato lo stesso localmente ma l'ho annullato anche io, dopo alcuni minuti.

EXISTS
Termina in mezzo secondo in questo SQLfiddle .

alternative

Se hai intenzione di eliminare la maggior parte delle righe , sarà più veloce selezionare i sopravvissuti in un'altra tabella, eliminare l'originale e rinominare la tabella dei sopravvissuti. Attenzione, questo ha implicazioni se si hanno chiavi di visualizzazione o esterne (o altre dipendenze) definite sull'originale.

Se si dispone di dipendenze e si desidera mantenerle, è possibile:

  • Rilascia tutte le chiavi esterne e gli indici - per prestazioni.
  • SELECT sopravvissuti a un tavolo temporaneo.
  • TRUNCATE l'originale.
  • Ri- INSERTsopravvissuti.
  • Reindicizza CREATEe chiavi esterne. Le viste possono semplicemente rimanere, non hanno alcun impatto sulle prestazioni. Più qui o qui .

++ per la soluzione esiste. Molto meglio del mio suggerimento.
gsiems

Potresti spiegare il confronto ctid nella clausola WHERE?
Kevin Meredith,

1
@KevinMeredith: ho aggiunto alcune spiegazioni.
Erwin Brandstetter,

6

Puoi usare il ctid per farlo. Per esempio:

Crea una tabella con duplicati:

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

Seleziona i dati duplicati:

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

Elimina i dati duplicati:

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

Nel tuo caso dovrebbe funzionare quanto segue:

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

Dove posso leggere di più al riguardo ctid? Grazie.
marcamillion

@marcamillion - La documentazione presenta una breve descrizione dei ctid su postgresql.org/docs/current/static/ddl-system-columns.html
gsiems

Cosa significa ctid?
marcamillion

@marcamillion - tid == "tuple id", non sono sicuro del significato di c.
gsiems
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.