¿Cómo eliminar entradas duplicadas?

92

Tengo que agregar una restricción única a una tabla existente. Esto está bien, excepto que la tabla ya tiene millones de filas y muchas de las filas violan la restricción única que necesito agregar.

¿Cuál es el método más rápido para eliminar las filas ofensivas? Tengo una declaración SQL que encuentra los duplicados y los elimina, pero tarda una eternidad en ejecutarse. ¿Existe otra forma de solucionar este problema? ¿Quizás hacer una copia de seguridad de la tabla y luego restaurar después de agregar la restricción?

gjrwebber
fuente

Respuestas:

101

Por ejemplo, podrías:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;
solo alguien
fuente
2
¿Puede diferenciarlo por grupo de columnas? ¿Quizás "SELECT DISTINCT (ta, tb, tc), * FROM t"?
gjrwebber
10
DISTINCT ON (a, b, c): postgresql.org/docs/8.2/interactive/sql-select.html
solo alguien
36
más fácil de escribir: CREATE TABLE tmp AS SELECT ...;. Entonces ni siquiera necesitas averiguar cuál es el diseño tmp. :)
Randal Schwartz
9
Esta respuesta en realidad no es muy buena por varias razones. @Randal nombró a uno. En la mayoría de los casos, especialmente si tiene objetos dependientes como índices, restricciones, vistas, etc., el enfoque superior es utilizar una TABLA TEMPORAL real , TRUNCAR el original y volver a insertar los datos.
Erwin Brandstetter
7
Tienes razón sobre los índices. Dejar caer y recrear es mucho más rápido. Pero otros objetos dependientes se romperán o evitarán que se caiga la tabla por completo, lo que el OP descubriría después de haber hecho la copia, tanto para el "enfoque más rápido". Aún así, tienes razón sobre el voto negativo. Es infundado, porque no es una mala respuesta. Simplemente no es tan bueno. Podría haber agregado algunos indicadores sobre índices o objetos dependientes o un enlace al manual como lo hizo en el comentario o cualquier tipo de explicación. Supongo que me frustré por cómo vota la gente. Se eliminó el voto negativo.
Erwin Brandstetter
173

Algunos de estos enfoques parecen un poco complicados y generalmente hago esto como:

Dada la tabla table, quiero que sea única en (campo1, campo2) manteniendo la fila con el campo máximo3:

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

Por ejemplo, tengo una tabla, user_accountsy quiero agregar una restricción única en el correo electrónico, pero tengo algunos duplicados. Diga también que quiero mantener el creado más recientemente (ID máximo entre duplicados).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Nota: USINGno es SQL estándar, es una extensión de PostgreSQL (pero muy útil), pero la pregunta original menciona específicamente a PostgreSQL.
Tim
fuente
4
¡Ese segundo enfoque es muy rápido en postgres! Gracias.
Eric Bowman - abstracto -
5
@Tim, ¿puedes explicar mejor qué hace USINGen postgresql?
Fopa Léon Constantin
3
Esta es, con mucho, la mejor respuesta. Incluso si no tiene una columna de serie en su tabla para usar para la comparación de identificación, vale la pena agregar una temporalmente para usar este enfoque simple.
Shane
2
Acabo de verificar. La respuesta es sí, lo hará. El uso de menos de (<) te deja solo con el ID máximo, mientras que mayor que (>) te deja solo con el ID mínimo, eliminando el resto.
André C. Andersen
1
@Shane uno puede usar: WHERE table1.ctid<table2.ctid- no es necesario agregar una columna de serie
alexkovelsky
25

En lugar de crear una nueva tabla, también puede volver a insertar filas únicas en la misma tabla después de truncarla. Hágalo todo en una sola transacción . Opcionalmente, puede eliminar la tabla temporal al final de la transacción automáticamente con ON COMMIT DROP. Vea abajo.

Este enfoque solo es útil cuando hay muchas filas para eliminar de toda la tabla. Para unos pocos duplicados, use un archivo DELETE.

Mencionaste millones de filas. Para que la operación sea más rápida , debe asignar suficientes búferes temporales para la sesión. La configuración debe ajustarse antes de que se use cualquier búfer temporal en su sesión actual. Descubra el tamaño de su mesa:

SELECT pg_size_pretty(pg_relation_size('tbl'));

Establecer en temp_buffersconsecuencia. Redondee generosamente porque la representación en memoria necesita un poco más 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;

Este método puede ser superior a la creación de una nueva tabla si existen objetos dependientes. Vistas, índices, claves externas u otros objetos que hagan referencia a la tabla. TRUNCATEte hace comenzar con una pizarra limpia de todos modos (nuevo archivo en segundo plano) y es mucho más rápido que DELETE FROM tblcon tablas grandes (en DELETErealidad, puede ser más rápido con tablas pequeñas).

