PL/SQLの強みはブロック構造を入れ子(ネスト)にできることです。BEGIN ... END;の中にさらにDECLARE ... BEGIN ... END;を書いて局所的な変数・例外・カーソルを切り分けられる、言わばC言語のブロックや関数のローカルスコープに相当する仕組みです。
入門記事の多くは「内側から外側の変数は見える、逆は見えない」という基本だけで終わりますが、実務では変数の隠蔽(Shadowing)をラベルで参照解決、例外が伝播する正確なルール、局所例外の再利用、ローカルサブプログラム、SAVEPOINT+ネストでの部分ロールバック、PRAGMA AUTONOMOUS_TRANSACTIONで独立トランザクション化まで押さえる必要があります。
この記事ではPL/SQLのネストブロックをスコープ理論・例外伝播・実務応用の3軸で徹底解説。ラベル付きブロック(<<name>>)、Shadowingされた外側変数へのアクセス、例外のネスト伝播ルール、ローカルサブプログラム(ブロック内定義)、ネストCursor、SAVEPOINT+ネスト構造、AUTONOMOUS_TRANSACTION、実務5パターン、アンチパターン7選まで網羅した決定版です。関連は【PL/SQL】基本構文完全ガイド/【PL/SQL】変数・定数完全ガイド/【PL/SQL】例外処理完全ガイドも併読推奨。
この記事で学べること
- ネストブロックの基本構文と可視性ルール
- ラベル付きブロック(
<<name>>)による外側変数の明示アクセス - Shadowing(変数隠蔽)の仕組みと解決方法
- 例外の伝播ルール完全版(どこで捕まらないと親へ伝わるか)
- 局所ユーザー定義例外のスコープ
- ローカルサブプログラム(ブロック内のプロシージャ/ファンクション)
- ネストしたCursorのスコープ管理
- SAVEPOINT+ネストブロックで部分ロールバック
PRAGMA AUTONOMOUS_TRANSACTIONで独立トランザクション化- ネスト深度の実用上限とリファクタリング指針
- 実務5パターン:try-catch風/リトライ/部分ロールバック/局所関数/例外の翻訳
- アンチパターン7選(深すぎるネスト/Shadowing無自覚等)
30秒クイックリファレンス:ネスト基本
<<outer>> -- 外側ラベル(省略可)
DECLARE
v_msg VARCHAR2(50) := 'outer';
BEGIN
DBMS_OUTPUT.PUT_LINE(v_msg); -- outer
<<inner>> -- 内側ラベル(省略可)
DECLARE
v_msg VARCHAR2(50) := 'inner'; -- 外側と同名(Shadowing)
BEGIN
DBMS_OUTPUT.PUT_LINE(v_msg); -- inner(内側が優先)
DBMS_OUTPUT.PUT_LINE(outer.v_msg); -- outer(ラベルで外側を明示)
EXCEPTION
WHEN OTHERS THEN
RAISE; -- 外側に伝播
END inner;
DBMS_OUTPUT.PUT_LINE(v_msg); -- outer(内側のv_msgはもう存在しない)
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('outer catch: ' || SQLERRM);
END outer;
/
3つの基本ルール:①内側のブロックは外側の変数を参照可能、②外側からは内側の変数は見えない、③同名変数は内側が優先(Shadowing)、ラベル付きブロックならラベル名.変数名で外側を参照できる。
スコープの厳密なルール
PL/SQLの識別子のスコープは静的(字面上の)スコープで、「宣言された場所からブロック終了まで有効」が原則。動的スコープ(呼び出し階層)とは違う点に注意。
可視性マトリクス
DECLARE
v_a NUMBER := 1; -- スコープ: 外側全体
BEGIN
-- v_a, v_b は参照可能
DECLARE
v_b NUMBER := 2; -- スコープ: 内側のみ
BEGIN
DBMS_OUTPUT.PUT_LINE(v_a || ',' || v_b); -- OK: 1,2
END;
-- ここで v_b は参照不可(PLS-00201)
-- DBMS_OUTPUT.PUT_LINE(v_b);
-- 兄弟ブロック:別の DECLARE で v_c を宣言しても兄弟間で見えない
DECLARE
v_c NUMBER := 3;
BEGIN
DBMS_OUTPUT.PUT_LINE(v_a || ',' || v_c); -- OK: 1,3
END;
END;
宣言の順序も重要
PL/SQLは前方参照不可。同じDECLARE内でも「下で宣言した変数を上の初期化で使う」のはエラー。互いに参照し合う変数は順序を意識して宣言するか、変数を2段階で宣言+代入(BEGIN内で代入)します。
ラベル付きブロック:Shadowing解決と明示脱出
Shadowingされた外側変数を参照
<<main>> -- ラベル名は識別子と同じ規則
DECLARE
v_total NUMBER := 0;
BEGIN
DECLARE
v_total NUMBER := 0; -- 外側の v_total を隠蔽
BEGIN
FOR i IN 1..10 LOOP
v_total := v_total + i; -- 内側の v_total(合計 55)
main.v_total := main.v_total + 1; -- 外側の v_total(+1ずつ加算 = 10)
END LOOP;
DBMS_OUTPUT.PUT_LINE(v_total); -- 55
END;
DBMS_OUTPUT.PUT_LINE(v_total); -- 10
END main;
ラベル付きループでネスト脱出
<<search>>
FOR i IN 1..10 LOOP
FOR j IN 1..10 LOOP
IF i * j > 30 THEN
EXIT search; -- 外側ループまで一気に脱出
END IF;
IF j > 5 THEN
CONTINUE search; -- 外側ループの次イテレーションへ
END IF;
DBMS_OUTPUT.PUT_LINE(i || ',' || j);
END LOOP;
END LOOP search;
ラベルの3大用途:①Shadowing解決(ラベル.変数)、②ネストループの明示脱出(EXIT/CONTINUE ラベル)、③END ラベルで対応明示(可読性UP)。ラベル名は処理の意味を表す(outerではなくprocess_each_dept等)と保守時に意図が伝わりやすい。
変数の隠蔽(Shadowing)と対処
-- ❌ Shadowingに気づかずバグ
DECLARE
status VARCHAR2(20) := 'active'; -- 外側
BEGIN
DECLARE
status VARCHAR2(20); -- 内側:NULL(初期化忘れ)
BEGIN
-- ここで status を使うと内側の NULL が優先される
IF status IS NULL THEN
DBMS_OUTPUT.PUT_LINE('status is NULL'); -- これが出力される
END IF;
END;
END;
-- ⭕ プレフィックスで回避
DECLARE
v_outer_status VARCHAR2(20) := 'active';
BEGIN
DECLARE
v_inner_status VARCHAR2(20);
BEGIN
-- 名前が違うので混乱しない
v_inner_status := v_outer_status;
END;
END;
列名とのShadowing事故:変数名がテーブル列名と同じだと、SQL内で変数が優先されるという罠があります。UPDATE users SET name = nameのように書くと、ローカル変数nameを代入しているのか列同士の代入なのか曖昧。変数名にv_プレフィックスを付ければ、列名との衝突が物理的に起きません。
例外の伝播:どこで捕まえるか
BEGIN
<<outer>>
BEGIN
<<inner>>
BEGIN
RAISE NO_DATA_FOUND;
EXCEPTION
WHEN TOO_MANY_ROWS THEN -- NO_DATA_FOUND はマッチしない
DBMS_OUTPUT.PUT_LINE('内側: TOO_MANY_ROWS');
END inner;
-- ここには到達しない(内側で未処理なので外側EXCEPTIONへ)
DBMS_OUTPUT.PUT_LINE('到達せず');
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('外側でキャッチ: NO_DATA_FOUND');
END outer;
END;
/
-- 出力:外側でキャッチ: NO_DATA_FOUND
伝播の5つのルール
厳密な伝播ルール
- 例外発生時、同ブロックのEXCEPTIONで一致する
WHENを探す - マッチするWHENが見つかればそこで処理→ブロックは正常終了として外側へ戻る
- マッチしない場合は親ブロックのEXCEPTIONへ伝播(内側のBEGIN以降の文は実行されない)
- 親でもマッチせねばさらに親へ(最終的にクライアントにエラーが返る)
- EXCEPTION節内で新たな例外発生→その例外はさらに外側へ伝播(同ブロックで再キャッチしない)
例外処理のネストで部分的にキャッチ
BEGIN
-- 主要な処理
process_main();
-- 補助処理は失敗しても続行したい
BEGIN
send_notification(); -- ここが失敗しても
EXCEPTION
WHEN OTHERS THEN
log_error(SQLCODE, SQLERRM); -- ログだけ残して
NULL; -- 処理は続ける
END;
-- 補助処理失敗でも後続は実行される
finalize_main();
EXCEPTION
WHEN OTHERS THEN
log_error(SQLCODE, SQLERRM);
RAISE;
END;
ネスト例外の最大の価値:「致命的な主処理」と「補助的な処理」を分離できる点。Javaのtry-catchと同じ発想で、許容可能なエラーは内側ブロックで吸収、致命的なエラーは外側まで伝播、という設計が書けます。
局所ユーザー定義例外のスコープ
BEGIN
<<outer>>
DECLARE
e_outer EXCEPTION;
BEGIN
<<inner>>
DECLARE
e_inner EXCEPTION;
BEGIN
RAISE e_inner;
EXCEPTION
WHEN e_inner THEN
DBMS_OUTPUT.PUT_LINE('inner caught');
-- e_outer はここでは見える(外側で宣言されている)
WHEN e_outer THEN
DBMS_OUTPUT.PUT_LINE('outer exception(ここでは発生しない)');
END inner;
EXCEPTION
-- e_inner はここでは参照不可(内側の宣言)
WHEN e_outer THEN
DBMS_OUTPUT.PUT_LINE('outer caught');
END outer;
END;
DECLARE
e_locked EXCEPTION;
PRAGMA EXCEPTION_INIT(e_locked, -54); -- ORA-00054 を名前付け
BEGIN
UPDATE orders SET status = 'processing' WHERE id = 1 FOR UPDATE NOWAIT;
EXCEPTION
WHEN e_locked THEN
DBMS_OUTPUT.PUT_LINE('他が更新中です');
END;
例外名の再利用
外側ブロックでe_custom EXCEPTION;を宣言しておけば、内側ブロック複数からRAISEしても外側で一括キャッチできます。「共通例外」は外側で宣言、「そのブロック固有の例外」は内側で宣言、という使い分け。詳細は【PL/SQL】例外処理完全ガイド参照。
ローカルサブプログラム(ブロック内プロシージャ/ファンクション)
DECLARE
-- ローカル変数
v_total NUMBER := 0;
-- ローカルファンクション
FUNCTION add_tax(p_amount IN NUMBER) RETURN NUMBER IS
BEGIN
RETURN ROUND(p_amount * 1.10);
END;
-- ローカルプロシージャ
PROCEDURE log(p_msg IN VARCHAR2) IS
BEGIN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SYSDATE, 'HH24:MI:SS') || ' ' || p_msg);
END;
BEGIN
FOR rec IN (SELECT amount FROM orders WHERE date = TRUNC(SYSDATE)) LOOP
v_total := v_total + add_tax(rec.amount);
END LOOP;
log('合計: ' || v_total);
END;
DECLARE
-- ❌ 下で定義される関数を上で使うとPLS-00201
-- v_x NUMBER := add_one(10); -- NG
FUNCTION add_one(p IN NUMBER) RETURN NUMBER IS
BEGIN
RETURN p + 1;
END;
-- ここでは使える
v_x NUMBER := add_one(10);
BEGIN
NULL;
END;
-- 相互再帰が必要ならforward declarationを使う
DECLARE
FUNCTION is_even(n IN NUMBER) RETURN BOOLEAN; -- 前方宣言
FUNCTION is_odd(n IN NUMBER) RETURN BOOLEAN IS
BEGIN
IF n = 0 THEN RETURN FALSE; END IF;
RETURN is_even(n - 1); -- is_evenを呼ぶ
END;
FUNCTION is_even(n IN NUMBER) RETURN BOOLEAN IS
BEGIN
IF n = 0 THEN RETURN TRUE; END IF;
RETURN is_odd(n - 1);
END;
BEGIN
IF is_even(10) THEN DBMS_OUTPUT.PUT_LINE('even'); END IF;
END;
ローカルサブプログラム vs パッケージ
ネストしたCursorのスコープ
DECLARE
CURSOR cur_dept IS SELECT id, name FROM departments;
BEGIN
FOR rec_d IN cur_dept LOOP
-- 内側で子カーソルを宣言(部門ごと)
DECLARE
CURSOR cur_emp IS
SELECT id, name FROM employees WHERE dept_id = rec_d.id;
BEGIN
FOR rec_e IN cur_emp LOOP
DBMS_OUTPUT.PUT_LINE(rec_d.name || ' - ' || rec_e.name);
END LOOP;
END;
END LOOP;
END;
DECLARE
CURSOR cur_dept IS SELECT id, name FROM departments;
CURSOR cur_emp(p_dept_id NUMBER) IS
SELECT id, name FROM employees WHERE dept_id = p_dept_id;
BEGIN
FOR rec_d IN cur_dept LOOP
FOR rec_e IN cur_emp(rec_d.id) LOOP
DBMS_OUTPUT.PUT_LINE(rec_d.name || ' - ' || rec_e.name);
END LOOP;
END LOOP;
END;
パラメータ付きカーソルのメリット
パラメータ付きカーソルを外側で宣言しておくと、内側のブロックを追加せずに済み、ネスト階層が浅くなります。また複数の場所で同じクエリ形を使い回せるため、DRY原則にも合います。詳細は【PL/SQL】カーソル完全ガイド参照。
SAVEPOINT+ネストブロックで部分ロールバック
BEGIN
INSERT INTO logs(msg) VALUES ('開始');
SAVEPOINT sp_before_critical;
BEGIN
-- 危険な処理
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
process_risky_operation();
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO sp_before_critical; -- 1行目のINSERTは残る、UPDATEはロールバック
log_error(SQLCODE, SQLERRM);
END;
INSERT INTO logs(msg) VALUES ('完了');
COMMIT;
END;
DECLARE
v_success NUMBER := 0;
v_error NUMBER := 0;
BEGIN
FOR rec IN (SELECT id, payload FROM batch_queue) LOOP
SAVEPOINT sp_row;
BEGIN
process_item(rec.id, rec.payload);
v_success := v_success + 1;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO sp_row;
v_error := v_error + 1;
log_error(SQLCODE, SQLERRM, rec.id);
END;
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('OK: ' || v_success || ' / NG: ' || v_error);
END;
SAVEPOINTの詳細は
SAVEPOINTを使った部分ロールバックの全パターンは【PL/SQL】SAVEPOINTを使った部分ロールバックの実装方法で詳解。命名規則、ネスト制限、トリガーとの組み合わせなど実務的な注意点を扱っています。
PRAGMA AUTONOMOUS_TRANSACTIONで独立トランザクション
-- ネストしたブロックを自律トランザクション化する定番パターン DECLARE PRAGMA AUTONOMOUS_TRANSACTION; -- 宣言セクション先頭に書く(ブロック全体が独立) BEGIN INSERT INTO audit_log(ts, msg) VALUES (SYSTIMESTAMP, 'checkpoint'); COMMIT; -- 呼び出し元のトランザクションとは独立にCOMMIT END;
DECLARE
-- 自律トランザクション付きローカルプロシージャ
PROCEDURE log_audit(p_msg IN VARCHAR2) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO audit_log(ts, msg) VALUES (SYSTIMESTAMP, p_msg);
COMMIT; -- メインがROLLBACKしてもこの記録は残る
END;
BEGIN
log_audit('処理開始');
INSERT INTO orders(...) VALUES (...);
log_audit('INSERT完了');
-- 何か失敗してROLLBACKしてもログは残っている
RAISE_APPLICATION_ERROR(-20001, 'テスト');
EXCEPTION
WHEN OTHERS THEN
log_audit('エラー発生: ' || SQLERRM);
ROLLBACK;
END;
AUTONOMOUS_TRANSACTIONの重要ルール:①自律トランザクション内で必ずCOMMITまたはROLLBACKする(そうしないとORA-06519)、②独立したundo領域・ロックを持つのでデッドロック注意、③メインのSELECTでロックしている行に自律側でUPDATEすると自己デッドロック。詳細は【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法。
実務5パターン
①try-catch風:補助処理の失敗を吸収
BEGIN
-- 本処理
update_main_data();
-- 通知はベストエフォート
BEGIN
send_email_notification();
EXCEPTION
WHEN OTHERS THEN
log_error(SQLCODE, SQLERRM);
-- 通知失敗でも本処理は完了扱い
END;
-- 統計更新もベストエフォート
BEGIN
update_stats();
EXCEPTION
WHEN OTHERS THEN
log_error(SQLCODE, SQLERRM);
END;
COMMIT;
END;
②リトライ with 内側ブロック
DECLARE
v_retry NUMBER := 0;
v_success BOOLEAN := FALSE;
BEGIN
WHILE v_retry < 3 AND NOT v_success LOOP
BEGIN
call_unstable_api();
v_success := TRUE;
EXCEPTION
WHEN OTHERS THEN
v_retry := v_retry + 1;
DBMS_LOCK.SLEEP(2 ** v_retry); -- 2,4,8秒
END;
END LOOP;
IF NOT v_success THEN
RAISE_APPLICATION_ERROR(-20001, 'リトライ上限');
END IF;
END;
③部分ロールバック(SAVEPOINT+ネスト)
BEGIN
FOR rec IN (SELECT id FROM batch_queue) LOOP
SAVEPOINT sp_item;
BEGIN
process(rec.id);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO sp_item;
log_error(SQLCODE, SQLERRM, rec.id);
END;
END LOOP;
COMMIT;
END;
④局所ヘルパー関数でループ処理
DECLARE
FUNCTION format_money(p_amt IN NUMBER) RETURN VARCHAR2 IS
BEGIN
RETURN '¥' || TO_CHAR(p_amt, 'FM999,999,999');
END;
FUNCTION calc_tax(p_amt IN NUMBER) RETURN NUMBER IS
BEGIN
RETURN ROUND(p_amt * 0.10);
END;
BEGIN
FOR rec IN (SELECT id, amount FROM orders) LOOP
DBMS_OUTPUT.PUT_LINE(
rec.id || ': ' || format_money(rec.amount) ||
' (tax=' || format_money(calc_tax(rec.amount)) || ')'
);
END LOOP;
END;
⑤例外の翻訳(下位例外→上位ビジネス例外)
DECLARE
e_duplicate_email EXCEPTION;
PRAGMA EXCEPTION_INIT(e_duplicate_email, -1); -- ORA-00001
BEGIN
BEGIN
INSERT INTO users(email) VALUES (p_email);
EXCEPTION
WHEN e_duplicate_email THEN
RAISE_APPLICATION_ERROR(
-20001,
'このメールアドレスは既に登録されています: ' || p_email
);
END;
send_welcome_email(p_email);
END;
アンチパターン7選
①深すぎるネスト(4階層以上)。可読性と保守性が著しく落ちる。早期returnやサブプログラム抽出でフラット化する。実務では2〜3階層までが限界。
②Shadowingを自覚せず同名変数。外側と内側で同じ名前を使うとバグの温床。v_outer_/v_inner_のようにプレフィックスで意図的に区別する。
③内側ブロックでWHEN OTHERS THEN NULL;。Smothering Exception。親のビジネスロジックが壊れても気づけない。必ずRAISEまたはlog_error+RAISE。
④内側の例外を新しい例外にすり替え。WHEN OTHERS THEN RAISE e_my_error;でBACKTRACEが失われる。独自例外を投げるならRAISE_APPLICATION_ERROR+第3引数TRUEでスタック保持。
⑤AUTONOMOUS_TRANSACTION内でCOMMIT忘れ。ORA-06519: active autonomous transaction detected。自律ブロック内は必ずCOMMITまたはROLLBACKで閉じる。
⑥SAVEPOINT名の衝突。ネストしたブロックで同名SAVEPOINTを使うと古い方が無効化される。sp_row_<処理名>のように区別する命名を。
⑦ローカルサブプログラムで同名関数乱立。内側ブロックで同名ローカル関数を再定義すると、外側の関数が隠蔽され意図しない関数が呼ばれる可能性。ブロック内関数はそのブロック固有の処理に限定。
よくある質問
ラベル.変数)でしかアクセスできなくなります。プレフィックス(v_outer_/v_inner_)で意図的に区別するのがベスト。RETURNするファンクション化、③OUTパラメータ付きローカルプロシージャ、のいずれか。RAISEして外側キャッチしても同じ動作です。COMMITしたら外側にも影響する?PRAGMA AUTONOMOUS_TRANSACTIONを内側ブロックに付けます。outer_loop、process_dept)が保守しやすい。予約語との衝突は避けられます。<<main>> DECLARE v_main NUMBER;は問題なく動作。ただし読み手が混乱するので、ラベル名と同じ名前の変数は避けるのが実務的。sp_outer_xxx/sp_inner_yyyのようにプレフィックス。EXCEPTION WHEN OTHERS THEN BEGIN log(SQLERRM); EXCEPTION WHEN OTHERS THEN NULL; END; RAISE;でログ失敗を吸収しつつ元例外を伝える。関連記事
- 【PL/SQL】基本構文完全ガイド — PL/SQLブロックの4セクション
- 【PL/SQL】変数・定数完全ガイド — 変数のスコープと命名規則
- 【PL/SQL】例外処理完全ガイド — 例外伝播の詳解
- 【PL/SQL】IF文完全ガイド — ラベル付きループとEXIT
- 【PL/SQL】ループ処理完全ガイド — ラベル付きループ脱出
- 【PL/SQL】カーソル完全ガイド — ネストカーソル
- 【PL/SQL】プロシージャ・ファンクション完全ガイド — ローカル vs スタンドアロン
- 【PL/SQL】SAVEPOINTを使った部分ロールバックの実装方法 — SAVEPOINT詳解
- 【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法 — 自律トランザクション
- 【PL/SQL】パッケージを使ったコード管理と再利用性向上 — 再利用はパッケージへ
まとめ
- PL/SQLは
DECLARE/BEGIN/EXCEPTION/END;をネストできる - 基本可視性ルール:内側から外側は見える/外側から内側は見えない
- 同名変数はShadowingで内側が優先、外側は
ラベル.変数で参照 - 例外は同ブロックのWHENを順に探し、マッチしなければ親へ伝播
- 局所例外/ユーザー定義例外は宣言ブロック内でのみ有効
- ローカルサブプログラムでブロック限定のヘルパー関数を定義可能
- Cursorのネストはパラメータ付きカーソルで階層を浅くできる
- SAVEPOINT+ネストブロックで部分ロールバック(バッチの部分成功)
PRAGMA AUTONOMOUS_TRANSACTIONで独立トランザクション(監査ログ等)- 実務5パターン:try-catch風/リトライ/部分ロールバック/局所関数/例外翻訳
- アンチパターン:深すぎるネスト/Shadowing無自覚/WHEN OTHERS握り潰し/RAISEで情報喪失/COMMIT忘れ/SAVEPOINT名衝突/同名局所関数
ネストブロックは変数・例外・トランザクションのスコープを局所化するPL/SQLの強力な設計ツールです。本記事のスコープルール・ラベル活用・例外伝播・SAVEPOINT+ネストパターンを押さえれば、保守性と堅牢性の高いPL/SQLコードが書けます。基本構文は基本構文完全ガイド、例外は例外処理完全ガイド、変数は変数・定数完全ガイド、SAVEPOINTはSAVEPOINT詳解、AUTONOMOUSはAUTONOMOUS TRANSACTIONと組み合わせて活用してください。

