Удаление большого количества данных в Oracle - PullRequest
14 голосов
/ 14 марта 2009

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

Мне нужно удалить 5,5 миллиона строк из таблицы Oracle, которая содержит около 100 миллионов строк. У меня есть все идентификаторы строк, которые мне нужно удалить во временной таблице. Если бы это было всего несколько тысяч строк, я бы сделал это:

DELETE FROM table_name WHERE id IN (SELECT id FROM temp_table);
COMMIT;

Есть ли что-то, что мне нужно знать и / или делать по-другому, потому что это 5,5 миллионов строк? Я думал о создании цикла, что-то вроде этого:

DECLARE
  vCT NUMBER(38) := 0;

BEGIN
  FOR t IN (SELECT id FROM temp_table) LOOP
    DELETE FROM table_name WHERE id = t.id;
    vCT := vCT + 1;
    IF MOD(vCT,200000) = 0 THEN
      COMMIT;
    END IF;
  END LOOP;
  COMMIT;
END;

Прежде всего - это делает то, о чем я думаю, - пакетирование коммитов по 200 000 одновременно? Предполагая, что это так, я до сих пор не уверен, что лучше сгенерировать 5,5 миллиона операторов SQL с фиксацией в партиях по 200 000 или иметь один оператор SQL и фиксировать все сразу.

Идеи? Лучшие практики?

РЕДАКТИРОВАТЬ : я запустил первый вариант, единственный оператор удаления, и на разработку ушло всего 2 часа. Исходя из этого, он стоит в очереди для запуска в производство.

Ответы [ 9 ]

14 голосов
/ 14 марта 2009

Первый подход лучше, потому что вы предоставляете оптимизатору запросов четкое представление о том, что вы пытаетесь сделать, вместо того, чтобы пытаться скрыть это. Механизм базы данных может использовать иной подход к удалению 5,5 м (или 5,5% таблицы) внутри системы, чем удаление 200 Кб (или 0,2%).

Вот также статья о массовом удалении в Oracle, которую вы, возможно, захотите прочитать.

8 голосов
/ 14 марта 2009

Самый быстрый способ - создать новый с CREATE TABLE AS SELECT, используя опцию NOLOGGING. Я имею в виду:

ALTER TABLE table_to_delete RENAME TO tmp;
CREATE TABLE table_to_delete NOLOGGING AS SELECT .... ;

Конечно, вы должны воссоздать ограничения без валидации, индексы с nologging, грантами, ... но это очень и очень быстро.

Если у вас возникли проблемы с производством, вы можете сделать следующее:

ALTER TABLE table_to_delete RENAME to tmp;
CREATE VIEW table_to_delete AS SELECT * FROM tmp;
-- Until there can be instantly
CREATE TABLE new_table NOLOGGING AS SELECT .... FROM tmp WHERE ...;
<create indexes with nologging>
<create constraints with novalidate>
<create other things...>
-- From here ...
DROP VIEW table_to_delete;
ALTER TABLE new_table RENAME TO table_to_delete;
-- To here, also instantly

Вы позаботились о:

  • Хранимые процедуры могут быть признаны недействительными, но они будут перекомпилированы при повторном вызове. Вы должны проверить это.
  • NOLOGGING означает, что генерируется минимальное повторное выполнение. Если у вас есть роль DBA, запустите ALTER SYSTEM CHECKPOINT, чтобы гарантировать, что данные не будут потеряны в случае сбоя экземпляра.
  • Для NOLOGGING табличное пространство должно быть также в NOLOGGING.

Другой вариант лучше, чем создавать миллионы вставок:

-- Create table with ids
DELETE FROM table_to_delete
 WHERE ID in (SELECT ID FROM table_with_ids WHERE ROWNUM < 100000);
DELETE FROM table_with_ids WHERE ROWNUM < 100000;
COMMIT;
-- Run this 50 times ;-)

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

