Eliminar registros duplicados en PostgreSQL

113

Tengo una tabla en una base de datos PostgreSQL 8.3.8, que no tiene claves / restricciones y tiene varias filas con exactamente los mismos valores.

Me gustaría eliminar todos los duplicados y conservar solo 1 copia de cada fila.

Hay una columna en particular (denominada "clave") que se puede utilizar para identificar duplicados (es decir, solo debe existir una entrada para cada "clave" distinta).

¿Cómo puedo hacer esto? (idealmente con un solo comando SQL) La velocidad no es un problema en este caso (solo hay unas pocas filas).

André Morujão
fuente

Respuestas:

80
DELETE FROM dupes a
WHERE a.ctid <> (SELECT min(b.ctid)
                 FROM   dupes b
                 WHERE  a.key = b.key);
un caballo sin nombre
fuente
20
¡No lo uses, es demasiado lento!
Paweł Malisak
5
Si bien esta solución definitivamente funciona, la siguiente solución de @rapimo se ejecuta mucho más rápido. Creo que esto tiene que ver con la declaración de selección interna aquí que se ejecuta N veces (para todas las N filas en la tabla de duplicados) en lugar de la agrupación que está sucediendo en la otra solución.
David
Para tablas grandes (varios millones de registros), esta realmente cabe en la memoria, a diferencia de la solución de @ rapimo. Entonces, en esos casos, este es el más rápido (sin intercambio).
Giel
1
Añadiendo explicación: funciona porque ctid es una columna especial de postgres que indica la ubicación física de la fila. Puede usar esto como una identificación única incluso si su tabla no posee una identificación única. postgresql.org/docs/8.2/ddl-system-columns.html
Eric Burel
194

Una solución más rápida es

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
fuente
20
¿Por qué es más rápido que la solución de a_horse_with_no_name?
Roberto
3
Esto es más rápido porque ejecuta solo 2 consultas. Primero uno para seleccionar todos los duplicados, luego uno para eliminar todos los elementos de la tabla. La consulta de @a_horse_with_no_name realiza una consulta para ver si coincide con cualquier otra para cada elemento de la tabla.
Aeolun
5
lo que es ctid?
techkuz
6
de docs: ctid. La ubicación física de la versión de fila dentro de su tabla. Tenga en cuenta que aunque el ctid se puede utilizar para localizar la versión de la fila muy rápidamente, el ctid de una fila cambiará cada vez que se actualice o se mueva por VACUUM FULL. Por lo tanto, ctid es inútil como identificador de fila a largo plazo.
Saim
1
Parece que esto no funciona cuando tiene más de 2 filas duplicadas, porque elimina solo un duplicado a la vez.
Frankie Drake
73

Esto es rápido y conciso:

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

Vea también mi respuesta en Cómo eliminar filas duplicadas sin un identificador único que incluye más información.

isapir
fuente
¿Qué significa ct? ¿contar?
techkuz
4
@trthhrtz ctidapunta a la ubicación física del registro en la tabla. Al contrario de lo que escribí en ese momento en el comentario, el uso del operador menor que no necesariamente apunta a la versión anterior, ya que ct puede ajustarse y un valor con un ctid más bajo podría ser más nuevo.
isapir
1
Solo para su información, probé esta solución y la aborté después de esperar 15 minutos. Probé la solución de rapimo y se completó en aproximadamente 10 segundos (eliminó ~ 700,000 filas).
Patrick
@Patrick no puede imaginarse si su base de datos no tiene un identificador único, ya que la respuesta de rapimo no funciona en ese caso.
Estuches el
@isapir Solo tengo curiosidad, las respuestas anteriores, ¿mantienen los registros más antiguos como lo seleccionaron min(ctid)? mientras que el tuyo se queda con los más nuevos? ¡Gracias!
Estuches el
17

Probé esto:

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

proporcionado por Postgres wiki:

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

Radu Gabriel
fuente
¿Alguna idea de la actuación en comparación con la respuesta de @ rapimo y la aceptada (@a_horse_with_no_name)?
tuxayo
3
Este no funcionará si, como dicen las preguntas, todas las columnas son idénticas, idincluidas.
ibizaman
Esta consulta eliminará tanto la copia original como los duplicados. la pregunta es sobre retener al menos una fila.
pyBomb
@pyBomb incorrecto, mantendrá el primero iddonde la columna1 ... 3 están duplicadas
Jeff
A partir de postgresql 12, esta es, con mucho, la solución más rápida (frente a 300 millones de filas). Acabo de probar todo lo propuesto en esta pregunta, incluida la respuesta aceptada, y esta solución "oficial" es en realidad la más rápida y cumple con todos los requisitos de OP (y el mío)
Jeff
7

Tuve que crear mi propia versión. La versión escrita por @a_horse_with_no_name es demasiado lenta en mi mesa (21M filas). Y @rapimo simplemente no borra dups.

Esto es lo que uso en 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);
experto
fuente
6

Usaría una tabla temporal:

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

Luego, elimine taby cambie el nombre tab_tempa tab.

Pablo Santa Cruz
fuente
8
Este enfoque no tiene en cuenta los desencadenantes, los índices y las estadísticas. Ciertamente, podría agregarlos, pero también agrega mucho más trabajo.
Jordania
No todo el mundo necesita eso. Este enfoque es extremadamente rápido y funcionó mucho mejor que el resto en 200k correos electrónicos (varchar 250) sin índices.
Sergey Telshevsky
Código completo: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

Otro enfoque (funciona solo si tiene un campo único como iden su tabla) para encontrar todos los identificadores únicos por columnas y eliminar otros identificadores que no están en la lista única

DELETE
FROM users
WHERE users.id NOT IN (SELECT DISTINCT ON (username, email) id FROM users);
Zaytsev Dmitry
fuente
La cuestión es que, en mi pregunta, las tablas no tenían identificadores únicos; los "duplicados" eran varias filas con exactamente los mismos valores en todas las columnas.
André Morujão
Bien, agregué algunas notas
Zaytsev Dmitry
1

Qué tal si:

CON
  u AS (SELECT DISTINCT * FROM your_table),
  x AS (ELIMINAR DE your_table)
INSERT INTO your_table SELECT * FROM u;

Me había preocupado la orden de ejecución, si el DELETE ocurriría antes del SELECT DISTINCT, pero funciona bien para mí. Y tiene la ventaja adicional de no necesitar ningún conocimiento sobre la estructura de la tabla.

Barrie Walker
fuente
El único inconveniente es que si tiene un tipo de datos que no admite la igualdad (por ejemplo json), esto no funcionará.
a_horse_with_no_name
0

Esto funcionó bien para mí. Tenía una tabla, términos, que contenía valores duplicados. Ejecutó una consulta para completar una tabla temporal con todas las filas duplicadas. Luego ejecuté una declaración de eliminación con esos identificadores en la tabla temporal. valor es la columna que contenía los duplicados.

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

Aquí hay una solución usando 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
fuente