Como deletar entradas duplicadas?


92

Tenho que adicionar uma restrição única a uma tabela existente. Isso é bom, exceto que a tabela já tem milhões de linhas e muitas das linhas violam a restrição exclusiva que preciso adicionar.

Qual é a abordagem mais rápida para remover as linhas problemáticas? Eu tenho uma instrução SQL que encontra as duplicatas e as exclui, mas está demorando muito para ser executada. Existe outra maneira de resolver este problema? Talvez fazendo backup da tabela e restaurando após a adição da restrição?

Respostas:


101

Por exemplo, você poderia:

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

2
Você pode torná-lo distinto para grupo de colunas. Talvez "SELECT DISTINCT (ta, tb, tc), * FROM t"?
gjrwebber


36
mais fácil de digitar: CREATE TABLE tmp AS SELECT ...;. Então você não precisa nem mesmo descobrir qual é o layout tmp. :)
Randal Schwartz de

9
Na verdade, essa resposta não é muito boa por vários motivos. @Randal nomeou um. Na maioria dos casos, especialmente se você tiver objetos dependentes como índices, restrições, visualizações etc., a abordagem superior é usar uma TABELA TEMPORÁRIA real , TRUNCAR o original e inserir novamente os dados.
Erwin Brandstetter

7
Você está certo sobre os índices. Soltar e recriar é muito mais rápido. Mas outros objetos dependentes quebrarão ou evitarão a queda total da tabela - o que o OP descobriria após ter feito a cópia - tanto para a "abordagem mais rápida". Ainda assim, você está certo sobre o downvote. É infundado, porque não é uma resposta ruim. Simplesmente não é tão bom. Você poderia ter adicionado algumas dicas sobre índices ou objetos dependentes ou um link para o manual como você fez no comentário ou qualquer tipo de explicação. Acho que fiquei frustrado com a forma como as pessoas votam. Removido o downvote.
Erwin Brandstetter

173

Algumas dessas abordagens parecem um pouco complicadas e geralmente faço isso como:

Dada a tabela table, deseja-se exclusivo em (campo1, campo2) mantendo a linha com o 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 exemplo, tenho uma tabela, user_accountse quero adicionar uma restrição exclusiva para e-mail, mas tenho algumas duplicatas. Diga também que desejo manter o criado mais recentemente (id máximo entre duplicatas).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Nota - USINGnão é SQL padrão, é uma extensão do PostgreSQL (mas muito útil), mas a pergunta original menciona especificamente o PostgreSQL.

4
Essa segunda abordagem é muito rápida no postgres! Obrigado.
Eric Bowman - abstracto -

5
@Tim você pode explicar melhor o que USINGfaz no postgresql?
Fopa Léon Constantin

3
Esta é de longe a melhor resposta. Mesmo que você não tenha uma coluna serial em sua tabela para usar na comparação de id, vale a pena adicionar temporariamente uma para usar essa abordagem simples.
Shane

2
Eu acabei de verificar. A resposta é sim, vai. Usar menor que (<) deixa você com apenas a id máxima, enquanto maior que (>) deixa você apenas com a id mínima, excluindo o resto.
André C. Andersen

1
@Shane pode-se usar: WHERE table1.ctid<table2.ctid- não há necessidade de adicionar coluna serial
alexkovelsky

25

Em vez de criar uma nova tabela, você também pode inserir novamente linhas exclusivas na mesma tabela depois de truncá-la. Faça tudo em uma transação . Opcionalmente, você pode descartar a tabela temporária no final da transação automaticamente com ON COMMIT DROP. Ver abaixo.

Essa abordagem só é útil quando há muitas linhas para excluir de toda a tabela. Para apenas algumas duplicatas, use um plano DELETE.

Você mencionou milhões de linhas. Para tornar a operação rápida, você deseja alocar buffers temporários suficientes para a sessão. A configuração deve ser ajustada antes que qualquer buffer temporário seja usado na sessão atual. Descubra o tamanho da sua mesa:

SELECT pg_size_pretty(pg_relation_size('tbl'));

Defina de temp_buffersacordo. Arredonde generosamente porque a representação na memória precisa de um pouco mais 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 pode ser superior à criação de uma nova tabela se existirem objetos dependentes. Exibições, índices, chaves estrangeiras ou outros objetos que fazem referência à tabela. TRUNCATEfaz com que você comece do zero de qualquer maneira (novo arquivo em segundo plano) e é muito mais rápido do que DELETE FROM tblcom tabelas grandes ( DELETEna verdade, pode ser mais rápido com tabelas pequenas).

