Come eliminare le voci duplicate?


92

Devo aggiungere un vincolo univoco a una tabella esistente. Questo va bene tranne che la tabella ha già milioni di righe e molte delle righe violano il vincolo univoco che devo aggiungere.

Qual è l'approccio più veloce per rimuovere le righe offensive? Ho un'istruzione SQL che trova i duplicati e li elimina, ma l'esecuzione impiega un'eternità. C'è un altro modo per risolvere questo problema? Forse eseguire il backup della tabella, quindi ripristinare dopo l'aggiunta del vincolo?

Risposte:


101

Ad esempio potresti:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;

2
Puoi renderlo distinto per un gruppo di colonne. Forse "SELECT DISTINCT (ta, tb, tc), * FROM t"?
gjrwebber


36
più facile da digitare: CREATE TABLE tmp AS SELECT ...;. Quindi non è necessario nemmeno capire qual è il layout di tmp. :)
Randal Schwartz

9
Questa risposta in realtà non è molto buona per diversi motivi. @Randal ha nominato uno. Nella maggior parte dei casi, specialmente se si hanno oggetti dipendenti come indici, vincoli, viste ecc., L'approccio migliore è usare una TABELLA TEMPORANEA reale , TRONCARE l'originale e reinserire i dati.
Erwin Brandstetter

7
Hai ragione sugli indici. Eliminare e ricreare è molto più veloce. Ma altri oggetti dipendenti romperanno o impediranno di far cadere del tutto il tavolo - cosa che l'OP scoprirà dopo aver fatto la copia - tanto per l '"approccio più veloce". Tuttavia, hai ragione sul voto negativo. È infondato, perché non è una cattiva risposta. Non è solo così buono. Potresti aver aggiunto alcuni puntatori sugli indici o oggetti dipendenti o un collegamento al manuale come hai fatto nel commento o in qualsiasi tipo di spiegazione. Credo di essere stato frustrato dal modo in cui le persone votano. Rimosso il downvote.
Erwin Brandstetter

173

Alcuni di questi approcci sembrano un po 'complicati e generalmente lo faccio come:

Data la tabella table, vuoi unirla su (field1, field2) mantenendo la riga con il max field3:

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

Ad esempio, ho una tabella user_accountse desidero aggiungere un vincolo univoco alla posta elettronica, ma ho alcuni duplicati. Dì anche che voglio mantenere quello creato più di recente (ID massimo tra i duplicati).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Nota: USINGnon è SQL standard, è un'estensione PostgreSQL (ma molto utile), ma la domanda originale menziona specificamente PostgreSQL.

4
Quel secondo approccio è molto veloce su postgres! Grazie.
Eric Bowman - abstracto -

5
@Tim puoi spiegare meglio cosa fa USINGin postgresql?
Fopa Léon Constantin

3
Questa è di gran lunga la migliore risposta. Anche se non hai una colonna seriale nella tua tabella da utilizzare per il confronto dell'ID, vale la pena aggiungerne una temporaneamente per utilizzare questo semplice approccio.
Shane il

2
Ho appena controllato. La risposta è sì, lo farà. L'uso di minore di (<) ti lascia solo con l'id max, mentre maggiore di (>) ti lascia solo con l'id minimo, eliminando il resto.
André C. Andersen

1
@Shane si può usare: WHERE table1.ctid<table2.ctid- non è necessario aggiungere una colonna seriale
alexkovelsky

25

Invece di creare una nuova tabella, puoi anche reinserire righe univoche nella stessa tabella dopo averla troncata. Fai tutto in un'unica transazione . Facoltativamente, puoi rilasciare automaticamente la tabella temporanea alla fine della transazione con ON COMMIT DROP. Vedi sotto.

Questo approccio è utile solo quando sono presenti molte righe da eliminare da tutta la tabella. Per pochi duplicati, usa un semplice DELETE.

Hai menzionato milioni di righe. Per rendere l'operazione veloce si desidera allocare abbastanza buffer temporanei per la sessione. L'impostazione deve essere regolata prima di utilizzare qualsiasi buffer temporaneo nella sessione corrente. Scopri le dimensioni del tuo tavolo:

SELECT pg_size_pretty(pg_relation_size('tbl'));

Imposta di temp_buffersconseguenza. Arrotondare generosamente perché la rappresentazione in memoria richiede un po 'più di 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;

Questo metodo può essere superiore alla creazione di una nuova tabella se esistono oggetti dipendenti. Viste, indici, chiavi esterne o altri oggetti che fanno riferimento alla tabella.TRUNCATEti fa comunque iniziare con una lavagna pulita (nuovo file in background) ed è molto più veloce che DELETE FROM tblcon le tabelle grandi ( DELETEpuò effettivamente essere più veloce con le tabelle piccole).

