【PL/SQL】DBMS_PARALLEL_EXECUTEによる大規模バッチの並列化戦略

【PL/SQL】DBMS_PARALLEL_EXECUTEによる大規模バッチの並列化戦略 PL/SQL

大規模なテーブル更新や集計処理を1セッションで実行すると、処理時間が長くなり、UNDO、REDO、ロック保持時間、障害時のやり直し範囲が大きくなります。DBMS_PARALLEL_EXECUTE は、対象データをチャンクに分割し、各チャンクに同じSQLやPL/SQLブロックを実行させるためのOracle標準パッケージです。単純に並列度を上げるだけでなく、失敗チャンクの再実行、進捗監視、処理単位の分離を設計できる点が、大規模バッチで使いやすい理由です。

基本仕様は DBMS_PARALLEL_EXECUTE完全ガイド、同時実行時のロック設計は PL/SQLの排他・同時実行制御 もあわせて確認すると理解しやすくなります。

この記事で扱うこと

  • CREATE_TASK から RUN_TASK までの基本手順
  • ROWID と数値キーによるチャンク分割
  • TASK_STATUSRESUME_TASK による再実行
  • USER_PARALLEL_EXECUTE_TASKS / USER_PARALLEL_EXECUTE_CHUNKS による監視
  • 権限、並列度、UNDO、冪等性などの運用上の注意点
スポンサーリンク

DBMS_PARALLEL_EXECUTEの基本構造

DBMS_PARALLEL_EXECUTE の処理は、タスク作成、チャンク作成、並列実行、状態確認、必要に応じた再開、後片付けの順に進めます。内部ではスケジューラジョブを使ってワーカーを起動するため、実行ユーザーにはジョブ実行に必要な権限も必要です。

  • CREATE_TASK:並列実行タスクを作成する
  • CREATE_CHUNKS_BY_ROWID:物理ROWID範囲でチャンクを作る
  • CREATE_CHUNKS_BY_NUMBER_COL:数値キーの範囲でチャンクを作る
  • RUN_TASK:各チャンクにSQLまたはPL/SQLブロックを実行する
  • TASK_STATUS:タスクの状態を数値定数で確認する
  • RESUME_TASK:失敗またはクラッシュしたタスクを再開する
  • DROP_TASK:タスクとチャンク情報を削除する

基本例:ROWIDで分割して並列UPDATEする

最も使いやすいのは、対象テーブルをROWID範囲で分割する方法です。RUN_TASK に渡すSQLでは、チャンク範囲を表す :start_id:end_id を必ず使います。

parallel-update-by-rowid.sql
BEGIN
  DBMS_PARALLEL_EXECUTE.CREATE_TASK('UPDATE_SALES');

  DBMS_PARALLEL_EXECUTE.CREATE_CHUNKS_BY_ROWID(
    task_name   => 'UPDATE_SALES',
    table_owner => 'HR',
    table_name  => 'SALES',
    by_row      => TRUE,
    chunk_size  => 200000
  );

  DBMS_PARALLEL_EXECUTE.RUN_TASK(
    task_name      => 'UPDATE_SALES',
    sql_stmt       => 'UPDATE hr.sales
                          SET total = qty * price
                        WHERE ROWID BETWEEN :start_id AND :end_id',
    language_flag  => DBMS_SQL.NATIVE,
    parallel_level => 10
  );
END;
/

by_row => TRUE の場合、chunk_size はおおよその行数として扱われます。FALSE にするとブロック数ベースの目安になります。テーブルの行サイズ、I/O、UNDO容量によって適切な値は変わるため、最初は小さめの並列度とチャンクサイズで試すのが安全です。

再実行を前提にした安全な実行パターン

DBMS_PARALLEL_EXECUTE には、RUN_TASKretry_limit を渡すような使い方はありません。失敗時は TASK_STATUS を確認し、完了していなければ RESUME_TASK で再開します。再実行される可能性があるため、SQLは冪等に近い形、または再実行しても二重更新にならない条件にしておくことが重要です。

resume-failed-task.sql
DECLARE
  v_status PLS_INTEGER;
  v_try    PLS_INTEGER := 0;
