Löschen Sie doppelte Datensätze in PostgreSQL

113

Ich habe eine Tabelle in einer PostgreSQL 8.3.8-Datenbank, die keine Schlüssel / Einschränkungen enthält und mehrere Zeilen mit genau denselben Werten enthält.

Ich möchte alle Duplikate entfernen und nur 1 Kopie jeder Zeile behalten.

Es gibt insbesondere eine Spalte (mit dem Namen "Schlüssel"), die zur Identifizierung von Duplikaten verwendet werden kann (dh es sollte nur ein Eintrag für jeden einzelnen "Schlüssel" vorhanden sein).

Wie kann ich das machen? (idealerweise mit einem einzelnen SQL-Befehl) Geschwindigkeit ist in diesem Fall kein Problem (es gibt nur wenige Zeilen).

André Morujão
quelle

Antworten:

80
DELETE FROM dupes a
WHERE a.ctid <> (SELECT min(b.ctid)
                 FROM   dupes b
                 WHERE  a.key = b.key);
ein Pferd ohne Name
quelle
20
Benutze es nicht, es ist zu langsam!
Paweł Malisak
5
Während diese Lösung definitiv funktioniert, wird die unten stehende Lösung von @rapimo viel schneller ausgeführt. Ich glaube, dies hat damit zu tun, dass die innere select-Anweisung hier N-mal ausgeführt wird (für alle N Zeilen in der Dupes-Tabelle) und nicht mit der Gruppierung, die in der anderen Lösung stattfindet.
David
Bei großen Tabellen (mehrere Millionen Datensätze) passt diese im Gegensatz zur Lösung von @ rapimo tatsächlich in den Speicher. In diesen Fällen ist dies also die schnellere (kein Tauschen).
Giel
1
Erklärung hinzufügen: Es funktioniert, weil ctid eine spezielle Postgres-Spalte ist, die den physischen Standort der Zeile angibt. Sie können dies als eindeutige ID verwenden, auch wenn Ihre Tabelle keine eindeutige ID besitzt. postgresql.org/docs/8.2/ddl-system-columns.html
Eric Burel
191

Eine schnellere Lösung ist

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
quelle
20
Warum ist es schneller als die Lösung von a_horse_with_no_name?
Roberto
3
Dies ist schneller, da hier nur 2 Abfragen ausgeführt werden. Zuerst eine, um alle Duplikate auszuwählen, dann eine, um alle Elemente aus der Tabelle zu löschen. Die Abfrage von @a_horse_with_no_name führt eine Abfrage durch, um festzustellen, ob sie für jedes einzelne Element in der Tabelle mit einem anderen übereinstimmt.
Aeolun
5
was ist ctid?
Techkuz
6
aus docs: ctid. Der physische Speicherort der Zeilenversion in ihrer Tabelle. Beachten Sie, dass die ctid zwar verwendet werden kann, um die Zeilenversion sehr schnell zu finden, die ctid einer Zeile sich jedoch jedes Mal ändert, wenn sie von VACUUM FULL aktualisiert oder verschoben wird. Daher ist ctid als langfristige Zeilenkennung unbrauchbar.
Saim
1
Scheint so, als würde dies nicht funktionieren, wenn mehr als 2 doppelte Zeilen vorhanden sind, da jeweils nur ein Duplikat gelöscht wird.
Frankie Drake
73

Das ist schnell und prägnant:

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

Siehe auch meine Antwort unter So löschen Sie doppelte Zeilen ohne eindeutige Kennung, die weitere Informationen enthält.

Isapir
quelle
Wofür steht ct? Anzahl?
Techkuz
4
@trthhrtz ctidzeigt auf den physischen Speicherort des Datensatzes in der Tabelle. Im Gegensatz zu dem, was ich damals im Kommentar geschrieben habe, verweist die Verwendung des Operators "kleiner als" nicht unbedingt auf die ältere Version, da das ct umlaufen kann und ein Wert mit einer niedrigeren ctid möglicherweise tatsächlich neuer ist.
Isapir
1
Nur zu Ihrer Information, ich habe diese Lösung ausprobiert und sie nach 15 Minuten Wartezeit abgebrochen. Versuchte Rapimos Lösung und es wurde in ungefähr 10 Sekunden abgeschlossen (~ 700.000 Zeilen gelöscht).
Patrick
@Patrick kann sich nicht vorstellen, ob Ihre Datenbank keine eindeutige Kennung hat, da die Antwort von Rapimo in diesem Fall nicht funktioniert.
Stucash
@isapir Ich bin nur neugierig, die Antworten oben, halten sie die älteren Aufzeichnungen richtig, wie sie ausgewählt haben min(ctid)? während deine die neueren behalten? Vielen Dank!
Stucash
17

