大規模なPL/SQLシステムでは、例外処理の品質がそのまま運用品質になります。WHEN OTHERS THEN NULL で握りつぶす、何でも再試行する、ログだけ残して正常終了する、といった設計は、障害の原因を隠し、二重更新や二重送信を引き起こします。
重要なのは、例外を再試行できる一時障害と再試行しても直らない恒久障害に分けることです。そのうえで、トランザクション境界、冪等キー、部分ロールバック、外部API副作用、監査ログ、ジョブ再実行をセットで設計します。PL/SQL例外処理の基本は PL/SQL例外処理完全ガイド、トランザクション全体の設計は PL/SQLトランザクション設計パターン もあわせて確認してください。
- 再試行してよい例外・してはいけない例外の分類
PRAGMA EXCEPTION_INITによるORAコードの名前付け- 指数バックオフ、ジッター、最大試行回数
- 冪等キー、外部API副作用、アウトボックス
SAVEPOINT、DBMS_ERRLOG、FORALL SAVE EXCEPTIONSの使い分けDBMS_SCHEDULERによるジョブ再実行と運用ログ
最初に結論:再試行は例外分類と冪等性が前提
再試行は便利ですが、失敗した処理をもう一度流すだけでは危険です。ロック競合や一時的なネットワーク断なら再試行で直る可能性がありますが、入力値不正、権限不足、制約違反、存在しないオブジェクトは再試行しても直りません。さらに、外部送信や請求処理のような副作用がある処理では、同じ処理を2回実行しても壊れない冪等性が必要です。
ORA-00054、ORA-00060、ORA-08177、一時的なHTTP失敗など、時間を置くと成功する可能性があるものです。例外を名前付きで分類する
PL/SQLでは、代表的なOracleエラーを PRAGMA EXCEPTION_INIT で名前付き例外にできます。SQLCODEを直接比較するより、意図が読みやすくなります。
再試行候補の例外を定義する
CREATE OR REPLACE PACKAGE retry_errors AS deadlock_ex EXCEPTION; lock_nowait_ex EXCEPTION; serialize_ex EXCEPTION; snapshot_old_ex EXCEPTION; PRAGMA EXCEPTION_INIT(deadlock_ex, -60); -- ORA-00060 PRAGMA EXCEPTION_INIT(lock_nowait_ex, -54); -- ORA-00054 PRAGMA EXCEPTION_INIT(serialize_ex, -8177); -- ORA-08177 PRAGMA EXCEPTION_INIT(snapshot_old_ex, -1555); -- ORA-01555 END; /
PRAGMA EXCEPTION_INIT は、Oracleエラー番号に業務上意味のある名前を付けるために使います。何でも名前付きにするのではなく、ハンドリング方針が明確なエラーだけを定義すると読みやすくなります。
再試行判断フロー
例外が起きたら、すぐループで再実行するのではなく、次の順に判断します。
ロック競合の代表例は ORA-00054の原因と対処、デッドロックは ORA-00060デッドロック完全ガイド、シリアライズ競合は ORA-08177の原因と解決方法 が関連します。
指数バックオフ付き再試行テンプレート
再試行では、すぐに同じ処理を叩き続けるのではなく、待機時間を少しずつ増やします。複数セッションが同時に再試行すると再び衝突するため、ジッターを入れてタイミングをずらします。
DBMS_LOCK.SLEEPで待機する例
DECLARE
c_max_attempts CONSTANT PLS_INTEGER := 5;
v_attempt PLS_INTEGER := 0;
v_sleep_sec NUMBER;
BEGIN
LOOP
v_attempt := v_attempt + 1;
BEGIN
process_payment(p_payment_id => :payment_id);
COMMIT;
EXIT;
EXCEPTION
WHEN retry_errors.lock_nowait_ex
OR retry_errors.deadlock_ex
OR retry_errors.serialize_ex THEN
ROLLBACK;
IF v_attempt >= c_max_attempts THEN
logger_pkg.write_log(
p_level => 'ERROR',
p_message => 'retry limit exceeded',
p_process_key => 'payment_id=' || :payment_id,
p_sql_code => SQLCODE,
p_error_message => SQLERRM,
p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
);
RAISE;
END IF;
v_sleep_sec := POWER(2, v_attempt - 1)
+ DBMS_RANDOM.VALUE(0, 0.5);
DBMS_LOCK.SLEEP(v_sleep_sec);
END;
END LOOP;
END;
/
DBMS_LOCK.SLEEP や DBMS_RANDOM の利用権限がない環境もあります。その場合は、アプリケーション側やジョブ制御側で待機・再実行する設計に寄せます。排他制御そのものは DBMS_LOCKで二重起動防止する方法 も参考になります。
再試行してはいけない例外
恒久障害は、再試行しても成功しません。むしろログを増やし、キューを詰まらせ、原因調査を遅らせます。入力データや定義に問題がある場合は、即失敗させて再処理待ちに回します。
長時間処理で出やすい ORA-01555 は、単純リトライで隠すよりUNDO、COMMIT頻度、SQL設計を見直すべきことがあります。詳しくは ORA-01555完全ガイド と 大量データ処理のコミット頻度とUNDO最適化 を確認してください。
冪等キーで二重処理を防ぐ
再試行を安全にするには、同じ依頼を何度実行しても結果が壊れない冪等性が必要です。外部から受け取ったリクエストID、ファイル名と行番号、ジョブID、業務キーなどを一意制約にします。
リクエストIDで二重登録を防ぐ
CREATE TABLE payment_request (
request_id VARCHAR2(100) PRIMARY KEY,
order_id NUMBER NOT NULL,
amount NUMBER(12,2) NOT NULL,
status VARCHAR2(20) NOT NULL,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP
);
MERGE INTO payment_request t
USING (
SELECT :request_id AS request_id,
:order_id AS order_id,
:amount AS amount
FROM dual
) s
ON (t.request_id = s.request_id)
WHEN NOT MATCHED THEN
INSERT (request_id, order_id, amount, status)
VALUES (s.request_id, s.order_id, s.amount, 'WAITING')
WHEN MATCHED THEN
UPDATE SET updated_at = SYSTIMESTAMP;
再試行できる設計は、トランザクション境界とも密接に関係します。詳しくは PL/SQLトランザクション設計パターン を参照してください。
SAVEPOINTで任意処理だけ戻す
1つの業務トランザクションの中で、一部の任意処理だけ失敗を許容する場合は SAVEPOINT を使います。ただし、SAVEPOINTは部分COMMITではありません。最後に全体ROLLBACKすれば、すべて戻ります。
任意処理を部分ロールバックする例
BEGIN
update_order_header(:order_id);
SAVEPOINT before_optional_coupon;
BEGIN
apply_coupon(:order_id, :coupon_code);
EXCEPTION
WHEN NO_DATA_FOUND THEN
ROLLBACK TO before_optional_coupon;
logger_pkg.write_log(
p_level => 'WARN',
p_message => 'coupon not applied',
p_process_key => 'order_id=' || :order_id
);
END;
update_order_total(:order_id);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
SAVEPOINTの実務パターンは SAVEPOINTで部分ロールバックを実装する完全ガイド、ネストブロックとの組み合わせは PL/SQLネストブロック完全ガイド が関連します。
大量処理はDBMS_ERRLOGとFORALLで失敗行を分離する
大量データ処理で1件失敗しただけで全体を止めたくない場合は、例外を行単位に分離します。SQL一発のINSERT/MERGEなら DBMS_ERRLOG、PL/SQL配列処理なら FORALL SAVE EXCEPTIONS が候補です。
DBMS_ERRLOGで失敗行を退避する
INSERT INTO target_orders(order_id, customer_id, amount)
SELECT order_id, customer_id, amount
FROM staging_orders
WHERE load_id = :load_id
LOG ERRORS INTO err$_target_orders ('LOAD_20260528')
REJECT LIMIT UNLIMITED;
COMMIT;
FORALL SAVE EXCEPTIONSで個別失敗を拾う
BEGIN
FORALL i IN 1 .. l_orders.COUNT SAVE EXCEPTIONS
UPDATE orders
SET status = 'CANCELED'
WHERE order_id = l_orders(i).order_id;
EXCEPTION
WHEN OTHERS THEN
FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
INSERT INTO batch_error_log(
error_index,
error_code,
error_message
) VALUES (
SQL%BULK_EXCEPTIONS(i).ERROR_INDEX,
SQL%BULK_EXCEPTIONS(i).ERROR_CODE,
SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE)
);
END LOOP;
RAISE;
END;
詳しくは DBMS_ERRLOGとLOG ERRORSの使い方、FORALL SAVE EXCEPTIONSで失敗行を個別ログ化する方法、Oracle BULK COLLECT / FORALL完全ガイド を参照してください。
外部APIの再試行は副作用を分離する
外部API、メール送信、ファイル出力は、DBトランザクションのROLLBACKでは取り消せません。DB更新と外部送信を同じ処理の中で直接行うと、DBはROLLBACKされたのに外部送信だけ済んだ、またはその逆が起こります。この場合は、アウトボックスや送信キューを作り、DB内には送信予定をCOMMITしてから別処理が送信します。
アウトボックスへ送信予定を残す
BEGIN
UPDATE orders
SET status = 'CONFIRMED'
WHERE order_id = :order_id;
INSERT INTO event_outbox(
event_id,
event_type,
aggregate_id,
request_id,
payload_json,
status,
retry_count,
next_retry_at,
created_at
) VALUES (
event_seq.NEXTVAL,
'ORDER_CONFIRMED',
:order_id,
:request_id,
:payload_json,
'WAITING',
0,
SYSTIMESTAMP,
SYSTIMESTAMP
);
COMMIT;
END;
PL/SQLからREST APIを呼ぶ場合は UTL_HTTPでREST API連携する方法、API向けの例外レスポンス設計は 例外をJSON形式で返すREST対応API設計 が関連します。
DBMS_SCHEDULERで再実行を運用する
バッチや外部送信の再試行は、PL/SQL内のループだけで抱え込まず、DBMS_SCHEDULER やジョブ管理テーブルへ逃がす方が運用しやすいことがあります。即時リトライは短時間の揺らぎに、スケジューラ再実行は数分後・数十分後の再処理に向いています。
再送キューを処理する例
BEGIN
FOR r IN (
SELECT event_id
FROM event_outbox
WHERE status = 'WAITING'
AND next_retry_at <= SYSTIMESTAMP
ORDER BY event_id
FOR UPDATE SKIP LOCKED
) LOOP
BEGIN
send_event(r.event_id);
UPDATE event_outbox
SET status = 'SENT',
sent_at = SYSTIMESTAMP
WHERE event_id = r.event_id;
EXCEPTION
WHEN OTHERS THEN
UPDATE event_outbox
SET retry_count = retry_count + 1,
next_retry_at = SYSTIMESTAMP + NUMTODSINTERVAL(5, 'MINUTE'),
last_error = SQLERRM
WHERE event_id = r.event_id;
END;
END LOOP;
COMMIT;
END;
ジョブ管理は DBMS_SCHEDULERでジョブ管理を極める、Oracle側の基本は DBMS_SCHEDULER完全ガイド、実行履歴の集中管理は ジョブ実行履歴の集中管理と異常検知 が関連します。
例外ログは本体処理と分けて残す
失敗時にROLLBACKすると、同じトランザクション内のログも消えます。障害調査ログだけは残したい場合、ログ専用の自律トランザクションを使います。ただし、本体データ更新を自律トランザクションへ逃がすのは不整合の原因です。
バックトレース付きログ
EXCEPTION
WHEN OTHERS THEN
logger_pkg.write_log(
p_level => 'ERROR',
p_message => 'order import failed',
p_process_key => 'load_id=' || :load_id,
p_sql_code => SQLCODE,
p_error_message => SQLERRM,
p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
);
ROLLBACK;
RAISE;
ログ・トレースの設計は インストゥルメンテーション設計、自律トランザクションは AUTONOMOUS_TRANSACTIONで独立処理を実装する完全ガイド が関連します。エラー箇所の読み方は ORA-06512の原因と読み方 も参考になります。
WHEN OTHERSの正しい使い方
WHEN OTHERS は悪ではありません。問題は、握りつぶすことです。最上位ではログを残し、必要に応じてROLLBACKし、原則として RAISE で呼び出し元へ失敗を伝えます。
握りつぶさない例外処理
BEGIN
run_business_process(:request_id);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
logger_pkg.write_log(
p_level => 'ERROR',
p_message => 'business process failed',
p_process_key => 'request_id=' || :request_id,
p_sql_code => SQLCODE,
p_error_message => SQLERRM,
p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
);
RAISE;
END;
エラーを業務コードへ変換して返す場合も、内部ログには元の SQLCODE とバックトレースを残します。
避けたい設計
本番前チェックリスト
- 例外を一時障害と恒久障害に分類している
- 再試行対象のORAコードが明確で、最大試行回数がある
- 再試行前にROLLBACKまたはSAVEPOINT戻しを行っている
- 冪等キーにより二重登録・二重送信を防いでいる
- 外部APIやメール送信はアウトボックスやキューに分離している
- 大量処理では失敗行をDBMS_ERRLOGやFORALL SAVE EXCEPTIONSで分離している
- WHEN OTHERSでログ後にRAISEしている
- SQLCODE、SQLERRM、FORMAT_ERROR_BACKTRACE、処理キーをログに残している
- ジョブ再実行時に前回成功分を重複処理しない
- 再試行不能になった処理を人間が再処理できる状態にしている
まとめ
PL/SQLの例外設計では、例外を捕まえることよりも、捕まえた後にどう扱うかが重要です。再試行できる一時障害、再試行すべきでない恒久障害、部分失敗として継続できる行単位エラー、外部副作用を伴う失敗を分けて設計します。
再試行は、冪等キー、ロールバック範囲、最大回数、待機時間、ログ、運用通知がそろって初めて安全になります。握りつぶさず、無限に回さず、失敗しても再実行できる状態を残すことが、大規模PL/SQLで強い例外設計です。