Para tabelas grandes, é regularmente mais rápido descartar índices e chaves estrangeiras, recarregar a tabela e recriar esses objetos. No que diz respeito às restrições fk, você deve ter certeza de que os novos dados são válidos, ou você encontrará uma exceção ao tentar criar o fk.

Observe que TRUNCATErequer um travamento mais agressivo do que DELETE. Isso pode ser um problema para tabelas com carga simultânea pesada.

Se TRUNCATEnão for uma opção ou geralmente para tabelas pequenas e médias, há uma técnica semelhante com um CTE de modificação de dados (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.

Mais lento para mesas grandes, porque TRUNCATElá é mais rápido. Mas pode ser mais rápido (e mais simples!) Para tabelas pequenas.

Se você não tiver nenhum objeto dependente, poderá criar uma nova tabela e excluir a antiga, mas dificilmente ganhará algo com essa abordagem universal.

Para tabelas muito grandes que não cabem na RAM disponível , criar uma nova tabela será consideravelmente mais rápido. Você terá que pesar isso contra possíveis problemas / sobrecarga com objetos dependentes.


2
Eu usei essa abordagem também. No entanto, pode ser pessoal, mas minha tabela temporária foi excluída e não está disponível após o truncamento ... Tenha cuidado ao fazer essas etapas se a tabela temporária foi criada com sucesso e está disponível.
xlash 01 de

@xlash: Você pode verificar a existência para ter certeza e usar um nome diferente para a tabela temporária ou reutilizar o que já existe. Eu adicionei um pouco à minha resposta.
Erwin Brandstetter

AVISO: Tenha cuidado com +1 para @xlash - eu tenho que reimportar meus dados porque a tabela temporária não existia depois TRUNCATE. Como disse Erwin, certifique-se de que ele existe antes de truncar sua mesa. Ver a resposta de @codebykat
Jordan Arseno

1
@JordanArseno: Mudei para uma versão sem ON COMMIT DROP, para que as pessoas que perderem a parte onde escrevi "em uma transação" não percam dados. E eu adicionei BEGIN / COMMIT para esclarecer "uma transação".
Erwin Brandstetter

1
solução com USING levou mais de 3 horas na mesa com 14 milhões de registros. Esta solução com temp_buffers levou 13 minutos. Obrigado.
castt

20

Você pode usar oid ou ctid, que normalmente são colunas "não visíveis" na tabela:

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

4
Para deletar no local , NOT EXISTSdeve ser consideravelmente mais 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)- ou use qualquer outra coluna ou conjunto de colunas para classificar para escolher um sobrevivente.
Erwin Brandstetter,

@ErwinBrandstetter, a consulta fornecida deve ser usada NOT EXISTS?
João

1
@John: Deve estar EXISTSaqui. Leia assim: "Exclua todas as linhas onde existe qualquer outra linha com o mesmo valor, dist_colmas maior ctid" O único sobrevivente por grupo de idiotas será aquele com o maior ctid.
Erwin Brandstetter,

A solução mais fácil se você tiver apenas algumas linhas duplicadas. Pode ser usado com LIMITse você souber o número de duplicatas.
Skippy le Grand Gourou de

19

A função de janela do PostgreSQL é útil para esse 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 Excluindo duplicatas .


E usando "ctid" em vez de "id", isso realmente funciona para linhas totalmente duplicadas.
bradw2k

Ótima solução. Tive que fazer isso para uma tabela com um bilhão de registros. Eu adicionei um WHERE ao SELECT interno para fazer isso em blocos.
janeiro

7

De uma lista de e-mails antiga do 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' );

Mais uma duplicata dupla

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

select oid, a, b from test;

Selecione as linhas 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
                 );

Excluir linhas duplicadas

Nota: PostgreSQL não suporta apelidos na tabela mencionada na fromcláusula de exclusão.

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

Sua explicação é muito inteligente, mas está faltando um ponto. Na criação da tabela, especifique o oid e, em seguida, acesse apenas a exibição da mensagem de erro oid else
Kalanidhi

@Kalanidhi Obrigado por seus comentários sobre a melhoria da resposta, vou levar em consideração este ponto.
Bhavik Ambani

Isso realmente veio de postgresql.org/message-id/…
Martin F

Você pode usar a coluna do sistema 'ctid' se 'oid' apresentar um erro.
sul4bh