Per le tabelle di grandi dimensioni, è regolarmente più veloce eliminare gli indici e le chiavi esterne, riempire nuovamente la tabella e ricreare questi oggetti. Per quanto riguarda i vincoli fk, devi essere certo che i nuovi dati siano ovviamente validi o incapperai in un'eccezione nel tentativo di creare fk.

Si noti che TRUNCATErichiede un blocco più aggressivo rispetto a DELETE. Questo potrebbe essere un problema per le tabelle con un carico pesante e simultaneo.

Se TRUNCATEnon è un'opzione o generalmente per tabelle di piccole e medie dimensioni esiste una tecnica simile con un CTE di modifica dei dati (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.

Più lento per tavoli grandi, perché TRUNCATE lì è più veloce. Ma può essere più veloce (e più semplice!) Per i tavolini.

Se non hai alcun oggetto dipendente, potresti creare una nuova tabella ed eliminare quella vecchia, ma difficilmente ottieni nulla da questo approccio universale.

Per tabelle molto grandi che non si adatterebbero alla RAM disponibile , la creazione di una nuova tabella sarà notevolmente più veloce. Dovrai soppesare questo contro possibili problemi / spese generali con oggetti dipendenti.


2
Anch'io ho usato questo approccio. Tuttavia, potrebbe essere personale, ma la mia tabella temporanea è stata eliminata e non disponibile dopo il troncamento ... Fai attenzione a eseguire questi passaggi se la tabella temporanea è stata creata correttamente ed è disponibile.
xlash

@xlash: puoi controllare l'esistenza per assicurartene, e utilizzare un nome diverso per la tabella temporanea o riutilizzare quello esistente .. Ho aggiunto un po 'alla mia risposta.
Erwin Brandstetter

ATTENZIONE: fai attenzione a fare +1 a @xlash: devo reimportare i miei dati perché la tabella temporanea era inesistente dopo TRUNCATE. Come ha detto Erwin, assicurati che esista prima di troncare la tabella. Vedi la risposta di @ codebykat
Jordan Arseno

1
@JordanArseno: sono passato a una versione senza ON COMMIT DROP, in modo che le persone che perdono la parte in cui ho scritto "in una transazione" non perdano dati. E ho aggiunto BEGIN / COMMIT per chiarire "una transazione".
Erwin Brandstetter

1
soluzione con USING ha richiesto più di 3 ore sul tavolo con 14 milioni di record. Questa soluzione con temp_buffer ha richiesto 13 minuti. Grazie.
lanciato il

20

Puoi utilizzare oid o ctid, che normalmente sono colonne "non visibili" nella tabella:

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);

4
Per l'eliminazione sul posto , NOT EXISTSdovrebbe essere considerevolmente più veloce : DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)- o utilizzare qualsiasi altra colonna o gruppo di colonne per l'ordinamento per scegliere un sopravvissuto.
Erwin Brandstetter

@ErwinBrandstetter, la query che fornisci dovrebbe essere utilizzata NOT EXISTS?
John

1
@ John: Deve essere EXISTSqui. Leggi in questo modo: "Elimina tutte le righe in cui esiste qualsiasi altra riga con lo stesso valore dist_colma in una più grande ctid". L'unico sopravvissuto per gruppo di creduloni sarà quello con il più grande ctid.
Erwin Brandstetter

La soluzione più semplice se hai solo poche righe duplicate. Può essere utilizzato con LIMITse si conosce il numero di duplicati.
Skippy le Grand Gourou

19

La funzione della finestra di PostgreSQL è utile per questo problema.

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

Vedere Eliminazione dei duplicati .


E usando "ctid" invece di "id", questo funziona effettivamente per righe completamente duplicate.
bradw2k

Ottima soluzione. Ho dovuto farlo per una tabella con un miliardo di record. Ho aggiunto un WHERE al SELECT interno per farlo in blocchi.
Jan

7

Da una vecchia mailing list postgresql.org :

create table test ( a text, b text );

Valori unici

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

Valori duplicati

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

Un altro doppio duplicato

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

select oid, a, b from test;

Seleziona le righe duplicate

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

Elimina le righe duplicate

Nota: PostgreSQL non supporta gli alias sulla tabella menzionata nella fromclausola di cancellazione.

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

La tua spiegazione è molto intelligente, ma ti manca un punto, nella creazione della tabella specifica l'oid quindi accedi solo alla visualizzazione del messaggio di errore oid else
Kalanidhi

@Kalanidhi Grazie per i tuoi commenti sul miglioramento della risposta, prenderò in considerazione questo punto.
Bhavik Ambani

Questo è venuto davvero da postgresql.org/message-id/…
Martin F

Puoi utilizzare la colonna di sistema "ctid" se "oid" ti dà un errore.
sul4bh

7

Query generalizzata per eliminare i duplicati:

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

