Как удалить повторяющиеся записи?


92

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

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

Ответы:


101

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

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;

2
Можете ли вы сделать его отдельным для группы столбцов. Может быть, «ВЫБРАТЬ ОТЛИЧИТЕЛЬНЫЙ (ta, tb, tc), * FROM t»?
gjrwebber


36
проще набрать: CREATE TABLE tmp AS SELECT ...;. Тогда не нужно даже разбираться, что такое макет tmp. :)
Randal Schwartz

9
Этот ответ на самом деле не очень хорош по нескольким причинам. @Randal назвал одного. В большинстве случаев, особенно если у вас есть зависимые объекты, такие как индексы, ограничения, представления и т. Д., Лучший подход - использовать фактическую ВРЕМЕННУЮ ТАБЛИЦУ , ОБРЕЗАТЬ оригинал и повторно вставить данные.
Эрвин Брандштеттер

7
Вы правы насчет индексов. Удаление и воссоздание происходит намного быстрее. Но другие зависящие объекты сломают таблицу или предотвратят ее полное удаление - что ОП обнаружит после создания копии - это достаточно для «самого быстрого подхода». Тем не менее, вы правы относительно отрицательного голоса. Это необоснованно, потому что это неплохой ответ. Это не так уж и хорошо. Вы могли бы добавить несколько указателей на индексы или зависимые объекты или ссылку на руководство, как вы это сделали в комментарии или любых объяснениях. Думаю, меня расстроило то, как люди голосуют. Убран голос против.
Эрвин Брандштеттер

173

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

Для tableданной таблицы нужно сделать ее уникальной (field1, field2), сохраняя строку с максимальным field3:

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.

4
Этот второй подход очень быстр в postgres! Спасибо.
Эрик Боуман - аннотация -

5
@ Тим, можешь лучше объяснить, что делает USINGв postgresql?
Fopa Léon Constantin

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

2
Я только что проверил. Ответ - да, будет. Использование меньше (<) оставляет вам только максимальный идентификатор, а больше (>) оставляет вам только минимальный идентификатор, удаляя остальные.
Андре К. Андерсен

1
@Shane можно использовать: WHERE table1.ctid<table2.ctid- столбец с порядковым
номером

25

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

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

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

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, изменяющим данные (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 там быстрее. Но может быть быстрее (и проще!) Для небольших столов.

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

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


2
Я тоже использовал этот подход. Тем не менее, это может быть личное, но моя временная таблица была удалена и недоступна после усечения ... Будьте осторожны при выполнении этих шагов, если временная таблица была успешно создана и доступна.
xlash 01

@xlash: Вы можете проверить наличие, чтобы убедиться, и либо использовать другое имя для временной таблицы, либо повторно использовать существующее .. Я добавил немного к своему ответу.
Эрвин Брандштеттер,

ПРЕДУПРЕЖДЕНИЕ: будьте осторожны +1 к @xlash - мне нужно повторно импортировать мои данные, потому что временная таблица после этого не существовала TRUNCATE. Как сказал Эрвин, обязательно убедитесь, что он существует, прежде чем обрезать вашу таблицу. См. Ответ @ codebykat
Джордан Арсено,

1
@JordanArseno: Я перешел на версию без ON COMMIT DROP, чтобы люди, пропустившие ту часть, где я написал «за одну транзакцию», не потеряли данные. И я добавил BEGIN / COMMIT, чтобы уточнить «одну транзакцию».
Эрвин Брандштеттер,

1
решение с USING заняло более 3 часов на таблице с 14 миллионами записей. Это решение с temp_buffers заняло 13 минут. Спасибо.
castt

20

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

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);

4
Для удаления на месте , NOT EXISTSдолжно быть значительно быстрее : DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)- или использовать любой другой столбец или набор столбцов для сортировки , чтобы выбрать выживший.
Эрвин Брандштеттер,

@ErwinBrandstetter, должен ли использоваться указанный вами запрос NOT EXISTS?
Джон

1
@ Джон: Это должно быть EXISTSздесь. Прочтите это так: «Удалите все строки, в которых существует другая строка с тем же значением, dist_colно с большим ctid». Единственным выжившим из группы обманутых будет тот, у кого больше всех ctid.
Эрвин Брандштеттер

Самое простое решение, если у вас всего несколько повторяющихся строк. Можно использовать с, LIMITесли известно количество дубликатов.
Скиппи ле Гран Гуру

19

Оконная функция 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);

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


А использование «ctid» вместо «id» на самом деле работает для полностью повторяющихся строк.
bradw2k

Отличное решение. Мне пришлось сделать это для таблицы с миллиардом записей. Я добавил WHERE во внутренний SELECT, чтобы делать это по частям.
Jan

8

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

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

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


1
единственный универсальный ответ! Работает без самостоятельного / декартового JOIN. Однако стоит добавить, что важно правильно указать GROUP BYпредложение - это должен быть «критерий уникальности», который сейчас нарушен, или если вы хотите, чтобы ключ обнаруживал дубликаты. Если указано неверное значение, он не будет работать правильно
msciwoj

7

Из старого списка рассылки 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
             );

Ваше объяснение очень умное, но вам не хватает одного момента. В таблице создания укажите oid, а затем получите доступ только к отображению сообщения об ошибке
oid else

