Як видалити повторювані записи?


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

10
ВИЗНАЧЕННЯ УВІМКНЕНО (a, b, c): postgresql.org/docs/8.2/interactive/sql-select.html
просто хтось

36
простіше набрати: CREATE TABLE tmp AS SELECT ...;. Тоді вам не потрібно навіть з’ясовувати, що таке макет tmp. :)
Рендал Шварц

9
Ця відповідь насправді не дуже хороша з кількох причин. @Randal назвав одного. У більшості випадків, особливо якщо у вас є залежні об’єкти, такі як індекси, обмеження, подання тощо, найкращий підхід полягає у використанні фактичної ЧАСОВОЇ ТАБЛИЦІ , обрізанні оригіналу та повторній вставці даних.
Ервін Брандштеттер

7
Ви маєте рацію щодо індексів. Викидання та відтворення відбувається набагато швидше. Але інші об'єкти, що залежать, зламаються або взагалі не дадуть скинути таблицю - що ОП виявить після того, як зробить копію - настільки, що "найшвидший підхід". Тим не менше, ти маєш рацію щодо голосу проти. Це необгрунтовано, оскільки це не погана відповідь. Це просто не так добре. Ви могли б додати деякі вказівки щодо індексів чи залежних об’єктів або посилання на посібник, як це було зроблено в коментарі чи будь-якому поясненні. Думаю, я розчарувався, як люди голосують. Видалено голос проти.
Ервін Брандштеттер

173

Деякі з цих підходів здаються дещо складними, і я, як правило, роблю це так:

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.

4
Цей другий підхід дуже швидкий для postgres! Дякую.
Ерік Боуман - абстракція -

5
@Tim, ти можеш краще пояснити, що робить USINGpostgresql?
Фопа Леон Константин

3
Це, безумовно, найкраща відповідь. Навіть якщо у вашій таблиці немає послідовного стовпця для використання для порівняння ідентифікаторів, варто тимчасово додати його, щоб скористатися цим простим підходом.
Шейн

2
Я щойно перевірив. Відповідь - так, буде. Використання менше ніж (<) залишає лише максимальний ідентифікатор, тоді як більше ніж (>) залишає лише мінімальний ідентифікатор, видаляючи решту.
Андре К. Андерсен,

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

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

@xlash: Ви можете перевірити наявність, щоб переконатися, або використовувати інше ім'я для тимчасової таблиці, або повторно використати існуючу. Я додав трохи до своєї відповіді.
Ервін Брандштеттер,

ПОПЕРЕДЖЕННЯ: Будьте обережні +1 до @xlash - я повинен повторно імпортувати свої дані, оскільки тимчасова таблиця після цього не існувала TRUNCATE. Як сказав Ервін, обов’язково переконайтеся, що він існує перед тим, як скоротити таблицю. Дивіться відповідь @ codebykat
Jordan Arseno

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якщо ви знаєте кількість дублікатів.
Skippy le Grand Gourou

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

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

7

Узагальнений запит на видалення дублікатів:

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

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


єдина універсальна відповідь! Працює без самостійного / декартового приєднання. Варто додати, що важливо правильно вказати GROUP BYречення - це має бути "критерій унікальності", який порушується зараз, або якщо ви хочете, щоб ключ виявив дублікати. Якщо вказано неправильно, це не працюватиме коректно
msciwoj

4

Я щойно використав відповідь Ервіна Брандштеттера для успішного видалення дублікатів у таблиці об’єднання (таблиці без власних основних ідентифікаторів), але виявив, що є одне важливе застереження.

У тому числі ON COMMIT DROPозначає, що тимчасова таблиця буде скинута в кінці транзакції. Для мене це означало, що тимчасова таблиця вже не була доступною на той момент, коли я пішов її вставляти!

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

Тимчасова таблиця випадає в кінці сеансу.


3

Ця функція видаляє дублікати без видалення індексів і робить це в будь-якій таблиці.

Використання: select remove_duplicates('mytable');

---
--- remove_duplicates (ім'я таблиці) видаляє повторювані записи з таблиці (перетворити з набору в унікальний набір)
---
СТВОРИТИ АБО ЗАМІНИТИ ФУНКЦІЮ remove_duplicates (текст) ПОВЕРНЕННЯ недійсним як $$
ЗАЯВИТИ
  ім'я таблиці ПСЕВІМИ ЗА $ 1;
ПОЧАТИ
  ВИКОНАВТИ 'СТВОРИТИ ЧАСОВУ ТАБЛИЦУ _DISTINCT_' || назва таблиці || 'AS (ВИБЕРІТЬ ВИЗНАЧЕННЯ * З' || ім'я таблиці || ');';
  ВИКОНАВТИ "ВИДАЛИТИ З" || назва таблиці || ';';
  ВИКОНАЙТЕ "ВСТАВИТИ В" || назва таблиці || '(SELECT * FROM _DISTINCT_' || ім'я таблиці || ');';
  ВИКОНАЙТЕ 'ТАБЛИЦЮ ПАДАННЯ _DISTINCT_' || назва таблиці || ';';
  ПОВЕРНЕННЯ;
КІНЕЦЬ;
$$ МОВА 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
Хіба це не вдасться, якщо кілька рядків у таблиці мають однакове значення у стовпці?
shreedhar

3

Якщо у вас є лише один або кілька дубльованих записів, і вони дійсно дублюються (тобто вони з’являються двічі), ви можете використовувати ctidстовпець "прихований" , як запропоновано вище, разом із LIMIT:

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

Буде видалено лише перший із вибраних рядків.


Я знаю, що це не стосується проблеми OP, яка багато дублювалась мільйонами рядків, але все одно може бути корисною.
Skippy le Grand Gourou

Це потрібно було б запустити один раз для кожного повторюваного рядка. відповідь shekwi потрібно запускати лише один раз.
bradw2k

3

По-перше, вам потрібно визначитися з тим, який із ваших «дублікатів» ви збережете. Якщо всі стовпці рівні, добре, ви можете видалити будь-який з них ... Але, можливо, ви хочете зберегти лише найсвіжіший або якийсь інший критерій?

Найшвидший спосіб залежить від вашої відповіді на вищезазначене питання, а також від% дублікатів у таблиці. Якщо ви викинете 50% своїх рядків, вам краще це зробити CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;, а якщо ви видалите 1% рядків, краще використовувати DELETE.

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


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

Я перевірив це, і це спрацювало; Я відформатував його для читабельності. Це виглядає досить вишукано, але для цього можна використати деякі пояснення. Як можна змінити цей приклад для власного випадку використання?
Тобіас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.