Para tablas grandes, normalmente es más rápido eliminar índices y claves externas, rellenar la tabla y volver a crear estos objetos. En lo que respecta a las restricciones de fk, debe estar seguro de que los nuevos datos son válidos, por supuesto, o se encontrará con una excepción al intentar crear el fk.

Tenga en cuenta que TRUNCATErequiere un bloqueo más agresivo que DELETE. Esto puede ser un problema para tablas con una carga concurrente y pesada.

Si TRUNCATEno es una opción o en general para tablas pequeñas a medianas, existe una técnica similar con un CTE de modificación de datos (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.

Más lento para mesas grandes, porque TRUNCATEallí es más rápido. Pero puede ser más rápido (¡y más simple!) Para mesas pequeñas.

Si no tiene ningún objeto dependiente, puede crear una nueva tabla y eliminar la anterior, pero apenas obtiene nada con este enfoque universal.

Para tablas muy grandes que no cabrían en la RAM disponible , crear una nueva tabla será considerablemente más rápido. Tendrá que sopesar esto contra posibles problemas / gastos generales con objetos dependientes.

Erwin Brandstetter
fuente
2
Yo también utilicé este enfoque. Sin embargo, puede ser personal, pero mi tabla temporal se eliminó y no está disponible después de truncar ... Tenga cuidado de seguir esos pasos si la tabla temporal se creó correctamente y está disponible.
xlash
@xlash: puede verificar la existencia para asegurarse, y usar un nombre diferente para la tabla temporal o reutilizar el existente ... Agregué un poco a mi respuesta.
Erwin Brandstetter
ADVERTENCIA: Tenga cuidado con +1 a @xlash: tengo que volver a importar mis datos porque la tabla temporal no existía después TRUNCATE. Como dijo Erwin, asegúrese de asegurarse de que exista antes de truncar su tabla. Vea la respuesta de @ codebykat
Jordan Arseno
1
@JordanArseno: Cambié a una versión sin ON COMMIT DROP, para que las personas que se pierdan la parte donde escribí "en una transacción" no pierdan datos. Y agregué BEGIN / COMMIT para aclarar "una transacción".
Erwin Brandstetter
1
La solución con USING tomó más de 3 horas en la mesa con 14 millones de registros. Esta solución con temp_buffers tomó 13 minutos. Gracias.
castt
20

Puede utilizar oid o ctid, que normalmente son columnas "no visibles" en la tabla:

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);
Jan Marek
fuente
4
Para eliminar en el lugar , NOT EXISTSdebería ser considerablemente más rápido : DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)- o usar cualquier otra columna o conjunto de columnas para clasificar para elegir un sobreviviente.
Erwin Brandstetter
@ErwinBrandstetter, ¿se supone que debe usar la consulta que proporcionaste NOT EXISTS?
John
1
@John: Debe estar EXISTSaquí. Léalo así: "Elimine todas las filas donde exista cualquier otra fila con el mismo valor dist_colpero una más grande ctid". El único superviviente por grupo de incautos será el que tenga el mayor ctid.
Erwin Brandstetter
La solución más sencilla si solo tiene unas pocas filas duplicadas. Se puede usar con LIMITsi conoce el número de duplicados.
Skippy le Grand Gourou
19

La función de ventana de PostgreSQL es útil para este 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);

Consulte Eliminar duplicados .

shekwi
fuente
Y al usar "ctid" en lugar de "id", esto realmente funciona para filas completamente duplicadas.
bradw2k
Gran solucion Tuve que hacer esto para una tabla con mil millones de registros. Agregué un DÓNDE al SELECCIONAR interno para hacerlo en trozos.
Ene
7

De una antigua lista de correo de postgresql.org :

create table test ( a text, b text );

Valores únicos

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

Valores duplicados

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 doble duplicado más

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

select oid, a, b from test;

Seleccionar filas duplicadas

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

Eliminar filas duplicadas

Nota: PostgreSQL no admite alias en la tabla mencionada en la fromcláusula de eliminación.

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
fuente
Su explicación es muy inteligente, pero le falta un punto.En la tabla de creación, especifique el oid y luego solo acceda a la pantalla del mensaje de error oid else
Kalanidhi
@Kalanidhi Gracias por sus comentarios sobre la mejora de la respuesta, tomaré en consideración este punto.
Bhavik Ambani
Esto realmente vino de postgresql.org/message-id/…
Martin F
Puede usar la columna del sistema 'ctid' si 'oid' le da un error.
sul4bh
7

Consulta generalizada para eliminar duplicados:

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

La columna ctides una columna especial disponible para cada tabla, pero no visible a menos que se mencione específicamente. El ctidvalor de la columna se considera único para cada fila de una tabla.

