大規模なテーブル更新や集計処理を1セッションで実行すると、処理時間が長くなり、UNDO、REDO、ロック保持時間、障害時のやり直し範囲が大きくなります。DBMS_PARALLEL_EXECUTE は、対象データをチャンクに分割し、各チャンクに同じSQLやPL/SQLブロックを実行させるためのOracle標準パッケージです。単純に並列度を上げるだけでなく、失敗チャンクの再実行、進捗監視、処理単位の分離を設計できる点が、大規模バッチで使いやすい理由です。
基本仕様は DBMS_PARALLEL_EXECUTE完全ガイド、同時実行時のロック設計は PL/SQLの排他・同時実行制御 もあわせて確認すると理解しやすくなります。
CREATE_TASKからRUN_TASKまでの基本手順ROWIDと数値キーによるチャンク分割TASK_STATUSとRESUME_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 を必ず使います。
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_TASK に retry_limit を渡すような使い方はありません。失敗時は TASK_STATUS を確認し、完了していなければ RESUME_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_CODE と ERROR_MESSAGE を見るのが基本です。
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 を条件に含めます。
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の偏りや欠番が多い場合はチャンクごとの負荷差が出やすくなります。
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 権限がない、またはジョブ実行が抑制されている環境では、タスクが期待どおりに進みません。運用前に権限、ジョブキュー、同時実行数、リソース上限を確認します。
-- 実行ユーザーへの権限付与例 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句に含めます。ERROR_CODE / ERROR_MESSAGE を確認します。まとめ
DBMS_PARALLEL_EXECUTE は、大規模バッチを単に速くするためだけの機能ではありません。チャンク分割、並列実行、再開、監視をセットで設計できるため、長時間UPDATEや大量データ補正を運用しやすくできます。
実務では、RUN_TASK のSQLにチャンク範囲を必ず入れること、TASK_STATUS と RESUME_TASK で再実行すること、エラー詳細を USER_PARALLEL_EXECUTE_CHUNKS で確認することが重要です。さらに、冪等性、UNDO容量、並列度、ジョブ権限まで含めて設計すれば、1億件規模の更新でも安全に段階実行しやすくなります。

