Comment supprimer les entrées en double?

92

Je dois ajouter une contrainte unique à une table existante. C'est bien sauf que la table contient déjà des millions de lignes et que de nombreuses lignes violent la contrainte unique que je dois ajouter.

Quelle est l'approche la plus rapide pour supprimer les lignes incriminées? J'ai une instruction SQL qui trouve les doublons et les supprime, mais cela prend une éternité à s'exécuter. Existe-t-il une autre façon de résoudre ce problème? Peut-être sauvegarder la table, puis restaurer après l'ajout de la contrainte?

gjrwebber
la source

Réponses:

101

Par exemple, vous pourriez:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;
juste quelqu'un
la source
2
Pouvez-vous le rendre distinct pour le groupe de colonnes. Peut-être "SELECT DISTINCT (ta, tb, tc), * FROM t"?
gjrwebber
10
DISTINCT ON (a, b, c): postgresql.org/docs/8.2/interactive/sql-select.html
juste quelqu'un
36
plus facile à taper: CREATE TABLE tmp AS SELECT ...;. Ensuite, vous n'avez même pas besoin de comprendre quelle est la disposition tmp. :)
Randal Schwartz
9
Cette réponse n'est en fait pas très bonne pour plusieurs raisons. @Randal en a nommé un. Dans la plupart des cas, en particulier si vous avez des objets dépendants tels que des index, des contraintes, des vues, etc., l'approche supérieure consiste à utiliser une véritable TABLE TEMPORAIRE , à TRONCER l'original et à réinsérer les données.
Erwin Brandstetter
7
Vous avez raison sur les index. Déposer et recréer est beaucoup plus rapide. Mais d'autres objets dépendants casseront ou empêcheront de faire tomber complètement la table - ce que l'OP découvrirait après avoir fait la copie - tant pour «l'approche la plus rapide». Pourtant, vous avez raison sur le vote défavorable. Ce n'est pas fondé, car ce n'est pas une mauvaise réponse. Ce n'est tout simplement pas si bon. Vous auriez pu ajouter des pointeurs sur les index ou les objets dépendants ou un lien vers le manuel comme vous l'avez fait dans le commentaire ou tout autre type d'explication. Je suppose que je suis frustré par la façon dont les gens votent. Suppression du vote défavorable.
Erwin Brandstetter
173

Certaines de ces approches semblent un peu compliquées, et je le fais généralement comme:

Compte tenu de la table table, souhaitez l'unifier sur (champ1, champ2) en gardant la ligne avec le champ max3:

DELETE FROM table USING table alias 
  WHERE table.field1 = alias.field1 AND table.field2 = alias.field2 AND
    table.max_field < alias.max_field

Par exemple, j'ai une table, user_accountset je veux ajouter une contrainte unique sur le courrier électronique, mais j'ai quelques doublons. Dites aussi que je veux garder le plus récemment créé (id max parmi les doublons).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Remarque - ce USINGn'est pas du SQL standard, c'est une extension PostgreSQL (mais très utile), mais la question originale mentionne spécifiquement PostgreSQL.
Tim
la source
4
Cette seconde approche est très rapide sur postgres! Merci.
Eric Bowman - abstracto -
5
@Tim pouvez-vous mieux expliquer ce que fait USINGpostgresql?
Fopa Léon Constantin
3
C'est de loin la meilleure réponse. Même si vous n'avez pas de colonne série dans votre table à utiliser pour la comparaison des identifiants, cela vaut la peine d'en ajouter temporairement une pour utiliser cette approche simple.
Shane
2
Je viens de vérifier. La réponse est oui, ce sera le cas. L'utilisation de less-than (<) ne vous laisse qu'avec l'id max, tandis que greater-than (>) vous laisse avec seulement l'id min, supprimant le reste.
André C. Andersen
1
@Shane on peut utiliser: WHERE table1.ctid<table2.ctid- pas besoin d'ajouter une colonne sérielle
alexkovelsky
25

Au lieu de créer une nouvelle table, vous pouvez également réinsérer des lignes uniques dans la même table après l'avoir tronquée. Faites tout cela en une seule transaction . Si vous le souhaitez, vous pouvez supprimer automatiquement la table temporaire à la fin de la transaction avec ON COMMIT DROP. Voir ci-dessous.

Cette approche n'est utile que s'il y a beaucoup de lignes à supprimer de partout dans la table. Pour quelques doublons, utilisez un simple DELETE.

Vous avez mentionné des millions de lignes. Pour accélérer l'opération, vous souhaitez allouer suffisamment de tampons temporaires pour la session. Le paramètre doit être ajusté avant qu'un tampon temporaire ne soit utilisé dans votre session en cours. Découvrez la taille de votre table:

SELECT pg_size_pretty(pg_relation_size('tbl'));

