PL/SQLで大量データを一括処理するとき、よく迷うのが COMMIT の頻度です。「1000件ごとにCOMMIT」「1万件ごとにCOMMIT」のような目安だけで決めると、UNDO不足、再実行不能、ロック競合、読み取り一貫性エラーの原因になります。
結論から言うと、コミット頻度は件数だけで決めず、業務的に再実行できる単位で決めるのが基本です。そのうえで、UNDO表領域、REDO量、処理時間、長時間SELECT、失敗行の扱いを見ながらチャンクサイズを調整します。トランザクションの基本は Oracleトランザクション完全ガイド、PL/SQLでのCOMMIT境界は COMMITとROLLBACKの正しい使い方 もあわせて確認してください。
- 大量DMLでCOMMIT頻度を決める考え方
- 行数、キー範囲、期間、経過時間によるチャンク設計
- UNDOとREDOを増やしすぎない処理方式
DBMS_ERRLOG、FORALL SAVE EXCEPTIONS、APPEND、NOLOGGINGの使い分けORA-01555、ORA-30036を避ける監視と運用- 失敗しても再実行できる本番前チェック
最初に結論:COMMIT頻度の決め方
大量処理では、COMMIT間隔を小さくすれば必ず安全になるわけではありません。頻繁なCOMMITはログ書き出しを増やし、処理を遅くし、再実行範囲を細かく分断します。一方で、最後に1回だけCOMMITする設計は、UNDOを大量に消費し、失敗時のロールバック時間も長くなります。
より広い設計判断は PL/SQLトランザクション設計パターン にまとめています。この記事では、その中でも大量処理に絞って掘り下げます。
避けたい考え方:件数だけでCOMMITする
「とりあえず1000件ごとにCOMMIT」は分かりやすいですが、業務的に中途半端な状態を作ることがあります。たとえば同じ顧客の明細が複数チャンクに分かれると、途中失敗時に顧客単位で再実行できません。また、読み取りカーソルを開いたままCOMMITを繰り返す実装は、長時間処理と相性が悪くなります。
DECLARE
v_count PLS_INTEGER := 0;
BEGIN
FOR r IN (
SELECT order_id, customer_id
FROM staging_orders
WHERE processed = 'N'
ORDER BY order_id
) LOOP
process_order(r.order_id);
v_count := v_count + 1;
IF MOD(v_count, 1000) = 0 THEN
-- NG: 業務単位を無視して機械的に区切っている
COMMIT;
END IF;
END LOOP;
COMMIT;
END;
/
件数基準を使う場合でも、業務キーや期間の境界を壊さないことが前提です。件数はあくまで上限ガードとして使い、主軸は再実行できる単位にします。
チャンク設計の判断リスト
大量処理のチャンクは、次のように選びます。テーブルではなくカード形式で整理します。
キー範囲でチャンクする実装例
主キーやロード用連番がある場合は、範囲単位で処理します。どの範囲まで処理したかをログに残せるため、失敗時に再開しやすい形です。
処理範囲を先に決める
DECLARE
c_chunk_size CONSTANT NUMBER := 50000;
v_from_id NUMBER;
v_to_id NUMBER;
BEGIN
SELECT MIN(stg_id), MAX(stg_id)
INTO v_from_id, v_to_id
FROM staging_sales
WHERE load_id = :load_id
AND processed = 'N';
WHILE v_from_id <= v_to_id LOOP
UPDATE target_sales t
SET amount = (
SELECT s.amount
FROM staging_sales s
WHERE s.sale_id = t.sale_id
AND s.stg_id BETWEEN v_from_id AND v_from_id + c_chunk_size - 1
)
WHERE EXISTS (
SELECT 1
FROM staging_sales s
WHERE s.sale_id = t.sale_id
AND s.stg_id BETWEEN v_from_id AND v_from_id + c_chunk_size - 1
);
INSERT INTO batch_progress(load_id, from_id, to_id, status, finished_at)
VALUES (:load_id, v_from_id, v_from_id + c_chunk_size - 1, 'DONE', SYSTIMESTAMP);
COMMIT;
v_from_id := v_from_id + c_chunk_size;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
/
ポイントは、チャンクの開始・終了キーをログに残すことです。失敗したら最後に完了していない範囲から再実行できます。バッチの再実行性を高めるには、自然キーやロードIDに一意制約を付けることも重要です。
期間チャンクとパーティションを組み合わせる
売上、ログ、履歴、明細のように年月で処理範囲が決まるデータは、期間チャンクが向いています。パーティション表であれば、対象パーティションだけを処理でき、不要データの削除や入れ替えも軽くできます。
月単位で処理する例
DECLARE
v_month DATE := DATE '2026-05-01';
BEGIN
INSERT /*+ APPEND */ INTO sales_fact
SELECT *
FROM staging_sales
WHERE sale_date >= v_month
AND sale_date < ADD_MONTHS(v_month, 1)
LOG ERRORS INTO err$_sales_fact ('LOAD_202605')
REJECT LIMIT UNLIMITED;
COMMIT;
END;
/
大量ロードではパーティション設計が効きます。パーティション運用のPL/SQL側の考え方は パーティション運用で大量データを安全に扱う方法、大量処理の別解としては パイプライン関数で大量データ処理を勝たせる完全ガイド も関連します。
UNDOは何に使われるか
UNDOは、未確定変更を取り消すためだけでなく、読み取り一貫性を保つためにも使われます。長時間SELECTが過去時点のデータを読み続けている間に、別セッションが大量更新してUNDOが上書きされると、ORA-01555 が発生しやすくなります。また、更新量に対してUNDO表領域が不足すると ORA-30036 が発生します。
ORA-01555 の詳しい原因と対処は ORA-01555完全ガイド、UNDO/TEMPを含む容量管理は Oracle表領域完全ガイド と Oracle容量不足の緊急対応ガイド が参考になります。
UNDOを増やしやすい処理
UNDOを大量に使うのは、主にUPDATEとDELETEです。INSERTもUNDOを使いますが、既存行の前イメージを大量に保持するUPDATE/DELETEほど重くなりやすいです。大量処理の前に、そもそもUPDATE/DELETEを避けられないか確認します。
集合志向SQLに寄せる
PL/SQLで1行ずつ処理するより、可能な限り1本のINSERT、UPDATE、MERGEに寄せた方が効率的です。コミット頻度を調整する前に、SQLの実行回数を減らせないか見直します。
MERGEでまとめて反映する例
MERGE INTO target_customer t
USING (
SELECT customer_id, rank_code, updated_at
FROM staging_customer
WHERE load_id = :load_id
) s
ON (t.customer_id = s.customer_id)
WHEN MATCHED THEN
UPDATE SET
t.rank_code = s.rank_code,
t.updated_at = s.updated_at
WHEN NOT MATCHED THEN
INSERT (customer_id, rank_code, updated_at)
VALUES (s.customer_id, s.rank_code, s.updated_at);
COMMIT;
1本のSQLで扱えるなら、PL/SQLループよりまず集合SQLを検討します。そのうえで、対象範囲が大きすぎる場合にキー範囲や期間でチャンク化します。
DBMS_ERRLOGで失敗行を退避する
大量INSERTやMERGEで一部行だけ制約違反・型変換エラーになる場合は、DBMS_ERRLOG と LOG ERRORS が有効です。失敗行をエラーテーブルへ退避し、成功行を先に確定できます。
ERRLOG付きINSERTの例
BEGIN
DBMS_ERRLOG.CREATE_ERROR_LOG(
dml_table_name => 'TARGET_SALES',
err_log_table_name => 'ERR$_TARGET_SALES'
);
END;
/
INSERT /*+ APPEND */ INTO target_sales(
sale_id,
sale_date,
amount
)
SELECT sale_id,
sale_date,
amount
FROM staging_sales
WHERE load_id = :load_id
LOG ERRORS INTO err$_target_sales ('LOAD_202605')
REJECT LIMIT UNLIMITED;
COMMIT;
DBMS_ERRLOGは「成功行を活かし、失敗行を後で直す」設計に向きます。詳細な使い方は DBMS_ERRLOGとLOG ERRORSの使い方 を参照してください。
FORALL SAVE EXCEPTIONSを使う場面
SQL一発で書けるならDBMS_ERRLOGが候補ですが、PL/SQL側で行ごとの前処理が必要な場合は FORALL SAVE EXCEPTIONS が有効です。BULK COLLECTの LIMIT で取得量を制限し、FORALLでDMLをまとめて送ります。
LIMITとFORALLの組み合わせ
DECLARE
CURSOR c_src IS
SELECT id, value
FROM staging_data
WHERE load_id = :load_id
AND processed = 'N'
ORDER BY id;
TYPE t_src IS TABLE OF c_src%ROWTYPE;
l_rows t_src;
BEGIN
OPEN c_src;
LOOP
FETCH c_src BULK COLLECT INTO l_rows LIMIT 10000;
EXIT WHEN l_rows.COUNT = 0;
BEGIN
FORALL i IN 1 .. l_rows.COUNT SAVE EXCEPTIONS
UPDATE target_data
SET value = l_rows(i).value
WHERE id = l_rows(i).id;
EXCEPTION
WHEN OTHERS THEN
FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
INSERT INTO batch_error_log(error_index, error_code)
VALUES (
SQL%BULK_EXCEPTIONS(i).ERROR_INDEX,
SQL%BULK_EXCEPTIONS(i).ERROR_CODE
);
END LOOP;
END;
COMMIT;
END LOOP;
CLOSE c_src;
END;
/
バルク処理全体は Oracle BULK COLLECT / FORALL完全ガイド と PL/SQLバルク処理完全ガイド、失敗行の扱いは FORALL SAVE EXCEPTIONSで失敗行を個別ログ化する方法 が関連します。
APPENDとNOLOGGINGの位置づけ
INSERT中心のロードでは、APPEND によるダイレクトパスインサートが候補になります。通常の空きブロック再利用ではなく、高水位マークの後ろへ書き込むため、大量INSERTで効果が出やすいです。ただし、同時参照・ロック・セグメント拡張・索引メンテナンスの影響を確認する必要があります。
APPENDロードの例
ALTER SESSION ENABLE PARALLEL DML;
INSERT /*+ APPEND PARALLEL(t 4) */ INTO target_sales t
SELECT /*+ PARALLEL(s 4) */
*
FROM staging_sales s
WHERE s.load_id = :load_id;
COMMIT;
NOLOGGING はREDO量削減に効く場面がありますが、障害復旧やData Guardとの整合に注意が必要です。本番で使う場合は、ロード後のバックアップ、FORCE LOGGING、スタンバイ反映、失敗時の再ロード手順までセットで決めます。詳しくは Oracleダイレクト・パス・インサート完全ガイド と Oracle NOLOGGING完全ガイド が関連します。
UPDATE/DELETEは作り直しも検討する
巨大なUPDATE/DELETEはUNDOを大きく消費します。更新対象が表の大半に及ぶなら、既存表を直接更新するより、CTASで新しい形を作り直し、検証後に入れ替える方が安定する場合があります。
CTASで作り直す考え方
CREATE TABLE sales_new NOLOGGING AS
SELECT sale_id,
sale_date,
CASE
WHEN status = 'OLD' THEN 'ARCHIVED'
ELSE status
END AS status,
amount
FROM sales_old;
-- 件数・合計・不整合を検証してから切り替える
SELECT COUNT(*), SUM(amount) FROM sales_old;
SELECT COUNT(*), SUM(amount) FROM sales_new;
パーティション表であれば、月単位で新しいパーティションを作って EXCHANGE PARTITION する設計も候補です。UPDATE/DELETEを減らせるほど、UNDOとロック競合を根本から抑えられます。
コミット書き出しポリシーは慎重に使う
OracleのCOMMITには WRITE 句があり、IMMEDIATE / BATCH、WAIT / NOWAIT の組み合わせでログ書き出し待ちの挙動を指定できます。大量バッチで待ち時間を調整したい場面はありますが、障害時の耐久性や基盤設定との整合を理解せずに使うべきではありません。
-- 通常は明示指定せず、基盤設定と運用要件に従う COMMIT; -- 遅延許容の一部バッチで検討されることがある COMMIT WRITE BATCH NOWAIT;
NOWAIT は、コミットの応答とREDO書き込み完了の関係に注意が必要です。業務上、確定応答後の障害リスクを許容できない処理では安易に使わない方が安全です。
長時間SELECTとfetch across commitを避ける
カーソルで大量行を読みながら同じ処理内でCOMMITを繰り返すと、読み取り一貫性の維持が難しくなります。特に長時間実行のSELECTと大量更新が重なる環境では ORA-01555 の温床になります。
先に対象キーを固定する例
CREATE GLOBAL TEMPORARY TABLE work_target_keys ( id NUMBER PRIMARY KEY ) ON COMMIT PRESERVE ROWS; INSERT INTO work_target_keys(id) SELECT id FROM staging_data WHERE load_id = :load_id; COMMIT; -- 以降は作業表のキーを範囲で処理する
作業キーを先に固定しておくと、処理中に元データ側の状態が変わっても再実行しやすくなります。GTTの使い方は Oracleグローバル一時表完全ガイド も参考になります。
UNDOと進捗を監視する
コミット頻度は机上だけでは決まりません。実行前に想定件数と更新量を見積もり、実行中はUNDO、TEMP、REDO、実行時間、処理済み件数を監視します。進捗ログを残すと、失敗時にどこから再開すべきか判断できます。
進捗ログの設計例
CREATE TABLE batch_progress ( batch_id VARCHAR2(50), chunk_no NUMBER, from_key NUMBER, to_key NUMBER, status VARCHAR2(20), processed_cnt NUMBER, error_cnt NUMBER, started_at TIMESTAMP, finished_at TIMESTAMP, error_message VARCHAR2(4000) );
進捗ログは処理本体と同じトランザクションにするか、監査用途として自律トランザクションにするかを決めます。本体がROLLBACKしても障害調査ログを残したい場合は、自律トランザクションを検討します。
本番前チェックリスト
- COMMIT間隔が件数だけでなく、業務的な再実行単位で決まっている
- 失敗時にどのチャンクから再開するか説明できる
- ロードID、キー範囲、期間、処理済み状態をログに残している
- 長時間SELECTと大量DMLが同じ時間帯に競合しない
- UNDO表領域、UNDO_RETENTION、TEMP、アーカイブログ容量を確認している
- DBMS_ERRLOGまたはFORALL SAVE EXCEPTIONSで失敗行を追跡できる
- 大量UPDATE/DELETEをCTASやパーティション入れ替えに置き換えられないか検討した
- APPEND、NOLOGGING、PARALLEL DMLの復旧・同期リスクを確認した
- COMMIT WRITEを使う場合は耐久性と基盤設定を確認した
- 再実行しても二重反映にならない冪等キーを持っている
まとめ
PL/SQLの大量データ処理では、コミット頻度を単純な行数で決めるのではなく、再実行できる業務単位で決めます。そのうえで、UNDO、REDO、ロック、長時間SELECT、失敗行の扱いを見ながらチャンクサイズを調整します。
まずは集合志向SQLへ寄せ、失敗行はDBMS_ERRLOGやFORALL SAVE EXCEPTIONSで分離します。INSERT中心ならAPPENDやNOLOGGING、UPDATE/DELETE中心ならCTASやパーティション入れ替えも候補です。最終的には、速い処理よりも失敗しても安全に再実行できる処理を目指すことが、本番で強いコミット頻度とUNDO最適化の考え方です。