ОБНОВЛЕНИЕ: Почему я могу гарантировать, что последний блок PLSQL будет работать? Потому что я полагаю, что:

  • Никто другой не использует эту временную таблицу по какой-либо причине (dba или задания, собирающие статистику, задачи dab, такие как перемещение, вставка записей и т. Д.). Это может быть обеспечено, потому что это вспомогательная таблица только для этого.
  • Затем, с последним утверждением, запрос будет выполнен точно с тем же планом и будет возвращать строки в том же порядке.
7 голосов
/ 16 марта 2009

При выполнении массовых удалений в Oracle, убедитесь, что у вас не заканчивается UNDO SEGMENTS.

При выполнении DML, Oracle сначала записывает все изменения в журнал REDO (старые данные вместе с новыми данными).

Когда журнал REDO заполнен или истекло время ожидания, Oracle выполняет log synchronization: он записывает данные new в файлы данных (в вашем случае помечает блоки данных как свободные) и записывает старые данные в табличное пространство UNDO (чтобы оно оставалось видимым для одновременных транзакций до тех пор, пока вы commit не внесете изменения).

Когда вы фиксируете свои изменения, пространство в UNDO сегментах, занятых вашей транзакцией, освобождается.

Это означает, что если вы удалите 5M строк данных, вам потребуется место для all этих строк в ваших UNDO сегментах, чтобы данные могли быть перемещены туда первыми (all at once) и удаляется только после коммита.

Это также означает, что параллельные запросы (если таковые имеются) должны будут считывать из REDO журналов или UNDO сегментов при выполнении сканирования таблиц. Это не самый быстрый способ доступа к данным.

Это также означает, что если оптимизатор выберет HASH JOIN для вашего запроса на удаление (что он, скорее всего, сделает), а временная таблица не будет вписываться в HASH_AREA_SIZE (что, скорее всего, будет иметь место), тогда для запроса потребуется several сканирование большой таблицы, а некоторые части таблицы уже будут перемещены в REDO или UNDO.

.

Учитывая все сказанное выше, вам, вероятно, лучше удалить данные в 200,000 кусках и зафиксировать изменения между ними.

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

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

Чтобы сделать это, убедитесь, что ваша временная таблица имеет первичный ключ на ID, и перепишите ваш запрос следующим образом:

DELETE  
FROM   (
       SELECT  /*+ USE_NL(tt, tn) */
               tn.id
       FROM    temp_table tt, table_name tn
       WHERE   tn.id = tt.id
       )

Вам потребуется первичный ключ на temp_table, чтобы этот запрос работал.

Сравните это со следующим:

DELETE  
FROM   (
       SELECT  /*+ USE_HASH(tn tt) */
               tn.id
       FROM    temp_table tt, table_name tn
       WHERE   tn.id = tt.id
       )

, посмотрите, что быстрее, и придерживайтесь этого.

6 голосов
/ 14 марта 2009

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

4 голосов
/ 15 марта 2009

Я бы порекомендовал запустить это как одно удаление.

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

Возможно, вам понадобится несколько способов проверить ход удаления во время его выполнения. См. Как проверить базу данных Oracle на длительные запросы?

Как и другие люди предложили, если вы хотите проверить воду, вы можете поставить: rownum <10000 в конце вашего запроса. </p>

4 голосов
/ 14 марта 2009

Если ваш исходный SQL занимает очень много времени, некоторые параллельные SQL-запросы могут работать медленно, так как им приходится использовать UNDO для восстановления версии данных без внесенных вами изменений.

Компромисс может быть что-то вроде

FOR i in 1..100 LOOP
  DELETE FROM table_name WHERE id IN (SELECT id FROM temp_table) AND ROWNUM < 100000;
  EXIT WHEN SQL%ROWCOUNT = 0;
  COMMIT;
END LOOP;

