Wie lösche ich doppelte Einträge?

92

Ich muss einer vorhandenen Tabelle eine eindeutige Einschränkung hinzufügen. Dies ist in Ordnung, außer dass die Tabelle bereits Millionen von Zeilen enthält und viele der Zeilen die eindeutige Einschränkung verletzen, die ich hinzufügen muss.

Was ist der schnellste Ansatz zum Entfernen der fehlerhaften Zeilen? Ich habe eine SQL-Anweisung, die die Duplikate findet und löscht, aber die Ausführung dauert ewig. Gibt es einen anderen Weg, um dieses Problem zu lösen? Vielleicht die Tabelle sichern und dann wiederherstellen, nachdem die Einschränkung hinzugefügt wurde?

gjrwebber
quelle

Antworten:

101

Zum Beispiel könnten Sie:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;
nur jemand
quelle
2
Können Sie es für eine Gruppe von Spalten unterscheiden. Vielleicht "SELECT DISTINCT (ta, tb, tc), * FROM t"?
Gjrwebber
10
DISTINCT ON (a, b, c): postgresql.org/docs/8.2/interactive/sql-select.html
nur jemand
36
einfacher zu tippen : CREATE TABLE tmp AS SELECT ...;. Dann müssen Sie nicht einmal herausfinden, wie das Layout tmpist. :)
Randal Schwartz
9
Diese Antwort ist aus mehreren Gründen eigentlich nicht sehr gut. @Randal nannte einen. In den meisten Fällen, vor allem , wenn Sie Objekte wie Indizes abhängig, Einschränkungen, Ansichten usw., der überlegene Ansatz ist ein tatsächliches verwenden TEMPORARY TABLE , TRUNCATE das Original und Wiedereinsatz der Daten.
Erwin Brandstetter
7
Sie haben Recht mit Indizes. Das Löschen und Wiederherstellen ist viel schneller. Aber andere abhängige Objekte brechen oder verhindern das Löschen der Tabelle insgesamt - was das OP nach dem Erstellen der Kopie herausfinden würde - so sehr für den "schnellsten Ansatz". Trotzdem haben Sie Recht mit der Ablehnung. Es ist unbegründet, weil es keine schlechte Antwort ist. Es ist einfach nicht so gut. Sie hätten einige Hinweise zu Indizes oder abhängigen Objekten oder einen Link zum Handbuch hinzufügen können, wie Sie es im Kommentar oder in einer Erklärung getan haben . Ich glaube, ich war frustriert darüber, wie die Leute abstimmen. Das Downvote wurde entfernt.
Erwin Brandstetter
173

Einige dieser Ansätze scheinen etwas kompliziert zu sein, und ich mache dies im Allgemeinen wie folgt:

Wenn die angegebene Tabelle tableeindeutig ist (Feld1, Feld2), wobei die Zeile mit dem maximalen Feld3 beibehalten wird:

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

Zum Beispiel habe ich eine Tabelle, user_accounts und möchte eine eindeutige Einschränkung für E-Mails hinzufügen, habe jedoch einige Duplikate. Sagen Sie auch, dass ich die zuletzt erstellte behalten möchte (maximale ID unter Duplikaten).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Hinweis - USINGist kein Standard-SQL, sondern eine PostgreSQL-Erweiterung (aber eine sehr nützliche), aber in der ursprünglichen Frage wird PostgreSQL ausdrücklich erwähnt.
Tim
quelle
4
Dieser zweite Ansatz ist bei Postgres sehr schnell! Vielen Dank.
Eric Bowman - abstracto -
5
@ Tim kannst du besser erklären, was USINGin postgresql macht?
Fopa Léon Constantin
3
Dies ist bei weitem die beste Antwort. Auch wenn Ihre Tabelle keine serielle Spalte für den ID-Vergleich enthält, lohnt es sich, vorübergehend eine hinzuzufügen, um diesen einfachen Ansatz zu verwenden.
Shane
2
Ich habe gerade nachgesehen. Die Antwort lautet ja, das wird es. Wenn Sie kleiner als (<) verwenden, wird nur die maximale ID angezeigt, während bei größer als (>) nur die minimale ID angezeigt wird und der Rest gelöscht wird.
André C. Andersen
1
@ Shane kann man verwenden: WHERE table1.ctid<table2.ctid- keine Notwendigkeit, serielle Spalte hinzuzufügen
alexkovelsky
25

