【PL/SQL】ループ処理完全ガイド|4種比較・Cursor FOR Loop・BULK COLLECT+FORALL・ラベル脱出・実務10パターン

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の使い分け、ラベル付きループによるネスト脱出、コレクション走査FIRSTLASTNEXT)、無限ループ暴走防止、ループ変数のスコープ、%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)の使い分け表
  • EXITEXIT WHENCONTINUECONTINUE WHENの使い分け
  • ラベル付きループネストループ脱出パターン
  • Cursor FOR Loop(実務最頻出)とImplicit Cursorの最適化
  • コレクション走査:.COUNT.FIRST.LAST.NEXTの活用
  • BULK COLLECT + FORALLによるバルク処理(ループの高速化)
  • 無限ループ暴走の防止策(回数上限+タイムアウト)
  • ループ変数のスコープと外部変数との衝突回避
  • REVERSE/STEP相当(PL/SQLにBY Nは無い)
  • 実務10パターン:バッチ/Cursor/配列/期間集計/リトライ
  • アンチパターン7選とリファクタリング
スポンサーリンク

30秒クイックリファレンス:4種のループテンプレ

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種ループの完全比較

項目 Basic LOOP WHILE Numeric FOR Cursor FOR
条件評価 任意位置 先頭 なし(範囲自動) なし(取得完了で自動)
最低実行回数 最低1回 0回(条件FALSEで入らず) 0回(範囲逆転で入らず) 0回(行0件で入らず)
変数インクリメント 手動 手動 自動(1ずつ) 不要
典型用途 do-while風、任意終了 条件ベース反復 回数確定の反復 SQL行ごと処理(最頻出)
暴走リスク 高(EXIT忘れ) 中(条件更新忘れ) 低(範囲固定) 低(取得完了で自動終了)

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には BY N が無い
-- ❌ 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のスコープ

  • ループ変数iLOOP内だけのスコープ(外から見えない)
  • ループ変数は読み取り専用i := 5はエラー)
  • 同名の外部変数があってもループ内では隠蔽される
  • 外部変数と区別するためFOR ii IN ...等のプレフィックスが実務では有用

Cursor FOR LOOP:実務最頻出パターン

SQLクエリの結果を1行ずつ処理する標準手段。OPEN/FETCH/CLOSEが自動化され、rec.colnameで行カラムにアクセスできます。Oracle 10g以降は内部的にBULK COLLECTで100件まとめて取得しパフォーマンスも明示カーソルと遜色ありません。

インラインSELECTパターン(最もシンプル)
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系(ループ脱出)

EXIT / EXIT WHEN
LOOP
  処理;
  
  -- 形式①:条件付きEXIT
  EXIT WHEN 条件;
  
  -- 形式②:IF + EXIT
  IF 条件 THEN EXIT; END IF;
END LOOP;

CONTINUE系(次のイテレーションへ、Oracle 11g以降)

CONTINUE / CONTINUE WHEN
-- 特定条件で以降の処理をスキップし次ループへ
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での走査が必要です。

密配列の走査(VARRAY/単純なネスト表)
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;
INDICES OF/VALUES OF(最適化版、疎配列対応)
-- 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倍高速になります。

❌ 遅い:1行ずつループ
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;
⭕ 高速:BULK COLLECT + FORALL
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;
大量データはLIMIT付きで分割
-- メモリ枯渇を避けるため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 &gt; 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 &gt; 600 THEN
      RAISE_APPLICATION_ERROR(-20002, 'タイムアウト(10分)');
    END IF;

    process();
  END LOOP;
END;

本番での無限ループは死活問題:PL/SQLセッションが返ってこずロックも保持し続け、他のトランザクションまで巻き込みます。外部条件に依存するループには必ず最大回数・タイムアウトを併設。監視側にはv$sessionv$sqlで実行中セッションを確認→ALTER SYSTEM KILL SESSIONの手段も用意しておきましょう。

実務パターン10選

①データ件数集計+行別処理

Cursor FOR Loop
DECLARE
  v_total NUMBER := 0;
BEGIN
  FOR rec IN (
    SELECT id, amount FROM orders
    WHERE order_date &gt;= 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)

LIMIT 1000分割
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リトライ

exponential backoff
DECLARE
  v_retry NUMBER := 0;
  v_max_retry CONSTANT NUMBER := 5;
  v_success BOOLEAN := FALSE;