naXa
fuente
¡la única respuesta universal! Funciona sin auto / cartesian JOIN. Sin embargo, vale la pena agregar que es esencial especificar correctamente la GROUP BYcláusula: este debería ser el 'criterio de unicidad' que se viola ahora o si desea que la clave detecte duplicados. Si se especifica mal que no funcionará correctamente
Msciwoj
4

Acabo de usar la respuesta de Erwin Brandstetter con éxito para eliminar duplicados en una tabla de combinación (una tabla que carece de sus propios ID principales), pero descubrí que hay una advertencia importante.

Incluir ON COMMIT DROPsignifica que la tabla temporal se eliminará al final de la transacción. Para mí, eso significaba que la tabla temporal ya no estaba disponible cuando fui a insertarla.

Simplemente lo hice CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;y todo funcionó bien.

La tabla temporal se elimina al final de la sesión.

codebykat
fuente
3

Esta función elimina duplicados sin eliminar índices y lo hace en cualquier tabla.

Uso: select remove_duplicates('mytable');

---
--- remove_duplicates (tablename) elimina los registros duplicados de una tabla (convertir de un conjunto a un conjunto único)
---
CREAR O REEMPLAZAR LA FUNCIÓN remove_duplicates (texto) DEVUELVE void AS $$
DECLARAR
  tablename ALIAS PARA $ 1;
EMPEZAR
  EJECUTAR 'CREAR TABLA TEMPORAL _DISTINCT_' || tablename || 'AS (SELECT DISTINCT * FROM' || nombre de tabla || ');';
  EJECUTAR 'ELIMINAR DE' || tablename || ';';
  EJECUTAR 'INSERT INTO' || tablename || '(SELECT * FROM _DISTINCT_' || nombre de tabla || ');';
  EJECUTAR 'DROP TABLE _DISTINCT_' || tablename || ';';
  REGRESO;
FINAL;
$$ LANGUAGE plpgsql;
Ole Tange
fuente
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
fuente
Eso es lo que estoy haciendo actualmente, pero está tardando mucho en ejecutarse.
gjrwebber
1
¿No fallaría esto si varias filas en la tabla tienen el mismo valor en la columna algo?
shreedhar
3

Si solo tiene una o unas pocas entradas duplicadas, y de hecho están duplicadas (es decir, aparecen dos veces), puede usar la ctidcolumna "oculta" , como se propuso anteriormente, junto con LIMIT:

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

Esto eliminará solo la primera de las filas seleccionadas.

Skippy le Grand Gourou
fuente
Sé que no aborda el problema de OP, que tiene muchos duplicados en millones de filas, pero de todos modos puede ser útil.
Skippy le Grand Gourou
Esto debería ejecutarse una vez para cada fila duplicada. La respuesta de shekwi solo necesita ejecutarse una vez.
bradw2k
3

En primer lugar, debe decidir cuál de sus "duplicados" conservará. Si todas las columnas son iguales, está bien, puede eliminar cualquiera de ellas ... ¿Pero tal vez desee mantener solo la más reciente o algún otro criterio?

La forma más rápida depende de su respuesta a la pregunta anterior y también del% de duplicados en la mesa. Si tira el 50% de sus filas, es mejor que lo hagaCREATE TABLE ... AS SELECT DISTINCT ... FROM ... ; , y si elimina el 1% de las filas, usar DELETE es mejor.

También para operaciones de mantenimiento como esta, generalmente es bueno configurar work_memuna buena parte de su RAM: ejecute EXPLAIN, verifique el número N de ordenamientos / hashes y configure work_mem en su RAM / 2 / N. Use mucha RAM; es bueno para la velocidad. Siempre que solo tenga una conexión simultánea ...

bobflux
fuente
1

Estoy trabajando con PostgreSQL 8.4. Cuando ejecuté el código propuesto, descubrí que en realidad no estaba eliminando los duplicados. Al ejecutar algunas pruebas, descubrí que agregar "DISTINCT ON (duplicate_column_name)" y "ORDER BY duplicate_column_name" hizo el truco. No soy un gurú de SQL, encontré esto en el 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;
CM.
fuente
1

Esto funciona muy bien y es muy rápido:

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

Elimine los duplicados por columna (s) y mantenga la fila con la identificación más baja. El patrón está tomado de la wiki de postgres.

Al usar CTE, puede lograr una versión más legible de lo anterior a través de este

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
fuente
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
fuente
Lo probé y funcionó; Lo formateé para facilitar la lectura. Parece bastante sofisticado, pero le vendría bien alguna explicación. ¿Cómo cambiaría uno este ejemplo para su propio caso de uso?
Tobias