【PL/SQL】カーソルFORループ vs 明示的カーソル完全ガイド|10軸比較・暗黙BULK・判断フロー・実務10シナリオ

【PL/SQL】カーソルFORループと明示的カーソルの使い分け|パフォーマンスと可読性の観点から PL/SQL

「PL/SQLで結果セットを1行ずつ処理したい時、カーソルFORループ明示的カーソルのどちらを使うべきか」——Oracle開発現場で必ず議論になるテーマです。入門記事では「FORループの方がシンプルで速い」「明示的カーソルは柔軟」と曖昧に紹介されがちですが、10g以降の暗黙BULK COLLECTFOR UPDATE+WHERE CURRENT OFとの相性、メモリ管理を考慮した選び方は深い知識が必要です。

この記事では現代のOracle(10g以降)においてカーソルFORループと明示的カーソルの実質的な差を明らかにし、10軸の完全比較、実測ベンチマーク、判断フローチャート、BULK COLLECT+FORALLを含めた3択の選び方、FOR UPDATEWHERE CURRENT OFの挙動、例外処理の差、動的SQLとの組み合わせ、実務シナリオ10選、アンチパターン7選まで網羅します。カーソル基本は【PL/SQL】カーソル完全ガイド、バルク処理は【PL/SQL】バルク処理完全ガイド、REF CURSORは【PL/SQL】REF CURSORを使った柔軟なデータ取得方法も併読推奨です。

この記事で学べること

  • 30秒で分かる結論:どれを選ぶかの黄金律
  • カーソルFORループと明示的カーソルの10軸比較
  • 10g以降の暗黙BULK COLLECT(一括取得100件)の真実
  • 実測ベンチマーク:FORループ/明示/BULK COLLECT+FORALLの速度差
  • 判断フローチャート(6問で正解)
  • FOR UPDATEWHERE CURRENT OFの両者での挙動差
  • 例外処理とリソース管理の違い
  • 動的SQL/REF CURSORとの組み合わせ
  • 実務シナリオ10選の正解
  • アンチパターン7選(明示カーソル乱用/CLOSE忘れ等)
スポンサーリンク

30秒結論:3択で迷わない黄金律

シーン 推奨 理由
単純な行単位処理 カーソルFORループ OPEN/CLOSE自動、暗黙BULK 100で高速、可読性◎
大量行のDML(万件〜) BULK COLLECT+FORALL 10〜100倍速、FOR+1行DMLは激遅
FOR UPDATE行ロック+行単位UPDATE 明示的カーソル WHERE CURRENT OFが必要/ROWID指定可
動的SQL結果セット REF CURSOR(SYS_REFCURSOR) FORループで動的SQLは不可
LIMIT分割バッチ 明示的カーソル+BULK COLLECT LIMIT FETCH回数/件数をカスタマイズ
同じクエリを複数回OPEN 明示的カーソル(パラメータ付き) CURSOR定義を再利用

最短判断:単純な読み取り処理→カーソルFORループ(9割の場合)、②大量DMLならBULK COLLECT+FORALL、③FOR UPDATE/LIMIT制御/複数回OPENなら明示的カーソル。この3択で実務の大半は決まります。

両者の構文比較

カーソルFORループ(インライン版)
BEGIN
  FOR rec IN (SELECT id, name FROM users WHERE status = 'active') LOOP
    DBMS_OUTPUT.PUT_LINE(rec.id || ': ' || rec.name);
  END LOOP;
END;
-- OPEN/CLOSE/FETCH/%NOTFOUND が自動管理される
-- 10g以降は内部で暗黙BULK COLLECT 100件
カーソルFORループ(宣言版)
DECLARE
  CURSOR cur_users IS
    SELECT id, name FROM users WHERE status = 'active';
BEGIN
  FOR rec IN cur_users LOOP
    DBMS_OUTPUT.PUT_LINE(rec.id || ': ' || rec.name);
  END LOOP;