Ich habe es versucht:

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

bereitgestellt von Postgres Wiki:

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

Radu Gabriel
quelle
Irgendeine Vorstellung von der Leistung im Vergleich zu @ rapimos Antwort und der akzeptierten (@a_horse_with_no_name)?
Smoking
3
Dieser funktioniert nicht, wenn, wie in den Fragen angegeben, alle Spalten identisch sind, ideinschließlich.
Ibizaman
Diese Abfrage löscht sowohl die Originalkopie als auch die Duplikate. Die Frage ist, ob mindestens eine Zeile beibehalten werden soll.
pyBomb
@pyBomb falsch, es wird die erste behalten, bei der idSpalte1 ... 3 doppelt vorhanden sind
Jeff
Ab Postgresql 12 ist dies bei weitem die schnellste Lösung (gegenüber 300 Millionen Zeilen). Ich habe gerade alles getestet, was in dieser Frage vorgeschlagen wurde, einschließlich der akzeptierten Antwort, und diese "offizielle" Lösung ist tatsächlich die schnellste und erfüllt alle Anforderungen von OP (und meiner)
Jeff
7

Ich musste meine eigene Version erstellen. Die von @a_horse_with_no_name geschriebene Version ist in meiner Tabelle viel zu langsam (21 Millionen Zeilen). Und @rapimo löscht einfach keine Dups.

Folgendes verwende ich unter 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);
Experte
quelle
6

Ich würde eine temporäre Tabelle verwenden:

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

Dann löschen tabund umbenennen tab_tempin tab.

Pablo Santa Cruz
quelle
8
Dieser Ansatz berücksichtigt keine Trigger, Indizes und Statistiken. Natürlich könnten Sie sie hinzufügen, aber es bringt auch viel mehr Arbeit.
Jordanien
Das braucht nicht jeder. Dieser Ansatz ist extrem schnell und hat bei 200.000 E-Mails (varchar 250) ohne Indizes viel besser funktioniert als der Rest.
Sergey Telshevsky
Vollständiger Code: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
1

Ein anderer Ansatz (funktioniert nur, wenn Sie ein eindeutiges Feld wie idin Ihrer Tabelle haben) besteht darin, alle eindeutigen IDs nach Spalten zu suchen und andere IDs zu entfernen, die nicht in der eindeutigen Liste enthalten sind

DELETE
FROM users
WHERE users.id NOT IN (SELECT DISTINCT ON (username, email) id FROM users);
Zaytsev Dmitry
quelle
Die Sache ist, in meiner Frage hatten die Tabellen keine eindeutigen IDs; Die "Duplikate" waren mehrere Zeilen mit genau den gleichen Werten in allen Spalten.
André Morujão
Richtig, ich habe einige Notizen hinzugefügt
Zaytsev Dmitry
1

Wie wäre es mit:

MIT
  u AS (SELECT DISTINCT * FROM your_table),
  x AS (DELETE FROM your_table)
INSERT INTO your_table SELECT * FROM u;

Ich war besorgt über die Ausführungsreihenfolge, würde das LÖSCHEN vor dem SELECT DISTINCT erfolgen, aber es funktioniert gut für mich. Und hat den zusätzlichen Vorteil, dass Sie keine Kenntnisse über die Tabellenstruktur benötigen.

Barrie Walker
quelle
Der einzige Nachteil ist, dass jsondies nicht funktioniert , wenn Sie einen Datentyp haben, der keine Gleichheit unterstützt (z. B. ).
a_horse_with_no_name
0

Das hat bei mir gut funktioniert. Ich hatte eine Tabelle, Begriffe, die doppelte Werte enthielten. Es wurde eine Abfrage ausgeführt, um eine temporäre Tabelle mit allen doppelten Zeilen zu füllen. Dann habe ich die Anweisung a delete mit diesen IDs in der temporären Tabelle ausgeführt. value ist die Spalte, die die Duplikate enthielt.

        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
quelle
0

Hier ist eine Lösung mit 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
quelle