Найкращий спосіб видалити мільйони рядків за ідентифікатором


79

Мені потрібно видалити близько 2 мільйонів рядків зі своєї бази даних PG. У мене є список ідентифікаторів, які мені потрібно видалити. Однак будь-який спосіб, яким я намагаюся це зробити, займає дні.

Я спробував помістити їх у таблицю і робити це партіями по 100. Через 4 дні це все ще працює, видалено лише 297268 рядків. (Мені довелося вибрати 100 ідентифікаторів з таблиці ідентифікаторів, видалити, де В тому списку, видалити з таблиці ідентифікаторів 100, які я вибрав).

Я намагався:

DELETE FROM tbl WHERE id IN (select * from ids)

Це теж вічно. Важко підрахувати, на скільки часу, оскільки я не бачу, як це прогресується до кінця, але запит все ще працював через 2 дні.

Просто шукаю найефективніший спосіб видалення з таблиці, коли я знаю конкретні ідентифікатори, які потрібно видалити, і є мільйони ідентифікаторів.


2
Скільки рядків залишиться? Альтернативою може бути виділення решти рядків до робочої таблиці, а потім перейменування таблиць.
Тіло

Відповіді:


98

Все залежить ...

  • Видаліть усі індекси (крім того на ідентифікаторі, який вам потрібен для видалення)
    Відтворіть їх потім (= набагато швидше, ніж додаткові оновлення індексів)

  • Перевірте, чи є у вас тригери, які можна безпечно тимчасово видалити / вимкнути

  • Чи посилаються зовнішні ключі на вашу таблицю? Чи можна їх видалити? Тимчасово видалено?

  • Залежно від налаштувань автовакууму, може допомогти запуск VACUUM ANALYZEперед операцією.

  • Припускаючи відсутність одночасного доступу до записів до залучених таблиць, або вам, можливо, доведеться блокувати таблиці виключно, або цей маршрут може бути зовсім не для вас.

  • Деякі пункти, перелічені у відповідному розділі посібника Заповнення бази даних, також можуть бути корисними, залежно від налаштування.

  • Якщо видалити великі частини таблиці, а решта поміститься в оперативну пам’ять, найшвидшим і найпростішим способом буде такий:

SET temp_buffers = '1000MB'; -- or whatever you can spare temporarily

CREATE TEMP TABLE tmp AS
SELECT t.*
FROM   tbl t
LEFT   JOIN del_list d USING (id)
WHERE  d.id IS NULL;      -- copy surviving rows into temporary table

TRUNCATE tbl;             -- empty table - truncate is very fast for big tables

INSERT INTO tbl
SELECT * FROM tmp;        -- insert back surviving rows.

Таким чином вам не доведеться відтворювати подання, зовнішні ключі та інші залежні об’єкти. Прочитайте про temp_buffersналаштування в посібнику . Цей метод є швидким, доки таблиця поміщається в пам’ять або принаймні більшу її частину. Майте на увазі, що ви можете втратити дані, якщо ваш сервер вийде з ладу в середині цієї операції. Ви можете обернути все це в транзакцію, щоб зробити його безпечнішим.

Біжи ANALYZEзгодом. Або VACUUM ANALYZEякщо ви не пішли на усічений шлях, або VACUUM FULL ANALYZEякщо хочете довести його до мінімального розміру. Для великих таблиць розгляньте альтернативи CLUSTER/ pg_repack:

Для маленьких столів простіший DELETEзамість TRUNCATEчасто швидший:

DELETE FROM tbl t
USING  del_list d
WHERE  t.id = d.id;

Читайте в Notes розділ для TRUNCATEв керівництві . Зокрема (як також зазначив Педро у своєму коментарі ):

TRUNCATEне можна використовувати в таблиці, яка має посилання на зовнішні ключі з інших таблиць, якщо всі такі таблиці також не усічені в одній команді. [...]

І:

TRUNCATEне буде запускати жоден ON DELETEтригер, який міг би існувати для таблиць.


На жаль, у мене є кілька зовнішніх ключів, однак я можу зробити те, що ви запропонували, вбивши всі ключі / видаливши / відтворивши. Потрібно більше часу, щоб не робити цього, а просто робити це. Дякую!
Ентоні Греко

@AnthonyGreco: Ви можете видалити зовнішні ключі та відтворити їх згодом. Звичайно, вам доведеться подбати і про посилання на видалені рядки. І цілісність посилань не гарантується протягом цього вікна.
Ервін Брандштеттер

1
Звичайно, це було не те, що я хотів зробити, але видалення індексу змусило мої видалення тепер злетіти ... Тепер я просто повинен зробити це у всіх зв’язаних таблицях із видаленими зв’язаними рядками, але біс, б’є весь час, який я витрачав, намагаючись змусити його працювати. без
Ентоні Греко

1
@AnthonyGreco: Класно! Не забудьте відтворити потім ті індекси, які вам все ще потрібні.
Ервін Брандштеттер

1
Це чудове рішення. Додамо лише, що ігнорує каскади видалення, якщо це не очевидно для когось.
Педро Борхес,

4

Ми знаємо, що продуктивність оновлення / видалення PostgreSQL не така потужна, як Oracle. Коли нам потрібно видалити мільйони або десятки мільйонів рядків, це дійсно важко і займає багато часу.

Однак ми все ще можемо зробити це у виробничих базах даних. Ось моя ідея:

По-перше, нам слід створити таблицю журналу з 2 стовпцями - id& flag( idвідноситься до ідентифікатора, який ви хочете видалити; flagможе бути Yабо null, зY що означає, що запис успішно видалено).

