「PL/SQLで結果セットを1行ずつ処理したい時、カーソルFORループと明示的カーソルのどちらを使うべきか」——Oracle開発現場で必ず議論になるテーマです。入門記事では「FORループの方がシンプルで速い」「明示的カーソルは柔軟」と曖昧に紹介されがちですが、10g以降の暗黙BULK COLLECTやFOR UPDATE+WHERE CURRENT OFとの相性、メモリ管理を考慮した選び方は深い知識が必要です。
この記事では現代のOracle(10g以降)においてカーソルFORループと明示的カーソルの実質的な差を明らかにし、10軸の完全比較、実測ベンチマーク、判断フローチャート、BULK COLLECT+FORALLを含めた3択の選び方、FOR UPDATE+WHERE 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 UPDATE+WHERE CURRENT OFの両者での挙動差- 例外処理とリソース管理の違い
- 動的SQL/REF CURSORとの組み合わせ
- 実務シナリオ10選の正解
- アンチパターン7選(明示カーソル乱用/CLOSE忘れ等)
30秒結論:3択で迷わない黄金律
最短判断:①単純な読み取り処理→カーソルFORループ(9割の場合)、②大量DMLならBULK COLLECT+FORALL、③FOR UPDATE/LIMIT制御/複数回OPENなら明示的カーソル。この3択で実務の大半は決まります。
両者の構文比較
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件
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軸完全比較表
現代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 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;
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方式の速度差
重要な観察:①読み取りのみなら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;
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が使えます。ただしインラインSELECT(FOR rec IN (SELECT ...))ではカーソル名がないのでWHERE CURRENT OFは使えません。宣言版カーソルFORループを使うのがポイント。
COMMITの落とし穴:FOR UPDATE中にループ内でCOMMITするとロックが解放され、以降のWHERE CURRENT OFがORA-01002でエラーになります。分割コミットが必要ならBULK COLLECT LIMIT+FORALL+ROWIDパターンへ切り替えましょう。
巨大データ処理での選び方: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】バルク処理完全ガイド参照
例外処理とリソース管理の違い
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;
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 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選:正解早見表
アンチパターン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 OFがORA-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 ...'+FETCH+CLOSE。
よくある質問
WHERE CURRENT OFは使える?CURSOR cur IS SELECT ... FOR UPDATE;と宣言しておけばFOR rec IN cur LOOP ... UPDATE ... WHERE CURRENT OF cur; END LOOP;が動きます。インライン版(FOR rec IN (SELECT ...))はカーソル名が無いので不可。FETCH BULK COLLECT INTO ... LIMIT Nパターンを使います。1バッチあたりの件数を自由に指定でき、PGAメモリを抑えながら大量データを処理できます。カーソルFORループは固定100件なので調整不可。OPEN v_rc FOR '動的SQL' USING バインド変数;で開きFETCH v_rc INTO ...; EXIT WHEN v_rc%NOTFOUND;でループ。カーソルFORループは静的SQL限定なので使えません。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で書き直せないか検討。IF cur%ISOPEN THEN CLOSE cur; END IF;が必要。%FOUNDや%ROWCOUNTは参照できる?cur%ROWCOUNT)。ただしFETCHは自動なので%NOTFOUNDでEXIT判定する意味はありません。%ROWCOUNTは現在までに処理した件数を示すため、進捗表示やログ出力に使えます。CURSOR cur IS SELECT ... FOR UPDATE SKIP LOCKED;で宣言し、明示OPEN/FETCH/CLOSEで制御するのが実務定石。関連記事
- 【PL/SQL】カーソル完全ガイド — 4フェーズ(DECLARE/OPEN/FETCH/CLOSE)とカーソル属性
- 【PL/SQL】バルク処理完全ガイド — BULK COLLECT+FORALLの詳解
- 【PL/SQL】REF CURSORを使った柔軟なデータ取得方法 — 動的SQL/結果セット返却
- 【PL/SQL】ループ処理完全ガイド — 4種ループ(Basic/WHILE/FOR/Cursor FOR)
- 【PL/SQL】例外処理完全ガイド — CLOSE忘れとORA-01000回避
- 【PL/SQL】IF文完全ガイド — カーソルループ内の条件分岐
- 【PL/SQL】変数・定数完全ガイド — %ROWTYPE でレコード変数
- 【PL/SQL】基本構文完全ガイド — ブロック構造の土台
- 【PL/SQL】プロシージャ・ファンクション完全ガイド — REF CURSORを返すファンクション
- 【PL/SQL】動的SQLのセキュアな書き方 — EXECUTE IMMEDIATE+バインド変数
まとめ
- 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詳解、ループ全般はループ処理完全ガイドと組み合わせて活用してください。

