Как удалить дублирующиеся записи?
-
20-09-2019 - |
Вопрос
Я должен добавить уникальное ограничение к существующей таблице.Это нормально, за исключением того, что в таблице уже есть миллионы строк, и многие из строк нарушают ограничение уникальности, которое мне нужно добавить.
Каков самый быстрый подход к удалению строк-нарушителей?У меня есть оператор SQL, который находит дубликаты и удаляет их, но для его запуска требуется вечность.Есть ли другой способ решить эту проблему?Может быть, создать резервную копию таблицы, а затем восстановить после добавления ограничения?
Решение
Например, вы можете:
CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;
Другие советы
Некоторые из этих подходов кажутся немного сложными, и я обычно делаю это следующим образом:
Данная таблица 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
, и я хочу добавить ограничение уникальности для электронной почты, но у меня есть несколько дубликатов.Скажите также, что я хочу сохранить самый последний созданный (максимальный идентификатор среди дубликатов).
DELETE FROM user_accounts USING user_accounts ua2
WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
- Примечание -
USING
это не стандартный SQL, это расширение PostgreSQL (но очень полезное), но в исходном вопросе конкретно упоминается PostgreSQL.
Вместо создания новой таблицы вы также можете повторно вставить уникальные строки в ту же таблицу после ее усечения.Делай все это в одной транзакции.При желании вы можете автоматически удалить временную таблицу в конце транзакции с помощью 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, изменяющий данные (Постгрес 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
там быстрее.Но может быть быстрее (и проще!) для небольших таблиц.
Если у вас вообще нет зависимых объектов, вы можете создать новую таблицу и удалить старую, но вы вряд ли что-то получите от этого универсального подхода.
Для очень больших таблиц, которые не помещаются в доступная оперативная память, создавая новый table будет работать значительно быстрее.Вам придется сопоставить это с возможными проблемами/накладными расходами на зависимых объектах.
Вы можете использовать oid или ctid, которые обычно являются «невидимыми» столбцами в таблице:
DELETE FROM table
WHERE ctid NOT IN
(SELECT MAX(s.ctid)
FROM table s
GROUP BY s.column_has_be_distinct);
Оконная функция 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);
Видеть Удаление дубликатов.
Обобщенный запрос на удаление дубликатов:
DELETE FROM table_name
WHERE ctid NOT IN (
SELECT max(ctid) FROM table_name
GROUP BY column1, [column 2, ...]
);
Колонна ctid
доступен ли специальный столбец для каждой таблицы, но не виден, если специально не указано иное.Тот Самый ctid
значение столбца считается уникальным для каждой строки в таблице.
От старый 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
);
я просто использовал Ответ Эрвина Брандштеттера успешно удалить дубликаты в объединяемой таблице (таблице, не имеющей собственных основных идентификаторов), но обнаружил, что есть одно важное предостережение.
Включая ON COMMIT DROP
означает, что временная таблица будет удалена в конце транзакции.Для меня это означало, что временная таблица была больше недоступно к тому времени, как я пошел вставлять его!
я только что сделал CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;
и все работало нормально.
Временная таблица удаляется в конце сеанса.
Эта функция удаляет дубликаты без удаления индексов и делает это с любой таблицей.
Использование: 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;
DELETE FROM table
WHERE something NOT IN
(SELECT MAX(s.something)
FROM table As s
GROUP BY s.this_thing, s.that_thing);
Если у вас есть только одна или несколько повторяющихся записей, и они действительно дублированный (то есть появляются дважды), можно использовать «скрытый» ctid
столбец, как предложено выше, вместе с LIMIT
:
DELETE FROM mytable WHERE ctid=(SELECT ctid FROM mytable WHERE […] LIMIT 1);
Это приведет к удалению только первой из выбранных строк.
Во-первых, вам нужно решить, какой из ваших «дубликатов» вы сохраните.Если все столбцы равны, ОК, вы можете удалить любой из них...Но, возможно, вы хотите сохранить только самые последние или какой-то другой критерий?
Самый быстрый способ зависит от вашего ответа на вопрос выше, а также от % дубликатов в таблице.Если вы выбросите 50% своих строк, вам лучше сделать CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;
, и если вы удалите 1% строк, лучше использовать DELETE.
Также для подобных операций по техническому обслуживанию обычно полезно установить work_mem
на большую часть вашей оперативной памяти:запустите EXPLAIN, проверьте количество N сортировок/хэшей и установите для work_mem значение вашей RAM / 2 / N.Используйте много оперативной памяти;это хорошо для скорости.Пока у вас есть только одно одновременное соединение...
Я работаю с PostgreSQL 8.4.Когда я предложил код, я обнаружил, что он не был удаляя дубликаты.Запустив несколько тестов, я обнаружил, что добавление "DISTINCT ON (дубликат_колоно_имя)" и "ORDER BY дубликат_колоно_имя" сделало свое дело.Я не гуру SQL, я нашел это в PostgreSQL 8.4 SELECT...ОТДЕЛЬНЫЙ документ.
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;
Это работает очень хорошо и очень быстро:
CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;
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);
Удалите дубликаты по столбцам и сохраните строку с наименьшим идентификатором.Выкройка взята из Постгрес вики
Используя 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)
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);