Как удалить дубликаты записей в таблице соединений в PostgreSQL?

9

У меня есть таблица с такой схемой:

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"

Я хотел бы удалить записи, которые являются дубликатами, то есть они имеют как одинаковую, так tag_idи question_idдругую запись.

Как выглядит SQL для этого?

marcamillion
источник

Ответы:

15

По моему опыту (и как показано во многих тестах), NOT INкак показал @gsiems, он довольно медленный и масштабируется ужасно. Обратное, INкак правило, быстрее (где вы можете переформулировать таким образом, как в этом случае), но этот запрос с EXISTS(делая именно то, что вы просили) должен быть еще быстрее - с большими таблицами на порядки :

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

Удаляет каждую строку , в которой другой ряд с тем же (tag_id, question_id)и меньше ctidсуществует . (Эффективно сохраняет первый экземпляр в соответствии с физическим порядком кортежей.) При использовании ctidв отсутствие лучшей альтернативы ваша таблица, похоже, не имеет PK или какого-либо другого уникального (набора) столбцов.

ctidявляется внутренним идентификатором кортежа, присутствующим в каждой строке и обязательно уникальным. Дальнейшее чтение:

Тестовое задание

Я выполнил тестовый пример с этой таблицей, соответствующей вашему вопросу и 100k строк:

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;

Индексы не помогают в этом случае.

Результаты

NOT IN
Время ожидания SQLfiddle истекло.
Пробовал то же самое локально, но я тоже отменил через несколько минут.

EXISTS
Заканчивается через полсекунды в этом SQLfiddle .

альтернативы

Если вы собираетесь удалить большинство строк , вам будет проще выбрать выживших в другую таблицу, отбросить оригинал и переименовать таблицу выживших. Осторожно, это имеет значение, если у вас есть вид или внешние ключи (или другие зависимости), определенные на оригинале.

Если у вас есть зависимости и вы хотите их сохранить, вы можете:

  • Отбросьте все внешние ключи и индексы - для производительности.
  • SELECT выжившие к временному столу.
  • TRUNCATE оригинал.
  • ПЕРЕУСТАНОВКИ INSERTвыжившие.
  • Переиндексирует CREATEи внешние ключи. Представления могут просто остаться, они не влияют на производительность. Больше здесь или здесь .
Эрвин Брандштеттер
источник
++ для существующего решения. Гораздо лучше, чем мое предложение.
gsiems
Не могли бы вы объяснить сравнение ctid в предложении WHERE?
Кевин Мередит
1
@KevinMeredith: я добавил некоторые объяснения.
Эрвин Брандштеттер
6

Вы можете использовать Ctid для достижения этой цели. Например:

Создайте таблицу с дубликатами:

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

Выберите дубликаты данных:

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

Удалить дубликаты данных:

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

В вашем случае должно работать следующее:

delete from questions_tags
    where ctid not in (
        select min (ctid) as min_ctid 
            from questions_tags 
            group by question_id, tag_id
        );
gsiems
источник
Где я могу прочитать больше об этом ctid? Спасибо.
marcamillion
@marcamillion - в документации есть краткое описание ctids на postgresql.org/docs/current/static/ddl-system-columns.html
gsiems,
Что означает ctid?
Маркамиллион
@marcamillion - tid == "идентификатор кортежа", не уверен, что означает c.
gsiems