¿Cómo elimino registros duplicados en una tabla de unión en PostgreSQL?


9

Tengo una tabla que tiene un 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"

Me gustaría eliminar registros que son duplicados, es decir, tienen ambos el mismo tag_idy question_idcomo otro registro.

¿Cómo se ve el SQL para eso?

Respuestas:


15

En mi experiencia (y como se muestra en muchas pruebas) NOT INcomo lo demostró @gsiems es bastante lento y se escala terriblemente. El inverso INsuele ser más rápido (donde puede reformular de esa manera, como en este caso), pero esta consulta con EXISTS(hacer exactamente lo que pidió) debería ser mucho más rápida todavía, con tablas grandes por orden de magnitud :

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

Elimina cada fila donde existe otra fila con la misma (tag_id, question_id)y una más pequeñactid . (Efectivamente mantiene la primera instancia de acuerdo con el orden físico de las tuplas). Utilizando ctiden ausencia de una mejor alternativa, su tabla no parece tener una PK o cualquier otra (s) columna (s) única (s).

ctides el identificador interno de tupla presente en cada fila y necesariamente único. Otras lecturas:

Prueba

Ejecuté un caso de prueba con esta tabla adaptada a su pregunta y 100k filas:

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;

Los índices no ayudan en este caso.

Resultados

NOT IN
El SQLfiddle agota el tiempo de espera.
Intenté lo mismo localmente pero también lo cancelé, después de varios minutos.

EXISTS
Termina en medio segundo en este SQLfiddle .

Alternativas

Si va a eliminar la mayoría de las filas , será más rápido seleccionar a los sobrevivientes en otra tabla, soltar el original y cambiar el nombre de la tabla del sobreviviente. Cuidado, esto tiene implicaciones si tiene claves externas o de vista (u otras dependencias) definidas en el original.

Si tiene dependencias y desea conservarlas, podría:

  • Descarte todas las claves e índices foráneos para obtener rendimiento.
  • SELECT sobrevivientes a una mesa temporal.
  • TRUNCATE el original.
  • Revivientes INSERT.
  • Re- CREATEíndices y claves foráneas. Las vistas solo pueden permanecer, no tienen impacto en el rendimiento. Más aquí o aquí .

++ para la solución existente. Mucho mejor que mi sugerencia.
gsiems

¿Podría explicar la comparación de ctid en su cláusula WHERE?
Kevin Meredith

1
@KevinMeredith: agregué alguna explicación.
Erwin Brandstetter

6

Puede usar el ctid para lograr eso. Por ejemplo:

Crea una tabla con duplicados:

=# 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)

Seleccione los datos 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)

Eliminar los datos 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)

En su caso, lo siguiente debería funcionar:

delete from questions_tags
    where ctid not in (
        select min (ctid) as min_ctid 
            from questions_tags 
            group by question_id, tag_id
        );

¿Dónde puedo leer más sobre esto ctid? Gracias.
marcamillion

@marcamillion - La documentación tiene una breve reseña sobre ctids en postgresql.org/docs/current/static/ddl-system-columns.html
gsiems

¿Qué significa ctid?
marcamillion

@marcamillion - tid == "tuple id", no estoy seguro de lo que significa c.
gsiems
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.