web-dev-qa-db-ja.com

Oracleで大量のデータを削除する

私はデータベースの専門家ではありません。私のデータベース作業のほとんどはMySQLを使用しているので、この質問の何かが信じられないほど素朴なものである場合はご容赦ください。

約1億行のOracleテーブルから550万行を削除する必要があります。一時テーブルに削除する必要のある行のすべてのIDがあります。それがほんの数千行だった場合、私はこれを行います:

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

550万行なので、知っておく必要があることや、別の方法で行う必要があることはありますか?私は次のようなループを行うことを考えました:

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のコミットをバッチ処理しますか?そうだとすると、550万のSQLステートメントを生成して200,000のバッチでコミットするのが良いのか、それとも1つのSQLステートメントを持って一度にコミットするのが良いのかはまだわかりません。

アイデア?ベストプラクティス?

[〜#〜] edit [〜#〜]:最初のオプションである単一のdeleteステートメントを実行しましたが、開発が完了するまでに2時間しかかかりませんでした。これに基づいて、本番環境で実行するためにキューに入れられます。

15
Sarah Mei

最初のアプローチの方が優れています。クエリオプティマイザーを非表示にするのではなく、実行しようとしていることを明確に把握できるからです。データベースエンジンは、内部で5.5m(またはテーブルの5.5%)を削除する方法と、200k(または0.2%)を削除する方法とは異なるアプローチを取る場合があります。

こちらも article Oracleでの大規模なDELETEについてお読みください。

15
Jiri Klouda

最速の方法は、NOLOGGINGオプションを使用してCREATE TABLE AS SELECTで新しいものを作成することです。というのは:

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

もちろん、検証なしの制約、ログなしのインデックス、許可などを再作成する必要がありますが、非常に高速です。

本番環境で問題が発生した場合は、次の操作を実行できます。

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

あなたは世話をしました:

  • ストアドプロシージャは無効にすることができますが、2回目に呼び出されたときに再コンパイルされます。あなたはそれをテストする必要があります。
  • NOLOGGINGは、最小 REDOが生成されることを意味します。 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 ;-)

続行したい開いたカーソル(ループしたカーソル)でコミット(およびトランザクションを閉じる)しているため、Snapshot too oldメッセージが作成される可能性があるため、PLSQLの選択はお勧めできません。それを使用します。オラクルはそれを許可していますが、それは良い習慣ではありません。

更新:最後のPLSQLブロックが機能することを確認できるのはなぜですか?私はそれを推測するので:

  • 他の誰もこの一時テーブルを何らかの理由で使用していません(dbaまたは統計を収集するジョブ、移動、レコードの挿入などのdabタスク)。これだけの補助テーブルなので、確実にできます。
  • 次に、最後のアサーションで、クエリは同じプランで正確に実行され、同じ順序で行が返されます。
9
FerranB

Oracleで大規模な削除を実行するときは、UNDO SEGMENTSが不足していないことを確認してください。

DMLを実行するとき、Oracleは最初にすべての変更をREDOログ(古いデータと新しいデータ)に書き込みます。

REDOログがいっぱいになるか、タイムアウトが発生すると、Oraclelog synchronizationを実行します。newデータをデータファイルに書き込み(この場合、データファイルブロックを空きとしてマークします)、古いデータをUNDOテーブルテーブルに書き込みます(変更をcommitするまで、同時トランザクションに表示されたままになります)。

変更をコミットすると、yuorトランザクションが占める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
       )

、何が速いかを見て、これに固執します。

8
Quassnoi

最初の例のように、すべてを一度に実行することをお勧めします。ただし、DBAは、パージ後に使用しなくなったブロックを再利用したい場合があるため、最初にDBAで確認します。また、通常はユーザーの観点からは見えないスケジューリングの懸念があるかもしれません。

6
Jon Ericson

これを1回の削除として実行することをお勧めします。

削除しようとしている子テーブルはありますか?その場合は、それらのテーブルの外部キーにインデックスが付けられていることを確認してください。そうしないと、削除するすべての行に対して子テーブルのフルスキャンを実行する可能性があり、処理が非常に遅くなる可能性があります。

実行中に削除の進行状況を確認する方法が必要になる場合があります。 Oracleデータベースで長時間実行されるクエリをチェックする方法 を参照してください。

他の人が示唆しているように、水をテストしたい場合は、クエリの最後にrownum <10000を入力できます。

4
WW.

元のSQLに非常に長い時間がかかる場合、コミットされていない変更を行わずにUNDOを使用してデータのバージョンを再構築する必要があるため、一部の並行SQLの実行が遅くなる可能性があります。

妥協は次のようなものかもしれません

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が小さいほど、コミットの頻度が高くなり、(おそらく)元に戻すを適用する必要があるという点で他のセッションへの影響が少なくなります。ただし、実行計画によっては、他の影響が生じる可能性があり、全体としてはさらに時間がかかる可能性があります。技術的には、EXITがループを終了するため、ループの「FOR」部分は不要です。しかし、無制限のループについては、行き詰まった場合にセッションを強制終了するのは面倒なので、私は妄想的です。

4
Gary Myers

ここでのすべての答えは素晴らしいですが、追加するのは1つだけです。テーブル内のレコードのallを削除したい場合、sureである必要はありません。ロールバックする場合は、truncate tableコマンドを使用します。

(あなたの場合、サブセットを削除したいだけですが、同様の問題が潜んでいる人のために、これを追加すると思いました)

0
Evan

私は過去にOracle7で同様のことを行いました。そこでは、数千のテーブルから数百万の行を削除する必要がありました。すべてのラウンドのパフォーマンス、特に大規模な削除(100万行に加えて1つのテーブル)では、このスクリプトはうまく機能しました。

少し変更する必要があります(つまり、ユーザー/パスワードを調べて、ロールバックセグメントを正しく取得します)。また、これについてDBAと話し合い、最初にTEST環境で実行する必要があります。そうは言っても、それはとても簡単です。関数delete_sql()は、指定したテーブルでROWIDのバッチを検索し、バッチごとに削除します。例えば;

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

上記の例は、SQLステートメントに基づいてテーブルMSF170から一度に500レコードを削除しています。

複数のテーブルからデータを削除する必要がある場合は、ファイルdelete-tables.sqlに追加のexec delete_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;
_

ああ、最後のヒント。速度が遅くなり、テーブルによってはダウンタイムが必要になる場合があります。テスト、タイミング、チューニングはここでのあなたの親友です。

0
Mark Nold