Вы можете настроить ROWNUM по мере необходимости. Меньшее ROWNUM означает более частые коммиты и (возможно) уменьшенное влияние на другие сеансы с точки зрения необходимости применять отмену. Однако, в зависимости от планов выполнения, могут быть и другие последствия, и, вероятно, это займет больше времени. Технически, часть «FOR» цикла не нужна, поскольку EXIT завершит цикл. Но я безрассудно отношусь к бесконечным циклам, так как убить сеанс, если они застрянут, будет больно.

0 голосов
/ 12 июля 2014

Самый простой способ для меня это: -

DECLARE
L_exit_flag VARCHAR2(2):='N';
L_row_count NUMBER:= 0;

BEGIN
   :exit_code        :=0;
   LOOP
      DELETE table_name
       WHERE condition(s) AND ROWNUM <= 200000;
       L_row_count := L_row_count + SQL%ROWCOUNT;
       IF SQL%ROWCOUNT = 0 THEN
          COMMIT;
          :exit_code :=0;
          L_exit_flag := 'Y';
       END IF;
      COMMIT;
      IF L_exit_flag = 'Y'
      THEN
         DBMS_OUTPUT.PUT_LINE ('Finally Number of Records Deleted : '||L_row_count);
         EXIT;
      END IF;
   END LOOP;
   --DBMS_OUTPUT.PUT_LINE ('Finally Number of Records Deleted : '||L_row_count);
EXCEPTION
   WHEN OTHERS THEN
      ROLLBACK;
      DBMS_OUTPUT.PUT_LINE ('Error Code: '||SQLCODE);
      DBMS_OUTPUT.PUT_LINE ('Error Message: '||SUBSTR (SQLERRM, 1, 240));
      :exit_code := 255;
END;
0 голосов
/ 18 марта 2009

Все ответы здесь великолепны, только одно, что нужно добавить: если вы хотите удалить все записей в таблице и уверены , вам не нужно для отката, затем вы хотите использовать команду truncate table .

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

0 голосов
/ 16 марта 2009

В прошлом я делал нечто подобное с Oracle 7, где мне приходилось удалять миллионы строк из тысяч таблиц. При всех показателях производительности и особенно при большом удалении (миллион строк плюс в одной таблице) этот сценарий работал хорошо.

Вам придется немного его изменить (т. Е. Изучить пользователей / пароли и правильно настроить сегменты отката). Также вам действительно нужно обсудить это с вашим администратором базы данных и сначала запустить его в среде TEST. Сказав все это, это довольно легко. Функция delete_sql () ищет пакет строк в указанной вами таблице, а затем удаляет их пакет за пакетом. Например;

exec delete_sql('MSF710', 'select rowid from msf710 s where  (s.equip_no, s.eq_tran_date, s.comp_data, s.rec_710_type, s.seq_710_no) not in  (select c.equip_no, c.eq_tran_date, c.comp_data, c.rec_710_type, c.seq_710_no  from  msf710_sched_comm c)', 500);

В приведенном выше примере одновременно удаляется 500 записей из таблицы MSF170 на основе оператора SQL.

Если вам нужно удалить данные из нескольких таблиц, просто включите дополнительные exec delete_sql(...) строки в файл delete-tables.sql

Да, и не забудьте поставить свои сегменты отката обратно в сеть, это не в сценарии.

spool delete-tables.log;
connect system/SYSTEM_PASSWORD
alter rollback segment r01 offline;
alter rollback segment r02 offline;
alter rollback segment r03 offline;
alter rollback segment r04 offline;

connect mims_3015/USER_PASSWORD

CREATE OR REPLACE PROCEDURE delete_sql (myTable in VARCHAR2, mySql in VARCHAR2, commit_size in number) is
  i           INTEGER;
  sel_id      INTEGER;
  del_id      INTEGER;
  exec_sel    INTEGER;
  exec_del    INTEGER;
  del_rowid   ROWID;

  start_date  DATE;
  end_date    DATE;
  s_date      VARCHAR2(1000);
  e_date      VARCHAR2(1000);
  tt          FLOAT;
  lrc         integer;