END;
明示的カーソル
DECLARE
  CURSOR cur_users IS
    SELECT id, name FROM users WHERE status = 'active';
  v_rec cur_users%ROWTYPE;
BEGIN
  OPEN cur_users;
  LOOP
    FETCH cur_users INTO v_rec;
    EXIT WHEN cur_users%NOTFOUND;
    DBMS_OUTPUT.PUT_LINE(v_rec.id || ': ' || v_rec.name);
  END LOOP;
  CLOSE cur_users;

EXCEPTION
  WHEN OTHERS THEN
    IF cur_users%ISOPEN THEN CLOSE cur_users; END IF;
    RAISE;
END;

見た目の差

FORループは5行、明示的カーソルは12行。単純な読み取りならFORループが圧倒的に簡潔で、OPEN/CLOSE忘れもありません。recの型は%ROWTYPE相当で自動決定されるため宣言不要。

10軸完全比較表

観点 カーソルFORループ 明示的カーソル
①OPEN/CLOSE管理 自動 手動(忘れるとORA-01000)
②レコード変数宣言 自動(%ROWTYPE相当) 手動(v_rec CURSOR%ROWTYPE
③コード行数 短い 長い(2倍程度)
④性能(10g+) ◎(暗黙BULK 100件) ○(1件ずつFETCHは遅い)
⑤LIMIT制御 ✗ 固定100件 ◎ BULK COLLECT LIMITで任意指定
⑥FOR UPDATE+WHERE CURRENT OF ◎ 使える(カーソル宣言でのFOR UPDATE) ◎ 使える(主用途)
⑦例外安全 ◎ 自動CLOSE △ EXCEPTIONでのIF ISOPEN+CLOSE必要
⑧同一カーソル複数OPEN △ 別途FORループを並べる ◎ パラメータ付きで使い回し
⑨動的SQL対応 ✗ できない SYS_REFCURSORで対応
⑩ストアドから結果セット返却 ✗ できない ◎ REF CURSORで返却

現代Oracle(10g+)の実質差:①OPEN/CLOSE自動化、②性能同等(暗黙BULK 100件)、③レコード変数自動化——これらのため、FORループで書ける限りはFORループが正解。明示が必要なのはLIMIT制御/REF CURSOR/結果セット返却の3ケース。

10g以降の暗黙BULK COLLECT:FORループが速い理由

Oracle 10g Release 1以降、カーソルFORループは内部で暗黙的にBULK COLLECT 100件に最適化されました。明示的カーソル+1件ずつFETCHと比べて大幅に高速です。

FORループの内部動作(概念)
-- 見た目
FOR rec IN (SELECT id FROM huge_table) LOOP
  process(rec.id);
END LOOP;

-- 内部動作(10g+)
-- OPEN cur;
-- LOOP
--   FETCH cur BULK COLLECT INTO v_batch LIMIT 100;  ← 暗黙BULK
--   EXIT WHEN v_batch.COUNT = 0;
--   FOR i IN 1..v_batch.COUNT LOOP
--     process(v_batch(i).id);
--   END LOOP;
-- END LOOP;
-- CLOSE cur;
明示的カーソル+1件ずつ(比較対象・遅い)
DECLARE
  CURSOR cur IS SELECT id FROM huge_table;
  v_rec cur%ROWTYPE;
BEGIN
  OPEN cur;
  LOOP
    FETCH cur INTO v_rec;   -- 1件ずつ(ラウンドトリップ多い)
    EXIT WHEN cur%NOTFOUND;
    process(v_rec.id);
  END LOOP;
  CLOSE cur;
END;
-- Oracle 9i以前はこの書き方しか選択肢がなく、性能の観点で必要だった
-- 10g+では暗黙BULKで自動最適化されるのでFORループで十分

歴史的経緯

Oracle 9i以前は明示的カーソル+FETCH ... BULK COLLECT LIMITが性能最速でした。10gの最適化によりFORループも自動的に同等の速さになったため、「明示的カーソルが速い」というのは現代では古い情報です。2007年以降のOracleではFORループ一択で十分な性能が出ます。

実測ベンチマーク:3方式の速度差

件数 明示+1件ずつ カーソルFORループ BULK COLLECT+FORALL
10,000行(読取のみ) 約 2.0秒 約 0.3秒 約 0.2秒
10,000行(1件ずつUPDATE) 約 15秒 約 15秒 約 0.3秒
100,000行(読取のみ) 約 20秒 約 3秒 約 2秒

重要な観察:読み取りのみならFORループBULK COLLECTはほぼ同等、②UPDATE/INSERT/DELETEが入るBULK COLLECT+FORALLが圧勝、③「明示的カーソル+1件ずつFETCH」は常に遅い——この書き方は10g以降推奨されません。バルク処理詳細は【PL/SQL】バルク処理完全ガイド参照。

判断フローチャート:6問で正解を選ぶ

フロー
Q1: 結果セットを1行ずつ処理したい?
  NO  → 集合演算(SELECTのみ)で済むか検討
  YES → Q2

Q2: ループ内でINSERT/UPDATE/DELETEする?
  NO  → Q3
  YES → Q4

Q3(読み取りのみ): LIMIT制御が必要?
  NO  → 【カーソルFORループ】(最シンプル)
  YES → 【明示的カーソル+BULK COLLECT LIMIT】

Q4(DMLあり): 件数は1000件以上?
  NO  → カーソルFORループ内でDMLでもOK(遅くても許容)
  YES → 【BULK COLLECT+FORALL】で10〜100倍速

Q5(FOR UPDATE使いたい): 行ロック+ROWID更新?
  YES → 【明示的カーソル+WHERE CURRENT OF】 or
         【カーソルFORループで宣言したCURSORに FOR UPDATE】

Q6(結果セット返却・動的SQL): ストアドから返す/動的SQL?
  YES → 【REF CURSOR (SYS_REFCURSOR)】

実務の現実:開発者が書くPL/SQLの8割はQ3のNOルート(単純な読み取り処理)でカーソルFORループが最適解。パフォーマンスチューニングを気にするのはQ4(大量DML)の場面だけで、ここはBULK COLLECT+FORALL一択。明示的カーソル単体で書く必要があるのは全体の1〜2割程度。

FOR UPDATE+WHERE CURRENT OF:両方で使える

明示的カーソルでの典型パターン
DECLARE
  CURSOR cur IS SELECT id, amount FROM orders WHERE status = 'pending'
                FOR UPDATE;
  v_rec cur%ROWTYPE;
BEGIN
  OPEN cur;
  LOOP
    FETCH cur INTO v_rec;
    EXIT WHEN cur%NOTFOUND;

    IF v_rec.amount > 10000 THEN
      UPDATE orders SET status = 'hold' WHERE CURRENT OF cur;
    ELSE
      UPDATE orders SET status = 'approved' WHERE CURRENT OF cur;
    END IF;
  END LOOP;
  CLOSE cur;
  COMMIT;
END;
カーソルFORループでも WHERE CURRENT OF 使える
DECLARE
  CURSOR cur IS SELECT id, amount FROM orders WHERE status = 'pending'
                FOR UPDATE;
BEGIN
  FOR rec IN cur LOOP
    IF rec.amount > 10000 THEN
      UPDATE orders SET status = 'hold' WHERE CURRENT OF cur;
    ELSE
      UPDATE orders SET status = 'approved' WHERE CURRENT OF cur;
    END IF;
  END LOOP;
  COMMIT;
END;

意外な事実:カーソルFORループでもカーソル宣言部にFOR UPDATEを書けばWHERE CURRENT OFが使えます。ただしインラインSELECTFOR rec IN (SELECT ...))ではカーソル名がないのでWHERE CURRENT OFは使えません。宣言版カーソルFORループを使うのがポイント。