Réglez en temp_buffersconséquence. Arrondissez généreusement car la représentation en mémoire nécessite un peu plus de RAM.

SET temp_buffers = 200MB;    -- example value

BEGIN;

-- CREATE TEMPORARY TABLE t_tmp ON COMMIT DROP AS -- drop temp table at commit
CREATE TEMPORARY TABLE t_tmp AS  -- retain temp table after commit
SELECT DISTINCT * FROM tbl;  -- DISTINCT folds duplicates

TRUNCATE tbl;

INSERT INTO tbl
SELECT * FROM t_tmp;
-- ORDER BY id; -- optionally "cluster" data while being at it.

COMMIT;

Cette méthode peut être supérieure à la création d'une nouvelle table si des objets dépendants existent. Vues, ​​index, clés étrangères ou autres objets référençant la table. TRUNCATEvous permet de commencer avec une ardoise vierge de toute façon (nouveau fichier en arrière-plan) et est beaucoup plus rapide qu'avec DELETE FROM tblde grandes tables ( DELETEpeut en fait être plus rapide avec de petites tables).

Pour les grandes tables, il est régulièrement plus rapide de supprimer les index et les clés étrangères, de remplir la table et de recréer ces objets. En ce qui concerne les contraintes fk, vous devez être certain que les nouvelles données sont bien sûr valides, sinon vous rencontrerez une exception en essayant de créer le fk.

Notez que cela TRUNCATEnécessite un verrouillage plus agressif que DELETE. Cela peut être un problème pour les tables à forte charge simultanée.

Si ce TRUNCATEn'est pas une option ou généralement pour les tables petites à moyennes, il existe une technique similaire avec un CTE de modification des données (Postgres 9.1 +):

WITH del AS (DELETE FROM tbl RETURNING *)
INSERT INTO tbl
SELECT DISTINCT * FROM del;
-- ORDER BY id; -- optionally "cluster" data while being at it.

Plus lent pour les grandes tables, car TRUNCATEc'est plus rapide là-bas. Mais peut-être plus rapide (et plus simple!) Pour les petites tables.

Si vous n'avez aucun objet dépendant du tout, vous pouvez créer une nouvelle table et supprimer l'ancienne, mais vous ne gagnerez pratiquement rien à cette approche universelle.

Pour les très grandes tables qui ne rentreraient pas dans la RAM disponible , la création d'une nouvelle table sera considérablement plus rapide. Vous devrez mettre cela en balance avec d'éventuels problèmes / frais généraux avec des objets dépendants.

Erwin Brandstetter
la source
2
J'ai aussi utilisé cette approche. Cependant, cela peut être personnel, mais ma table temporaire a été supprimée, et non disponible après la troncature ... Faites attention à faire ces étapes si la table temporaire a été créée avec succès et est disponible.
xlash
@xlash: Vous pouvez vérifier l'existence pour vous en assurer, et soit utiliser un nom différent pour la table temporaire, soit réutiliser celle qui existe .. J'ai ajouté un peu à ma réponse.
Erwin Brandstetter
AVERTISSEMENT: faites attention +1 à @xlash - Je dois réimporter mes données car la table temporaire était inexistante après TRUNCATE. Comme l'a dit Erwin, assurez-vous qu'il existe avant de tronquer votre table. Voir la réponse de @ codebykat
Jordan Arseno
1
@JordanArseno: Je suis passé à une version sans ON COMMIT DROP, pour que les personnes qui manquent la partie où j'ai écrit "en une seule transaction" ne perdent pas de données. Et j'ai ajouté BEGIN / COMMIT pour clarifier "une transaction".
Erwin Brandstetter
1
solution avec USING a pris plus de 3 heures sur table avec 14 millions d'enregistrements. Cette solution avec temp_buffers a pris 13 minutes. Merci.
castt
20

Vous pouvez utiliser oid ou ctid, qui sont normalement des colonnes "non visibles" dans le tableau:

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);
Jan Marek
la source
4
Pour la suppression sur place , NOT EXISTSdevrait être considérablement plus rapide : DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)- ou utilisez toute autre colonne ou ensemble de colonnes pour le tri pour choisir un survivant.
Erwin Brandstetter
@ErwinBrandstetter, la requête que vous fournissez est-elle censée être utilisée NOT EXISTS?
John
1
@John: Ça doit être EXISTSici. Lisez-le comme ceci: "Supprimez toutes les lignes où une autre ligne existe avec la même valeur dist_colmais une plus grande ctid". Le seul survivant par groupe de dupes sera celui qui aura le plus gros ctid.
Erwin Brandstetter
Solution la plus simple si vous n'avez que quelques lignes dupliquées. Peut être utilisé avec LIMITsi vous connaissez le nombre de doublons.
Skippy le Grand Gourou
19

La fonction de fenêtre PostgreSQL est pratique pour ce problème.

