Найкращий спосіб видалити дуже великий набір записів в Oracle


18

Я керую програмою, яка має дуже велику кількість (майже 1 ТБ даних з більш ніж 500 мільйонами рядків в одній таблиці). База даних насправді нічого не робить (ні SProcs, ні тригери, ні щось), це лише сховище даних.

Щомісяця від нас вимагається очищення записів із двох основних таблиць. Критерії очищення різняться і являють собою поєднання віку рядка та пари полів статусу. Зазвичай ми чистимо від 10 до 50 мільйонів рядків на місяць (ми додаємо приблизно 3-5 мільйонів рядків на тиждень за допомогою імпорту).

В даний час ми повинні зробити це видалення партіями приблизно в 50 000 рядків (тобто. Видалити 50000, comit, видалити 50000, ввести, повторити). Спроба видалити всю партію за один раз робить базу даних невідповідною протягом приблизно години (залежно від кількості рядків). Видалення рядків такими партіями є дуже грубим у системі, і ми зазвичай мусимо робити це "як дозволяє час" протягом тижня; дозволяючи сценарію постійно працювати, може призвести до зниження продуктивності, неприйнятного для користувача.

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

Ось сценарій, який використовує один з наших ІТ-людей для цього очищення:

BEGIN
LOOP

delete FROM tbl_raw 
  where dist_event_date < to_date('[date]','mm/dd/yyyy') and rownum < 50000;

  exit when SQL%rowcount < 49999;

  commit;

END LOOP;

commit;

END;

Ця база даних повинна перевищувати 99,99999%, і ми маємо лише 2-денне вікно обслуговування один раз на рік.

Я шукаю кращого методу для видалення цих записів, але ще не знайшов жодного. Будь-які пропозиції?


Також зауважте, що тут грають 30+ індексів
jcolebrand

Відповіді:


17

Логіка з "A" і "B" може бути "прихована" за віртуальним стовпцем, на якому ви могли б зробити розділ:

alter session set nls_date_format = 'yyyy-mm-dd';
drop   table tq84_partitioned_table;

create table tq84_partitioned_table (
  status varchar2(1)          not null check (status in ('A', 'B')),
  date_a          date        not null,
  date_b          date        not null,
  date_too_old    date as
                       (  case status
                                 when 'A' then add_months(date_a, -7*12)
                                 when 'B' then            date_b
                                 end
                        ) virtual,
  data            varchar2(100) 
)
partition   by range  (date_too_old) 
( 
  partition p_before_2000_10 values less than (date '2000-10-01'),
  partition p_before_2000_11 values less than (date '2000-11-01'),
  partition p_before_2000_12 values less than (date '2000-12-01'),
  --
  partition p_before_2001_01 values less than (date '2001-01-01'),
  partition p_before_2001_02 values less than (date '2001-02-01'),
  partition p_before_2001_03 values less than (date '2001-03-01'),
  partition p_before_2001_04 values less than (date '2001-04-01'),
  partition p_before_2001_05 values less than (date '2001-05-01'),
  partition p_before_2001_06 values less than (date '2001-06-01'),
  -- and so on and so forth..
  partition p_ values less than (maxvalue)
);

insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('B', date '2008-04-14', date '2000-05-17', 
 'B and 2000-05-17 is older than 10 yrs, must be deleted');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('B', date '1999-09-19', date '2004-02-12', 
 'B and 2004-02-12 is younger than 10 yrs, must be kept');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('A', date '2000-06-16', date '2010-01-01', 
 'A and 2000-06-16 is older than 3 yrs, must be deleted');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('A', date '2009-06-09', date '1999-08-28', 
 'A and 2009-06-09 is younger than 3 yrs, must be kept');

select * from tq84_partitioned_table order by date_too_old;

-- drop partitions older than 10 or 3 years, respectively:

alter table tq84_partitioned_table drop partition p_before_2000_10;
alter table tq84_partitioned_table drop partition p_before_2000_11;
alter table tq84_partitioned_table drop partition p2000_12;

select * from tq84_partitioned_table order by date_too_old;

Можливо, я надто спростив логіку того, як визначаються записи для очищення, але це дуже цікава ідея. Однак одне, що потрібно враховувати, - це щоденне виконання. Очищення - це "наша проблема", клієнт не прийме зниженої продуктивності просто для вирішення цього питання. З деяких коментарів та відповіді Гері звучить, що це може бути проблема з розділенням?
Кодування

Я не впевнений, чи це відповідь, яку ми шукаємо, але це, безумовно, дуже цікавий підхід, який ми вивчимо.
Кодування Горілла

14

Класичне рішення для цього - розділити таблиці, наприклад, за місяцем або за тижнем. Якщо ви раніше не стикалися з ними, розділена таблиця подібна до декількох однаково структурованих таблиць із неявним UNIONпід час вибору, і Oracle автоматично зберігатиме рядок у відповідному розділі при вставлянні її на основі критеріїв розділення. Ви згадуєте індекси - ну і кожен розділ отримує свої власні розділені індекси. Дуже дешева операція в Oracle - це скинути розділ (це аналог аTRUNCATEз точки зору навантаження, оскільки саме цим ви дійсно займаєтесь - обрізаючи або відкидаючи одну з цих невидимих ​​підтаблиць). Це буде значна кількість переробки до розділу "після факту", але немає сенсу плакати над пролитим молоком - переваги робити поки що переважають витрати. Щомісяця ви розділяєте верхній розділ, щоб створити новий розділ для даних наступного місяця (ви можете легко автоматизувати ths за допомогою a DBMS_JOB).

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


