PL/SQLのループはBasic LOOP/WHILE LOOP/Numeric FOR LOOP/Cursor FOR LOOPの4種類があり、用途で使い分けると可読性と性能の両方が向上します。入門記事では構文の列挙で終わりがちですが、実務で最も使うのはCursor FOR Loop、大量データ処理で欠かせないのはBULK COLLECT + FORALL。これらを知らないと「正解コード」と「本番で遅いコード」が紙一重になります。
さらにCONTINUE/CONTINUE WHEN(11g以降)/EXIT/EXIT WHENの使い分け、ラベル付きループによるネスト脱出、コレクション走査(FIRST/LAST/NEXT)、無限ループ暴走防止、ループ変数のスコープ、%BULK_EXCEPTIONSとの連携まで押さえる必要があります。
この記事ではPL/SQLの4種ループを実務で使える深さまで解説します。4種の完全比較表、EXIT/CONTINUEの使い分け、Cursor FOR Loop、BULK COLLECT+FORALLとの対比、実務10パターン、アンチパターン7選まで、2026年の現場で差がつく決定版ガイドです。関連する【PL/SQL】IF文完全ガイド/【PL/SQL】例外処理完全ガイド/【PL/SQL】カーソルでSQLクエリで取得した複数の行を1行ずつ処理する方法/【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方も併読推奨。
この記事で学べること
- ループ4種(Basic LOOP/WHILE/Numeric FOR/Cursor FOR)の使い分け表
EXIT/EXIT WHEN/CONTINUE/CONTINUE WHENの使い分け- ラベル付きループとネストループ脱出パターン
- Cursor FOR Loop(実務最頻出)とImplicit Cursorの最適化
- コレクション走査:
.COUNT/.FIRST/.LAST/.NEXTの活用 - BULK COLLECT + FORALLによるバルク処理(ループの高速化)
- 無限ループ暴走の防止策(回数上限+タイムアウト)
- ループ変数のスコープと外部変数との衝突回避
- REVERSE/STEP相当(PL/SQLに
BY Nは無い) - 実務10パターン:バッチ/Cursor/配列/期間集計/リトライ
- アンチパターン7選とリファクタリング
30秒クイックリファレンス:4種のループテンプレ
-- ① Basic LOOP(最もシンプル、必ずEXITが必要) LOOP 処理; EXIT WHEN 条件; -- または IF 条件 THEN EXIT; END IF; END LOOP; -- ② WHILE LOOP(条件が真の間繰り返し、先頭評価) WHILE 条件 LOOP 処理; END LOOP; -- ③ Numeric FOR LOOP(範囲指定、自動INC/DEC) FOR i IN 1..10 LOOP 処理; END LOOP; FOR i IN REVERSE 10..1 LOOP -- 逆順 処理; END LOOP; -- ④ Cursor FOR LOOP(クエリ結果を1行ずつ処理) FOR rec IN (SELECT id, name FROM users WHERE status = 'A') LOOP DBMS_OUTPUT.PUT_LINE(rec.id || ' ' || rec.name); END LOOP;
選び方の黄金律:①Cursor FOR Loop=SQL結果を1行ずつ処理(最頻出)、②Numeric FOR Loop=回数固定の繰り返し(1..10等)、③WHILE LOOP=条件がTRUEの間(外部状態で終了する時)、④Basic LOOP=最低1回実行する独自脱出条件がある時(do-while相当)。
4種ループの完全比較
Cursor FOR Loopが最強の理由
Cursor FOR Loopは①OPEN/CLOSEの自動化、②rec変数の型自動定義(%ROWTYPE相当)、③暗黙的にBULK COLLECT 100件ずつ(10g以降の最適化)、④例外安全(ループ脱出時に自動CLOSE)、⑤可読性最高、という5つのメリットで最頻出。明示カーソルは特殊要件(FOR UPDATE等)以外で使う理由がほぼありません。
Basic LOOP:最小構成+EXIT
-- EXIT WHENで終了条件(推奨)
DECLARE
v_count NUMBER := 1;
BEGIN
LOOP
DBMS_OUTPUT.PUT_LINE('iter ' || v_count);
v_count := v_count + 1;
EXIT WHEN v_count > 5;
END LOOP;
END;
-- IF + EXITでも同等
LOOP
処理;
IF 条件 THEN
EXIT;
END IF;
END LOOP;
Basic LOOPの最大の罠:EXIT忘れで無限ループ。特にIF+EXITで複雑な条件を書く時、どの分岐にもEXITが無い枝が生まれやすい。EXIT WHENをLOOP末尾に書くパターンが最も安全。本番投入前に必ずデッドコード(EXIT経路が無いパス)をレビューで確認してください。
WHILE LOOP:先頭評価の条件ループ
DECLARE
v_count NUMBER := 1;
BEGIN
WHILE v_count <= 5 LOOP
DBMS_OUTPUT.PUT_LINE('iter ' || v_count);
v_count := v_count + 1; -- ← 更新忘れで無限ループ注意
END LOOP;
END;
-- 外部状態を監視するケース
DECLARE
v_job_status VARCHAR2(20) := 'RUNNING';
v_wait_sec NUMBER := 0;
BEGIN
WHILE v_job_status = 'RUNNING' AND v_wait_sec < 600 LOOP -- 最大10分
SELECT status INTO v_job_status FROM batch_jobs WHERE id = p_job_id;
DBMS_LOCK.SLEEP(5);
v_wait_sec := v_wait_sec + 5;
END LOOP;
END;
WHILE LOOPは「条件を満たさなければ1回も実行しない」挙動が肝。最低1回実行したい処理はBasic LOOP(do-while相当)を使います。外部状態を待つ場合は必ずタイムアウトカウンタを併用して無限ループを物理的に防ぐ設計を。
Numeric FOR LOOP:回数固定+REVERSE
-- 基本(1..5を順に)
FOR i IN 1..5 LOOP
DBMS_OUTPUT.PUT_LINE('i=' || i);
END LOOP;
-- 出力: i=1 / i=2 / i=3 / i=4 / i=5
-- REVERSE(5..1を逆順に)
FOR i IN REVERSE 1..5 LOOP -- ※ 書き方は 1..5 のまま
DBMS_OUTPUT.PUT_LINE('i=' || i);
END LOOP;
-- 出力: i=5 / i=4 / i=3 / i=2 / i=1
-- 変数を範囲に使える
FOR i IN v_start..v_end LOOP
...
END LOOP;
-- 範囲逆転ならループ実行されない(注意)
FOR i IN 5..1 LOOP -- REVERSE無しで逆範囲 → 実行されない
DBMS_OUTPUT.PUT_LINE('実行されない');
END LOOP;
STEP(ステップ増分)相当
-- ❌ PL/SQLにSTEPやBY句は存在しない
-- FOR i IN 1..10 BY 2 LOOP -- PLS-00103 error
-- ⭕ ワークアラウンド①:計算で飛ばす
FOR i IN 1..5 LOOP
DECLARE
v_val NUMBER := 1 + (i - 1) * 2; -- 1,3,5,7,9
BEGIN
DBMS_OUTPUT.PUT_LINE('val=' || v_val);
END;
END LOOP;
-- ⭕ ワークアラウンド②:MODでスキップ
FOR i IN 1..10 LOOP
CONTINUE WHEN MOD(i, 2) = 0; -- 偶数をスキップ
DBMS_OUTPUT.PUT_LINE('odd=' || i);
END LOOP;
Numeric FOR LOOPのスコープ
- ループ変数
iはLOOP内だけのスコープ(外から見えない) - ループ変数は読み取り専用(
i := 5はエラー) - 同名の外部変数があってもループ内では隠蔽される
- 外部変数と区別するため
FOR ii IN ...等のプレフィックスが実務では有用
Cursor FOR LOOP:実務最頻出パターン
SQLクエリの結果を1行ずつ処理する標準手段。OPEN/FETCH/CLOSEが自動化され、rec.colnameで行カラムにアクセスできます。Oracle 10g以降は内部的にBULK COLLECTで100件まとめて取得しパフォーマンスも明示カーソルと遜色ありません。
BEGIN
FOR rec IN (
SELECT id, email, created_at
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
) LOOP
-- rec.id, rec.email, rec.created_at で参照
DBMS_OUTPUT.PUT_LINE(rec.id || ' / ' || rec.email);
-- ループ内でDML可能(同じテーブルでなければ)
INSERT INTO user_log VALUES (rec.id, SYSDATE);
END LOOP;
COMMIT;
END;
DECLARE
CURSOR cur_active_users IS
SELECT id, email
FROM users
WHERE status = 'active';
BEGIN
FOR rec IN cur_active_users LOOP
process_user(rec.id, rec.email);
END LOOP;
END;
DECLARE
CURSOR cur_by_status(p_status VARCHAR2) IS
SELECT id, email FROM users WHERE status = p_status;
BEGIN
FOR rec IN cur_by_status('active') LOOP
...
END LOOP;
FOR rec IN cur_by_status('pending') LOOP
...
END LOOP;
END;
Cursor FOR Loopの5つの利点:①OPEN/FETCH/CLOSEが自動、②recの型は%ROWTYPE相当で自動定義、③10g以降は暗黙BULK COLLECT 100件で高速、④例外発生時も自動CLOSE(リソースリーク無し)、⑤コード可読性が明示カーソルより圧倒的に高い。特殊要件(FOR UPDATE/巨大データのBULK制御)以外はCursor FOR Loop一択。詳細は【PL/SQL】カーソルで複数の行を1行ずつ処理する方法参照。
EXIT/CONTINUE/ラベル付きループの制御フロー
EXIT系(ループ脱出)
LOOP 処理; -- 形式①:条件付きEXIT EXIT WHEN 条件; -- 形式②:IF + EXIT IF 条件 THEN EXIT; END IF; END LOOP;
CONTINUE系(次のイテレーションへ、Oracle 11g以降)
-- 特定条件で以降の処理をスキップし次ループへ
FOR rec IN (SELECT id, amount FROM orders) LOOP
CONTINUE WHEN rec.amount IS NULL OR rec.amount <= 0;
-- 正常な行だけ処理
process_order(rec.id);
END LOOP;
-- IF + CONTINUEでも同等
FOR rec IN cur_orders LOOP
IF rec.status = 'cancelled' THEN
CONTINUE;
END IF;
process_order(rec.id);
END LOOP;
ラベル付きループ(ネスト脱出)
<<outer_loop>>
FOR i IN 1..10 LOOP
<<inner_loop>>
FOR j IN 1..10 LOOP
IF i * j > 50 THEN
-- 内側だけ脱出
EXIT inner_loop;
END IF;
IF some_condition THEN
-- 外側まで一気に脱出
EXIT outer_loop;
END IF;
IF another_condition THEN
-- 外側の次イテレーションへ
CONTINUE outer_loop;
END IF;
DBMS_OUTPUT.PUT_LINE(i || ',' || j);
END LOOP inner_loop;
END LOOP outer_loop;
ラベルは可読性も上げる
ラベル付きループは脱出先が明示されるため、「どのループから抜けるのか」がコードレベルで明白。ネスト3段以上はラベルを付けるのが実務推奨。ラベル名は処理内容を表す命名(<<process_each_dept>>等)にすると将来の保守者にも優しい。
コレクション走査:.COUNT/.FIRST/.LAST/.NEXT
配列/ネスト表/連想配列をループで処理する時のパターン。密配列(隙間なし)はFOR i IN 1..COUNT、疎配列(削除で歯抜け)はFIRST/NEXTでの走査が必要です。
DECLARE
TYPE t_ids IS TABLE OF NUMBER;
v_ids t_ids := t_ids(10, 20, 30, 40, 50);
BEGIN
-- 空チェック必須
IF v_ids IS NULL OR v_ids.COUNT = 0 THEN
RETURN;
END IF;
FOR i IN 1..v_ids.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(i || ': ' || v_ids(i));
END LOOP;
END;
DECLARE
TYPE t_map IS TABLE OF VARCHAR2(50) INDEX BY PLS_INTEGER;
v_map t_map;
v_idx PLS_INTEGER;
BEGIN
v_map(10) := 'A';
v_map(25) := 'B';
v_map(100) := 'C';
v_map.DELETE(10); -- 歯抜け作成
-- ❌ COUNTベースは使えない(インデックスが連続しない)
-- FOR i IN 1..v_map.COUNT LOOP ... -- NG
-- ⭕ FIRST/NEXTで疎配列を辿る
v_idx := v_map.FIRST;
WHILE v_idx IS NOT NULL LOOP
DBMS_OUTPUT.PUT_LINE(v_idx || ' => ' || v_map(v_idx));
v_idx := v_map.NEXT(v_idx);
END LOOP;
END;
-- Oracle 10g以降:INDICES OF で疎配列も自然に走査
DECLARE
TYPE t_map IS TABLE OF VARCHAR2(50) INDEX BY PLS_INTEGER;
v_map t_map;
BEGIN
v_map(10) := 'A';
v_map(25) := 'B';
FORALL i IN INDICES OF v_map
INSERT INTO dump_table VALUES (i, v_map(i));
-- ↑ FORALLはバルク処理、FORではないが疎配列指定パターンとして
END;
コレクション+IF+ループの鉄則
ループ前に必ずv_coll IS NOT NULL AND v_coll.COUNT > 0でガード。NULL参照例外(COLLECTION_IS_NULL)を回避。コレクション詳細は【PL/SQL】コレクション(配列・ネスト表)の基本と活用例参照。
パフォーマンス比較:ループ vs BULK COLLECT+FORALL
1万件のデータを更新する時、1行ずつのループではRDBMSとのラウンドトリップ1万回が発生し遅い。BULK COLLECT+FORALLでまとめて1回にすると10〜100倍高速になります。
BEGIN
FOR rec IN (SELECT id FROM orders WHERE status = 'pending') LOOP
UPDATE orders SET status = 'processing' WHERE id = rec.id;
-- 1回のUPDATE=1ラウンドトリップ、1万行なら1万回
END LOOP;
COMMIT;
END;
DECLARE
TYPE t_ids IS TABLE OF orders.id%TYPE;
v_ids t_ids;
BEGIN
-- 1回のSELECTで全ID取得
SELECT id BULK COLLECT INTO v_ids
FROM orders WHERE status = 'pending';
-- 1回のFORALLで全UPDATE
FORALL i IN 1..v_ids.COUNT
UPDATE orders SET status = 'processing' WHERE id = v_ids(i);
COMMIT;
END;
-- メモリ枯渇を避けるため1000件ずつ処理
DECLARE
TYPE t_ids IS TABLE OF orders.id%TYPE;
v_ids t_ids;
CURSOR cur IS SELECT id FROM orders WHERE status = 'pending';
BEGIN
OPEN cur;
LOOP
FETCH cur BULK COLLECT INTO v_ids LIMIT 1000;
EXIT WHEN v_ids.COUNT = 0;
FORALL i IN 1..v_ids.COUNT
UPDATE orders SET status = 'processing' WHERE id = v_ids(i);
COMMIT; -- 定期的にコミットしてundo縮小
END LOOP;
CLOSE cur;
END;
性能差の実例:1万行UPDATEで、1行ずつFOR Loop=約30秒、BULK COLLECT+FORALL=約0.5秒。本番で効果絶大なパターンですが、BULK COLLECTはメモリに全件ロードするためデータ量が大きい場合はLIMIT 1000等で分割必須。詳しくは【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方参照。
無限ループ暴走の防止策
DECLARE
v_max_iter CONSTANT NUMBER := 100000;
v_iter NUMBER := 0;
BEGIN
LOOP
-- 本来の終了条件
EXIT WHEN 本来条件;
-- 安全ネット:最大回数
v_iter := v_iter + 1;
IF v_iter > v_max_iter THEN
RAISE_APPLICATION_ERROR(-20001, 'ループ最大回数超過: ' || v_max_iter);
END IF;
-- 処理
process();
END LOOP;
END;
DECLARE
v_start TIMESTAMP := SYSTIMESTAMP;
v_elapsed_sec NUMBER;
BEGIN
LOOP
EXIT WHEN 本来条件;
-- 10分経過で強制終了
v_elapsed_sec := EXTRACT(SECOND FROM (SYSTIMESTAMP - v_start))
+ EXTRACT(MINUTE FROM (SYSTIMESTAMP - v_start)) * 60;
IF v_elapsed_sec > 600 THEN
RAISE_APPLICATION_ERROR(-20002, 'タイムアウト(10分)');
END IF;
process();
END LOOP;
END;
本番での無限ループは死活問題:PL/SQLセッションが返ってこずロックも保持し続け、他のトランザクションまで巻き込みます。外部条件に依存するループには必ず最大回数・タイムアウトを併設。監視側にはv$session/v$sqlで実行中セッションを確認→ALTER SYSTEM KILL SESSIONの手段も用意しておきましょう。
実務パターン10選
①データ件数集計+行別処理
DECLARE
v_total NUMBER := 0;
BEGIN
FOR rec IN (
SELECT id, amount FROM orders
WHERE order_date >= TRUNC(SYSDATE) - 30
) LOOP
v_total := v_total + rec.amount;
process_order(rec.id);
END LOOP;
DBMS_OUTPUT.PUT_LINE('合計: ' || v_total);
END;
②大量更新のバッチ(LIMIT付きBULK)
DECLARE
TYPE t_ids IS TABLE OF users.id%TYPE;
v_ids t_ids;
CURSOR cur IS SELECT id FROM users WHERE status = 'pending';
BEGIN
OPEN cur;
LOOP
FETCH cur BULK COLLECT INTO v_ids LIMIT 1000;
EXIT WHEN v_ids.COUNT = 0;
FORALL i IN 1..v_ids.COUNT
UPDATE users SET status = 'active', activated_at = SYSDATE
WHERE id = v_ids(i);
COMMIT;
END LOOP;
CLOSE cur;
END;
③外部APIリトライ
DECLARE
v_retry NUMBER := 0;
v_max_retry CONSTANT NUMBER := 5;
v_success BOOLEAN := FALSE;
BEGIN
WHILE v_retry < v_max_retry AND NOT v_success LOOP
BEGIN
call_api(p_id);
v_success := TRUE;
EXCEPTION
WHEN OTHERS THEN
v_retry := v_retry + 1;
DBMS_OUTPUT.PUT_LINE('retry ' || v_retry || ': ' || SQLERRM);
DBMS_LOCK.SLEEP(2 ** v_retry); -- 2, 4, 8, 16, 32秒
END;
END LOOP;
IF NOT v_success THEN
RAISE_APPLICATION_ERROR(-20001, 'リトライ上限到達');
END IF;
END;
④期間(日単位)集計
DECLARE
v_start DATE := TRUNC(SYSDATE) - 30;
v_count NUMBER;
BEGIN
FOR i IN 0..29 LOOP
SELECT COUNT(*) INTO v_count
FROM orders
WHERE TRUNC(order_date) = v_start + i;
DBMS_OUTPUT.PUT_LINE((v_start + i) || ': ' || v_count);
END LOOP;
END;
⑤ネストループで全組み合わせ処理
DECLARE
TYPE t_depts IS TABLE OF NUMBER;
v_depts t_depts := t_depts(10, 20, 30);
BEGIN
FOR i IN 1..v_depts.COUNT LOOP
FOR m IN 1..12 LOOP
compute_monthly(v_depts(i), m);
END LOOP;
END LOOP;
END;
⑥条件付きSKIP(CONTINUE WHEN)
FOR rec IN (SELECT id, amount, status FROM transactions) LOOP
CONTINUE WHEN rec.amount IS NULL;
CONTINUE WHEN rec.status IN ('cancelled', 'void');
CONTINUE WHEN rec.amount <= 0;
-- ここに来た行だけが有効
process_valid_tx(rec.id);
END LOOP;
⑦部分成功バッチ(SAVEPOINT+ループ)
DECLARE
v_success NUMBER := 0;
v_error NUMBER := 0;
BEGIN
FOR rec IN (SELECT id, data FROM import_queue) LOOP
SAVEPOINT sp_row;
BEGIN
process_item(rec.id, rec.data);
v_success := v_success + 1;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO sp_row;
v_error := v_error + 1;
log_error(SQLCODE, SQLERRM, 'id=' || rec.id);
END;
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('OK: ' || v_success || ' / NG: ' || v_error);
END;
⑧コレクション疎配列走査
DECLARE
TYPE t_attrs IS TABLE OF VARCHAR2(100) INDEX BY VARCHAR2(50);
v_attrs t_attrs;
v_key VARCHAR2(50);
BEGIN
v_attrs('color') := 'red';
v_attrs('size') := 'M';
v_attrs('price') := '1000';
v_key := v_attrs.FIRST;
WHILE v_key IS NOT NULL LOOP
DBMS_OUTPUT.PUT_LINE(v_key || ' = ' || v_attrs(v_key));
v_key := v_attrs.NEXT(v_key);
END LOOP;
END;
⑨ネスト脱出パターン
<<outer>>
FOR dept IN (SELECT id FROM departments) LOOP
FOR emp IN (SELECT id FROM employees WHERE dept_id = dept.id) LOOP
IF is_critical_error(emp.id) THEN
EXIT outer; -- 外側ループも抜ける
END IF;
END LOOP;
END LOOP outer;
⑩タイムアウト付きポーリング
DECLARE
v_status VARCHAR2(20);
v_waited NUMBER := 0;
v_timeout CONSTANT NUMBER := 300; -- 5分
BEGIN
LOOP
SELECT status INTO v_status FROM jobs WHERE id = p_job_id;
EXIT WHEN v_status IN ('completed', 'failed');
IF v_waited >= v_timeout THEN
RAISE_APPLICATION_ERROR(-20001, 'タイムアウト');
END IF;
DBMS_LOCK.SLEEP(5);
v_waited := v_waited + 5;
END LOOP;
DBMS_OUTPUT.PUT_LINE('終了状態: ' || v_status);
END;
アンチパターン7選
①EXIT忘れでBasic LOOPが無限ループ。IF+EXIT の各分岐にEXITが無いパスがあると暴走。対策:EXIT WHEN 条件を末尾に書く/最大回数ガードを併設。
②WHILE条件の更新忘れ。WHILE i <= 10 LOOP ... END LOOP;でiをインクリメントし忘れて暴走。ループ末尾で必ず条件変数を更新する規律を。
③1行ずつのUPDATE/INSERT(Cursor FOR Loop内のDML連打)。10000件で数秒〜数十秒の遅延。BULK COLLECT+FORALL化で10〜100倍高速。
④ループ内で同じSELECTを繰り返す。FOR i IN 1..1000 LOOP SELECT ... END LOOP;で毎回ラウンドトリップ。ループ前に1回SELECTしてコレクションに格納→ループ内で参照に変更。
⑤ループ内で COMMIT 連打。トランザクションが細切れになり、ORA-01555 snapshot too old が発生しやすい。LIMIT 1000〜10000程度でまとめてCOMMITする。
⑥コレクションの空チェック無しでアクセス。FOR i IN 1..v_coll.COUNT LOOP ...はv_collがNULLでCOLLECTION_IS_NULL例外。必ずv_coll IS NOT NULL AND v_coll.COUNT > 0でガード。
⑦ループ内で例外を握り潰す。EXCEPTION WHEN OTHERS THEN NULL; END;で続行すると失敗ログが残らず、後で何が起きたか追跡不能。SAVEPOINTで部分ロールバック+ログ記録→続行が正しいパターン。詳細は【PL/SQL】例外処理完全ガイド参照。
よくある質問
CONTINUE WHEN MOD(i, 2) = 0でスキップするか、ループ変数をそのまま使わずv := 1 + (i-1) * 2のように計算値を別変数に代入します。1..5のままで、REVERSEキーワードを前に付けるのが正しい。FOR i IN REVERSE 1..5で5→4→3→2→1と進みます。FOR i IN 5..1(REVERSE無し)は範囲逆転とみなされループ実行されません。GOTOでラベルへジャンプするか、IF 〜 ELSE 実処理 END IF;で代用。ほとんどの環境で11g以上なのでCONTINUEを使って問題ありません。FOR UPDATEで行ロック、②BULK COLLECT LIMIT Nでサイズ調整、③FETCHタイミング制御、のいずれか。詳細は【PL/SQL】カーソルで複数の行を1行ずつ処理する方法参照。iはループ内だけのスコープで外から見えません。ループ終了後の値を知りたい場合は外部変数に代入が必要:DECLARE v_last NUMBER; BEGIN FOR i IN 1..10 LOOP v_last := i; END LOOP; END;。Cursor FOR Loopのrecも同様にスコープ内のみ。GOTO ラベル名;)が、使用は強く非推奨。スパゲッティコードの原因で保守性が劇的に低下します。Oracle 11g以降はCONTINUEやEXIT ラベルで代替可能なのでGOTOを使う理由はほぼありません。FOR i IN 1..v_coll.COUNTは使えません(インデックスが連続しない)。代わりにFIRST/NEXTでWHILE走査:v_idx := v_coll.FIRST; WHILE v_idx IS NOT NULL LOOP ... v_idx := v_coll.NEXT(v_idx); END LOOP;。バルク処理ならFORALL i IN INDICES OF v_collも使えます。<<outer>>でラベル付け、EXIT outer;で外側ループを一気に脱出できます。CONTINUE outer;で外側の次イテレーションへスキップも可能。関連記事
- 【PL/SQL】IF文完全ガイド — ループ内でのIF分岐/CONTINUE WHEN
- 【PL/SQL】例外処理完全ガイド — ループ内の例外処理・SAVEPOINT連携
- 【PL/SQL】カーソルでSQLクエリで取得した複数の行を1行ずつ処理する方法 — Cursor FOR Loop詳解
- 【PL/SQL】変数・定数の使い方 — ループ変数の型・スコープ
- 【PL/SQL】初心者でもわかる基本構文とブロック構造の書き方 — PL/SQLブロック構造
- 【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方 — ループの高速化
- 【PL/SQL】カーソルFORループと明示的カーソルの使い分け — Cursorパフォーマンス比較
- 【PL/SQL】コレクション(配列・ネスト表)の基本と活用例 — ループと配列の組み合わせ
- 【PL/SQL】FORALLとSAVE EXCEPTIONSでバルクDMLのエラーを個別処理する方法 — FORALLエラー処理
- 【PL/SQL】SAVEPOINTを使った部分ロールバックの実装方法 — ループでの部分成功パターン
まとめ
- ループ4種:Basic LOOP/WHILE/Numeric FOR/Cursor FORを用途で使い分け
- Cursor FOR Loopが実務最頻出(内部でBULK COLLECT 100件自動化)
- Numeric FOR LoopはREVERSEで逆順、
BY Nは無い(CONTINUE WHEN MOD等で代用) EXIT/EXIT WHEN/CONTINUE/CONTINUE WHEN(11g+)を使い分け- ラベル付きループで
EXIT outer/CONTINUE outer可能 - コレクション走査:密は
1..COUNT/疎はFIRST/NEXT - 空コレクションの
COLLECTION_IS_NULLをIS NOT NULL AND COUNT > 0でガード - 大量DMLはBULK COLLECT+FORALL+LIMIT 1000で10〜100倍高速化
- 無限ループ防止:最大回数+タイムアウトカウンタを必須併設
- 部分成功バッチはSAVEPOINT+ループで実装
- アンチパターン:EXIT忘れ/条件更新忘れ/1行ずつDML/ループ内重複SELECT/COMMIT連打/空チェック無し/例外握り潰し
PL/SQLのループは4種を適切に使い分けることで、コードの可読性と性能の両方が大きく変わります。本記事の4種比較・実務10パターン・アンチパターン7選をベースに、Cursor FOR LoopとBULK COLLECT+FORALLを自由に使いこなせるようになれば、万行オーダーのバッチ処理も秒単位で完了します。IF文はIF文完全ガイド、例外は例外処理完全ガイド、Cursorはカーソル処理、バルク処理はFORALL/BULK COLLECTと組み合わせてご活用ください。