La colonna ctidè una colonna speciale disponibile per ogni tabella ma non visibile se non specificatamente menzionata. Il ctidvalore della colonna è considerato univoco per ogni riga di una tabella.


l'unica risposta universale! Funziona senza JOIN auto / cartesiano. Vale la pena aggiungere però che è essenziale specificare correttamente la GROUP BYclausola: questo dovrebbe essere il "criterio di unicità" che è stato violato ora o se si desidera che la chiave rilevi i duplicati. Se specificato in modo errato non funzionerà correttamente
msciwoj

4

Ho appena usato con successo la risposta di Erwin Brandstetter per rimuovere i duplicati in una tabella di join (una tabella priva dei propri ID primari), ma ho scoperto che c'è un avvertimento importante.

Includere ON COMMIT DROPsignifica che la tabella temporanea verrà eliminata alla fine della transazione. Per me, ciò significava che la tabella temporanea non era più disponibile quando sono andata a inserirla!

L'ho appena fatto CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl; e tutto ha funzionato bene.

La tabella temporanea viene eliminata alla fine della sessione.


3

Questa funzione rimuove i duplicati senza rimuovere gli indici e lo fa su qualsiasi tabella.

Utilizzo: select remove_duplicates('mytable');

---
--- remove_duplicates (tablename) rimuove i record duplicati da una tabella (converti da set a set univoco)
---
CREA O SOSTITUISCI FUNZIONE remove_duplicates (testo) RESTITUISCE void AS $$
DICHIARARE
  tablename ALIAS PER $ 1;
INIZIO
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || 'AS (SELECT DISTINCT * FROM' || tablename || ');';
  EXECUTE 'DELETE FROM' || tablename || ';';
  ESEGUI "INSERT INTO" || tablename || '(SELEZIONA * DA _DISTINCT_' || tablename || ');';
  ESEGUI "DROP TABLE _DISTINCT_" || tablename || ';';
  RITORNO;
FINE;
$$ LANGUAGE plpgsql;

3
DELETE FROM table
  WHERE something NOT IN
    (SELECT     MAX(s.something)
      FROM      table As s
      GROUP BY  s.this_thing, s.that_thing);

Questo è quello che sto facendo attualmente, ma ci vuole molto tempo per funzionare.
gjrwebber

1
Questo non fallirebbe se più righe nella tabella avessero lo stesso valore nella colonna qualcosa?
shreedhar

3

Se hai solo una o poche voci duplicate, e sono effettivamente duplicate (cioè appaiono due volte), puoi usare la ctidcolonna "nascosta" , come proposto sopra, insieme a LIMIT:

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

Ciò eliminerà solo la prima delle righe selezionate.


So che non risolve il problema di OP, che ha molti duplicati in milioni di righe, ma potrebbe essere utile comunque.
Skippy le Grand Gourou

Dovrebbe essere eseguito una volta per ogni riga duplicata. La risposta di Shekwi deve essere eseguita solo una volta.
bradw2k

3

Per prima cosa, devi decidere quale dei tuoi "duplicati" manterrai. Se tutte le colonne sono uguali, OK, puoi eliminarne qualcuna ... Ma forse vuoi mantenere solo la più recente o qualche altro criterio?

Il modo più veloce dipende dalla tua risposta alla domanda sopra e anche dalla% di duplicati sul tavolo. Se butti via il 50% delle tue righe, farai meglio a farloCREATE TABLE ... AS SELECT DISTINCT ... FROM ... ; , e se elimini l'1% delle righe, è meglio usare DELETE.

Anche per operazioni di manutenzione come questa, è generalmente bene impostare work_memuna buona parte della RAM: esegui EXPLAIN, controlla il numero N di sorts / hash e imposta work_mem su RAM / 2 / N. Usa molta RAM; fa bene alla velocità. Finché hai solo una connessione simultanea ...


1

Sto lavorando con PostgreSQL 8.4. Quando ho eseguito il codice proposto, ho scoperto che non stava effettivamente rimuovendo i duplicati. Durante l'esecuzione di alcuni test, ho scoperto che l'aggiunta di "DISTINCT ON (duplicate_column_name)" e "ORDER BY duplicate_column_name" ha funzionato. Non sono un guru di SQL, l'ho trovato nel documento 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;

1

Funziona molto bene ed è molto veloce:

CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;

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

Elimina i duplicati per colonna (e) e mantieni la riga con l'ID più basso. Lo schema è tratto dal wiki di postgres

Utilizzando CTE è possibile ottenere una versione più leggibile di quanto sopra attraverso questo

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)

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

L'ho testato e ha funzionato; L'ho formattato per leggibilità. Sembra piuttosto sofisticato, ma potrebbe usare qualche spiegazione. Come si cambierà questo esempio per il proprio caso d'uso?
Tobias
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.