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

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

大規模なテーブル更新や集計処理では、単一セッションで全件を処理するとUNDOやREDOが膨張し、実行時間が数時間に達することも珍しくありません。OracleのDBMS_PARALLEL_EXECUTEを使えば、処理をチャンク単位に分割し、複数セッションで並列実行することで、安定したスループットを維持しつつ安全に高速化できます。ここでは、DBMS_PARALLEL_EXECUTEの基本構造から、チャンク設計、リトライ制御、モニタリング、運用上の注意点までを体系的に整理します。

DBMS_PARALLEL_EXECUTEの基本構造

DBMS_PARALLEL_EXECUTEは、指定したSQLやPL/SQLブロックをチャンク単位に分割し、複数のスレッド(セッション)で並列実行するためのビルトインAPIです。内部的にはDBMS_SCHEDULERジョブを利用しており、チャンク生成・実行・監視を統合的に管理します。

主なプロシージャは以下の通りです。

  • CREATE_TASK:並列実行タスクを定義
  • CREATE_CHUNKS_BY_ROWID:テーブルをROWID範囲でチャンク分割
  • RUN_TASK:定義したタスクを並列実行
  • DROP_TASK:タスクの削除

基本構文例:行単位の並列UPDATE

次の例では、1000万件のレコードを50チャンクに分割して並列更新します。

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  -- 約20万行単位で分割
  );

  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   -- 同時実行セッション数
  );

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

これにより、10セッションで同時にチャンク単位の更新が進行し、完了後に自動でクリーンアップされます。

PL/SQLブロックによる柔軟な処理

SQL文では対応できない複雑なロジックを含む場合は、PL/SQLブロックを渡して実行することも可能です。

BEGIN
  DBMS_PARALLEL_EXECUTE.RUN_TASK(
    task_name => 'UPDATE_SALES',
    sql_stmt => 'BEGIN
                   UPDATE hr.sales
                      SET discount = calc_discount(customer_id, total)
                    WHERE ROWID BETWEEN :start_id AND :end_id;
                 END;',
    language_flag  => DBMS_SQL.NATIVE,
    parallel_level => 8
  );
END;
/

このように、関数呼び出しや複数ステートメントを含める場合でも、チャンク範囲パラメータ(:start_id / :end_id)を維持して動作します。

チャンク設計のポイント

チャンクサイズは、対象テーブルのデータ量・行サイズ・インデックス構成によって適切な値が異なります。一般的には、1チャンクあたり数万〜数十万件が目安です。

  • 大きすぎると:UNDO肥大化、スレッド間負荷の偏り
  • 小さすぎると:ジョブ数過多によるスケジューラオーバーヘッド

チャンク分割方法は次の3種類から選べます。

  1. CREATE_CHUNKS_BY_ROWID:均等に分割(汎用的)
  2. CREATE_CHUNKS_BY_NUMBER_COL:数値キーで範囲分割
  3. CREATE_CHUNKS_BY_SQL:独自SELECTで柔軟に分割

失敗チャンクの再実行とエラーハンドリング

一部のチャンクが失敗した場合、該当部分のみ再実行できます。

DBMS_PARALLEL_EXECUTE.RESUME_TASK('UPDATE_SALES');

タスク状態はDBA_PARALLEL_EXECUTE_TASKSビューで確認可能です。

SELECT task_name, status, error_message
  FROM dba_parallel_execute_tasks
 WHERE task_name = 'UPDATE_SALES';

また、エラーを検知してログ化する仕組みを併用すると運用が安定します。エラー発生時にはリトライ回数を設定しておくのが推奨です。

DBMS_PARALLEL_EXECUTE.RUN_TASK(
  task_name      => 'UPDATE_SALES',
  sql_stmt       => '...',
  language_flag  => DBMS_SQL.NATIVE,
  parallel_level => 10,
  retry_limit    => 3    -- 3回まで自動リトライ
);

