Лучший способ удалить очень большой набор записей в Oracle

18

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

Каждый месяц мы обязаны удалять записи из двух основных таблиц. Критерии очистки различаются и представляют собой комбинацию возраста строки и пары полей состояния. Обычно мы чистим от 10 до 50 миллионов строк в месяц (добавляем около 3-5 миллионов строк в неделю за счет импорта).

В настоящее время мы должны сделать это в пакетах по 50000 строк (т.е. удалить 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с точки зрения нагрузки, потому что это то, что вы действительно делаете - усечение или удаление одной из этих невидимых вложенных таблиц). Это будет значительный объем обработки для разделения «по факту», но нет смысла плакать над пролитым молоком - преимущества такого перевешивания перевешивают затраты. Каждый месяц вы делите верхний раздел, чтобы создать новый раздел для данных следующего месяца (вы можете легко автоматизировать это с помощью DBMS_JOB).

А с разделами вы также можете использовать параллельный запрос и исключение разделов , что должно сделать ваших пользователей очень счастливыми ...

Gaius
источник
FWIW мы используем использовать эту технику на моем сайте в базе данных 30Tb +
Gaius
Проблема с разделением состоит в том, что нет четкого способа разделения данных. В одной из двух таблиц (не той, что показана ниже) критерии, используемые для очистки, основаны на двух разных (и разных) полях даты и поле состояния. Например, если статус « Aто», если DateAон старше 3 лет, он очищается. Если статус Bи DateBстарше 10 лет, он очищается. Если мое понимание разбиения верно, то разбиение не будет полезным в такой ситуации (по крайней мере, в отношении очистки).
Кодирование гориллы
Вы можете разделить по статусу и подразделу по диапазону дат. Но если статус (или дата) изменяется, он фактически удаляет из одного подраздела и вставляет в другой. Короче говоря, вы можете получить удовольствие от ваших повседневных процессов, чтобы сэкономить время на чистку.
Гари
6
В качестве альтернативы вы можете создать виртуальный столбец, который показывает DateA, когда статус A, и DateB, когда статус B, а затем разделить виртуальный столбец. Произойдет та же миграция раздела, но это поможет вам очистить. Похоже, это уже было опубликовано в качестве ответа.
Ли Риффель
4

Один аспект, который следует учитывать, - это то, сколько производительности удаления зависит от индексов, а сколько - от необработанной таблицы. Каждая запись, удаленная из таблицы, требует одинакового удаления строки из каждого индекса btree. Если у вас есть более 30 индексов btree, я подозреваю, что большую часть времени вы тратите на обслуживание индексов.

Это влияет на полезность разбиения. Скажем, у вас есть индекс на имя. Стандартный индекс Btree, все в одном сегменте, возможно, должен сделать четыре перехода, чтобы добраться от корневого блока к листовому блоку, и пятое чтение, чтобы получить строку. Если этот индекс разделен на 50 сегментов и у вас нет ключа разделения в качестве части запроса, то каждый из этих 50 сегментов необходимо будет проверить. Каждый сегмент будет меньше, поэтому вам, возможно, придется сделать только 2 прыжка, но вы все равно можете сделать 100 операций чтения, а не предыдущие 5.

Если они являются растровыми индексами, уравнения разные. Вы, вероятно, не используете индексы для идентификации отдельных строк, а скорее их наборы. Таким образом, вместо запроса с использованием 5 операций ввода-вывода для возврата одной записи, он использовал 10 000 операций ввода-вывода. Таким образом, дополнительные издержки в дополнительных разделах для индекса не будут иметь значения.

Gary
источник
2

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

Джейсон Якоб
источник
А как насчет генерации 'undo' и 'redo'? Delete? Это также душит IO ... подход, основанный на 'delete', безусловно, должен быть NO .. NO для больших таблиц.
пахариайоги
1

Если на диске недостаточно места, вы можете создать «рабочую» копию таблицы, скажем my_table_new, с помощью CTAS («Создать таблицу как выбор») с критериями, при которых записи будут отбрасываться. Вы можете выполнить оператор 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
Это может быть рассмотрено, если (а) чистка является одноразовой задачей. (б) если вам нужно сохранить меньше строк и удалить большую часть данных ...
pahariayogi
0

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

«Обычно мы чистим от 10 до 50 миллионов строк в месяц»

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

iceburge5
источник
1
Если у вас есть первичный ключ, то удаление раздела не должно приводить к невозможности использования глобальных индексов. Но если у ОП много глобальных индексов, то удаление разделов будет дорого стоить. В идеальном случае, когда кто-то разбивает таблицу, разбиение основывается на первичном ключе, и им не нужны глобальные индексы. То, что каждый запрос может использовать сокращение раздела.
Gandolf989
@ Gandolf989 удаление раздела всегда сделает глобальный индекс непригодным для использования
miracle173