PL/SQLでバッチ処理やAPI実装を書いていると「失敗行だけ取り消して、他の処理は続けたい」という要件に必ず出会います。たとえば1万件のCSVを取り込む処理で1件だけ重複エラーになったとき、ROLLBACKで全件取り消したら他の9999件の処理が無駄になります。これを解決する仕組みがSAVEPOINTとROLLBACK TO SAVEPOINTによる部分ロールバックです。
ただしSAVEPOINTは誤解されがちな機能でもあります。「ネストトランザクションのようなもの」と説明されることが多いですが、Oracleのトランザクションはネストしません。SAVEPOINTは同じトランザクション内で「戻り先のしおり」を打つ仕組みであり、AUTONOMOUS_TRANSACTIONとは根本的に異なります。この違いを理解せず使うと「ROLLBACK TOしたのにロックが解放されない」「カーソルがクリアされて続行できない」といった事故になります。
この記事ではSAVEPOINTの内部動作・実戦パターン・制限事項を徹底解説します。基本構文から始めて、ROLLBACK TOの正確な挙動、業務シナリオ別の高度パターン(行単位エラー隔離・Saga・分割API)、JDBC連携、SAVEPOINT vs AUTONOMOUS_TRANSACTION の使い分け、カーソル・ロックの保持挙動、アンチパターン、FAQまで2026年版で整理します。
この記事でわかること
- SAVEPOINTの内部動作(UNDOセグメントとSCNの仕組み)
- ROLLBACK TO実行時に「何が戻り、何が残るか」の正確な理解
- 同名SAVEPOINTの上書きと階層管理パターン
- 失敗行だけ隔離するループ処理の標準実装
- 例外処理(EXCEPTION句)との統合テンプレート
- Sagaパターンでの分散トランザクション風の実装
- JDBC/JavaのConnection.setSavepoint()との連携
- SAVEPOINT vs AUTONOMOUS_TRANSACTION の決定的違いと使い分け
- SAVEPOINT後にカーソル・ロック・FORALLがどうなるかの制限事項
- 本番で踏むアンチパターン6選
30秒でわかるSAVEPOINTの結論
忙しい読者向けの結論先出しです。
| 結論 | 理由・効果 |
|---|---|
| ① SAVEPOINTは同一トランザクション内のしおり | ネストトランザクションではない。COMMIT前の戻り先機能 |
② ROLLBACK TO sp1でsp1以降のDMLだけ取消 |
sp1以前は残る。トランザクション全体は継続中 |
| ③ ループの行単位エラー隔離は各反復先頭でSAVEPOINT | 失敗した1行だけ取消し、ログを記録して次へ |
| ④ 同名SAVEPOINTは上書きされる | 戻り先は最後のSAVEPOINTの位置になる |
| ⑤ DDL/TRUNCATEで全SAVEPOINTが消える | 暗黙コミットでトランザクション境界がリセット |
| ⑥ ROLLBACK TO 後はカーソルがクリアされる | FOR LOOP中のカーソルがROLLBACKで失効する罠 |
| ⑦ 別トランザクションが必要ならAUTONOMOUS_TRANSACTION | SAVEPOINTは「同一トランザクション内」、独立性が必要なら別機能 |
SAVEPOINTの内部動作|UNDO・SCN・ロックの仕組み
SAVEPOINTを「ROLLBACK の戻り先」と表面的に理解するだけでは実装で罠を踏みます。Oracleの内部でSAVEPOINTが何をしているかを把握しましょう。
SAVEPOINT実行時に何が起きるか
SAVEPOINTを宣言すると、Oracleは現在のSCN(System Change Number、トランザクションの順序を表す内部時計)とUNDOセグメントの位置情報をマーカーとして保持します。実体としてはセッション内のメタデータ更新だけで、テーブルへの書き込みもCOMMITも一切発生しません。極めて軽量な操作なので、ループ内で毎反復SAVEPOINTを打ってもパフォーマンスへの影響はほぼ皆無です。
ROLLBACK TO実行時に何が起きるか
SAVEPOINT以降に発生したDMLのUNDO情報を逆順に適用してその時点のデータ状態を復元します。トランザクション自体は終わっていないので、取得済みのロックや内部状態は基本的に維持されます。ただしROLLBACK TO 以降に取得した行ロックは解放されます。一方、SAVEPOINT前から保持しているロックは残り続けます。
SAVEPOINTで保持されないもの
「すべてが綺麗に戻る」と思うと事故ります。SAVEPOINTで戻らない/消えるものを把握してください。①カーソルの位置(FOR LOOP内のカーソルはROLLBACKでクリアされる)、②パッケージ変数(PL/SQL変数は戻らない)、③シーケンスNEXTVAL(採番済み番号は戻らない)、④DDLの結果(暗黙コミットで全SAVEPOINTが消える)。これらは別途自分で管理する必要があります。
SCNの観点で見るSAVEPOINT:SCNはトランザクション内のすべてのDMLに番号を振っており、SAVEPOINTはその番号の特定の値をマーカーとして覚えるだけです。ROLLBACK TOは「このSCN以降のDMLをUNDOで戻す」という処理。この理解があると「なぜシーケンス番号は戻らないのか」(シーケンスはトランザクション外で動くから)のような疑問が論理的に解けます。
ROLLBACK TO の正確な挙動|何が戻り、何が残るか
「ROLLBACK TO で部分的に戻る」と説明されますが、具体的に何が戻り何が残るかを正確に把握していないと本番で予想外の挙動に遭遇します。実例で確認しましょう。
-- 準備
CREATE TABLE t(id NUMBER, val VARCHAR2(20));
CREATE SEQUENCE seq_t START WITH 100 INCREMENT BY 1;
DECLARE
v_seq1 NUMBER;
v_seq2 NUMBER;
v_seq3 NUMBER;
BEGIN
-- 開始:何もない状態
INSERT INTO t VALUES (1, 'A');
v_seq1 := seq_t.NEXTVAL; -- 100が採番される
DBMS_OUTPUT.PUT_LINE('seq1=' || v_seq1);
SAVEPOINT sp1; -- ★しおり①
INSERT INTO t VALUES (2, 'B');
v_seq2 := seq_t.NEXTVAL; -- 101が採番される
DBMS_OUTPUT.PUT_LINE('seq2=' || v_seq2);
SAVEPOINT sp2; -- ★しおり②
INSERT INTO t VALUES (3, 'C');
v_seq3 := seq_t.NEXTVAL; -- 102が採番される
DBMS_OUTPUT.PUT_LINE('seq3=' || v_seq3);
-- sp1まで戻す
ROLLBACK TO SAVEPOINT sp1;
-- 何が残っている?
-- テーブル:(1, 'A') だけ。(2,'B') と (3,'C') は戻る
-- PL/SQL変数:v_seq1=100, v_seq2=101, v_seq3=102 全て残る
-- シーケンス:次は103から(採番済みの100,101,102は消費済み)
-- sp2:消える(sp1より新しいSAVEPOINTは無効化)
-- sp1:残る(同じSAVEPOINTには何度でも戻れる)
-- 続行可能:トランザクションは継続中
INSERT INTO t VALUES (4, 'D');
COMMIT;
END;
/
ROLLBACK TO で「戻るもの/残るもの」一覧
| 対象 | 挙動 | 備考 |
|---|---|---|
| テーブルのDML(INSERT/UPDATE/DELETE) | 戻る | UNDO情報で復元 |
| PL/SQL変数の値 | 残る | PL/SQLメモリは戻らない。手動で再代入が必要 |
| シーケンスNEXTVAL | 残る(消費済) | 採番した番号は戻らない |
| SAVEPOINT以降に取得した行ロック | 解放 | ROLLBACK対象のロックは消える |
| SAVEPOINT前から保持の行ロック | 保持 | トランザクションは継続中 |
| ループ中のカーソル | クリア | FOR LOOP途中のROLLBACK TOで挙動不定 |
| SAVEPOINTより新しいSAVEPOINT | 消失 | sp2を打った後ROLLBACK TO sp1するとsp2は無効化 |
| SAVEPOINTそれ自体 | 残る | 同じ点に何度でもROLLBACK可能 |
| パッケージステート | 残る | PL/SQL変数と同様にDBMSの管理外 |
カーソルクリアの罠:FOR rec IN cur LOOP ... ROLLBACK TO ... END LOOP;のようにループ内でROLLBACK TO(セーブポイントなしのROLLBACK含む)するとカーソルがクローズされて以降のFETCHが失敗します。PL/SQLレベルのFOR LOOPは多くの場合自動再オープンしますが、明示的なOPEN/FETCH/CLOSEパターンでは要注意。安全策として「ROLLBACK TOはBEGIN〜END副ブロックで例外捕捉時のみ使う」パターンを徹底してください。
業務シナリオ別の実戦パターン
SAVEPOINTを使う典型シナリオを4つ紹介します。いずれも実務で頻出する設計で、テンプレートとしてそのまま流用可能です。
パターン1|ループ内の行単位エラー隔離
1万件のCSV取り込み・APIバッチ処理など、1行失敗しても全体は止めたくない処理の標準形です。各反復の先頭でSAVEPOINTを打ち、例外時はその行だけROLLBACKしてログ記録します。
DECLARE
v_ok PLS_INTEGER := 0;
v_ng PLS_INTEGER := 0;
BEGIN
FOR rec IN (SELECT id, name, email FROM staging_data) LOOP
SAVEPOINT before_row;
BEGIN
INSERT INTO customers(id, name, email)
VALUES(rec.id, rec.name, rec.email);
v_ok := v_ok + 1;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
ROLLBACK TO before_row;
INSERT INTO err_log(staging_id, reason, ts)
VALUES(rec.id, 'duplicate', SYSTIMESTAMP);
v_ng := v_ng + 1;
WHEN OTHERS THEN
ROLLBACK TO before_row;
INSERT INTO err_log(staging_id, reason, ts)
VALUES(rec.id, SUBSTR(SQLERRM,1,200), SYSTIMESTAMP);
v_ng := v_ng + 1;
END;
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('OK=' || v_ok || ' / NG=' || v_ng);
END;
/
パターン2|複数ステップの段階的ロールバック
「処理1→処理2→処理3」のステップで、処理2で失敗したら処理1の結果は残し、処理2だけ取り消すという業務要件で活用するパターンです。
CREATE OR REPLACE PROCEDURE multi_step_process(
p_order_id IN NUMBER
) AS
e_validation_failed EXCEPTION;
BEGIN
-- ステップ1:注文の在庫予約(必ず成功させたい)
pkg_inventory.reserve(p_order_id);
SAVEPOINT after_step1;
-- ステップ2:与信チェック(失敗したら2だけ取消、1は残す)
BEGIN
pkg_credit.check_and_freeze(p_order_id);
EXCEPTION
WHEN pkg_credit.e_credit_exceeded THEN
ROLLBACK TO after_step1;
pkg_log.warn('credit failed for order=' || p_order_id);
RAISE e_validation_failed;
END;
-- ステップ3:通知(失敗しても致命的ではない)
BEGIN
pkg_notify.send(p_order_id);
EXCEPTION
WHEN OTHERS THEN
pkg_log.warn('notify failed for order=' || p_order_id);
-- 通知失敗は無視して続行(ROLLBACKしない)
END;
COMMIT;
EXCEPTION
WHEN e_validation_failed THEN
-- ステップ1だけCOMMIT or ROLLBACK判断
-- 在庫予約も含めて全部取り消すなら:
ROLLBACK;
RAISE_APPLICATION_ERROR(-20001, 'validation failed');
END;
/
パターン3|Sagaパターン(補償トランザクション)
分散トランザクションが使えない環境で「失敗時に逆操作で補償する」Sagaパターンを実装するときもSAVEPOINTが活きます。各ステップの前にSAVEPOINTを打ち、失敗時に直近まで戻して補償処理を走らせる形です。
CREATE OR REPLACE PROCEDURE saga_book_travel(
p_user_id IN NUMBER, p_trip_id IN NUMBER
) AS
e_rollback_needed EXCEPTION;
BEGIN
-- 1. ホテル予約
pkg_hotel.book(p_user_id, p_trip_id);
SAVEPOINT after_hotel;
-- 2. 飛行機予約
BEGIN
pkg_flight.book(p_user_id, p_trip_id);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO after_hotel;
pkg_hotel.cancel(p_user_id, p_trip_id); -- 補償
RAISE e_rollback_needed;
END;
SAVEPOINT after_flight;
-- 3. 決済
BEGIN
pkg_payment.charge(p_user_id, p_trip_id);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO after_flight;
pkg_flight.cancel(p_user_id, p_trip_id); -- 補償
pkg_hotel.cancel(p_user_id, p_trip_id); -- 補償
RAISE e_rollback_needed;
END;
COMMIT;
EXCEPTION
WHEN e_rollback_needed THEN
ROLLBACK;
RAISE_APPLICATION_ERROR(-20002, 'saga failed, compensated');
END;
/
パターン4|複数の独立処理を一括実行(部分成功OK)
「10個のジョブを順次実行、失敗したジョブだけスキップして成功した分はCOMMIT」というパターン。各ジョブ前にSAVEPOINTを打ち、例外時にROLLBACK TOで巻き戻して次のジョブへ進みます。
CREATE OR REPLACE PROCEDURE run_independent_jobs AS
v_succeeded PLS_INTEGER := 0;
v_failed PLS_INTEGER := 0;
BEGIN
FOR rec IN (SELECT job_id, job_proc FROM job_queue
WHERE status = 'PENDING'
ORDER BY priority DESC) LOOP
SAVEPOINT before_job;
BEGIN
EXECUTE IMMEDIATE 'BEGIN ' || rec.job_proc || '; END;';
UPDATE job_queue SET status = 'DONE'
WHERE job_id = rec.job_id;
v_succeeded := v_succeeded + 1;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO before_job;
UPDATE job_queue
SET status = 'FAILED',
last_err = SUBSTR(SQLERRM,1,500),
failed_at = SYSTIMESTAMP
WHERE job_id = rec.job_id;
v_failed := v_failed + 1;
END;
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('Succeeded: ' || v_succeeded || ' / Failed: ' || v_failed);
END;
/
JDBC/Java側のSavepoint連携
JavaやAPサーバから呼び出される処理では、JDBCのConnection.setSavepoint()でJava側からもSAVEPOINTを制御できます。Spring等のフレームワークでは@Transactional(propagation=NESTED)が内部的にJDBCのSavepointを使ってネスト実装されています。
import java.sql.*;
try (Connection conn = DriverManager.getConnection(URL, USER, PASS)) {
conn.setAutoCommit(false);
try (PreparedStatement ps1 = conn.prepareStatement(
"INSERT INTO customers VALUES(?, ?)")) {
ps1.setLong(1, 100);
ps1.setString(2, "Alice");
ps1.executeUpdate();
}
// ★ JavaからSavepointを設定
Savepoint sp1 = conn.setSavepoint("after_customer");
try (PreparedStatement ps2 = conn.prepareStatement(
"INSERT INTO orders VALUES(?, ?)")) {
ps2.setLong(1, 1001);
ps2.setLong(2, 100);
ps2.executeUpdate();
} catch (SQLException e) {
// 注文だけ取消し、顧客は残す
conn.rollback(sp1);
}
conn.commit();
}
SpringのNESTED伝播を使う場合、@Transactional(propagation=Propagation.NESTED)を付けたメソッドの入口で自動的にSAVEPOINTが打たれ、例外発生時にそのSAVEPOINTにROLLBACKされます。実装としてはJDBCのsetSavepoint()がそのまま使われており、PL/SQL側のSAVEPOINTとも互換的に動作します。ネストされたサービスメソッドの個別失敗を吸収する設計に向きます。
SAVEPOINT vs AUTONOMOUS_TRANSACTION|決定的な違いと使い分け
「親のトランザクションに影響を与えずに処理したい」要件でSAVEPOINTとAUTONOMOUS_TRANSACTIONはよく混同されますが、本質的に役割が異なります。正しく使い分けないと意図しない動作になります。
SAVEPOINT|同一トランザクション内のしおり
同じトランザクション内で戻り先を決める機能。親と子は同じトランザクションを共有しているため、ROLLBACKで全体が消えます。「途中の処理だけ取り消したいが全体としては1つのトランザクション」という場面に適します。
AUTONOMOUS_TRANSACTION|独立した別トランザクション
PRAGMA AUTONOMOUS_TRANSACTIONで宣言すると、そのプロシージャは呼び出し元と完全に独立した別トランザクションで動きます。親がROLLBACKされても自分のCOMMITは生き残り、その逆もまた然り。監査ログ・エラー記録など「親が失敗しても残したい」処理に使います。詳細はAUTONOMOUS TRANSACTIONで独立した処理を行う方法を参照してください。
判断基準
- 「処理途中の一部だけ取消、続きは同じトランザクションで進める」→ SAVEPOINT
- 「親が失敗しても必ず残したい記録/別系統の処理」→ AUTONOMOUS_TRANSACTION
- 「親の進行中にデータを別経路で確認したい(ダーティリードしたい)」→ AUTONOMOUS_TRANSACTION(READ_COMMITTED分離レベル)
- 「失敗時の補償処理を呼び出すため独立トランザクションが必要」→ AUTONOMOUS_TRANSACTION(先述のSagaパターンとは別軸)
混同で起きる事故:①「失敗時に必ず記録したいログ」をSAVEPOINTで実装→親のROLLBACKでログまで消えてしまう、②「途中状態を取消したい」をAUTONOMOUS_TRANSACTIONで実装→別トランザクションなので親側からは見えない・親のロックも持ち越せない、などの典型的な誤実装。「同一トランザクションか別トランザクションか」を最初に判定してから機能を選んでください。
本番で踏むアンチパターン6選
① DDLを混在させてSAVEPOINTを失う
SAVEPOINTを打った後にCREATE/ALTER/TRUNCATEなどのDDLを実行すると暗黙コミットが発火して全SAVEPOINTが消えます。部分ロールバック設計のなかでDDLを実行する処理が必要なら別ジョブ・別トランザクションに切り出してください。6383(COMMIT/ROLLBACKの正しい使い方)の暗黙コミット節も合わせて確認してください。
② パッケージ変数の整合性を忘れる
ROLLBACK TOではDBの状態は戻りますがPL/SQL変数・パッケージ変数の値は戻りません。「失敗時にパッケージ変数の状態が中途半端」という事故が発生します。ROLLBACK後はパッケージ変数を明示的に再初期化するか、そもそもパッケージ変数に依存する設計を避けるのが安全です。
③ ループ中のFOR LOOP内でROLLBACK TO
カーソルがクリアされる挙動と相まって、FOR LOOPのカウンタが不定になる場合があります。行単位の例外隔離はループの中にBEGIN〜END副ブロックを作ってその中でROLLBACK TOする形が正解です。ループ自体のカーソル変数とROLLBACK TOを直接組み合わせないでください。
④ 同名SAVEPOINTを意図せず上書き
同じ名前で複数回SAVEPOINTすると最後の位置に上書きされ、初期に打ったしおりが失われます。SAVEPOINT名は意味のあるユニークな名前を付け、階層的な処理ではsp_step1/sp_step2のように番号付きで区別してください。
⑤ シーケンスNEXTVALを呼んだ後にROLLBACK TO
シーケンスはトランザクションの外で動くため、NEXTVALで採番した番号はROLLBACK TOしても戻りません。「失敗で取り消したつもりが番号だけ消費された」状況になります。採番をやり直したい場合は別シーケンスを使うか、採番直前にSAVEPOINTを打って失敗を判別してから再採番する設計に。
⑥ AUTONOMOUS_TRANSACTIONと混同して使う
「親と独立したい」のにSAVEPOINTを使うのは目的違いの誤実装。「同一トランザクション内で戻したい」のがSAVEPOINT、「親と切り離したい」のがAUTONOMOUS_TRANSACTION。本記事の判断基準セクションを参考に、まず「同一か独立か」を決めてください。
よくある質問
AUTONOMOUS_TRANSACTIONで別トランザクションに切り出すのが定石です。SAVEPOINT sp;を3回打つと、ROLLBACK TO spは最後に打った位置に戻ります。初期のしおりは無効化されるので、異なる位置にしおりが必要なら必ず別名で打ってください。ROLLBACK TO sp1でsp1まで戻った後のROLLBACKはトランザクション開始時点まで戻すのでsp1以前の変更も消えます。部分ロールバックと全体ロールバックは別物として理解してください。FORALL ... SAVE EXCEPTIONSを使い、失敗行をSQL%BULK_EXCEPTIONSから取得して個別処理します。SAVEPOINTとは別物の機能です。ROLLBACK TOを書かないと「予期しない中途半端なデータが見える」状態になりがち。EXCEPTION句では必ずROLLBACK TO sp_xxxかRAISEのどちらかを書く規約を徹底してください。握りつぶし+無対処は最悪の組み合わせです。関連記事で深掘りする
SAVEPOINTに関連する周辺技術もあわせて押さえておきましょう。
- 【PL/SQL】COMMITとROLLBACKの正しい使い方(トランザクション境界とコーディング規約)
- 【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法(独立トランザクションとの使い分け)
- 【Oracle】トランザクション完全ガイド(COMMIT/ROLLBACK/SAVEPOINT/分離レベル全般)
- 【PL/SQL】ネストブロック完全ガイド(スコープと例外伝播でSAVEPOINTを併用)
- 【PL/SQL】例外処理完全ガイド(EXCEPTION句との統合)
- 【PL/SQL】大量データの一括処理におけるコミット頻度とUNDO最適化(ループでのSAVEPOINT併用)
- 【PL/SQL】バルク処理完全ガイド(FORALL SAVE EXCEPTIONSとの違い)
- 【PL/SQL】MERGE文でUPSERTを高速・安全に実装(LOG ERRORS INTOとSAVEPOINTの使い分け)
- 【PL/SQL】パッケージ設計でコード管理と再利用性を極める(層別のトランザクション境界設計)
- 【Oracle】ORA-01555: スナップショットが古すぎます完全ガイド(長時間トランザクションの罠)
まとめ|SAVEPOINTで耐障害性の高いトランザクション設計を実装
SAVEPOINTは「失敗しても先へ進む」堅牢なトランザクション設計の中核です。内部動作(UNDO・SCN)の理解、ROLLBACK TOの正確な挙動、業務シナリオ別のパターン、AUTONOMOUS_TRANSACTIONとの使い分けを押さえれば、整合性を保ちながら部分失敗を許容する設計が組めます。本記事の要点を7つに集約します。
- SAVEPOINTは同一トランザクション内のしおり。AUTONOMOUS_TRANSACTIONと別物
- ROLLBACK TOで戻るのはDMLだけ。PL/SQL変数・シーケンスは戻らない
- 行単位エラー隔離は各反復先頭でSAVEPOINT+BEGIN〜END副ブロック
- 段階的ロールバックでステップ単位の失敗を吸収する設計が組める
- Sagaパターンで補償処理と組み合わせて分散風実装が可能
- JDBCのsetSavepoint()/SpringのNESTED伝播で言語側からも制御できる
- DDL混在で全SAVEPOINTが消える、カーソルクリアの罠など制限事項を理解
レガシーコードで「ROLLBACKで全部消えてバッチがやり直しになる」事故が頻発しているなら、SAVEPOINTで行単位エラー隔離に置き換えるだけで運用負荷が大きく下がります。本記事の4パターンを実装テンプレとして自プロジェクトのバッチ処理を見直してみてください。

