Comment supprimer des enregistrements en double dans une table de jointure dans PostgreSQL?

9

J'ai une table qui a un schéma comme celui-ci:

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"

Je voudrais supprimer les enregistrements qui sont des doublons, c'est-à-dire qu'ils ont à la fois le même tag_idet question_idun autre enregistrement.

À quoi ressemble le SQL pour ça?

marcamillion
la source

Réponses:

15

D'après mon expérience (et comme le montrent de nombreux tests) NOT INcomme démontré par @gsiems est plutôt lent et évolue terriblement. L'inverse INest généralement plus rapide (où vous pouvez reformuler de cette façon, comme dans ce cas), mais cette requête avec EXISTS(faire exactement ce que vous avez demandé) devrait être encore beaucoup plus rapide - avec de grands tableaux par ordre de grandeur :

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

Supprime chaque ligne où une autre ligne avec la même (tag_id, question_id)et une plus petite ctidexiste . (Conserve effectivement la première instance selon l'ordre physique des tuples.) En utilisant ctiden l'absence d'une meilleure alternative, votre table ne semble pas avoir de PK ou toute autre (ensemble de) colonne (s) unique (s).

ctidest l'identifiant de tuple interne présent dans chaque ligne et nécessairement unique. Lectures complémentaires:

Tester

J'ai exécuté un cas de test avec ce tableau correspondant à votre question et 100 000 lignes:

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;

Les index n'aident pas dans ce cas.

Résultats

NOT IN
Le SQLfiddle expire .
J'ai essayé la même chose localement, mais je l'ai également annulée après plusieurs minutes.

EXISTS
Termine en une demi-seconde dans ce SQLfiddle .

Alternatives

Si vous allez supprimer la plupart des lignes , il sera plus rapide de sélectionner les survivants dans une autre table, de supprimer l'original et de renommer la table des survivants. Attention, cela a des implications si vous avez des clés de vue ou étrangères (ou d'autres dépendances) définies sur l'original.

Si vous avez des dépendances et que vous souhaitez les conserver, vous pouvez:

  • Supprimez toutes les clés et index étrangers - pour des performances.
  • SELECT survivants à une table temporaire.
  • TRUNCATE l'original.
  • Re- INSERTsurvivants.
  • Réindexation CREATEet clés étrangères. Les vues peuvent simplement rester, elles n'ont aucun impact sur les performances. Plus ici ou ici .
Erwin Brandstetter
la source
++ pour la solution existante. Beaucoup mieux que ma suggestion.
gsiems
Pourriez-vous expliquer la comparaison ctid dans votre clause WHERE?
Kevin Meredith
1
@KevinMeredith: J'ai ajouté quelques explications.
Erwin Brandstetter
6

Vous pouvez utiliser le ctid pour accomplir cela. Par exemple:

Créez un tableau avec des doublons:

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

Sélectionnez les données en double:

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

Supprimez les données en double:

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

Dans votre cas, les éléments suivants devraient fonctionner:

delete from questions_tags
    where ctid not in (
        select min (ctid) as min_ctid 
            from questions_tags 
            group by question_id, tag_id
        );
gsiems
la source
Où puis-je en savoir plus à ce sujet ctid? Merci.
marcamillion
@marcamillion - La documentation a une courte description sur ctids à postgresql.org/docs/current/static/ddl-system-columns.html
gsiems
Qu'est - ce que ctidveut dire?
marcamillion
@marcamillion - tid == "tuple id", je ne sais pas ce que signifie le c.
gsiems