【PL/SQL】大量データ処理のコミット頻度とUNDO最適化|チャンク設計・ORA-01555対策

【PL/SQL】大量データの一括処理におけるコミット頻度とUNDO最適化 PL/SQL

PL/SQLで大量データを一括処理するとき、よく迷うのが COMMIT の頻度です。「1000件ごとにCOMMIT」「1万件ごとにCOMMIT」のような目安だけで決めると、UNDO不足、再実行不能、ロック競合、読み取り一貫性エラーの原因になります。

結論から言うと、コミット頻度は件数だけで決めず、業務的に再実行できる単位で決めるのが基本です。そのうえで、UNDO表領域、REDO量、処理時間、長時間SELECT、失敗行の扱いを見ながらチャンクサイズを調整します。トランザクションの基本は Oracleトランザクション完全ガイド、PL/SQLでのCOMMIT境界は COMMITとROLLBACKの正しい使い方 もあわせて確認してください。

この記事で扱うこと

  • 大量DMLでCOMMIT頻度を決める考え方
  • 行数、キー範囲、期間、経過時間によるチャンク設計
  • UNDOとREDOを増やしすぎない処理方式
  • DBMS_ERRLOGFORALL SAVE EXCEPTIONSAPPENDNOLOGGING の使い分け
  • ORA-01555ORA-30036 を避ける監視と運用
  • 失敗しても再実行できる本番前チェック
スポンサーリンク

最初に結論:COMMIT頻度の決め方

大量処理では、COMMIT間隔を小さくすれば必ず安全になるわけではありません。頻繁なCOMMITはログ書き出しを増やし、処理を遅くし、再実行範囲を細かく分断します。一方で、最後に1回だけCOMMITする設計は、UNDOを大量に消費し、失敗時のロールバック時間も長くなります。

画面系の一括更新利用者が1回の操作として期待する範囲を1トランザクションにします。途中COMMITは原則避けます。
夜間バッチ顧客ID、年月、部門、ファイル、ロードIDなど、再実行しやすい業務単位で区切ります。
単純ロードINSERT中心ならAPPEND、NOLOGGING、パーティション、DBMS_ERRLOGを検討します。
UPDATE/DELETE中心巨大な更新・削除を直接流す前に、CTAS、MERGE、パーティション入れ替えで置き換えられないか検討します。
外部連携ありCOMMITとメール/API送信を同時に扱わず、アウトボックスや送信キューに分離します。

より広い設計判断は PL/SQLトランザクション設計パターン にまとめています。この記事では、その中でも大量処理に絞って掘り下げます。

避けたい考え方:件数だけでCOMMITする

「とりあえず1000件ごとにCOMMIT」は分かりやすいですが、業務的に中途半端な状態を作ることがあります。たとえば同じ顧客の明細が複数チャンクに分かれると、途中失敗時に顧客単位で再実行できません。また、読み取りカーソルを開いたままCOMMITを繰り返す実装は、長時間処理と相性が悪くなります。

bad-row-count-commit.sql
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;
/

件数基準を使う場合でも、業務キーや期間の境界を壊さないことが前提です。件数はあくまで上限ガードとして使い、主軸は再実行できる単位にします。

チャンク設計の判断リスト

大量処理のチャンクは、次のように選びます。テーブルではなくカード形式で整理します。

キー範囲チャンク主キーや連番で範囲を切ります。再開しやすく、ログにも範囲を残しやすいです。
期間チャンク売上月、処理月、登録日などで切ります。月次・日次バッチと相性がよく、パーティションとも組み合わせやすいです。
業務単位チャンク顧客、契約、請求グループ、ファイル単位で切ります。整合性を守りやすく、再実行も説明しやすいです。
行数チャンク業務単位が十分小さい場合の上限ガードとして使います。行数だけで意味のある整合性単位にはなりません。
経過時間チャンク処理時間やロック保持時間を抑える目的で使います。ただし中断位置をログに残せる設計が必要です。

キー範囲でチャンクする実装例

主キーやロード用連番がある場合は、範囲単位で処理します。どの範囲まで処理したかをログに残せるため、失敗時に再開しやすい形です。

処理範囲を先に決める

key-range-chunk.sql
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に一意制約を付けることも重要です。

期間チャンクとパーティションを組み合わせる

売上、ログ、履歴、明細のように年月で処理範囲が決まるデータは、期間チャンクが向いています。パーティション表であれば、対象パーティションだけを処理でき、不要データの削除や入れ替えも軽くできます。

月単位で処理する例

month-chunk.sql
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 が発生します。

ロールバック用失敗時に未確定変更を戻すための前イメージを保持します。大きなDMLほど多く消費します。
読み取り一貫性用別セッションが更新しても、SELECT開始時点の一貫した結果を見せるために使います。
フラッシュバック用Flashback Queryなどでも過去イメージを参照するため、保持期間と容量が重要になります。

ORA-01555 の詳しい原因と対処は ORA-01555完全ガイド、UNDO/TEMPを含む容量管理は Oracle表領域完全ガイドOracle容量不足の緊急対応ガイド が参考になります。

UNDOを増やしやすい処理

UNDOを大量に使うのは、主にUPDATEとDELETEです。INSERTもUNDOを使いますが、既存行の前イメージを大量に保持するUPDATE/DELETEほど重くなりやすいです。大量処理の前に、そもそもUPDATE/DELETEを避けられないか確認します。

大量UPDATE既存行の変更前イメージが必要です。索引が多いほどREDO/UNDOとメンテナンスコストも増えます。
大量DELETE削除前イメージを保持します。期間削除ならTRUNCATE PARTITIONやEXCHANGEを検討します。
行ループDMLSQL実行回数が増え、コンテキストスイッチも増えます。集合SQLやFORALLへ寄せます。
トリガが多い表1行ごとに追加処理が走るため、見た目以上にUNDO/REDOが増えることがあります。

集合志向SQLに寄せる

PL/SQLで1行ずつ処理するより、可能な限り1本のINSERT、UPDATE、MERGEに寄せた方が効率的です。コミット頻度を調整する前に、SQLの実行回数を減らせないか見直します。

MERGEでまとめて反映する例

merge-bulk.sql
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_ERRLOGLOG ERRORS が有効です。失敗行をエラーテーブルへ退避し、成功行を先に確定できます。

ERRLOG付きINSERTの例

errlog-insert.sql
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の組み合わせ

forall-limit.sql
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ロードの例

append-load.sql
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で作り直す考え方

ctas-rebuild.sql
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 / BATCHWAIT / NOWAIT の組み合わせでログ書き出し待ちの挙動を指定できます。大量バッチで待ち時間を調整したい場面はありますが、障害時の耐久性や基盤設定との整合を理解せずに使うべきではありません。

commit-write.sql
-- 通常は明示指定せず、基盤設定と運用要件に従う
COMMIT;

-- 遅延許容の一部バッチで検討されることがある
COMMIT WRITE BATCH NOWAIT;

NOWAIT は、コミットの応答とREDO書き込み完了の関係に注意が必要です。業務上、確定応答後の障害リスクを許容できない処理では安易に使わない方が安全です。

長時間SELECTとfetch across commitを避ける

カーソルで大量行を読みながら同じ処理内でCOMMITを繰り返すと、読み取り一貫性の維持が難しくなります。特に長時間実行のSELECTと大量更新が重なる環境では ORA-01555 の温床になります。

先に対象キーを固定する例

materialize-target-keys.sql
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、実行時間、処理済み件数を監視します。進捗ログを残すと、失敗時にどこから再開すべきか判断できます。

進捗ログの設計例

batch-progress-log.sql
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最適化の考え方です。