Пізніше ми створимо функцію. Завдання видалення ми робимо кожні 10 000 рядків. Ви можете побачити більше деталей у моєму блозі . Хоча це китайською мовою, ви все одно можете отримати потрібну інформацію із SQL-коду там.

Переконайтеся, що idстовпець обох таблиць - це індекси, оскільки він буде працювати швидше.


1
Ну, я в основному робив логіку цього, щоб робити це в пакетному режимі, проте через мої індекси це зайняло далеко-довго. Нарешті я скинув усі свої індекси (чогось, що я не хотів робити), і рядки швидко очистилися, як біс. Тепер будую всі мої індекси. Хоча дякую!
Ентоні Греко,

2

Ви можете спробувати скопіювати всі дані в таблиці, крім ідентифікаторів, які ви хочете видалити, у нову таблицю, потім перейменувати, а потім поміняти місця таблицями (за умови, що у вас є достатньо ресурсів для цього).

Це не порада фахівця.


Це може спрацювати залежно від кількості рядків, які слід зберегти, і від того, наскільки хитрими є інші зовнішні ключі. Можна також скопіювати хороші рядки в temp. Зрізати поточну таблицю. Потім скопіюйте назад із temp.
nclu

2

Дві можливі відповіді:

  1. До вашої таблиці може бути додано багато обмежень або тригерів, коли ви намагаєтесь видалити запис. Це спричинить багато циклів процесора та перевірку з інших таблиць.

  2. Можливо, вам доведеться помістити цю заяву всередину транзакції.


1. У мене є обмеження (зовнішні ключі), які автоматично видаляються, коли рядок у таблиці видаляється
Ентоні Греко

Спробуйте explain (analyze,buffers,timing) ...зрозуміти, яких індексів вам не вистачає.
Мікко Ранталайнен

2

Спочатку переконайтеся, що у вас є індекс у полях ідентифікатора, як у таблиці, з якої потрібно видалити, так і в таблиці, яку ви використовуєте для видалення ідентифікаторів.

100 за раз здається замалим. Спробуйте 1000 або 10000.

Не потрібно нічого видаляти з таблиці ідентифікаторів видалення. Додайте новий стовпець для номера партії та заповніть його 1000 для партії 1, 1000 для партії 2 тощо та переконайтесь, що запит на видалення містить номер партії.


2
З’ясувалося, що жодна матерія, що я пробував, це вбивали мене ключі. Навіть лише 15 забирали хвилину чи близько того, ось чому я зробив лише 100. Як тільки я вбив індекс, він полетів. Хоча дякую!
Ентоні Греко,

2

Я просто зачепив цю проблему сам, і для мене, безумовно, найшвидшим методом було використання WITH Queries у поєднанні з USING

В основному WITH-запит створює тимчасову таблицю з первинними ключами для видалення в таблиці, з якої потрібно видалити.

WITH to_delete AS (
   SELECT item_id FROM other_table WHERE condition_x = true
)
DELETE FROM table 
USING to_delete 
WHERE table.item_id = to_delete.item_id 
  AND NOT to_delete.item_id IS NULL;

Звичайно SELECT внутрішній запит WITH може бути таким же складним, як і будь-який інший вибір із кількома об’єднаннями і т. Д. Він просто повинен повернути один або кілька стовпців, які використовуються для ідентифікації елементів у цільовій таблиці, які потрібно видалити.

ПРИМІТКА : AND NOT to_delete.item_id IS NULLшвидше за все, це не потрібно, але я не наважився спробувати.

Інші речі, на які слід звернути увагу

  1. створення індексів в інших таблицях із посиланням на цю за допомогою зовнішнього ключа . Що в певних ситуаціях може скоротити видалення, яке триватиме години, до простих секунд
  2. відстрочка перевірок обмежень : Незрозуміло, наскільки це вдається до будь-якого покращення, але відповідно до цього він може збільшити продуктивність. Недоліком є ​​те, що якщо у вас є порушення зовнішнього ключа, ви дізнаєтесь це лише в останній момент.
  3. НЕБЕЗПЕЧНО, але великий можливий прискорення: вимкніть перевірки консистентів та тригери під час видалення

Ви навіть можете створити кілька таких таблиць, які посилаються одна на одну, як це мені доводилося робити в одному випадку, коли я хотів видалити всі рядки, які були сиротами, і на які більше не посилається жодна інша таблиця. ( WITH existing_items AS ( ... ), to_delete AS ( SELECT item_id FROM table LEFT JOIN existing_items e ON table.item_id = e.item_id WHERE e.item_id IS NULL ) DELETE FROM ...)
Торге

1

Найпростіший спосіб зробити це - скинути всі обмеження, а потім виконати видалення.


Я справді намагаюся цього уникнути, тому що тоді мені просто доведеться переробити процес на всіх зовнішніх ключах, але мені цілком можливо доведеться. Дякую
Ентоні Греко

0

Якщо на таблицю, з якої ви видаляєте посилання some_other_table(і ви не хочете скидати зовнішні ключі навіть тимчасово), переконайтеся, що у вас є індекс у стовпці посилань у some_other_table!

У мене була аналогічна проблема , і використовується auto_explainз auto_explain.log_nested_statements = true, який показав , що deleteфактично робить seq_scans на some_other_table:

    Query Text: SELECT 1 FROM ONLY "public"."some_other_table" x WHERE $1 OPERATOR(pg_catalog.=) "id" FOR KEY SHARE OF x    
    LockRows  (cost=[...])  
      ->  Seq Scan on some_other_table x  (cost=[...])  
            Filter: ($1 = id)

Очевидно, він намагається заблокувати посилання на рядки в іншій таблиці (яка не повинна існувати, інакше видалення не вдасться). Після того, як я створив індекси в таблицях посилань, видалення було на порядок швидше.

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