7

Consulta generalizada para excluir duplicatas:

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

A coluna ctidé uma coluna especial disponível para cada tabela, mas não visível, a menos que seja especificamente mencionada. O ctidvalor da coluna é considerado único para cada linha de uma tabela.


a única resposta universal! Funciona sem JOIN próprio / cartesiano. Vale a pena acrescentar que é essencial especificar a GROUP BYcláusula corretamente - este deve ser o 'critério de exclusividade' que é violado agora ou se você quiser que a chave detecte duplicatas. Se especificado incorretamente, não funcionará corretamente
msciwoj

4

Acabei de usar a resposta de Erwin Brandstetter com sucesso para remover duplicatas em uma tabela de junção (uma tabela sem seus próprios IDs primários), mas descobri que há uma advertência importante.

Incluir ON COMMIT DROPsignifica que a tabela temporária será eliminada no final da transação. Para mim, isso significava que a tabela temporária não estava mais disponível no momento em que fui inseri-la!

Eu apenas fiz CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;e tudo funcionou bem.

A tabela temporária é eliminada no final da sessão.


3

Esta função remove duplicatas sem remover índices e faz isso em qualquer tabela.

Uso: select remove_duplicates('mytable');

---
--- remove_duplicates (tablename) remove registros duplicados de uma tabela (converte de conjunto para conjunto único)
---
CREATE OR REPLACE FUNCTION remove_duplicates (text) RETURNS void AS $$
DECLARAR
  nome da tabela ALIAS POR $ 1;
INÍCIO
  EXECUTE 'CRIAR TABELA TEMPORÁRIA _DISTINCT_' || tablename || 'AS (SELECT DISTINCT * FROM' || tablename || ');';
  EXECUTAR 'EXCLUIR DE' || tablename || ';';
  EXECUTE 'INSERT INTO' || tablename || '(SELECT * FROM _DISTINCT_' || tablename || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
  RETORNA;
FIM;
$$ 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);

Isso é o que estou fazendo atualmente, mas está demorando muito para ser executado.
gjrwebber

1
Isso não falharia se várias linhas na tabela tivessem o mesmo valor na coluna alguma coisa?
shreedhar

3

Se você tem apenas uma ou algumas entradas duplicadas, e elas estão realmente duplicadas (ou seja, aparecem duas vezes), você pode usar a ctidcoluna "oculta" , conforme proposto acima, junto com LIMIT:

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

Isso excluirá apenas a primeira das linhas selecionadas.


Eu sei que isso não aborda o problema de OP, que tem muitas linhas duplicadas em milhões de linhas, mas pode ser útil de qualquer maneira.
Skippy le Grand Gourou

Isso teria que ser executado uma vez para cada linha duplicada. A resposta de shekwi só precisa ser executada uma vez.
bradw2k

3

Primeiro, você precisa decidir quais de suas "duplicatas" você manterá. Se todas as colunas forem iguais, OK, você pode excluir qualquer uma delas ... Mas talvez você queira manter apenas a mais recente, ou algum outro critério?

O caminho mais rápido depende da sua resposta à pergunta acima, e também da% de duplicatas na tabela. Se você descartar 50% de suas linhas, é melhor fazer isso CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;, e se você excluir 1% das linhas, usar DELETE é melhor.

Também para operações de manutenção como essa, geralmente é bom definir work_memuma boa parte de sua RAM: execute EXPLAIN, verifique o número N de tipos / hashes e defina work_mem para sua RAM / 2 / N. Use muita RAM; é bom para velocidade. Contanto que você tenha apenas uma conexão simultânea ...


1

Estou trabalhando com PostgreSQL 8.4. Quando executei o código proposto, descobri que ele não estava realmente removendo as duplicatas. Ao executar alguns testes, descobri que adicionar "DISTINCT ON (duplicate_column_name)" e "ORDER BY duplicate_column_name" funcionou. Não sou um guru de SQL, encontrei isso no 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

Isso funciona muito bem e é muito rápido:

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

Exclua duplicatas por coluna (s) e mantenha a linha com o id mais baixo. O padrão é retirado do wiki postgres

Usando CTEs, você pode obter uma versão mais legível do acima por meio deste

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

Eu testei e funcionou; Eu formatei para facilitar a leitura. Parece bastante sofisticado, mas precisa de alguma explicação. Como alguém mudaria esse exemplo para seu próprio caso de uso?
Tobias
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.