COMMITの落とし穴:FOR UPDATE中にループ内でCOMMITするとロックが解放され、以降のWHERE CURRENT OFORA-01002でエラーになります。分割コミットが必要ならBULK COLLECT LIMITFORALLROWIDパターンへ切り替えましょう。

巨大データ処理での選び方:3層戦略

戦略①:小〜中規模(〜1万件、読み取りメイン)→ カーソルFORループ

FOR rec IN (SELECT id, amount FROM orders WHERE order_date = TRUNC(SYSDATE)) LOOP
  process_report_row(rec.id, rec.amount);
END LOOP;
-- 暗黙BULK 100件で十分高速、コードもシンプル

戦略②:中〜大規模(〜10万件、DMLあり)→ BULK COLLECT+FORALL

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 5000;
    EXIT WHEN v_ids.COUNT = 0;
    FORALL i IN 1..v_ids.COUNT
      UPDATE orders SET status = 'processing' WHERE id = v_ids(i);
    COMMIT;
  END LOOP;
  CLOSE cur;
END;

戦略③:超大規模(100万件〜)→ SQL集合演算 or パーティション

-- 1行ずつのロジックが不要なら単一SQLで処理
UPDATE orders SET status = 'closed'
 WHERE status = 'pending'
   AND order_date < SYSDATE - 90;

