Supprimer les enregistrements en double dans PostgreSQL

113

J'ai une table dans une base de données PostgreSQL 8.3.8, qui n'a aucune clé / contrainte dessus, et a plusieurs lignes avec exactement les mêmes valeurs.

Je souhaite supprimer tous les doublons et ne conserver qu'une seule copie de chaque ligne.

Il y a une colonne en particulier (nommée «clé») qui peut être utilisée pour identifier les doublons (c'est-à-dire qu'il ne devrait exister qu'une seule entrée pour chaque «clé» distincte).

Comment puis-je faire ceci? (idéalement avec une seule commande SQL) La vitesse n'est pas un problème dans ce cas (il n'y a que quelques lignes).

André Morujão
la source

Réponses:

80
DELETE FROM dupes a
WHERE a.ctid <> (SELECT min(b.ctid)
                 FROM   dupes b
                 WHERE  a.key = b.key);
un cheval sans nom
la source
20
Ne l'utilisez pas, c'est trop lent!
Paweł Malisak
5
Bien que cette solution fonctionne définitivement, la solution de @rapimo ci-dessous s'exécute beaucoup plus rapidement. Je pense que cela a à voir avec l'instruction select interne ici exécutée N fois (pour toutes les N lignes de la table dupes) plutôt qu'avec le regroupement qui se produit dans l'autre solution.
David le
Pour les tables énormes (plusieurs millions d'enregistrements), celle-ci tient en fait en mémoire, contrairement à la solution @ rapimo. Donc, dans ces cas, c'est le plus rapide (pas d'échange).
Giel
1
Ajout d'explication: cela fonctionne car ctid est une colonne postgres spéciale indiquant l'emplacement physique de la ligne. Vous pouvez l'utiliser comme identifiant unique même si votre table ne possède pas d'identifiant unique. postgresql.org/docs/8.2/ddl-system-columns.html
Eric Burel
194

Une solution plus rapide est

DELETE FROM dups a USING (
      SELECT MIN(ctid) as ctid, key
        FROM dups 
        GROUP BY key HAVING COUNT(*) > 1
      ) b
      WHERE a.key = b.key 
      AND a.ctid <> b.ctid
rapimo
la source
20
Pourquoi est-ce plus rapide que la solution de a_horse_with_no_name?
Roberto
3
C'est plus rapide car cela n'exécute que 2 requêtes. Un premier pour sélectionner tous les doublons, puis un pour supprimer tous les éléments du tableau. La requête de @a_horse_with_no_name effectue une requête pour voir si elle correspond à un autre pour chaque élément de la table.
Aeolun
5
qu'est ce que c'est ctid?
techkuz
6
à partir de documents: ctid. L'emplacement physique de la version de ligne dans sa table. Notez que bien que le ctid puisse être utilisé pour localiser très rapidement la version de ligne, le ctid d'une ligne changera chaque fois qu'il est mis à jour ou déplacé par VACUUM FULL. Par conséquent, ctid est inutile comme identificateur de ligne à long terme.
Saim
1
Il semble que cela ne fonctionne pas lorsque vous avez plus de 2 lignes en double, car cela ne supprime qu'un seul doublon à la fois.
Frankie Drake
74

C'est rapide et concis:

DELETE FROM dupes T1
    USING   dupes T2
WHERE   T1.ctid < T2.ctid  -- delete the older versions
    AND T1.key  = T2.key;  -- add more columns if needed

Voir aussi ma réponse à Comment supprimer les lignes en double sans identifiant unique qui comprend plus d'informations.

isapir
la source
que signifie ct? compter?
techkuz
4
@trthhrtz ctidpointe vers l'emplacement physique de l'enregistrement dans la table. Contrairement à ce que j'ai écrit à l'époque dans le commentaire, l'utilisation de l'opérateur less than ne pointe pas nécessairement vers l'ancienne version car le ct peut s'enrouler et une valeur avec un ctid inférieur pourrait en fait être plus récente.
isapir le
1
Juste pour info, j'ai essayé cette solution et je l'ai abandonnée après avoir attendu 15 minutes. J'ai essayé la solution de rapimo et elle s'est terminée en environ 10 secondes (supprimée ~ 700 000 lignes).
Patrick
@Patrick ne peut pas imaginer si votre base de données n'a pas d'identifiant unique car la réponse de rapimo ne fonctionne pas dans ce cas.
stucash le
@isapir Je suis juste curieux, les réponses ci-dessus, ils conservent les anciens enregistrements tels qu'ils les ont sélectionnés min(ctid)? alors que les vôtres gardent les plus récents? Merci!
stucash le
17

J'ai essayé ceci:

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

fourni par Postgres wiki:

https://wiki.postgresql.org/wiki/Deleting_duplicates

Radu Gabriel
la source
Une idée de la performance par rapport à la réponse de @ rapimo et celle acceptée (@a_horse_with_no_name)?
tuxayo
3
Celui-ci ne fonctionnera pas si, comme les états des questions, toutes les colonnes sont identiques, y idcompris.
ibizaman
Cette requête supprimera à la fois la copie originale et les doublons. la question est de conserver au moins une ligne.
pyBomb
@pyBomb faux, il conservera le premier idoù colonne1 ... 3 sont en double
Jeff
Depuis postgresql 12, c'est de loin la solution la plus rapide (contre 300 millions de lignes). Je viens de tester tout ce qui est proposé dans cette question, y compris la réponse acceptée, et cette solution "officielle" est en fait la plus rapide et répond à toutes les exigences d'OP (et de la mienne)
Jeff le
7

J'utiliserais une table temporaire:

create table tab_temp as
select distinct f1, f2, f3, fn
  from tab;

Ensuite, supprimez tabet renommez tab_tempen tab.

Pablo Santa Cruz
la source
9
Cette approche ne tient pas compte des déclencheurs, des index et des statistiques. Vous pouvez certainement les ajouter, mais cela ajoute également beaucoup plus de travail.
Jordanie
1
Tout le monde n'en a pas besoin. Cette approche est extrêmement rapide et fonctionne bien mieux que les autres sur 200 000 e-mails (varchar 250) sans index.
Sergey Telshevsky
1
Code complet:DROP TABLE IF EXISTS tmp; CREATE TABLE tmp as ( SELECT * from (SELECT DISTINCT * FROM your_table) as t ); DELETE from your_table; INSERT INTO your_table SELECT * from tmp; DROP TABLE tmp;
Eric Burel
7

J'ai dû créer ma propre version. La version écrite par @a_horse_with_no_name est bien trop lente sur ma table (21 millions de lignes). Et @rapimo ne supprime tout simplement pas les doublages.

Voici ce que j'utilise sur PostgreSQL 9.5

DELETE FROM your_table
WHERE ctid IN (
  SELECT unnest(array_remove(all_ctids, actid))
  FROM (
         SELECT
           min(b.ctid)     AS actid,
           array_agg(ctid) AS all_ctids
         FROM your_table b
         GROUP BY key1, key2, key3, key4
         HAVING count(*) > 1) c);
expert
la source
1

Une autre approche (fonctionne uniquement si vous avez un champ unique comme iddans votre table) pour trouver tous les identifiants uniques par colonnes et supprimer les autres identifiants qui ne sont pas dans la liste unique

DELETE
FROM users
WHERE users.id NOT IN (SELECT DISTINCT ON (username, email) id FROM users);
Zaytsev Dmitry
la source
Le fait est que, dans ma question, les tables n'avaient aucun identifiant unique; les "doublons" étaient des lignes multiples avec exactement les mêmes valeurs sur toutes les colonnes.
André Morujão
Bien, j'ai ajouté quelques notes
Zaytsev Dmitry
1

Que diriez-vous:

AVEC
  u AS (SELECT DISTINCT * FROM your_table),
  x AS (SUPPRIMER DE votre_table)
INSÉRER DANS votre_table SELECT * FROM u;

J'avais été préoccupé par l'ordre d'exécution, est-ce que le DELETE aurait lieu avant le SELECT DISTINCT, mais cela fonctionne bien pour moi. Et a l'avantage supplémentaire de ne pas avoir besoin de connaissances sur la structure de la table.

Barrie Walker
la source
Le seul inconvénient est que si vous avez un type de données qui ne prend pas en charge l'égalité (par exemple json), cela ne fonctionnera pas.
a_horse_with_no_name
0

Cela a bien fonctionné pour moi. J'avais une table, des termes, qui contenait des valeurs en double. Exécuté une requête pour remplir une table temporaire avec toutes les lignes en double. Ensuite, j'ai exécuté l'instruction a delete avec ces identifiants dans la table temporaire. valeur est la colonne contenant les doublons.

        CREATE TEMP TABLE dupids AS
        select id from (
                    select value, id, row_number() 
over (partition by value order by value) 
    as rownum from terms
                  ) tmp
                  where rownum >= 2;

delete from [table] where id in (select id from dupids)
Beanwah
la source
0

Voici une solution utilisant PARTITION BY:

DELETE FROM dups
USING (
  SELECT
    ctid,
    (ctid != min(ctid) OVER (PARTITION BY key_column1, key_column2 [...])) AS is_duplicate
  FROM dups 
) dups_find_duplicates
WHERE dups.ctid == dups_find_duplicates.ctid
AND dups_find_duplicates.is_duplicate
LeoRochael
la source