DELETE FROM tablename
WHERE id IN (SELECT id
              FROM (SELECT id,
                             row_number() over (partition BY column1, column2, column3 ORDER BY id) AS rnum
                     FROM tablename) t
              WHERE t.rnum > 1);

Voir Suppression des doublons .

shekwi
la source
Et en utilisant "ctid" au lieu de "id", cela fonctionne pour les lignes entièrement dupliquées.
bradw2k
Excellente solution. J'ai dû faire ça pour une table avec un milliard d'enregistrements. J'ai ajouté un WHERE au SELECT interne pour le faire en morceaux.
Jan
7

À partir d' une ancienne liste de diffusion postgresql.org :

create table test ( a text, b text );

Des valeurs uniques

insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );

Dupliquer les valeurs

insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );

Encore un double double

insert into test values ( 'x', 'y');

select oid, a, b from test;

Sélectionnez les lignes en double

select o.oid, o.a, o.b from test o
    where exists ( select 'x'
                   from test i
                   where     i.a = o.a
                         and i.b = o.b
                         and i.oid < o.oid
                 );

Supprimer les lignes en double

Remarque: PostgreSQL ne prend pas en charge les alias sur la table mentionnée dans la fromclause de suppression.

delete from test
    where exists ( select 'x'
                   from test i
                   where     i.a = test.a
                         and i.b = test.b
                         and i.oid < test.oid
             );
Bhavik Ambani
la source
Votre explication est très intelligente, mais il vous manque un point, dans la création de la table, spécifiez l'oid puis accédez uniquement à l'affichage du message d'erreur oid else
Kalanidhi
@Kalanidhi Merci pour vos commentaires concernant l'amélioration de la réponse, je prendrai en considération ce point.
Bhavik Ambani
Cela vient vraiment de postgresql.org/message-id/...
Martin F
Vous pouvez utiliser la colonne système 'ctid' si 'oid' vous donne une erreur.
sul4bh
7

Requête généralisée pour supprimer les doublons:

DELETE FROM table_name
WHERE ctid NOT IN (
  SELECT max(ctid) FROM table_name
  GROUP BY column1, [column 2, ...]
);

La colonne ctidest une colonne spéciale disponible pour chaque table mais non visible sauf mention contraire. La ctidvaleur de la colonne est considérée comme unique pour chaque ligne d'une table.

naXa
la source
la seule réponse universelle! Fonctionne sans jointure auto / cartésienne. Il vaut la peine d'ajouter cependant qu'il est essentiel de spécifier correctement la GROUP BYclause - cela devrait être le `` critère d'unicité '' qui est violé maintenant ou si vous souhaitez que la clé détecte les doublons. Si spécifié incorrect, cela ne fonctionnera pas correctement
msciwoj
4

Je viens d'utiliser la réponse d'Erwin Brandstetter avec succès pour supprimer les doublons dans une table de jointure (une table ne disposant pas de ses propres ID primaires), mais j'ai trouvé qu'il y avait une mise en garde importante.

Y compris ON COMMIT DROPsignifie que la table temporaire sera supprimée à la fin de la transaction. Pour moi, cela signifiait que la table temporaire n'était plus disponible au moment où je suis allé l'insérer!

Je viens de le faire CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;et tout a bien fonctionné.

La table temporaire est supprimée à la fin de la session.

codebykat
la source
3

Cette fonction supprime les doublons sans supprimer les index et le fait à n'importe quelle table.

Usage: select remove_duplicates('mytable');

---
--- remove_duplicates (nomtable) supprime les enregistrements en double d'une table (conversion d'un ensemble en ensemble unique)
---
FONCTION CRÉER OU REMPLACER remove_duplicates (text) RETURNS void AS $$
DÉCLARER
  nom de table ALIAS FOR $ 1;
COMMENCER
  EXÉCUTER 'CRÉER UNE TABLE TEMPORAIRE _DISTINCT_' || nom_table || 'AS (SELECT DISTINCT * FROM' || nomtable || ');';
  EXÉCUTER «SUPPRIMER DE» || nom_table || ';';
  EXÉCUTER «INSÉRER DANS» || nom_table || '(SELECT * FROM _DISTINCT_' || nomtable || ');';
  EXÉCUTER 'DROP TABLE _DISTINCT_' || nom_table || ';';
  REVENIR;
FIN;
$$ LANGUAGE plpgsql;
Ole Tange
la source
3
DELETE FROM table
  WHERE something NOT IN
    (SELECT     MAX(s.something)
      FROM      table As s
      GROUP BY  s.this_thing, s.that_thing);
Secko
la source
C'est ce que je fais actuellement, mais cela prend beaucoup de temps à fonctionner.
gjrwebber
1
Cela n'échouerait-il pas si plusieurs lignes de la table avaient la même valeur dans la colonne quelque chose?
shreedhar
3

