私はデータベースの専門家ではありません。私のデータベース作業のほとんどは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時間しかかかりませんでした。これに基づいて、本番環境で実行するためにキューに入れられます。
最初のアプローチの方が優れています。クエリオプティマイザーを非表示にするのではなく、実行しようとしていることを明確に把握できるからです。データベースエンジンは、内部で5.5m(またはテーブルの5.5%)を削除する方法と、200k(または0.2%)を削除する方法とは異なるアプローチを取る場合があります。
こちらも article Oracleでの大規模なDELETEについてお読みください。
最速の方法は、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
あなたは世話をしました:
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ブロックが機能することを確認できるのはなぜですか?私はそれを推測するので:
Oracle
で大規模な削除を実行するときは、UNDO SEGMENTS
が不足していないことを確認してください。
DML
を実行するとき、Oracle
は最初にすべての変更をREDO
ログ(古いデータと新しいデータ)に書き込みます。
REDO
ログがいっぱいになるか、タイムアウトが発生すると、Oracle
はlog 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
)
、何が速いかを見て、これに固執します。
最初の例のように、すべてを一度に実行することをお勧めします。ただし、DBAは、パージ後に使用しなくなったブロックを再利用したい場合があるため、最初にDBAで確認します。また、通常はユーザーの観点からは見えないスケジューリングの懸念があるかもしれません。
これを1回の削除として実行することをお勧めします。
削除しようとしている子テーブルはありますか?その場合は、それらのテーブルの外部キーにインデックスが付けられていることを確認してください。そうしないと、削除するすべての行に対して子テーブルのフルスキャンを実行する可能性があり、処理が非常に遅くなる可能性があります。
実行中に削除の進行状況を確認する方法が必要になる場合があります。 Oracleデータベースで長時間実行されるクエリをチェックする方法 を参照してください。
他の人が示唆しているように、水をテストしたい場合は、クエリの最後にrownum <10000を入力できます。
元の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」部分は不要です。しかし、無制限のループについては、行き詰まった場合にセッションを強制終了するのは面倒なので、私は妄想的です。
ここでのすべての答えは素晴らしいですが、追加するのは1つだけです。テーブル内のレコードのallを削除したい場合、sureである必要はありません。ロールバックする場合は、truncate tableコマンドを使用します。
(あなたの場合、サブセットを削除したいだけですが、同様の問題が潜んでいる人のために、これを追加すると思いました)
私は過去に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;
_
ああ、最後のヒント。速度が遅くなり、テーブルによってはダウンタイムが必要になる場合があります。テスト、タイミング、チューニングはここでのあなたの親友です。