-- パーティション指定で絞り込み
UPDATE orders PARTITION (p2026_04) SET status = 'closed'
 WHERE status = 'pending';

-- PL/SQLを使うとしてもパーティション単位で
FOR rec IN (SELECT DISTINCT partition_name
            FROM user_tab_partitions WHERE table_name = 'ORDERS') LOOP
  EXECUTE IMMEDIATE
    'UPDATE orders PARTITION (' || rec.partition_name || 
    ') SET status = ''closed'' WHERE ...';
  COMMIT;
END LOOP;

選択の判断基準

  • 1行ずつの判定や加工が必要→PL/SQLループ(カーソルFOR or BULK+FORALL)
  • 単純な集合更新→単一SQL(最速)
  • 100万件以上→パーティション指定+並列DML(PARALLEL DML ヒント)
  • バルク処理の詳細は【PL/SQL】バルク処理完全ガイド参照

例外処理とリソース管理の違い

FORループは例外安全(自動CLOSE)
BEGIN
  FOR rec IN cur_users LOOP
    IF rec.age < 0 THEN
      RAISE_APPLICATION_ERROR(-20001, '不正な年齢');
    END IF;
    process(rec.id);
  END LOOP;
EXCEPTION
  WHEN OTHERS THEN
    log_error(SQLCODE, SQLERRM);
    RAISE;    -- カーソルは自動CLOSE済み
END;
明示的カーソルはEXCEPTION内で手動CLOSE必須
DECLARE
  CURSOR cur IS SELECT id FROM users;
  v_rec cur%ROWTYPE;
BEGIN
  OPEN cur;
  LOOP
    FETCH cur INTO v_rec;
    EXIT WHEN cur%NOTFOUND;
    process(v_rec.id);   -- ここで例外
  END LOOP;
  CLOSE cur;
EXCEPTION
  WHEN OTHERS THEN
    IF cur%ISOPEN THEN   -- ← 手動CLOSE必須
      CLOSE cur;
    END IF;
    log_error(SQLCODE, SQLERRM);
    RAISE;
END;

明示的カーソルのCLOSE忘れはセッションあたりのOPEN_CURSORSパラメータ(通常300)を超えるとORA-01000: maximum open cursors exceededでアプリ停止。FORループなら自動CLOSEなのでこの問題が起きません。

動的SQLとREF CURSOR

FORループは動的SQL不可→REF CURSORへ
-- ❌ これはコンパイルエラー
-- FOR rec IN ('SELECT id FROM ' || p_table_name) LOOP ... END LOOP;

-- ⭕ SYS_REFCURSORで動的SQL対応
DECLARE
  v_rc SYS_REFCURSOR;
  v_id NUMBER;
BEGIN
  OPEN v_rc FOR 'SELECT id FROM ' || p_table_name || ' WHERE status = :s' 
    USING 'active';
  LOOP
    FETCH v_rc INTO v_id;
    EXIT WHEN v_rc%NOTFOUND;
    process(v_id);
  END LOOP;
  CLOSE v_rc;
