Удалить повторяющиеся записи в PostgreSQL

113

У меня есть таблица в базе данных PostgreSQL 8.3.8, которая не имеет ключей / ограничений и имеет несколько строк с точно такими же значениями.

Я хочу удалить все дубликаты и оставить только по одной копии каждой строки.

В частности, есть один столбец (с именем «ключ»), который может использоваться для идентификации дубликатов (т.е. должна существовать только одна запись для каждого отдельного «ключа»).

Как я могу это сделать? (в идеале с помощью одной команды SQL) Скорость в этом случае не проблема (есть только несколько строк).

Андре Моружао
источник

Ответы:

81
DELETE FROM dupes a
WHERE a.ctid <> (SELECT min(b.ctid)
                 FROM   dupes b
                 WHERE  a.key = b.key);
a_horse_with_no_name
источник
20
Не используйте его, он слишком медленный!
Paweł Malisak
5
Хотя это решение определенно работает, приведенное ниже решение @rapimo выполняется намного быстрее. Я считаю, что это связано с внутренним оператором выбора, который здесь выполняется N раз (для всех N строк в таблице дубликатов), а не с группировкой, которая происходит в другом решении.
Дэвид
Для огромных таблиц (несколько миллионов записей) эта действительно умещается в памяти, в отличие от решения @ rapimo. Так что в этих случаях это более быстрый вариант (без замены).
Giel
1
Добавление пояснения: это работает, потому что ctid - это специальный столбец postgres, указывающий физическое расположение строки. Вы можете использовать это как уникальный идентификатор, даже если ваша таблица не имеет уникального идентификатора. postgresql.org/docs/8.2/ddl-system-columns.html
Эрик Бурел
194

Более быстрое решение

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
рапимо
источник
20
Почему это быстрее, чем решение a_horse_with_no_name?
Роберто
3
Это быстрее, потому что выполняется всего 2 запроса. Сначала один для выбора всех дубликатов, затем один для удаления всех элементов из таблицы. Запрос @a_horse_with_no_name выполняет запрос, чтобы увидеть, соответствует ли он любому другому для каждого отдельного элемента в таблице.
Aeolun
5
что есть ctid?
techkuz
6
из документов: ctid. Физическое расположение версии строки в таблице. Обратите внимание, что хотя ctid можно использовать для очень быстрого поиска версии строки, ctid строки будет изменяться каждый раз, когда она обновляется или перемещается с помощью VACUUM FULL. Следовательно, ctid бесполезен как долгосрочный идентификатор строки.
Саим
1
Похоже, это не работает, если у вас более двух повторяющихся строк, потому что он удаляет только один дубликат за раз.
Фрэнки Дрейк
74

Это быстро и кратко:

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

См. Также мой ответ в разделе Как удалить повторяющиеся строки без уникального идентификатора, в котором содержится дополнительная информация.

Isapir
источник
что означает ct? считать?
techkuz
4
@trthhrtz ctidуказывает на физическое расположение записи в таблице. Вопреки тому, что я написал в то время в комментарии, использование оператора «меньше» не обязательно указывает на старую версию, поскольку ct может оборачиваться, а значение с более низким ctid может фактически быть более новым.
isapir
1
К вашему сведению, я попробовал это решение и прервал его, подождав 15 минут. Пробовал решение rapimo, и оно завершилось примерно за 10 секунд (удалено ~ 700 000 строк).
Патрик
@Patrick не может себе представить, если у вашего db нет уникального идентификатора, поскольку ответ rapimo в этом случае не работает.
штукатурка
@isapir Мне просто любопытно, ответы выше, они хранят старые записи так, как они выбрали min(ctid)? а у вас новые? Спасибо!
штукатурка
17

Я пробовал это:

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

предоставлено вики Postgres:

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

Раду Габриэль
источник
Любое представление о производительности по сравнению с ответом @ rapimo и принятым (@a_horse_with_no_name)?
tuxayo
3
Этот не будет работать, если, как указано в вопросе, все столбцы идентичны, idвключая включенные.
ibizaman
Этот запрос удалит как исходную копию, так и дубликаты. вопрос заключается в том, чтобы сохранить хотя бы одну строку.
pyBomb
@pyBomb неверно, он сохранит первый, idгде дублируются столбцы 1 ... 3
Джефф
Начиная с postgresql 12, это НАМНОГО самое быстрое решение (против 300 миллионов строк). Я только что протестировал все, что было предложено в этом вопросе, включая принятый ответ, и это «официальное» решение на самом деле является самым быстрым и отвечает всем требованиям OP (и моего)
Джефф
7

Я бы использовал временную таблицу:

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

Затем удалите tabи переименуйте tab_tempв tab.

Пабло Санта Крус
источник
9
Этот подход не учитывает триггеры, индексы и статистику. Конечно, вы можете добавить их, но это также добавляет намного больше работы.
Jordan
1
Не всем это нужно. Этот подход очень быстр и работает намного лучше, чем остальные, для 200k писем (varchar 250) без индексов.
Сергей Тельшевский
1
Полный код: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;
Эрик Бурел
7

Пришлось создать свою версию. Версия, написанная @a_horse_with_no_name, слишком медленная для моей таблицы (21 миллион строк). А @rapimo просто не удаляет дубли.

Вот что я использую в 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);
эксперт
источник
1

Другой подход (работает, только если у вас есть какое-либо уникальное поле, как idв вашей таблице), чтобы найти все уникальные идентификаторы по столбцам и удалить другие идентификаторы, которых нет в уникальном списке.

DELETE
FROM users
WHERE users.id NOT IN (SELECT DISTINCT ON (username, email) id FROM users);
Зайцев Дмитрий
источник
Дело в том, что в моем вопросе у таблиц не было уникальных идентификаторов; «дубликаты» - это несколько строк с одинаковыми значениями во всех столбцах.
André Morujão
Правильно, я добавил примечания
Зайцев Дмитрий
1

Как насчет:

С УЧАСТИЕМ
  u КАК (ВЫБРАТЬ ОТЛИЧИЕ * ИЗ your_table),
  x КАК (УДАЛИТЬ ИЗ your_table)
ВСТАВИТЬ В your_table SELECT * FROM u;

Я был обеспокоен порядком выполнения, произойдет ли DELETE перед SELECT DISTINCT, но для меня это работает нормально. И имеет дополнительный бонус в виде отсутствия каких-либо знаний о структуре таблицы.

Барри Уокер
источник
Единственный недостаток в том, что если у вас есть тип данных, не поддерживающий равенство (например json), это не сработает.
a_horse_with_no_name
0

У меня это сработало. У меня была таблица, условия, содержащие повторяющиеся значения. Выполните запрос, чтобы заполнить временную таблицу всеми повторяющимися строками. Затем я запустил оператор удаления с этими идентификаторами во временной таблице. value - это столбец, содержащий дубликаты.

        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
источник
0

Вот решение, использующее 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
источник