FWIW ми використовуємо цю техніку на моєму сайті в базі даних 30Tb +
Gaius

Проблема з розділенням полягає в тому, що немає чіткого способу розбиття даних. В одній з двох таблиць (не в тій, що показана нижче) критерії, які використовуються для очищення, базуються на двох різних (і різних) полях дати та полі статусу. Наприклад, якщо статус є Aтоді, якщо DateAвін старший 3 років, його очищають. Якщо статус є Bі DateBстарше 10 років, його очищають. Якщо моє розуміння розподілу є правильним, то перегородка не буде корисною в такій ситуації (принаймні, що стосується чистки).
Кодування Горилла

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

6
Крім того, ви можете створити віртуальний стовпчик, який показує DateA, коли статус A, і DateB, коли статус B, а потім розділ на віртуальний стовпець. Ця міграція розділів відбудеться, але це допоможе вашій чистці. Схоже, це вже було розміщено як відповідь.
Лі Ріффер

4

Один з аспектів, який слід врахувати, полягає в тому, яка кількість результатів видалення є результатом індексів і скільки з необробленої таблиці. Кожен запис, видалений із таблиці, вимагає однакового видалення рядка з кожного індексу btree. Якщо у вас є 30+ btree індексів, я підозрюю, що більша частина вашого часу витрачається на обслуговування індексів.

Це впливає на корисність розподілу. Скажіть, у вас є індекс імені. Стандартний індекс Btree, весь в одному сегменті, може мати чотири стрибки, щоб дістатись із кореневого блоку до блоку листків та п'ятий зчитування, щоб отримати рядок. Якщо цей індекс розділений на 50 сегментів і у вас немає ключа розділу як частини запиту, то кожен з цих 50 сегментів потрібно буде перевірити. Кожен сегмент буде меншим, тому вам, можливо, доведеться робити лише два стрибки, але ви все одно можете зробити 100 читання, а не попередні 5.

Якщо вони є растровими індексами, рівняння різні. Напевно ви не використовуєте індекси для ідентифікації окремих рядків, а скоріше їх набори. Отже, замість запиту, який використовує 5 вводу-виводу для повернення однієї записи, він використовував 10 000 ввід. Таким чином, додаткові накладні витрати в додаткових розділах для індексу не матимуть значення.


2

видалення 50 мільйонів записів на місяць партіями по 50 000 - це лише 1000 ітерацій. якщо ви робите 1 видалення кожні 30 хвилин, це повинно відповідати вашим вимогам. заплановане завдання запустити опублікований вами запит, але видалити цикл, щоб він виконувався лише один раз, не повинен викликати помітну деградацію у користувачів. Ми робимо приблизно такий самий обсяг записів на нашому виробничому заводі, який працює цілком 24/7 і відповідає нашим потребам. Ми фактично розповсюджуємо його трохи більше 10 000 записів кожні 10 хвилин, які виконуються приблизно за 1 або 2 секунди, працюючи на наших серверах Oracle Unix.


Що з масовим "скасувати" та "повторно" видалити "генеруватиме? Він також задавлює IO ... підхід на основі "видалення", безумовно, повинен бути НІ .. НЕТ для великих таблиць.
pahariayogi

1

Якщо дисковий простір не є надбавним, ви зможете створити "робочу" копію таблиці, скажімо my_table_new, за допомогою CTAS (Create Table As Select) з критеріями, які б опускали записи, які потрібно скидати. Ви можете зробити оператор create паралельно та з додаванням підказки, щоб зробити його швидким, а потім створити всі свої індекси. Потім, після її закінчення (і випробування), перейменуйте існуючу таблицю в my_table_oldта перейменуйте таблицю "робочої" в my_table. Одного разу вам все зручно, drop my_table_old purgeщоб позбутися старого столу. Якщо у вас є купа обмежень із зовнішніми ключами, погляньте на dbms_redefinition пакет PL / SQL . Він буде клонувати ваші індекси, протипоказання тощо при використанні відповідних опцій. Це підсумок пропозиції Тома Кейта з AskTomслава. Після першого запуску ви можете автоматизувати все, і таблиця створення повинна проходити набагато швидше, і це можна зробити під час роботи системи, а час простою програми буде обмежений менш ніж на хвилину, щоб зробити перейменування таблиць. Використання CTAS буде набагато швидше, ніж виконання декількох пакетних видалень. Цей підхід може бути особливо корисним, якщо у вас немає ліцензії на розділення.

Зразок CTAS, зберігаючи рядки з даними за останні 365 днів flag_inactive = 'N':

create /*+ append */ table my_table_new 
   tablespace data as
   select /*+ parallel */ * from my_table 
       where some_date >= sysdate -365 
       and flag_inactive = 'N';

-- test out my_table_new. then if all is well:

alter table my_table rename to my_table_old;
alter table my_table_new rename to my_table;
-- test some more
drop table my_table_old purge;

1
Це можна врахувати, якщо (а) очищення є одноразовим завданням. (b) якщо у вас буде менше рядків для збереження, а більшість даних для видалення ...
pahariayogi

0

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

"Зазвичай ми чистимо від 10 до 50 мільйонів рядків на місяць"

Я б рекомендував використовувати PL / SQL пакетне видалення, кілька годин це нормально, я думаю.


1
Якщо у вас є первинний ключ, то видалення розділу не повинно робити жодних глобальних індексів непридатними. Але якщо в ОП багато глобальних індексів, випадання розділів буде високою вартістю. В ідеальному випадку, коли хтось розбиває таблицю, розділ заснований на первинному ключі, і їм не потрібні глобальні індекси. Щоб кожен запит міг скористатися обрізкою розділів.
Gandolf989

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