END;

REF CURSORの詳細は

REF CURSORの完全ガイドは【PL/SQL】REF CURSORを使った柔軟なデータ取得方法。強い型付け/ストアドから返却/Javaやクライアントでの受け取りなどを詳解しています。

実務シナリオ10選:正解早見表

シナリオ 正解 理由
①レポート生成(全行出力) FORループ シンプル、暗黙BULKで十分速い
②1万件のステータス一括更新 BULK COLLECT+FORALL 1件ずつDMLは激遅
③条件判定の行更新(件数少) FORループ内DML 記述が明確、性能影響少
④100万件アーカイブ→削除 明示+BULK COLLECT LIMIT LIMIT/分割COMMIT制御が必要
⑤FOR UPDATE+複雑な更新 明示(またはFORループ+宣言カーソル) WHERE CURRENT OFが必要
⑥動的テーブル名のクエリ REF CURSOR FORループでは不可
⑦ストアドから結果セット返却 REF CURSOR Javaや.NETが受け取れる
⑧親テーブル→子テーブル走査 ネストFORループ+パラメータ付きカーソル 階層が深すぎるならJOINを検討
⑨同じクエリを引数違いで複数回 明示+パラメータ付きカーソル CURSOR定義を再利用
⑩キュー型処理(SKIP LOCKED) 明示+FOR UPDATE SKIP LOCKED ワーカー並列処理に最適

アンチパターン7選

①「性能のため」に明示カーソルを選ぶ。10g以降はFORループも暗黙BULK 100件で同等に速い。理由もなく明示にするとコード量が2倍、CLOSE忘れリスク。

②明示カーソルのCLOSE忘れORA-01000: maximum open cursors exceededでアプリ停止。EXCEPTION節でIF cur%ISOPEN THEN CLOSE cur; END IF;必須。

③FORループ内で1件ずつDML。1万件で数十秒〜数分の遅延。BULK COLLECT+FORALLに書き換えで10〜100倍速。

④LIMIT無しのBULK COLLECT。1億件を全件ロードするとPGA枯渇(ORA-04030)。LIMIT 1000〜10000で分割。

⑤FOR UPDATE中のループ内COMMIT。ロック解放でWHERE CURRENT OFORA-01002。分割コミットが必要ならBULK COLLECT+FORALL+ROWIDへ。

⑥インラインSELECTでWHERE CURRENT OF。インライン版FOR rec IN (SELECT...)にはカーソル名がないのでWHERE CURRENT OFが書けない。宣言版CURSOR cur IS...+FORループへ。

⑦動的SQLをEXECUTE IMMEDIATEだけで書く。結果セット取得にはSYS_REFCURSORを使う。結果を1件ずつ処理したいならOPEN v_rc FOR 'SELECT ...'FETCHCLOSE

よくある質問