Si vous n'avez qu'une ou quelques entrées dupliquées, et qu'elles sont effectivement dupliquées (c'est-à-dire qu'elles apparaissent deux fois), vous pouvez utiliser la ctidcolonne "masquée" , comme proposé ci-dessus, avec LIMIT:

DELETE FROM mytable WHERE ctid=(SELECT ctid FROM mytable WHERE […] LIMIT 1);

Cela supprimera uniquement la première des lignes sélectionnées.

Skippy le Grand Gourou
la source
Je sais que cela ne résout pas le problème d'OP, qui en a beaucoup dupliqué sur des millions de lignes, mais cela peut quand même être utile.
Skippy le Grand Gourou
Cela devrait être exécuté une fois pour chaque ligne en double. La réponse de shekwi ne doit être exécutée qu'une seule fois.
bradw2k
3

Tout d'abord, vous devez décider lesquels de vos "doublons" vous conserverez. Si toutes les colonnes sont égales, OK, vous pouvez supprimer l'une d'entre elles ... Mais peut-être voulez-vous ne conserver que le plus récent, ou un autre critère?

Le moyen le plus rapide dépend de votre réponse à la question ci-dessus, ainsi que du% de doublons sur la table. Si vous jetez 50% de vos lignes, vous feriez mieux de le faire CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;, et si vous supprimez 1% des lignes, il est préférable d'utiliser DELETE.

Aussi pour des opérations de maintenance comme celle-ci, il est généralement bon de définir work_memune bonne partie de votre RAM: exécutez EXPLAIN, vérifiez le nombre N de tris / hachages et définissez work_mem sur votre RAM / 2 / N. Utilisez beaucoup de RAM; c'est bon pour la vitesse. Tant que vous n'avez qu'une seule connexion simultanée ...

bobflux
la source
1

Je travaille avec PostgreSQL 8.4. Lorsque j'ai exécuté le code proposé, j'ai constaté qu'il ne supprimait pas réellement les doublons. En exécutant certains tests, j'ai trouvé que l'ajout de "DISTINCT ON (duplicate_column_name)" et de "ORDER BY duplicate_column_name" a fait l'affaire. Je ne suis pas un gourou SQL, j'ai trouvé cela dans la doc de PostgreSQL 8.4 SELECT ... DISTINCT.

CREATE OR REPLACE FUNCTION remove_duplicates(text, text) RETURNS void AS $$
DECLARE
  tablename ALIAS FOR $1;
  duplicate_column ALIAS FOR $2;
BEGIN
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || ' AS (SELECT DISTINCT ON (' || duplicate_column || ') * FROM ' || tablename || ' ORDER BY ' || duplicate_column || ' ASC);';
  EXECUTE 'DELETE FROM ' || tablename || ';';
  EXECUTE 'INSERT INTO ' || tablename || ' (SELECT * FROM _DISTINCT_' || tablename || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
  RETURN;
END;
$$ LANGUAGE plpgsql;
CM.
la source
1

Cela fonctionne très bien et est très rapide:

CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;
Mark Cupitt
la source
1
DELETE FROM tablename
WHERE id IN (SELECT id
    FROM (SELECT id,ROW_NUMBER() OVER (partition BY column1, column2, column3 ORDER BY id) AS rnum
                 FROM tablename) t
          WHERE t.rnum > 1);

Supprimez les doublons par colonne (s) et conservez la ligne avec l'ID le plus bas. Le motif est tiré du wiki postgres

En utilisant les CTE, vous pouvez obtenir une version plus lisible de ce qui précède grâce à ce

WITH duplicate_ids as (
    SELECT id, rnum 
    FROM num_of_rows
    WHERE rnum > 1
),
num_of_rows as (
    SELECT id, 
        ROW_NUMBER() over (partition BY column1, 
                                        column2, 
                                        column3 ORDER BY id) AS rnum
        FROM tablename
)
DELETE FROM tablename 
WHERE id IN (SELECT id from duplicate_ids)
Denplis
la source
1
CREATE TABLE test (col text);
INSERT INTO test VALUES
 ('1'),
 ('2'), ('2'),
 ('3'),
 ('4'), ('4'),
 ('5'),
 ('6'), ('6');
DELETE FROM test
 WHERE ctid in (
   SELECT t.ctid FROM (
     SELECT row_number() over (
               partition BY col
               ORDER BY col
               ) AS rnum,
            ctid FROM test
       ORDER BY col
     ) t
    WHERE t.rnum >1);
PC Shamseer
la source
Je l'ai testé et cela a fonctionné; Je l'ai formaté pour plus de lisibilité. Cela a l'air assez sophistiqué, mais cela pourrait nécessiter une explication. Comment changerait-on cet exemple pour son propre cas d'utilisation?
Tobias