Como removo registros duplicados em uma tabela de junção no PostgreSQL?

9

Eu tenho uma tabela que tem um esquema como este:

create_table "questions_tags", :id => false, :force => true do |t|
        t.integer "question_id"
        t.integer "tag_id"
      end

      add_index "questions_tags", ["question_id"], :name => "index_questions_tags_on_question_id"
      add_index "questions_tags", ["tag_id"], :name => "index_questions_tags_on_tag_id"

Gostaria de remover registros duplicados, ou seja, eles têm o mesmo tag_ide question_idcomo outro registro.

Como é o SQL para isso?

marcamillion
fonte

Respostas:

15

Na minha experiência (e como mostrado em muitos testes), NOT INcomo demonstrado por @gsiems, é bastante lento e dimensiona terrivelmente. O inverso INé geralmente mais rápido (onde você pode reformular dessa maneira, como neste caso), mas essa consulta com EXISTS(fazendo exatamente o que você pediu) deve ser muito mais rápida ainda - com grandes tabelas por ordem de magnitude :

DELETE FROM questions_tags q
WHERE  EXISTS (
   SELECT FROM questions_tags q1
   WHERE  q1.ctid < q.ctid
   AND    q1.question_id = q.question_id
   AND    q1.tag_id = q.tag_id
   );

Exclui todas as linhas em que existe outra linha com a mesma (tag_id, question_id)e menorctid . (Mantém efetivamente a primeira instância de acordo com a ordem física das tuplas.) Usando ctidna ausência de uma alternativa melhor, sua tabela parece não ter uma PK ou qualquer outra (s) coluna (s) exclusiva (s).

ctidé o identificador de tupla interno presente em todas as linhas e necessariamente exclusivo. Leitura adicional:

Teste

Executei um caso de teste com esta tabela correspondente à sua pergunta e 100 mil linhas:

CREATE TABLE questions_tags(
  question_id integer NOT NULL
, tag_id      integer NOT NULL
);

INSERT INTO questions_tags (question_id, tag_id)
SELECT (random()* 100)::int, (random()* 100)::int
FROM   generate_series(1, 100000);

ANALYZE questions_tags;

Os índices não ajudam nesse caso.

Resultados

NOT IN
O tempo limite do SQLfiddle .
Tentei o mesmo localmente, mas também o cancelei depois de alguns minutos.

EXISTS
Termina em meio segundo neste SQLfiddle .

Alternativas

Se você deseja excluir a maioria das linhas , será mais rápido selecionar os sobreviventes em outra tabela, soltar o original e renomear a tabela de sobreviventes. Cuidado, isso tem implicações se você tiver chaves de exibição ou estrangeiras (ou outras dependências) definidas no original.

Se você possui dependências e deseja mantê-las, pode:

  • Solte todas as chaves e índices estrangeiros - para obter desempenho.
  • SELECT sobreviventes para uma mesa temporária.
  • TRUNCATE o original.
  • Re- INSERTsobreviventes.
  • Reindexa CREATEe chaves estrangeiras. As visualizações podem permanecer, elas não têm impacto no desempenho. Mais aqui ou aqui .
Erwin Brandstetter
fonte
++ para a solução existente. Muito melhor do que minha sugestão.
gsiems
Poderia explicar a comparação ctid na sua cláusula WHERE?
Kevin Meredith
11
@ KevinMeredith: Eu adicionei algumas explicações.
Erwin Brandstetter
6

Você pode usar o ctid para fazer isso. Por exemplo:

Crie uma tabela com duplicatas:

=# create table foo (id1 integer, id2 integer);
CREATE TABLE

=# insert into foo values (1,1), (1, 2), (1, 2), (1, 3);
INSERT 0 4

=# select * from foo;
 id1 | id2 
-----+-----
   1 |   1
   1 |   2
   1 |   2
   1 |   3
(4 rows)

Selecione os dados duplicados:

=# select foo.ctid, foo.id1, foo.id2, foo2.min_ctid
-#  from foo
-#  join (
-#      select id1, id2, min(ctid) as min_ctid 
-#          from foo 
-#          group by id1, id2 
-#          having count (*) > 1
-#      ) foo2 
-#      on foo.id1 = foo2.id1 and foo.id2 = foo2.id2
-#  where foo.ctid <> foo2.min_ctid ;
 ctid  | id1 | id2 | min_ctid 
-------+-----+-----+----------
 (0,3) |   1 |   2 | (0,2)
(1 row)

Exclua os dados duplicados:

=# delete from foo
-# where ctid not in (select min (ctid) as min_ctid from foo group by id1, id2);
DELETE 1

=# select * from foo;
 id1 | id2 
-----+-----
   1 |   1
   1 |   2
   1 |   3
(3 rows)

No seu caso, o seguinte deve funcionar:

delete from questions_tags
    where ctid not in (
        select min (ctid) as min_ctid 
            from questions_tags 
            group by question_id, tag_id
        );
gsiems
fonte
Onde posso ler mais sobre isso ctid? Obrigado.
Marcamillion 13/03/2013
@marcamillion - A documentação tem uma breve sinopse sobre CTIDs em postgresql.org/docs/current/static/ddl-system-columns.html
gsiems
O que ctidsignifica?
Marcamillion 14/03
@marcamillion - tid == "tuple id", não sei o que significa c.
gsiems