BEGIN
  --dbms_output.put_line('SQL is ' || mySql);
  i := 0;
  start_date:= SYSDATE;
  s_date:=TO_CHAR(start_date,'DD/MM/YY HH24:MI:SS');


  --dbms_output.put_line('Deleting ' || myTable);
  sel_id := DBMS_SQL.OPEN_CURSOR;
  DBMS_SQL.PARSE(sel_id,mySql,dbms_sql.v7);
  DBMS_SQL.DEFINE_COLUMN_ROWID(sel_id,1,del_rowid);
  exec_sel := DBMS_SQL.EXECUTE(sel_id);
  del_id := DBMS_SQL.OPEN_CURSOR;
  DBMS_SQL.PARSE(del_id,'delete from ' || myTable || ' where rowid = :del_rowid',dbms_sql.v7);
 LOOP
   IF DBMS_SQL.FETCH_ROWS(sel_id) >0 THEN
      DBMS_SQL.COLUMN_VALUE(sel_id,1,del_rowid);
      lrc := dbms_sql.last_row_count;
      DBMS_SQL.BIND_VARIABLE(del_id,'del_rowid',del_rowid);
      exec_del := DBMS_SQL.EXECUTE(del_id);

      -- you need to get the last_row_count earlier as it changes.
      if mod(lrc,commit_size) = 0 then
        i := i + 1;
        --dbms_output.put_line(myTable || ' Commiting Delete no ' || i || ', Rowcount : ' || lrc);
        COMMIT;
      end if;
   ELSE 
       exit;
   END IF;
 END LOOP;
  i := i + 1;
  --dbms_output.put_line(myTable || ' Final Commiting Delete no ' || i || ', Rowcount : ' || dbms_sql.last_row_count);
  COMMIT;
  DBMS_SQL.CLOSE_CURSOR(sel_id);
  DBMS_SQL.CLOSE_CURSOR(del_id);

  end_date := SYSDATE;
  e_date := TO_CHAR(end_date,'DD/MM/YY HH24:MI:SS');
  tt:= trunc((end_date - start_date) * 24 * 60 * 60,2);
  dbms_output.put_line('Deleted ' || myTable || ' Time taken is ' || tt || 's from ' || s_date || ' to ' || e_date || ' in ' || i || ' deletes and Rows = ' || dbms_sql.last_row_count);

END;
/

CREATE OR REPLACE PROCEDURE delete_test (myTable in VARCHAR2, mySql in VARCHAR2, commit_size in number) is
  i integer;
  start_date DATE;
  end_date DATE;
  s_date VARCHAR2(1000);
  e_date VARCHAR2(1000);
  tt FLOAT;
BEGIN
  start_date:= SYSDATE;
  s_date:=TO_CHAR(start_date,'DD/MM/YY HH24:MI:SS');
  i := 0;
  i := i + 1;
  dbms_output.put_line(i || ' SQL is ' || mySql);
  end_date := SYSDATE;
  e_date := TO_CHAR(end_date,'DD/MM/YY HH24:MI:SS');
  tt:= round((end_date - start_date) * 24 * 60 * 60,2);
  dbms_output.put_line(i || ' Time taken is ' || tt || 's from ' || s_date || ' to ' || e_date);
END;
/

show errors procedure delete_sql
show errors procedure delete_test

SET SERVEROUTPUT ON FORMAT WRAP SIZE 200000; 

exec delete_sql('MSF710', 'select rowid from msf710 s where  (s.equip_no, s.eq_tran_date, s.comp_data, s.rec_710_type, s.seq_710_no) not in  (select c.equip_no, c.eq_tran_date, c.comp_data, c.rec_710_type, c.seq_710_no  from  msf710_sched_comm c)', 500);






spool off;

Да, и последний совет. Это будет медленно, и в зависимости от таблицы может потребоваться некоторое время простоя. Тестирование, выбор времени и настройка - ваш лучший друг.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...