PL/SQLのトランザクション設計で大事なのは、COMMIT と ROLLBACK を呼ぶ場所だけではありません。どこからどこまでを1つの業務単位にするか、どの順番でロックするか、途中失敗をどう扱うか、再実行しても二重処理にならないかまで含めて設計する必要があります。
単純な登録処理なら1回の COMMIT で十分です。しかし、受注、在庫引当、請求、外部連携、バッチ投入、監査ログのように複数の更新が絡むと、設計を間違えた瞬間に不整合、デッドロック、ロック待ち、再処理不能な中途半端データが発生します。基本仕様は Oracleのトランザクション完全ガイド、PL/SQLでの境界設計は COMMITとROLLBACKの正しい使い方 もあわせて確認してください。
- COMMIT境界を業務単位で決める考え方
- 制約・一意キー・外部キーを中心に整合性を守る設計
SELECT FOR UPDATE、NOWAIT、SKIP LOCKEDの使い分けSAVEPOINT、DBMS_ERRLOG、FORALL SAVE EXCEPTIONSによる部分失敗の扱い- 監査ログ、アウトボックス、冪等キー、再処理設計
- デッドロック、ロック待ち、ORA-01555を避ける実務チェック
- トランザクション設計の結論
- 基本形:最上位だけでCOMMITする
- 共通部品ではCOMMITしない
- 制約ファーストで整合性を守る
- 設計判断早見リスト
- SELECT FOR UPDATEで更新対象を固定する
- ロック順序を固定してデッドロックを防ぐ
- SAVEPOINTで部分的に戻す
- 大量DMLはDBMS_ERRLOGで失敗行を分離する
- FORALL SAVE EXCEPTIONSで配列処理の失敗を拾う
- AUTONOMOUS_TRANSACTIONは監査ログに限定する
- 外部連携はアウトボックスで分離する
- バッチのCOMMIT間隔は件数だけで決めない
- 冪等性を持たせて再実行できるようにする
- 分離レベルは必要な場面だけ変更する
- 例外処理ではROLLBACKしてRAISEする
- 使ってよい設計・避けたい設計
- 本番前チェックリスト
- まとめ
トランザクション設計の結論
PL/SQLでは、原則として呼び出し元が業務トランザクションの境界を決める設計にします。下位の共通プロシージャが勝手に COMMIT すると、呼び出し元は一連の業務処理をまとめて取り消せなくなります。そのため、登録・更新・検証の部品はCOMMITしない、画面処理やジョブの最上位だけがCOMMITする、という規約を先に決めます。
基本形:最上位だけでCOMMITする
最も安全な形は、業務処理の入口で例外をまとめて受け、成功時だけ COMMIT、失敗時は ROLLBACK する構成です。下位プロシージャはDMLだけを実行し、確定・取消は行いません。
業務処理の入口で確定する例
CREATE OR REPLACE PROCEDURE register_order(
p_order_id IN NUMBER
) AS
BEGIN
validate_order(p_order_id);
insert_order_header(p_order_id);
insert_order_detail(p_order_id);
reserve_stock(p_order_id);
write_order_history(p_order_id);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
/
この形なら、在庫だけ更新されて受注が作られない、履歴だけ残って本体がない、といった中途半端な状態を避けられます。一方で、処理時間が長い場合はロック保持時間も長くなるため、どこまでを同じ業務単位にするかは慎重に決めます。
共通部品ではCOMMITしない
共通部品の中で COMMIT してしまうと、呼び出し元は全体をロールバックできません。これはPL/SQLで最もよくあるトランザクション設計ミスです。
避けたい共通部品の例
CREATE OR REPLACE PROCEDURE update_customer_rank(
p_customer_id IN NUMBER
) AS
BEGIN
UPDATE customers
SET rank_code = 'A'
WHERE customer_id = p_customer_id;
-- NG: 共通部品が勝手に確定している
COMMIT;
END;
/
共通部品は、成功・失敗を呼び出し元へ返すだけにします。どうしても単体実行用の確定処理が必要なら、業務用プロシージャと単体実行用ラッパーを分けます。
確定するラッパーを分ける例
CREATE OR REPLACE PROCEDURE update_customer_rank_core(
p_customer_id IN NUMBER
) AS
BEGIN
UPDATE customers
SET rank_code = 'A'
WHERE customer_id = p_customer_id;
END;
/
CREATE OR REPLACE PROCEDURE update_customer_rank_job(
p_customer_id IN NUMBER
) AS
BEGIN
update_customer_rank_core(p_customer_id);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
/
制約ファーストで整合性を守る
PL/SQLだけで整合性を守ろうとすると、別セッションからの同時更新に弱くなります。一意性、参照整合性、必須項目、状態遷移の一部は、可能な限りテーブル制約で守ります。PL/SQLは制約違反を捕捉して、業務メッセージやログに変換する役割に寄せます。
ALTER TABLE orders ADD CONSTRAINT uk_orders_request
UNIQUE (request_id);
ALTER TABLE order_details ADD CONSTRAINT fk_order_details_header
FOREIGN KEY (order_id) REFERENCES orders(order_id);
ALTER TABLE orders ADD CONSTRAINT ck_orders_status
CHECK (status IN ('NEW', 'RESERVED', 'CANCELED', 'SHIPPED'));
再実行される可能性がある処理では、リクエストIDや外部連携IDなどの冪等キーを一意制約にします。これにより、同じ依頼が2回届いても二重登録ではなく、既存処理として扱えます。
設計判断早見リスト
トランザクション設計では、機能ごとに使う技術を選びます。以下は実務で迷いやすい場面の判断軸です。
COMMIT は最上位に置きます。途中で部分確定せず、失敗時は全体を ROLLBACK します。SAVEPOINT、DBMS_ERRLOG、FORALL SAVE EXCEPTIONS を検討します。件単位の再処理キーも残します。SELECT FOR UPDATE を使います。待たせるのか、即エラーにするのか、スキップするのかを業務要件で選びます。DBMS_LOCK による排他制御を検討します。AUTONOMOUS_TRANSACTION を使います。本体データ更新には使いません。SELECT FOR UPDATEで更新対象を固定する
在庫引当や残高更新のように、現在値を読んでから更新する処理では、対象行を先にロックします。読み取り後に別セッションが更新してしまうと、古い値を前提にした処理になるためです。
読み取り直後に行ロックする例
DECLARE
v_stock_qty stock_items.stock_qty%TYPE;
BEGIN
SELECT stock_qty
INTO v_stock_qty
FROM stock_items
WHERE item_id = :item_id
FOR UPDATE;
IF v_stock_qty < :order_qty THEN
RAISE_APPLICATION_ERROR(-20001, '在庫不足です');
END IF;
UPDATE stock_items
SET stock_qty = stock_qty - :order_qty
WHERE item_id = :item_id;
END;
ロック待ちを避けたい画面処理では NOWAIT、キュー処理では SKIP LOCKED が候補になります。ロック競合のエラーとしては ORA-00054の原因と対処、セッションロックの調査は DBMS_LOCKで二重起動防止する方法 も参考になります。
NOWAITとSKIP LOCKEDの使い分け
-- 画面: 他ユーザーが編集中ならすぐ戻す SELECT * FROM orders WHERE order_id = :order_id FOR UPDATE NOWAIT; -- ジョブ: ロックされていない待ち行だけ処理する SELECT request_id FROM send_queue WHERE status = 'WAITING' ORDER BY request_id FOR UPDATE SKIP LOCKED;
ロック順序を固定してデッドロックを防ぐ
複数テーブルや複数キーを更新する場合は、全処理で同じ順番にロックします。A処理は顧客を先にロックし、B処理は受注を先にロックする、という設計はデッドロックの典型です。
-- 全処理で「顧客 -> 受注 -> 明細」の順に固定する SELECT * FROM customers WHERE customer_id = :customer_id FOR UPDATE; SELECT * FROM orders WHERE order_id = :order_id FOR UPDATE; SELECT * FROM order_details WHERE order_id = :order_id FOR UPDATE;
デッドロックはOracleが検出して片方を失敗させますが、アプリ側から見ると突然処理が落ちたように見えます。原因調査と予防策は ORA-00060デッドロックの完全ガイド にまとめています。
SAVEPOINTで部分的に戻す
1つの大きな業務トランザクションの中で、一部だけやり直したい場合は SAVEPOINT を使います。ただし、SAVEPOINTはCOMMITではありません。最後にROLLBACKすれば、SAVEPOINT以降だけでなく全体を戻すこともできます。
任意処理だけ取り消す例
BEGIN
update_order_header(:order_id);
SAVEPOINT before_optional_coupon;
BEGIN
apply_coupon(:order_id, :coupon_code);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO before_optional_coupon;
write_warning_log(:order_id, SQLERRM);
END;
update_order_total(:order_id);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
部分ロールバックの実装パターンは SAVEPOINTで部分ロールバックを実装する完全ガイド で詳しく扱っています。ネストブロックと組み合わせる場合は PL/SQLネストブロック完全ガイド も有効です。
大量DMLはDBMS_ERRLOGで失敗行を分離する
大量INSERTやUPDATEで、1件のエラーだけで全件を止めたくない場合は DBMS_ERRLOG と LOG ERRORS を使います。これはSQL単位で失敗行をエラーテーブルへ退避し、成功行は処理できる仕組みです。
BEGIN
DBMS_ERRLOG.CREATE_ERROR_LOG(
dml_table_name => 'ORDERS',
err_log_table_name => 'ERR$_ORDERS'
);
END;
/
INSERT INTO orders(order_id, customer_id, order_date, amount)
SELECT order_id, customer_id, order_date, amount
FROM staging_orders
LOG ERRORS INTO err$_orders ('IMPORT_20260528')
REJECT LIMIT UNLIMITED;
エラー行を後から再処理する設計まで含めるなら、DBMS_ERRLOGとLOG ERRORSの使い方 を参照してください。
FORALL SAVE EXCEPTIONSで配列処理の失敗を拾う
PL/SQL配列を使って大量DMLを行う場合は、FORALL SAVE EXCEPTIONS で失敗要素を後から確認できます。SQL一発の投入ならDBMS_ERRLOG、PL/SQL側で配列ごとに制御したいならFORALLを使う、という切り分けです。
DECLARE
TYPE t_order_ids IS TABLE OF orders.order_id%TYPE;
l_order_ids t_order_ids := t_order_ids(101, 102, 103);
BEGIN
FORALL i IN 1 .. l_order_ids.COUNT SAVE EXCEPTIONS
UPDATE orders
SET status = 'CANCELED'
WHERE order_id = l_order_ids(i);
COMMIT;
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;
ROLLBACK;
RAISE;
END;
配列DMLの実装は Oracle BULK COLLECT / FORALL完全ガイド、PL/SQL側の例外収集は FORALL SAVE EXCEPTIONSで失敗行を個別ログ化する方法 が関連します。
AUTONOMOUS_TRANSACTIONは監査ログに限定する
AUTONOMOUS_TRANSACTION は、呼び出し元とは独立してCOMMITできる強力な機能です。便利ですが、本体更新に使うと「本体はROLLBACKされたのに関連データだけ残る」という不整合を作りやすくなります。基本的には監査ログ、処理ログ、障害調査ログのような用途に限定します。
CREATE OR REPLACE PROCEDURE write_audit_log( p_message IN VARCHAR2 ) AS PRAGMA AUTONOMOUS_TRANSACTION; BEGIN INSERT INTO audit_logs(logged_at, message) VALUES (SYSTIMESTAMP, p_message); COMMIT; END; /
自律トランザクションの使いどころと危険な使い方は AUTONOMOUS_TRANSACTIONで独立処理を実装する完全ガイド で整理しています。
外部連携はアウトボックスで分離する
DB更新と外部API送信は、同じトランザクションとして完全には扱えません。DBをCOMMITした直後にAPI送信で失敗する、API送信後にDB更新がROLLBACKされる、といったズレが発生します。この問題は、送信予定をDBに記録してからCOMMITし、別ジョブが送信するアウトボックスパターンで扱います。
送信予定をDBに残してから確定する例
BEGIN
UPDATE orders
SET status = 'CONFIRMED'
WHERE order_id = :order_id;
INSERT INTO event_outbox(
event_id,
event_type,
aggregate_id,
payload,
status,
created_at
) VALUES (
event_seq.NEXTVAL,
'ORDER_CONFIRMED',
:order_id,
:payload_json,
'WAITING',
SYSTIMESTAMP
);
COMMIT;
END;
送信ジョブは WAITING の行を FOR UPDATE SKIP LOCKED で取り、成功したら SENT、失敗したらリトライ回数を増やします。これにより、DB更新と外部送信を疎結合にしつつ、再送可能な状態を保てます。
バッチのCOMMIT間隔は件数だけで決めない
バッチ処理では「1000件ごとにCOMMIT」のような実装をよく見ます。件数単位のCOMMITは悪くありませんが、業務単位をまたいで区切ると再処理が難しくなります。たとえば同じ顧客の複数明細が別トランザクションに分かれると、途中失敗時に整合性確認が複雑になります。
DECLARE
v_current_customer_id NUMBER := NULL;
BEGIN
FOR r IN (
SELECT customer_id, order_id
FROM staging_orders
ORDER BY customer_id, order_id
) LOOP
IF v_current_customer_id IS NOT NULL
AND v_current_customer_id <> r.customer_id THEN
COMMIT;
END IF;
process_customer_order(r.customer_id, r.order_id);
v_current_customer_id := r.customer_id;
END LOOP;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
長時間カーソルを開いたまま細かくCOMMITすると、読み取り一貫性やUNDOの問題が出ることがあります。特に fetch across commit に絡む問題は ORA-01555の原因と対処 を確認してください。
冪等性を持たせて再実行できるようにする
トランザクション設計は、失敗しないことだけを目標にすると弱くなります。実務ではネットワーク断、タイムアウト、ロック競合、ジョブ停止が起きます。大事なのは、失敗後に同じ処理をもう一度流しても壊れない設計です。
リクエストIDで二重処理を防ぐ例
MERGE INTO payment_requests 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, created_at)
VALUES (s.request_id, s.order_id, s.amount, 'WAITING', SYSTIMESTAMP)
WHEN MATCHED THEN
UPDATE SET last_seen_at = SYSTIMESTAMP;
冪等キーを持たない処理は、再実行時に二重請求、二重送信、二重在庫引当を起こします。外部から受け取ったリクエストID、業務キー、ファイル名と行番号など、再実行時にも同じ値になるキーを残します。
分離レベルは必要な場面だけ変更する
Oracleの通常の読み取り一貫性は多くの業務で十分です。安易に分離レベルを上げると、競合やリトライ設計が難しくなります。レポートや締め処理で時点を固定したい場合は、要件に応じて SET TRANSACTION READ ONLY などを検討します。
SET TRANSACTION READ ONLY; SELECT SUM(amount) FROM orders WHERE order_date >= DATE '2026-05-01' AND order_date < DATE '2026-06-01'; COMMIT;
更新処理で分離レベルを変える場合は、アプリ側のリトライ、エラー時の利用者メッセージ、ロック待ち時間までセットで決めます。
例外処理ではROLLBACKしてRAISEする
例外を握りつぶすと、呼び出し元は成功したのか失敗したのか判断できません。トランザクション境界を持つ最上位では、ログを残し、ROLLBACKし、必要なら業務例外に変換して再送出します。
CREATE OR REPLACE PROCEDURE close_month(
p_target_month IN DATE
) AS
BEGIN
validate_month(p_target_month);
create_billing(p_target_month);
update_closing_status(p_target_month);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
write_audit_log(
'close_month failed: ' || SQLERRM
);
RAISE;
END;
/
ここで注意したいのは、write_audit_log が自律トランザクションでなければ、直前のROLLBACKに巻き込まれる点です。ログだけ残したい場合は、ログ専用の自律トランザクションを検討します。
使ってよい設計・避けたい設計
本番前チェックリスト
- COMMITするプロシージャとCOMMITしないプロシージャが分かれている
- 業務キー、冪等キー、一意制約が設計されている
- 親子登録や在庫引当が1つの業務単位として扱われている
- ロックするテーブルと順序が処理間で統一されている
- ロック待ち時に待つ、即エラー、スキップのどれにするか決まっている
- 大量DMLの失敗行を後から特定できる
- バッチの再実行範囲が業務キーで判断できる
- 監査ログと本体更新のトランザクション分離が意図通りになっている
- 外部連携はCOMMIT後に再送可能な状態で処理される
- デッドロック、ORA-00054、ORA-01555の調査観点が運用手順にある
まとめ
PL/SQLのトランザクション設計は、単に COMMIT と ROLLBACK を書く作業ではありません。業務単位、制約、ロック、部分失敗、監査ログ、外部連携、再処理をまとめて設計してはじめて、障害時にも復旧しやすい処理になります。
基本は、共通部品ではCOMMITせず、最上位で確定することです。そのうえで、部分失敗にはSAVEPOINTやDBMS_ERRLOG、配列DMLにはFORALL SAVE EXCEPTIONS、外部連携にはアウトボックス、監査ログには必要最小限のAUTONOMOUS_TRANSACTIONを使います。失敗しない処理ではなく、失敗しても再実行できる処理を目指すのが、実務で強いトランザクション設計です。

