Excluir registros duplicados no PostgreSQL

113

Eu tenho uma tabela em um banco de dados PostgreSQL 8.3.8, que não possui chaves / restrições e possui várias linhas com exatamente os mesmos valores.

Eu gostaria de remover todas as duplicatas e manter apenas 1 cópia de cada linha.

Há uma coluna em particular (chamada "chave") que pode ser usada para identificar duplicatas (ou seja, deve haver apenas uma entrada para cada "chave" distinta).

Como posso fazer isso? (de preferência com um único comando SQL) A velocidade não é um problema neste caso (existem apenas algumas linhas).

André Morujão
fonte

Respostas:

80
DELETE FROM dupes a
WHERE a.ctid <> (SELECT min(b.ctid)
                 FROM   dupes b
                 WHERE  a.key = b.key);
um cavalo sem nome
fonte
20
Não use, é muito lento!
Paweł Malisak
5
Embora essa solução definitivamente funcione, a solução de @rapimo abaixo é executada muito mais rápido. Eu acredito que isso tem a ver com a instrução select interna aqui sendo executada N vezes (para todas as N linhas na tabela de duvidosos) em vez do agrupamento que está acontecendo na outra solução.
David
Para tabelas enormes (vários milhões de registros), este realmente cabe na memória, ao contrário da solução de @ rapimo. Então, nesses casos, este é o mais rápido (sem troca).
Giel
1
Adicionando explicação: funciona porque ctid é uma coluna especial do postgres que indica a localização física da linha. Você pode usar isso como um id único, mesmo se sua tabela não possuir um id único. postgresql.org/docs/8.2/ddl-system-columns.html
Eric Burel
193

Uma solução mais rápida é

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
fonte
20
Por que é mais rápido do que a solução de a_horse_with_no_name?
Roberto
3
Isso é mais rápido porque executa apenas 2 consultas. Primeiro, um para selecionar todas as duplicatas e, em seguida, outro para excluir todos os itens da tabela. A consulta por @a_horse_with_no_name faz uma consulta para ver se corresponde a qualquer outro para cada item da tabela.
Aeolun
5
o que é ctid?
techkuz
6
dos documentos: ctid. A localização física da versão da linha em sua tabela. Observe que embora o ctid possa ser usado para localizar a versão da linha muito rapidamente, o ctid de uma linha mudará cada vez que for atualizado ou movido por VACUUM FULL. Portanto, ctid é inútil como um identificador de linha de longo prazo.
Saim de
1
Parece que isso não funciona quando há mais de 2 linhas duplicadas, porque exclui apenas uma duplicata por vez.
Frankie Drake
73

Isso é rápido e 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

Veja também minha resposta em Como excluir linhas duplicadas sem identificador exclusivo, que inclui mais informações.

isapir
fonte
o que significa ct? contagem?
techkuz
4
@trthhrtz ctidaponta para a localização física do registro na tabela. Ao contrário do que escrevi no momento no comentário, usar o operador menos que não aponta necessariamente para a versão mais antiga, pois o ct pode ser agrupado e um valor com um ctid inferior pode realmente ser mais recente.
isapir
1
Apenas para sua informação, tentei essa solução e abortei após esperar 15 minutos. Tentei a solução do rapimo e ela foi concluída em cerca de 10 segundos (excluiu ~ 700.000 linhas).
Patrick
@Patrick não pode imaginar se seu banco de dados não tiver um identificador único, já que a resposta do rapimo não funciona nesse caso.
stucash
@isapir Estou apenas curioso, as respostas acima, eles estão mantendo os registros antigos corretos conforme selecionaram min(ctid)? enquanto o seu está mantendo os mais novos? obrigado!
stucash
17

Eu tentei isso:

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

fornecido pelo wiki Postgres:

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

Radu Gabriel
fonte
Alguma ideia do desempenho em comparação com a resposta de @ rapimo e a aceita (@a_horse_with_no_name)?
tuxayo
3
Este não funcionará se, como os estados das perguntas, todas as colunas forem idênticas, as idincluídas.
ibizaman 01 de
Esta consulta excluirá a cópia original e as duplicatas. a questão é manter pelo menos uma linha.
pyBomb
@pyBomb errado, ele manterá o primeiro idonde coluna1 ... 3 são duplicados
Jeff
A partir do postgresql 12, esta é de longe a solução mais rápida (contra 300 milhões de linhas). Acabei de testar tudo o que foi proposto nesta pergunta, incluindo a resposta aceita, e esta solução "oficial" é realmente a mais rápida e atende a todos os requisitos do OP (e do meu)
Jeff
7

Tive que criar minha própria versão. A versão escrita por @a_horse_with_no_name é muito lenta na minha tabela (21 milhões de linhas). E @rapimo simplesmente não exclui cópias.

Aqui está o que eu uso no 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);
especialista
fonte
6

Eu usaria uma mesa temporária:

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

Em seguida, exclua tabe renomeie tab_temppara tab.

Pablo Santa Cruz
fonte
8
Essa abordagem não leva em conta gatilhos, índices e estatísticas. Certamente você pode adicioná-los, mas acrescenta muito mais trabalho também.
Jordan
Nem todo mundo precisa disso. Essa abordagem é extremamente rápida e funcionou muito melhor do que o resto em 200 mil emails (varchar 250) sem í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

Outra abordagem (funciona apenas se você tiver qualquer campo único como idem sua tabela) para encontrar todos os ids únicos por colunas e remover outros ids que não estão na lista única

DELETE
FROM users
WHERE users.id NOT IN (SELECT DISTINCT ON (username, email) id FROM users);
Zaytsev Dmitry
fonte
A questão é que, na minha pergunta, as tabelas não tinham ids exclusivos; as "duplicatas" eram várias linhas com exatamente os mesmos valores em todas as colunas.
André Morujão 11/12/1919
Certo, adicionei algumas notas
Zaytsev Dmitry
1

E se:

COM
  u AS (SELECIONE DISTINTO * DE sua_tabela),
  x AS (DELETE FROM your_table)
INSERT INTO your_table SELECT * FROM u;

Eu estava preocupado com a ordem de execução, se DELETE aconteceria antes de SELECT DISTINCT, mas funciona bem para mim. E tem a vantagem adicional de não precisar de nenhum conhecimento sobre a estrutura da tabela.

Barrie Walker
fonte
A única desvantagem é que, se você tiver tipos de dados que não suportam igualdade (por exemplo json), isso não funcionará.
a_horse_with_no_name
0

Isto funcionou bem para mim. Eu tinha uma tabela de termos que continha valores duplicados. Executou uma consulta para preencher uma tabela temporária com todas as linhas duplicadas. Em seguida, executei a instrução a delete com esses ids na tabela temporária. valor é a coluna que continha as duplicatas.

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

Aqui está uma solução 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
fonte