Anstatt eine neue Tabelle zu erstellen, können Sie auch eindeutige Zeilen nach dem Abschneiden wieder in dieselbe Tabelle einfügen. Machen Sie alles in einer Transaktion . Optional können Sie die temporäre Tabelle am Ende der Transaktion automatisch mit löschen ON COMMIT DROP. Siehe unten.

Dieser Ansatz ist nur nützlich, wenn in der gesamten Tabelle viele Zeilen gelöscht werden müssen. Verwenden Sie für nur wenige Duplikate eine EbeneDELETE .

Sie haben Millionen von Zeilen erwähnt. Um den Vorgang zu beschleunigen , möchten Sie genügend temporäre Puffer für die Sitzung zuweisen . Die Einstellung muss angepasst werden, bevor in Ihrer aktuellen Sitzung ein temporärer Puffer verwendet wird. Finden Sie die Größe Ihres Tisches heraus:

SELECT pg_size_pretty(pg_relation_size('tbl'));

temp_buffersEntsprechend einstellen . Großzügig aufrunden, da die In-Memory-Darstellung etwas mehr RAM benötigt.

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;

Diese Methode kann dem Erstellen einer neuen Tabelle überlegen sein, wenn abhängige Objekte vorhanden sind. Ansichten, Indizes, Fremdschlüssel oder andere Objekte, die auf die Tabelle verweisen. TRUNCATESie beginnen ohnehin mit einer sauberen Tafel (neue Datei im Hintergrund) und sind viel schneller als DELETE FROM tblbei großen Tabellen ( DELETEkönnen bei kleinen Tabellen sogar schneller sein).

Bei großen Tabellen ist es regelmäßig schneller , Indizes und Fremdschlüssel zu löschen, die Tabelle neu zu füllen und diese Objekte neu zu erstellen. In Bezug auf fk-Einschränkungen müssen Sie natürlich sicher sein, dass die neuen Daten gültig sind. Andernfalls tritt beim Versuch, fk zu erstellen, eine Ausnahme auf.

Beachten Sie, dass TRUNCATEeine aggressivere Verriegelung erforderlich ist als DELETE. Dies kann ein Problem für Tabellen mit hoher gleichzeitiger Belastung sein.

Wenn dies TRUNCATEkeine Option ist oder generell für kleine bis mittlere Tabellen gilt, gibt es eine ähnliche Technik mit einem datenmodifizierenden CTE (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.

Langsamer für große Tische, weil TRUNCATEes dort schneller ist. Kann aber für kleine Tische schneller (und einfacher!) Sein.

Wenn Sie überhaupt keine abhängigen Objekte haben, können Sie eine neue Tabelle erstellen und die alte löschen, aber Sie gewinnen kaum etwas über diesen universellen Ansatz.

Bei sehr großen Tabellen, die nicht in den verfügbaren Arbeitsspeicher passen , ist das Erstellen einer neuen Tabelle erheblich schneller. Sie müssen dies gegen mögliche Probleme / Overhead mit abhängigen Objekten abwägen.

Erwin Brandstetter
quelle
2
Ich habe diesen Ansatz auch verwendet. Es kann jedoch persönlich sein, aber meine temporäre Tabelle wurde gelöscht und ist nach dem Abschneiden nicht verfügbar ... Führen Sie diese Schritte sorgfältig aus, wenn die temporäre Tabelle erfolgreich erstellt wurde und verfügbar ist.
xlash
@xlash: Sie können überprüfen, ob es eine Existenz gibt, und entweder einen anderen Namen für die temporäre Tabelle verwenden oder den existierenden wiederverwenden. Ich habe meiner Antwort ein wenig hinzugefügt.
Erwin Brandstetter
WARNUNG: Seien Sie vorsichtig +1 bis @xlash - Ich muss meine Daten erneut importieren, da die temporäre Tabelle danach nicht mehr vorhanden war TRUNCATE. Stellen Sie, wie Erwin sagte, sicher, dass es vorhanden ist, bevor Sie Ihre Tabelle abschneiden. Siehe @ Codebykats Antwort
Jordan Arseno
1
@ JordanArseno: Ich habe zu einer Version ohne gewechselt ON COMMIT DROP, damit Leute, die den Teil verpassen, in dem ich "in einer Transaktion" geschrieben habe, keine Daten verlieren. Und ich habe BEGIN / COMMIT hinzugefügt, um "eine Transaktion" zu verdeutlichen.
Erwin Brandstetter
1
Die Lösung mit USING dauerte mehr als 3 Stunden mit 14 Millionen Datensätzen. Diese Lösung mit temp_buffers dauerte 13 Minuten. Vielen Dank.
Castt
20

Sie können oid oder ctid verwenden, bei denen es sich normalerweise um "nicht sichtbare" Spalten in der Tabelle handelt:

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);
Jan Marek
quelle
4
Das Löschen an Ort und Stelle NOT EXISTSsollte erheblich schneller sein : DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)- oder verwenden Sie eine andere Spalte oder einen anderen Satz von Spalten zum Sortieren, um einen Überlebenden auszuwählen.
Erwin Brandstetter
@ErwinBrandstetter, soll die von Ihnen bereitgestellte Abfrage verwendet werden NOT EXISTS?
John
1
@ John: Es muss EXISTShier sein. Lesen Sie es so: "Löschen Sie alle Zeilen, in denen eine andere Zeile mit demselben Wert in dist_coleiner größeren vorhanden ist ctid". Der einzige Überlebende pro Gruppe von Betrügern wird der mit dem größten sein ctid.
Erwin Brandstetter
Einfachste Lösung, wenn Sie nur wenige doppelte Zeilen haben. Kann verwendet werden, LIMITwenn Sie die Anzahl der Duplikate kennen.
Skippy le Grand Gourou
19