@Kalanidhi Спасибо за ваши комментарии относительно улучшения ответа, я учту этот момент.
Бхавик Амбани

Это действительно пришло с postgresql.org/message-id/…
Мартин Ф

Вы можете использовать системный столбец ctid, если oid выдает ошибку.
sul4bh

4

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

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

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

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


3

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

Применение: select remove_duplicates('mytable');

---
--- remove_duplicates (tablename) удаляет повторяющиеся записи из таблицы (преобразование из набора в уникальный набор)
---
СОЗДАТЬ ИЛИ ЗАМЕНИТЬ ФУНКЦИЮ remove_duplicates (текст) RETURNS void AS $$
ЗАЯВИТЬ
  tablename ALIAS FOR $ 1;
НАЧАТЬ
  ВЫПОЛНИТЬ «СОЗДАТЬ ВРЕМЕННУЮ ТАБЛИЦУ _DISTINCT_» || tablename || 'AS (SELECT DISTINCT * FROM' || имя таблицы || ');';
  ВЫПОЛНИТЬ «УДАЛИТЬ ИЗ» || tablename || ';';
  ВЫПОЛНИТЬ 'INSERT INTO' || tablename || '(SELECT * FROM _DISTINCT_' || имя таблицы || ');';
  ВЫПОЛНИТЬ 'DROP TABLE _DISTINCT_' || tablename || ';';
  ВОЗВРАЩЕНИЕ;
КОНЕЦ;
$$ LANGUAGE plpgsql;

3
DELETE FROM table
  WHERE something NOT IN
    (SELECT     MAX(s.something)
      FROM      table As s
      GROUP BY  s.this_thing, s.that_thing);

Это то, чем я сейчас занимаюсь, но это занимает очень много времени.
gjrwebber

1
Разве это не сработает, если несколько строк в таблице будут иметь одинаковое значение в столбце something?
shreedhar

3

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

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

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


Я знаю, что это не решает проблему OP, у которого многие дублируются в миллионах строк, но в любом случае это может быть полезно.
Скиппи ле Гран Гуру

Это нужно будет запускать один раз для каждой повторяющейся строки. Ответ shekwi нужно запускать только один раз.
bradw2k

3

Во-первых, вам нужно решить, какие из ваших «дубликатов» вы сохраните. Если все столбцы равны, хорошо, вы можете удалить любой из них ... Но, может быть, вы хотите оставить только самые последние или какой-то другой критерий?

Самый быстрый способ зависит от вашего ответа на вопрос выше, а также от процента дубликатов в таблице. Если вы выбросите 50% строк, вам будет лучше CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;, а если вы удалите 1% строк, лучше использовать DELETE.

Также для таких операций обслуживания, как правило, хорошо установить work_memхороший кусок вашей RAM: запустите EXPLAIN, проверьте количество N сортов / хэшей и установите work_mem равным RAM / 2 / N. Используйте много RAM; это хорошо для скорости. Пока у вас есть только одно одновременное соединение ...


1

Я работаю с PostgreSQL 8.4. Когда я запустил предложенный код, я обнаружил, что на самом деле он не удаляет дубликаты. Выполняя некоторые тесты, я обнаружил, что добавление «DISTINCT ON (duplicate_column_name)» и «ORDER BY duplicate_column_name» помогло. Я не гуру SQL, я нашел это в документе PostgreSQL 8.4 SELECT ... DISTINCT.

CREATE OR REPLACE FUNCTION remove_duplicates(text, text) RETURNS void AS $$
DECLARE
  tablename ALIAS FOR $1;
  duplicate_column ALIAS FOR $2;
BEGIN
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || ' AS (SELECT DISTINCT ON (' || duplicate_column || ') * FROM ' || tablename || ' ORDER BY ' || duplicate_column || ' ASC);';
  EXECUTE 'DELETE FROM ' || tablename || ';';
  EXECUTE 'INSERT INTO ' || tablename || ' (SELECT * FROM _DISTINCT_' || tablename || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
  RETURN;
END;
$$ LANGUAGE plpgsql;

1

Это работает очень хорошо и очень быстро:

CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;

1
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

Используя CTE, вы можете получить более читаемую версию вышеуказанного с помощью этого

WITH duplicate_ids as (
    SELECT id, rnum 
    FROM num_of_rows
    WHERE rnum > 1
),
num_of_rows as (
    SELECT id, 
        ROW_NUMBER() over (partition BY column1, 
                                        column2, 
                                        column3 ORDER BY id) AS rnum
        FROM tablename
)
DELETE FROM tablename 
WHERE id IN (SELECT id from duplicate_ids)

1
CREATE TABLE test (col text);
INSERT INTO test VALUES
 ('1'),
 ('2'), ('2'),
 ('3'),
 ('4'), ('4'),
 ('5'),
 ('6'), ('6');
DELETE FROM test
 WHERE ctid in (
   SELECT t.ctid FROM (
     SELECT row_number() over (
               partition BY col
               ORDER BY col
               ) AS rnum,
            ctid FROM test
       ORDER BY col
     ) t
    WHERE t.rnum >1);

Я протестировал это, и он работал; Форматировал для удобочитаемости. Это выглядит довольно изощренно, но требует некоторых пояснений. Как можно изменить этот пример для своего собственного варианта использования?
Tobias
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.