スケジューラとの関係とセッション管理

内部的にはDBMS_SCHEDULERがワーカーを生成して並列実行します。そのため、スケジューラジョブ実行権限が必要です。

GRANT CREATE JOB TO HR;

実行中のセッションを確認するには次のSQLが便利です。

SELECT task_name, status, start_time, end_time
  FROM user_parallel_execute_chunks
 WHERE task_name = 'UPDATE_SALES'
 ORDER BY chunk_id;

パフォーマンス最適化のヒント

  • 処理前にALTER SESSION ENABLE PARALLEL DMLを有効化しても効果的
  • UPDATE対象に不要なトリガー・インデックスが多い場合は一時的に無効化を検討
  • I/Oボトルネックを避けるため、チャンクは異なるストレージブロックを跨がない範囲に調整
  • 処理後は統計情報の再収集を忘れずに(DBMS_STATS.GATHER_TABLE_STATS

複数テーブルの同時処理と依存制御

チャンク単位に複数テーブルをJOINする処理を行う場合、同一チャンクキーで分割し、外側のループでテーブルを固定すると整合性が保ちやすくなります。

BEGIN
  DBMS_PARALLEL_EXECUTE.CREATE_TASK('MERGE_CUSTOMER_ORDER');
  DBMS_PARALLEL_EXECUTE.CREATE_CHUNKS_BY_NUMBER_COL(
    task_name   => 'MERGE_CUSTOMER_ORDER',
    table_owner => 'HR',
    table_name  => 'CUSTOMERS',
    table_column => 'CUSTOMER_ID',
    chunk_size  => 1000
  );
  DBMS_PARALLEL_EXECUTE.RUN_TASK(
    task_name => 'MERGE_CUSTOMER_ORDER',
    sql_stmt  => 'MERGE INTO orders o USING customers c
                  ON (o.customer_id = c.customer_id)
                  WHEN MATCHED THEN UPDATE SET o.name = c.name
                  WHERE c.customer_id BETWEEN :start_id AND :end_id',
    language_flag => DBMS_SQL.NATIVE,
    parallel_level => 5
  );
  DBMS_PARALLEL_EXECUTE.DROP_TASK('MERGE_CUSTOMER_ORDER');
END;
/

ジョブ監視と進捗可視化

進行状況を監視するには、USER_PARALLEL_EXECUTE_CHUNKSビューを利用します。

SELECT task_name,
       chunk_id,
       start_rowid,
       end_rowid,
       status
  FROM user_parallel_execute_chunks
 WHERE task_name = 'UPDATE_SALES';

各チャンクのステータス(PROCESSED/PROCESSED_WITH_ERROR/PROCESSED_WITH_RETRYなど)を確認し、進捗率を可視化できます。

トランザクション制御とコミット設計

DBMS_PARALLEL_EXECUTEはチャンク単位で独立したトランザクションを実行します。チャンク完了ごとに自動コミットされるため、全体の一貫性が不要なバッチ処理に適しています。一方、チャンク間で依存関係がある場合は、タスク分割を慎重に設計する必要があります。

実運用でのチェックポイント設計

長時間バッチでは、処理済みチャンクをメタデータ表に記録し、再起動時に未処理部分のみを再実行できるようにしておくと安全です。失敗チャンクのみを抽出して再試行する仕組みを組み合わせると、夜間バッチが途中停止しても安全に再開できます。

まとめ

DBMS_PARALLEL_EXECUTEは、Oracleが提供する安全で柔軟な並列バッチ実行フレームワークです。
行やキー範囲単位で分割し、チャンクごとに独立トランザクションで実行できるため、従来のFORALL+COMMIT間隔方式よりも堅牢です。
正しいチャンクサイズとリトライ設計、進捗監視を組み合わせることで、1億件クラスの更新でも安定して完了できる大規模バッチを構築できます。
適切に設計すれば、PL/SQL単体でも高い並列性能と信頼性を両立することが可能です。