BEGIN
  v_status := DBMS_PARALLEL_EXECUTE.TASK_STATUS('UPDATE_SALES');

  WHILE v_status <> DBMS_PARALLEL_EXECUTE.FINISHED
    AND v_try < 2
  LOOP
    v_try := v_try + 1;
    DBMS_PARALLEL_EXECUTE.RESUME_TASK('UPDATE_SALES');
    v_status := DBMS_PARALLEL_EXECUTE.TASK_STATUS('UPDATE_SALES');
  END LOOP;

  IF v_status <> DBMS_PARALLEL_EXECUTE.FINISHED THEN
    RAISE_APPLICATION_ERROR(-20000, 'parallel task failed');
  END IF;

  DBMS_PARALLEL_EXECUTE.DROP_TASK('UPDATE_SALES');
END;
/

デッドロックやロック取得失敗が頻発する処理では、まずSQL側のロック順序や対象範囲を見直します。代表的な原因は ORA-00060の原因と解決方法、即時ロック取得失敗は ORA-00054の原因と対処 を参照してください。

失敗チャンクを確認する監視SQL

タスク全体の状態は USER_PARALLEL_EXECUTE_TASKS、チャンクごとの状態やエラー詳細は USER_PARALLEL_EXECUTE_CHUNKS で確認します。エラーメッセージはタスクビューではなく、チャンクビュー側の ERROR_CODEERROR_MESSAGE を見るのが基本です。

monitor-task-status.sql
SELECT task_name,
       status,
       chunk_type,
       parallel_level
  FROM user_parallel_execute_tasks
 WHERE task_name = 'UPDATE_SALES';

SELECT chunk_id,
       status,
       start_rowid,
       end_rowid,
       error_code,
       error_message
  FROM user_parallel_execute_chunks
 WHERE task_name = 'UPDATE_SALES'
 ORDER BY chunk_id;

本番運用では、上記の結果をジョブログや監視テーブルに残しておくと、夜間バッチの途中停止や一部失敗を追跡しやすくなります。失敗チャンクだけを再開できる点が、このパッケージを使う大きな利点です。

PL/SQLブロックを渡す場合の注意点

単純なUPDATEだけでなく、PL/SQLブロックを sql_stmt に渡すこともできます。ただし、チャンク範囲条件を外すと全ワーカーが同じ対象を処理してしまうため、必ず :start_id:end_id を条件に含めます。

run-plsql-block-per-chunk.sql
BEGIN
  DBMS_PARALLEL_EXECUTE.RUN_TASK(
    task_name => 'UPDATE_SALES',
    sql_stmt  => 'BEGIN
                    UPDATE hr.sales
                       SET discount = calc_discount(customer_id, total),
                           processed_at = SYSTIMESTAMP
                     WHERE ROWID BETWEEN :start_id AND :end_id
                       AND processed_at IS NULL;
                  END;',
    language_flag  => DBMS_SQL.NATIVE,
    parallel_level => 8
  );
END;
/

processed_at IS NULL のような条件を入れておくと、再実行時に同じ行を再処理しにくくなります。完全な冪等性が必要な場合は、処理済みフラグ、処理ID、履歴テーブルなどで再実行境界を明確にしてください。

数値キーでチャンク分割する

主キーや連番のような数値列で自然に範囲分割できる場合は、CREATE_CHUNKS_BY_NUMBER_COL も使えます。ROWIDより業務キーに近い単位で分割できる一方、IDの偏りや欠番が多い場合はチャンクごとの負荷差が出やすくなります。

chunk-by-number-column.sql
BEGIN
  DBMS_PARALLEL_EXECUTE.CREATE_TASK('AGG_BY_CUSTOMER');

  DBMS_PARALLEL_EXECUTE.CREATE_CHUNKS_BY_NUMBER_COL(
    task_name    => 'AGG_BY_CUSTOMER',
    table_owner  => 'HR',
    table_name   => 'CUSTOMERS',
    table_column => 'CUSTOMER_ID',
    chunk_size   => 1000
  );

  DBMS_PARALLEL_EXECUTE.RUN_TASK(
    task_name      => 'AGG_BY_CUSTOMER',
    sql_stmt       => 'UPDATE hr.customers
                          SET score = calc_customer_score(customer_id)
                        WHERE customer_id BETWEEN :start_id AND :end_id',
    language_flag  => DBMS_SQL.NATIVE,
    parallel_level => 5
  );
