Как удалить повторяющиеся записи? - PullRequest
93 голосов
/ 17 ноября 2009

Я должен добавить уникальное ограничение к существующей таблице. Это хорошо, за исключением того, что в таблице уже есть миллионы строк, и многие строки нарушают уникальное ограничение, которое мне нужно добавить.

Какой самый быстрый подход к удалению поврежденных строк? У меня есть оператор SQL, который находит дубликаты и удаляет их, но для его запуска требуется вечность. Есть ли другой способ решить эту проблему? Может быть, резервное копирование таблицы, а затем восстановление после добавления ограничения?

Ответы [ 16 ]

173 голосов
/ 14 декабря 2010

Некоторые из этих подходов кажутся немного сложными, и я обычно делаю это так:

Учитывая таблицу table, хотите, чтобы она была уникальной (field1, field2), сохраняя строку с максимальным полем3:

DELETE FROM table USING table alias 
  WHERE table.field1 = alias.field1 AND table.field2 = alias.field2 AND
    table.max_field < alias.max_field

Например, у меня есть таблица, user_accounts, и я хочу добавить уникальное ограничение на электронную почту, но у меня есть некоторые дубликаты. Скажите также, что я хочу сохранить последний созданный (максимальный идентификатор среди дубликатов).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Примечание - USING - это не стандартный SQL, это расширение PostgreSQL (но очень полезное), но в оригинальном вопросе конкретно упоминается PostgreSQL.
100 голосов
/ 17 ноября 2009

Например, вы могли бы:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;
25 голосов
/ 12 января 2012

Вместо создания новой таблицы вы также можете повторно вставить уникальные строки в ту же таблицу после ее усечения. Сделайте все это в одной транзакции . При желании вы можете автоматически удалить временную таблицу в конце транзакции с помощью ON COMMIT DROP. Смотри ниже.

Этот подход полезен только тогда, когда есть много строк, которые нужно удалить из всей таблицы. Для нескольких дубликатов, используйте простой DELETE.

Вы упомянули миллионы строк. Для выполнения операции fast необходимо выделить достаточно временных буферов для сеанса. Настройка должна быть отрегулирована до того, как будет использоваться любой временный буфер в текущем сеансе. Узнайте размер вашего стола:

SELECT pg_size_pretty(pg_relation_size('tbl'));

Установите temp_buffers соответственно. Обильно округляйте, потому что представление в памяти требует немного больше оперативной памяти.

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;

Этот метод может быть лучше создания новой таблицы , если существуют зависимые объекты. Представления, индексы, внешние ключи или другие объекты, ссылающиеся на таблицу. TRUNCATE в любом случае позволяет начинать с чистого листа (новый файл в фоновом режиме) и на намного быстрее, чем DELETE FROM tbl с большими таблицами (DELETE на самом деле быстрее с столики).

Для больших таблиц это регулярно быстрее для удаления индексов и внешних ключей, пополнения таблицы и воссоздания этих объектов. Что касается ограничений fk, вы должны быть уверены, что новые данные действительны, конечно, или вы столкнетесь с исключением при попытке создать fk.

Обратите внимание, что TRUNCATE требует более агрессивной блокировки, чем DELETE. Это может быть проблемой для таблиц с большой одновременной нагрузкой.

Если TRUNCATE не является опцией или, как правило, для малых и средних таблиц , существует аналогичная методика с CTE * 1045, модифицирующим данные (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.

Медленнее для больших столов, потому что TRUNCATE там быстрее. Но может быть быстрее (и проще!) Для небольших столов.

Если у вас вообще нет зависимых объектов, вы можете создать новую таблицу и удалить старую, но вряд ли вы что-то получите по сравнению с этим универсальным подходом.

Для очень больших таблиц, которые не помещаются в доступную оперативную память , создание новой таблицы будет значительно быстрее. Вам придется взвесить это против возможных неприятностей / накладных расходов в зависимости от объектов.

20 голосов
/ 12 мая 2011

Вы можете использовать oid или ctid, которые обычно являются «невидимыми» столбцами в таблице:

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);
19 голосов
/ 04 апреля 2013

Для этой проблемы удобна оконная функция PostgreSQL.

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

См. Удаление дубликатов .

8 голосов
/ 12 апреля 2016

Обобщенный запрос на удаление дубликатов:

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

Столбец ctid - это специальный столбец, доступный для каждой таблицы, но не видимый, если не указано иное. Значение столбца ctid считается уникальным для каждой строки таблицы.

7 голосов
/ 13 февраля 2012

С старого списка рассылки postgresql.org :

create table test ( a text, b text );

Уникальные значения

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

Дублирующиеся значения

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

Еще один двойной дубликат

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

select oid, a, b from test;

Выберите повторяющиеся строки

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

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

Примечание: PostgreSQL не поддерживает псевдонимы для таблица, упомянутая в предложении from удаления.

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
             );
4 голосов
/ 03 ноября 2013

Я только что использовал ответ Эрвина Брандштеттера , чтобы успешно удалить дубликаты в объединяющей таблице (в таблице отсутствуют собственные первичные идентификаторы), но обнаружил, что есть одно важное предупреждение.

Включая ON COMMIT DROP означает, что временная таблица будет удалена в конце транзакции. Для меня это означало, что временная таблица была больше не доступна к тому времени, как я ее вставил!

Я только что сделал CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl; и все работало нормально.

Временная таблица удаляется в конце сеанса.

3 голосов
/ 15 сентября 2014

Если у вас есть только одна или несколько дублированных записей, и они действительно дублированы (то есть они появляются дважды), вы можете использовать «скрытый» столбец ctid, как предложено выше, вместе с LIMIT:

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

Это удалит только первую из выбранных строк.

3 голосов
/ 01 декабря 2009

Эта функция удаляет дубликаты без удаления индексов и делает это для любой таблицы.

Использование: select remove_duplicates('mytable');

---
--- remove_duplicates(tablename) removes duplicate records from a table (convert from set to unique set)
---
CREATE OR REPLACE FUNCTION remove_duplicates(text) RETURNS void AS $$
DECLARE
  tablename ALIAS FOR $1;
BEGIN
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || ' AS (SELECT DISTINCT * FROM ' || tablename || ');';
  EXECUTE 'DELETE FROM ' || tablename || ';';
  EXECUTE 'INSERT INTO ' || tablename || ' (SELECT * FROM _DISTINCT_' || tablename || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
  RETURN;
END;
$$ LANGUAGE plpgsql;
...