Die PostgreSQL-Fensterfunktion ist für dieses Problem praktisch.

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

Siehe Löschen von Duplikaten .

shekwi
quelle
Bei Verwendung von "ctid" anstelle von "id" funktioniert dies tatsächlich für vollständig doppelte Zeilen.
Bradw2k
Tolle Lösung. Ich musste das für einen Tisch mit einer Milliarde Datensätzen tun. Ich habe dem inneren SELECT ein WHERE hinzugefügt, um es in Stücken zu tun.
Jan
7

Aus einer alten Mailingliste von postgresql.org :

create table test ( a text, b text );

Einzigartige Werte

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

Doppelte Werte

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

Noch ein doppeltes Duplikat

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

select oid, a, b from test;

Wählen Sie doppelte Zeilen aus

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

Doppelte Zeilen löschen

Hinweis: PostgreSQL unterstützt keine Aliase für die in der fromKlausel eines Löschvorgangs erwähnte Tabelle .

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
quelle
Ihre Erklärung ist sehr klug, aber Ihnen fehlt ein Punkt. Geben Sie in der Tabelle erstellen die OID an und greifen Sie dann nur auf die Anzeige der OID-Fehlermeldung zu
Kalanidhi,
@ Kalanidhi Vielen Dank für Ihre Kommentare zur Verbesserung der Antwort, ich werde diesen Punkt berücksichtigen.
Bhavik Ambani
Dies kam wirklich von postgresql.org/message-id/…
Martin F
Sie können die Systemspalte 'ctid' verwenden, wenn 'oid' einen Fehler anzeigt.
Sul4bh
7

Verallgemeinerte Abfrage zum Löschen von Duplikaten:

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

Die Spalte ctidist eine spezielle Spalte, die für jede Tabelle verfügbar ist, jedoch nur sichtbar ist, wenn dies ausdrücklich erwähnt wird. Der ctidSpaltenwert wird für jede Zeile in einer Tabelle als eindeutig betrachtet.

naXa
quelle
die einzige universelle Antwort! Funktioniert ohne Selbst- / Kartesian JOIN. Es lohnt sich jedoch hinzuzufügen, dass es wichtig ist, die GROUP BYKlausel korrekt anzugeben - dies sollte das "Eindeutigkeitskriterium" sein, gegen das jetzt verstoßen wird, oder wenn der Schlüssel Duplikate erkennen soll. Wenn falsch angegeben, funktioniert es nicht richtig
msciwoj
4

Ich habe gerade Erwin Brandstetters Antwort erfolgreich verwendet, um Duplikate in einer Join-Tabelle zu entfernen (eine Tabelle ohne eigene primäre IDs), aber festgestellt, dass es eine wichtige Einschränkung gibt.

Einschließen ON COMMIT DROPbedeutet, dass die temporäre Tabelle am Ende der Transaktion gelöscht wird. Für mich bedeutete dies, dass die temporäre Tabelle zum Zeitpunkt des Einfügens nicht mehr verfügbar war !

Ich habe es gerade getan CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl; und alles hat gut funktioniert.

Die temporäre Tabelle wird am Ende der Sitzung gelöscht.

Codebykat
quelle
3

Diese Funktion entfernt Duplikate, ohne Indizes zu entfernen, und führt sie für jede Tabelle aus.