END;
/

権限とジョブ実行環境を確認する

DBMS_PARALLEL_EXECUTE は内部でスケジューラジョブを利用します。実行ユーザーに CREATE JOB 権限がない、またはジョブ実行が抑制されている環境では、タスクが期待どおりに進みません。運用前に権限、ジョブキュー、同時実行数、リソース上限を確認します。

check-job-prerequisites.sql
-- 実行ユーザーへの権限付与例
GRANT CREATE JOB TO HR;

-- ジョブ実行状況の確認例
SELECT job_name, state, enabled
  FROM user_scheduler_jobs
 WHERE job_name LIKE 'TASK$_%';

-- タスクの直近実行設定を確認
SELECT task_name, status, parallel_level, job_class
  FROM user_parallel_execute_tasks
 WHERE task_name = 'UPDATE_SALES';

ジョブ制御の考え方は PL/SQLのDBMS_SCHEDULER解説 とも関係します。並列度を大きくしすぎると、バッチ単体は速く見えても、他業務のSQLやI/Oを圧迫することがあります。

チャンクサイズと並列度の決め方

チャンクサイズと parallel_level は、テーブル件数だけでは決められません。1行あたりの更新コスト、インデックス数、トリガー、UNDO表領域、REDO生成量、I/O帯域を見ながら調整します。

  • チャンクが大きすぎる:失敗時のやり直し範囲が大きく、UNDOも膨らみやすい
  • チャンクが小さすぎる:ジョブ起動やメタデータ管理のオーバーヘッドが増える
  • 並列度が高すぎる:CPU、I/O、ロック競合が増え、全体として遅くなることがある
  • 並列度が低すぎる:チャンク分割のメリットが出にくい

最初は parallel_level を4から8程度に抑え、AWR、実行時間、待機イベント、UNDO使用量を見て広げるのが実務では安全です。更新後に統計情報が大きく変わる場合は、DBMS_STATS.GATHER_TABLE_STATS も計画に含めます。

トランザクションと冪等性の設計

DBMS_PARALLEL_EXECUTE では、チャンク境界を処理単位として考えます。つまり、途中失敗したときに「どのチャンクまで終わったか」「同じチャンクを再実行してよいか」を説明できる設計が必要です。特に外部API呼び出し、メール送信、ファイル出力など、DB外に副作用がある処理は直接チャンク内で実行しない方が安全です。

DB外の副作用が必要な場合は、まずDB内にアウトボックステーブルへイベントを書き込み、別ジョブで送信する構成にします。これにより、チャンク再実行時の二重送信や不整合を避けやすくなります。

よくある失敗パターン

全件を各ワーカーが処理してしまう:start_id / :end_id の条件が抜けているケースです。必ずチャンク範囲をWHERE句に含めます。
再実行で二重更新になる処理済み条件や更新対象条件が曖昧なケースです。処理済みフラグ、日時、処理IDで境界を作ります。
エラー原因が追えないタスクビューだけを見ているケースです。チャンクビューの ERROR_CODE / ERROR_MESSAGE を確認します。
並列度を上げても速くならないI/O、インデックス更新、ロック競合がボトルネックになっている可能性があります。

まとめ

DBMS_PARALLEL_EXECUTE は、大規模バッチを単に速くするためだけの機能ではありません。チャンク分割、並列実行、再開、監視をセットで設計できるため、長時間UPDATEや大量データ補正を運用しやすくできます。

実務では、RUN_TASK のSQLにチャンク範囲を必ず入れること、TASK_STATUSRESUME_TASK で再実行すること、エラー詳細を USER_PARALLEL_EXECUTE_CHUNKS で確認することが重要です。さらに、冪等性、UNDO容量、並列度、ジョブ権限まで含めて設計すれば、1億件規模の更新でも安全に段階実行しやすくなります。