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

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

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

Ответ 1

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

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

Ответ 2

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

Приведенная таблица table, хочет ее уникальность (поле1, поле2), сохраняя строку с максимальным полем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, и я хочу добавить уникальное ограничение по электронной почте, но у меня есть несколько дубликатов. Скажем также, что я хочу сохранить недавно созданный (максимальный id среди дубликатов).

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

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

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

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

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

Ответ 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
             );

Ответ 8

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

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

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

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

Ответ 9

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

Ответ 10

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

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

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

Ответ 11

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

Использование: 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;

Ответ 12

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

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

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

Ответ 13

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

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;

Ответ 14

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

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

Ответ 15

CREATE TABLE test (col text);
INSERT INTO test VALUES ('1');
INSERT INTO test VALUES ('2');
INSERT INTO test VALUES ('2');
INSERT INTO test VALUES ('3');
INSERT INTO test VALUES ('4');
INSERT INTO test VALUES ('4');
INSERT INTO test VALUES ('5');
INSERT INTO test VALUES ('6');
INSERT INTO test VALUES ('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)

Ответ 16

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 wiki

Используя 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)