Verwendung: select remove_duplicates('mytable');

--- ---.
--- remove_duplicates (Tabellenname) entfernt doppelte Datensätze aus einer Tabelle (Konvertierung von Satz in eindeutigen Satz)
--- ---.
CREATE OR REPLACE FUNCTION remove_duplicates (text) RETURNS void AS $$
ERKLÄREN
  Tabellenname ALIAS FÜR $ 1;
START
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || Tabellenname || 'AS (SELECT DISTINCT * FROM' || Tabellenname || ');';
  EXECUTE 'DELETE FROM' || Tabellenname || ';';
  EXECUTE 'INSERT INTO' || Tabellenname || '(SELECT * FROM _DISTINCT_' || Tabellenname || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || Tabellenname || ';';
  RÜCKKEHR;
ENDE;
$$ LANGUAGE plpgsql;
Ole Tange
quelle
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
quelle
Das ist es, was ich gerade mache, aber es dauert sehr lange, bis es läuft.
Gjrwebber
1
Würde dies nicht fehlschlagen, wenn mehrere Zeilen in der Tabelle den gleichen Wert in der Spalte haben?
Shreedhar
3

Wenn Sie nur einen oder mehrere doppelte Einträge haben und diese tatsächlich doppelt vorhanden sind (dh zweimal angezeigt werden), können Sie ctiddie oben vorgeschlagene Spalte "versteckt" zusammen mit LIMIT:

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

Dadurch wird nur die erste der ausgewählten Zeilen gelöscht.

Skippy le Grand Gourou
quelle
Ich weiß, dass es nicht das Problem von OP anspricht, bei dem viele in Millionen von Zeilen dupliziert wurden, aber es kann trotzdem hilfreich sein.
Skippy le Grand Gourou
Dies müsste einmal für jede doppelte Zeile ausgeführt werden. Die Antwort von shekwi muss nur einmal ausgeführt werden.
Bradw2k
3

Zunächst müssen Sie entscheiden, welche Ihrer "Duplikate" Sie behalten möchten. Wenn alle Spalten gleich sind, können Sie jede von ihnen löschen ... Aber vielleicht möchten Sie nur das aktuellste oder ein anderes Kriterium beibehalten?

Der schnellste Weg hängt von Ihrer Antwort auf die obige Frage und auch vom Prozentsatz der Duplikate in der Tabelle ab. Wenn Sie 50% Ihrer Zeilen wegwerfen, ist es besser, dies zu tunCREATE TABLE ... AS SELECT DISTINCT ... FROM ... ; , wenn Sie 1% der Zeilen löschen, ist die Verwendung von LÖSCHEN besser.

Auch für work_memsolche Wartungsvorgänge ist es im Allgemeinen gut, einen guten Teil Ihres Arbeitsspeichers festzulegen: Führen Sie EXPLAIN aus, überprüfen Sie die Anzahl N von Sortierungen / Hashes und setzen Sie work_mem auf Ihren Arbeitsspeicher / 2 / N. Verwenden Sie viel Arbeitsspeicher. Es ist gut für die Geschwindigkeit. Solange Sie nur eine gleichzeitige Verbindung haben ...

Bobflux
quelle
1

Ich arbeite mit PostgreSQL 8.4. Als ich den vorgeschlagenen Code ausführte, stellte ich fest, dass die Duplikate nicht tatsächlich entfernt wurden. Bei einigen Tests habe ich festgestellt, dass das Hinzufügen von "DISTINCT ON (duplicate_column_name)" und "ORDER BY duplicate_column_name" den Trick getan hat. Ich bin kein SQL-Guru, ich habe dies im PostgreSQL 8.4 SELECT ... DISTINCT-Dokument gefunden.

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.
quelle
1

Das funktioniert sehr gut und geht sehr schnell:

CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;
Mark Cupitt
quelle
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);

Löschen Sie Duplikate nach Spalte (n) und behalten Sie die Zeile mit der niedrigsten ID bei. Das Muster stammt aus dem Postgres-Wiki

Mit CTEs können Sie dadurch eine besser lesbare Version der oben genannten erreichen

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
quelle
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);
Shamseer PC
quelle
Ich habe es getestet und es hat funktioniert. Ich habe es zur besseren Lesbarkeit formatiert. Es sieht ziemlich raffiniert aus, könnte aber eine Erklärung gebrauchen. Wie würde man dieses Beispiel für seinen eigenen Anwendungsfall ändern?
Tobias