QカーソルFORループと明示的カーソル、どちらが速い?
A10g以降はほぼ同等です。カーソルFORループは内部で暗黙BULK COLLECT 100件に最適化されており、明示的カーソル+1件ずつFETCHよりむしろ速い。「明示の方が速い」は9i以前の古い常識。
QカーソルFORループでもWHERE CURRENT OFは使える?
A使えます(宣言版のみ)。CURSOR cur IS SELECT ... FOR UPDATE;と宣言しておけばFOR rec IN cur LOOP ... UPDATE ... WHERE CURRENT OF cur; END LOOP;が動きます。インライン版FOR rec IN (SELECT ...))はカーソル名が無いので不可。
QLIMIT制御が必要な場合は?
A明示的カーソル+FETCH BULK COLLECT INTO ... LIMIT Nパターンを使います。1バッチあたりの件数を自由に指定でき、PGAメモリを抑えながら大量データを処理できます。カーソルFORループは固定100件なので調整不可。
Q動的SQLを1行ずつ処理したい
ASYS_REFCURSOR(REF CURSOR)を使います。OPEN v_rc FOR '動的SQL' USING バインド変数;で開きFETCH v_rc INTO ...; EXIT WHEN v_rc%NOTFOUND;でループ。カーソルFORループは静的SQL限定なので使えません。
Qネストしたカーソルはどっちがいい?
Aパラメータ付き宣言カーソル+FORループが最もクリーン。親ループで取得した値を子カーソルに渡す設計で、CURSOR cur_emp(p_dept_id NUMBER) IS SELECT ... WHERE dept_id = p_dept_id;とし、FOR rec IN cur_emp(dept.id) LOOP ...のように使います。ただしネスト2段以上ならJOINで書き直せないか検討。
Q明示的カーソルを使うべき明確な場面は?
A4つだけです:①BULK COLLECT LIMITでメモリ制御、②SYS_REFCURSOR(動的SQL/結果セット返却)、③FETCH回数を手動制御したい特殊ケース、④パラメータ付きカーソルを複数条件で再利用。これ以外はFORループで十分です。
QカーソルFORループで例外発生したらCLOSEは?
A自動でCLOSEされます。FORループの内部でCLOSEが実行されるため、例外が発生してブロックを抜ける時もリソース漏れはありません。明示的カーソルではEXCEPTION節でIF cur%ISOPEN THEN CLOSE cur; END IF;が必要。
QCursor FOR Loopと BULK COLLECT+FORALLの使い分け
A読み取りのみ+軽量ロジック→Cursor FOR Loop(シンプル)、大量行にDML→BULK COLLECT+FORALL(10〜100倍速)、条件判定のためSELECTして数件UPDATE→どちらでも速度差小。バルク処理詳細は【PL/SQL】バルク処理完全ガイド参照。
QカーソルFORループで%FOUND%ROWCOUNTは参照できる?
A宣言版なら参照可能cur%ROWCOUNT)。ただしFETCHは自動なので%NOTFOUNDでEXIT判定する意味はありません。%ROWCOUNTは現在までに処理した件数を示すため、進捗表示やログ出力に使えます。
QSKIP LOCKED はどっちでも使える?
Aどちらでも使えますが、明示的カーソルの方が意図が明確。キュー型処理(複数ワーカーで並列処理)はCURSOR cur IS SELECT ... FOR UPDATE SKIP LOCKED;で宣言し、明示OPEN/FETCH/CLOSEで制御するのが実務定石。

関連記事

まとめ

  • 2026年現代のOracle(10g+)では、カーソルFORループも暗黙BULK COLLECT 100件に最適化
  • 性能はFORループ=明示+BULK COLLECT 100でほぼ同等、明示+1件ずつは遅い(過去の書き方)
  • 「明示的カーソルが速い」は9i以前の古い常識
  • 大量DML(万件〜)はBULK COLLECT+FORALLが10〜100倍速
  • 明示的カーソルが必要なのはLIMIT制御/REF CURSOR/結果セット返却/パラメータ再利用の4ケース
  • FOR UPDATE+WHERE CURRENT OFは宣言版FORループでも使える(インライン版は不可)
  • FORループは例外安全(自動CLOSE)、明示はEXCEPTION節でIF ISOPEN THEN CLOSE必須
  • 動的SQL/結果セット返却はSYS_REFCURSORを使う
  • 実務の8割はカーソルFORループで十分、BULK COLLECT+FORALLが1.5割、明示的カーソル単体は0.5割
  • アンチパターン:性能目的の明示選択/CLOSE忘れ/1件ずつDML/LIMIT無しBULK/FOR UPDATE中COMMIT/インラインSELECTでWHERE CURRENT OF

カーソルFORループと明示的カーソルの使い分けは、「用途で選ぶ」のが現代的。性能の観点ではFORループで十分(暗黙BULKの恩恵)、柔軟性が必要な場面で明示を使う、というシンプルな判断軸です。カーソル基本はカーソル完全ガイド、バルクはバルク処理完全ガイド、REF CURSORはREF CURSOR詳解、ループ全般はループ処理完全ガイドと組み合わせて活用してください。