Ответ 1
Например, вы могли бы:
CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;
Мне нужно добавить уникальное ограничение для существующей таблицы. Это прекрасно, за исключением того, что в таблице уже есть миллионы строк, и многие из строк нарушают уникальное ограничение, которое мне нужно добавить.
Каков самый быстрый подход к удалению оскорбительных строк? У меня есть оператор 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
, и я хочу добавить уникальное ограничение по электронной почте, но у меня есть несколько дубликатов. Скажем также, что я хочу сохранить недавно созданный (максимальный 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.Вместо создания новой таблицы вы также можете повторно вставить уникальные строки в одну и ту же таблицу после ее усечения. Сделайте все за одну транзакцию. При желании вы можете автоматически удалить временную таблицу в конце транзакции с помощью 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
там быстрее. Но может быть быстрее (и проще!) Для небольших таблиц.
Если у вас нет каких-либо зависимых объектов, вы можете создать новую таблицу и удалить старую, но вы вряд ли получите что-либо в этом универсальном подходе.
Для очень больших таблиц, которые не помещаются в доступную оперативную память, создание новой таблицы будет значительно быстрее. Вам придется взвесить это на возможные проблемы/накладные расходы с помощью зависимых объектов.
Вы можете использовать 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
);
Я просто использовал Erwin Brandstetter answer успешно, чтобы удалить дубликаты в таблице соединений (таблица, в которой отсутствуют собственные первичные идентификаторы), но обнаружил, что существует одна важная оговорка.
Включение ON COMMIT DROP
означает, что временная таблица будет удалена в конце транзакции. Для меня это означало, что временная таблица была больше недоступна к тому времени, когда я пошел, чтобы вставить ее!
Я просто сделал CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;
, и все сработало нормально.
Временная таблица будет удалена в конце сеанса.
DELETE FROM table
WHERE something NOT IN
(SELECT MAX(s.something)
FROM table As s
GROUP BY s.this_thing, s.that_thing);
Во-первых, вам нужно решить, какой из ваших "дубликатов" вы сохраните. Если все столбцы равны, ОК, вы можете удалить любой из них... Но, возможно, вы хотите сохранить только последний или какой-либо другой критерий?
Самый быстрый способ зависит от вашего ответа на вопрос выше, а также от% дубликатов на столе. Если вы выбросите 50% своих строк, вам лучше сделать CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;
, и если вы удалите 1% строк, лучше использовать DELETE.
Также для таких операций обслуживания обычно полезно установить work_mem
на хороший кусок вашей оперативной памяти: запустите EXPLAIN, проверьте число N сортировок/хэшей и установите work_mem в RAM/2/N. Используйте много ОЗУ; это хорошо для скорости. Пока у вас есть только одно одновременное соединение...
Эта функция удаляет дубликаты без удаления индексов и делает это в любой таблице.
Использование: 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;
Если у вас есть только одна или несколько дублированных записей, и они действительно дублированы (т.е. они появляются дважды), вы можете использовать столбец "скрытый" ctid
, как было предложено выше, вместе с LIMIT
:
DELETE FROM mytable WHERE ctid=(SELECT ctid FROM mytable WHERE […] LIMIT 1);
Это приведет к удалению только первой из выбранных строк.
Я работаю с 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;
Это работает очень хорошо и очень быстро:
CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;
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)
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)