BEGIN
  WHILE v_retry &lt; 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;

④期間(日単位)集計

FOR i IN … で日付ループ
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 &lt;= 0;

  -- ここに来た行だけが有効
  process_valid_tx(rec.id);
END LOOP;

⑦部分成功バッチ(SAVEPOINT+ループ)

1件失敗しても全体を止めない
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;

⑧コレクション疎配列走査

FIRST/NEXT パターン
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;

⑨ネスト脱出パターン

ラベル付き外側脱出
&lt;&lt;outer&gt;&gt;
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 &gt;= 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】例外処理完全ガイド参照。

よくある質問

QBY N(STEP)はPL/SQLに無い?
Aありません。PL/SQLのNumeric FOR Loopは1ずつしか進めません。飛ばしたい場合はCONTINUE WHEN MOD(i, 2) = 0でスキップするか、ループ変数をそのまま使わずv := 1 + (i-1) * 2のように計算値を別変数に代入します。
QREVERSEのFOR LOOPで範囲の書き方
A範囲は1..5のままで、REVERSEキーワードを前に付けるのが正しい。FOR i IN REVERSE 1..5で5→4→3→2→1と進みます。FOR i IN 5..1(REVERSE無し)は範囲逆転とみなされループ実行されません。
QCONTINUEはいつから使える?
AOracle 11g Release 1(2007年)以降で使えます。10g以前はGOTOでラベルへジャンプするか、IF 〜 ELSE 実処理 END IF;で代用。ほとんどの環境で11g以上なのでCONTINUEを使って問題ありません。
QCursor FOR Loopと明示カーソル、どちらが速い?
AOracle 10g以降はCursor FOR Loopが内部でBULK COLLECT 100件するためパフォーマンス差はほぼありません。明示カーソルが必要なのは①FOR UPDATEで行ロック、②BULK COLLECT LIMIT Nでサイズ調整、③FETCHタイミング制御、のいずれか。詳細は【PL/SQL】カーソルで複数の行を1行ずつ処理する方法参照。
Qループ内で例外発生したらループは止まる?
A止まります(同じブロックのEXCEPTION句へ飛ぶ)。続行したいならループ内でBEGIN/EXCEPTION/ENDを書いて例外を個別処理しSAVEPOINT+ROLLBACK TOで部分ロールバックするのが定石。本記事「実務パターン⑦」を参照。
Q1万件のUPDATEが遅い
ACursor FOR Loop内で1行ずつUPDATEしているのが原因。BULK COLLECT+FORALLに書き換えると10〜100倍高速。大量データ(10万件以上)はLIMIT 1000〜5000で分割処理しメモリ圧迫を避けます。詳細は【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方
Qループ変数は外から参照できる?
ANumeric FOR Loopのiループ内だけのスコープで外から見えません。ループ終了後の値を知りたい場合は外部変数に代入が必要:DECLARE v_last NUMBER; BEGIN FOR i IN 1..10 LOOP v_last := i; END LOOP; END;。Cursor FOR Loopのrecも同様にスコープ内のみ。
QGOTO文はある?
Aあります(GOTO ラベル名;)が、使用は強く非推奨。スパゲッティコードの原因で保守性が劇的に低下します。Oracle 11g以降はCONTINUEEXIT ラベルで代替可能なのでGOTOを使う理由はほぼありません。
Q疎配列(歯抜け)はどう走査?
AFOR 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も使えます。
Qネストループから一気に抜けるには?
Aラベル付きループを使います。<<outer>>でラベル付け、EXIT outer;で外側ループを一気に脱出できます。CONTINUE outer;で外側の次イテレーションへスキップも可能。

関連記事

まとめ

  • ループ4種:Basic LOOP/WHILE/Numeric FOR/Cursor FORを用途で使い分け
  • Cursor FOR Loopが実務最頻出(内部でBULK COLLECT 100件自動化)
  • Numeric FOR LoopはREVERSEで逆順、BY Nは無い(CONTINUE WHEN MOD等で代用)
  • EXITEXIT WHENCONTINUECONTINUE WHEN(11g+)を使い分け
  • ラベル付きループEXIT outerCONTINUE outer可能
  • コレクション走査:密は1..COUNT/疎はFIRST/NEXT
  • 空コレクションのCOLLECTION_IS_NULLIS 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と組み合わせてご活用ください。