【PL/SQL】カーソル完全ガイド|4フェーズ・%ROWTYPE・FOR UPDATE+SKIP LOCKED・BULK COLLECT LIMIT・実務10パターン

PL/SQLのカーソル(CURSOR)は、SQLクエリの結果セット(複数行)を1行ずつ処理するための仕組みで、Oracleバッチ処理の根幹を成す機能です。SELECT INTOが1行しか扱えないのに対し、カーソルは100万行でも順次処理できます。

入門記事の多くはOPEN/FETCH/CLOSEの基本構文で終わっていますが、実務では%ROWTYPEによるレコード変数、%FOUND/%NOTFOUND/%ROWCOUNT/%ISOPENの4属性、パラメータ付きカーソル、FOR UPDATE+WHERE CURRENT OF、BULK COLLECT LIMIT、SYS_REFCURSORの返却、例外安全なクローズ処理まで理解して初めて使いこなせるようになります。

この記事では、カーソルの4フェーズ(DECLARE/OPEN/FETCH/CLOSE)を基礎から深掘りし、暗黙/明示/カーソルFORループ/REF CURSORの使い分け、FOR UPDATE+WHERE CURRENT OFによる安全な行ロック更新、BULK COLLECT LIMITでの超高速バッチ、実務パターン10選、アンチパターン7選まで網羅した決定版ガイドです。明示カーソルとCursor FOR Loopの性能比較特化は【PL/SQL】カーソルFORループと明示的カーソルの使い分け、REF CURSOR詳解は【PL/SQL】REF CURSORを使った柔軟なデータ取得方法、ループ処理全般は【PL/SQL】ループ処理完全ガイドを併読推奨。

この記事で学べること

  • カーソルの4フェーズ(DECLARE→OPEN→FETCH→CLOSE)完全理解
  • 暗黙カーソル/明示カーソル/Cursor FOR Loop/REF CURSORの使い分けマトリクス
  • カーソル属性4種%FOUND%NOTFOUND%ROWCOUNT%ISOPEN
  • %ROWTYPEでレコード変数を自動定義(保守性が劇的向上)
  • パラメータ付きカーソルで同じクエリ形を複数条件で使い回す
  • FOR UPDATE+WHERE CURRENT OF行ロック+安全UPDATE
  • BULK COLLECT LIMITで大量データを10〜100倍高速処理
  • SYS_REFCURSOR/カーソル変数でストアドから結果セット返却
  • 例外安全なクローズ処理(リソースリーク防止)
  • 実務パターン10選(分割コミット/ネストカーソル/ステータス遷移等)
  • アンチパターン7選(CLOSE忘れ/1行DML連打/巨大BULK等)
スポンサーリンク

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

明示カーソルの基本形(4フェーズ)
DECLARE
  -- ① DECLARE: カーソル宣言
  CURSOR cur_active_users IS
    SELECT id, email FROM users WHERE status = 'active';

  -- レコード変数(%ROWTYPEで自動定義)
  v_user cur_active_users%ROWTYPE;
BEGIN
  -- ② OPEN: カーソルを開いてSQL実行
  OPEN cur_active_users;

  -- ③ FETCH: 1行ずつ取得してループ処理
  LOOP
    FETCH cur_active_users INTO v_user;
    EXIT WHEN cur_active_users%NOTFOUND;

    DBMS_OUTPUT.PUT_LINE(v_user.id || ': ' || v_user.email);
  END LOOP;

  -- ④ CLOSE: カーソルを閉じる(重要)
  CLOSE cur_active_users;
END;
/

実務で推奨はCursor FOR Loop(OPEN/CLOSE自動化)。明示的なOPEN/FETCH/CLOSEが必要になるのはFOR UPDATEで行ロック、BULK COLLECT LIMITでサイズ制御、SYS_REFCURSORで戻り値化、という特殊要件のときだけ。

最も簡潔:Cursor FOR Loop版

同じ処理をCursor FOR Loopで
BEGIN
  FOR rec IN (
    SELECT id, email FROM users WHERE status = 'active'
  ) LOOP
    DBMS_OUTPUT.PUT_LINE(rec.id || ': ' || rec.email);
  END LOOP;
  -- OPEN/CLOSE/FETCH/%NOTFOUND は自動管理
END;

カーソルの内部構造:3つの要素

カーソルは「SQLクエリ実行のためのメモリ領域+結果セット位置ポインタ」の集合です。Oracle側で私用SQL領域(private SQL area)としてメモリに確保されます。

カーソルが管理する3要素
1. 解析済みのSQL文(parsed SQL statement)
2. 結果セットの位置ポインタ(current row pointer)
3. 実行状態(OPEN/CLOSE/属性値)

カーソル ≒ プログラムからの「結果セットへのハンドル」

暗黙カーソルと明示カーソルの違い

観点 暗黙カーソル 明示カーソル
管理者 Oracleが自動管理 開発者が明示制御
対象SQL SELECT INTO/DMLすべて CURSOR 〜 IS SELECTで宣言したもの
対象行数 1行(SELECT INTO 複数行
属性参照 SQL%FOUND <cursor_name>%FOUND
用途 単一行取得、DML後の行数確認 複数行の順次処理、行ロック、再利用
暗黙カーソルの属性利用例
BEGIN
  UPDATE users SET status = 'inactive' WHERE last_login &lt; SYSDATE - 365;

  -- 暗黙カーソル SQL の属性で更新行数取得
  IF SQL%ROWCOUNT = 0 THEN
    DBMS_OUTPUT.PUT_LINE('更新対象なし');
  ELSE
    DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT || '件更新');
  END IF;

  COMMIT;
END;

明示カーソルの4フェーズ詳解

①DECLARE:宣言

宣言パターン
DECLARE
  -- シンプルな宣言
  CURSOR cur_users IS
    SELECT id, email FROM users WHERE status = 'active';

  -- パラメータ付き
  CURSOR cur_by_status(p_status VARCHAR2) IS
    SELECT id, email FROM users WHERE status = p_status;

  -- FOR UPDATEで行ロック
  CURSOR cur_pending_orders IS
    SELECT * FROM orders WHERE status = 'pending'
    FOR UPDATE;   -- FETCH時に行ロック

  -- RETURN句で戻り型明示(パッケージ定義向き)
  CURSOR cur_active_user RETURN users%ROWTYPE IS
    SELECT * FROM users WHERE status = 'active';
BEGIN
  NULL;
END;

②OPEN:SQL実行

OPEN動作
BEGIN
  -- OPENで宣言済SQLがパース&実行される
  OPEN cur_users;

  -- パラメータ付きは引数指定
  OPEN cur_by_status('active');

  -- この時点ではまだ結果セットの先頭のポインタが立っているだけ
  -- 実際のデータはFETCHしないと取れない
  NULL;
END;

③FETCH:行取得

FETCH 3パターン
DECLARE
  CURSOR cur IS SELECT id, email FROM users;
  v_id users.id%TYPE;
  v_email users.email%TYPE;
  v_user users%ROWTYPE;
  TYPE t_users IS TABLE OF users%ROWTYPE;
  v_users t_users;
BEGIN
  OPEN cur;

  -- パターン①:個別変数へFETCH
  FETCH cur INTO v_id, v_email;

  -- パターン②:レコード変数へFETCH(%ROWTYPE)
  FETCH cur INTO v_user;
  DBMS_OUTPUT.PUT_LINE(v_user.id || ' ' || v_user.email);

  -- パターン③:BULK COLLECTで一括取得(後述)
  FETCH cur BULK COLLECT INTO v_users LIMIT 1000;

  CLOSE cur;
END;

④CLOSE:リソース解放

CLOSE忘れの怖さ
DECLARE
  CURSOR cur IS SELECT id FROM users;
BEGIN
  OPEN cur;
  -- ... 処理 ...
  CLOSE cur;  -- 必須:Oracleが管理するメモリを解放

  -- CLOSE忘れるとセッション中リソースリーク
  -- 特にループ内でOPENするカーソルは要注意

  -- 例外安全なパターン(EXCEPTION節でも閉じる)
  OPEN cur;
  BEGIN
    -- ... 処理 ...
    CLOSE cur;
  EXCEPTION
    WHEN OTHERS THEN
      IF cur%ISOPEN THEN
        CLOSE cur;
      END IF;
      RAISE;
  END;
END;

CLOSE忘れの実害:①セッションあたりのカーソル数上限(OPEN_CURSORSパラメータ、通常300)を超えるとORA-01000: maximum open cursors exceededでアプリが停止、②メモリ枯渇、③ネストブロックで同じカーソルを再OPENするとCURSOR_ALREADY_OPEN(ORA-06511)例外。EXCEPTION節でもIF cur%ISOPEN THEN CLOSE cur;でガードするのが鉄則。

カーソル属性4種:%FOUND/%NOTFOUND/%ROWCOUNT/%ISOPEN

属性 返り値 OPEN前 FETCH後
%ISOPEN BOOLEAN FALSE TRUE(CLOSEまで)
%FOUND BOOLEAN INVALID_CURSOR例外 行取得TRUE/取得不可FALSE
%NOTFOUND BOOLEAN INVALID_CURSOR例外 %FOUNDの反対
%ROWCOUNT NUMBER INVALID_CURSOR例外 これまでFETCHした総行数
4属性の使用例
DECLARE
  CURSOR cur IS SELECT id, name FROM users;
  v_row cur%ROWTYPE;
BEGIN
  -- ISOPEN:OPEN前の確認
  IF NOT cur%ISOPEN THEN
    OPEN cur;
  END IF;

  LOOP
    FETCH cur INTO v_row;

    -- NOTFOUND:取得できない=ループ終了
    EXIT WHEN cur%NOTFOUND;

    -- ROWCOUNT:現在までの取得行数
    DBMS_OUTPUT.PUT_LINE(cur%ROWCOUNT || ': ' || v_row.name);
  END LOOP;

  DBMS_OUTPUT.PUT_LINE('取得総行数: ' || cur%ROWCOUNT);
  CLOSE cur;
END;

暗黙カーソル属性(SQL%)

DML後の行数チェック
BEGIN
  -- UPDATE実行
  UPDATE users SET status = 'inactive'
   WHERE last_login &lt; SYSDATE - 365;

  -- 直前SQLの暗黙カーソル属性を参照
  DBMS_OUTPUT.PUT_LINE('更新行数: ' || SQL%ROWCOUNT);

  -- 0件なら処理を分岐
  IF SQL%NOTFOUND THEN
    DBMS_OUTPUT.PUT_LINE('更新対象なし');
  END IF;
END;

%FOUND/%NOTFOUND/%ROWCOUNT は FETCH後のみ有効。OPEN直後(FETCH前)に参照するとINVALID_CURSOR例外。必ずFETCHEXIT WHEN cur%NOTFOUNDの順で使用する。

%ROWTYPEによるレコード変数:保守性が劇的向上

%ROWTYPEパターン
DECLARE
  -- テーブル%ROWTYPE
  v_user users%ROWTYPE;
  -- ↑ usersテーブルの全カラムを持つ構造体

  -- カーソル%ROWTYPE(実務推奨)
  CURSOR cur_users IS
    SELECT id, email, created_at FROM users;
  v_row cur_users%ROWTYPE;
  -- ↑ カーソルのSELECT列だけを持つ構造体(テーブル全体より軽量)
BEGIN
  -- テーブル行を丸ごと取得
  SELECT * INTO v_user FROM users WHERE id = 1;
  DBMS_OUTPUT.PUT_LINE(v_user.email);

  -- カーソル経由
  OPEN cur_users;
  LOOP
    FETCH cur_users INTO v_row;
    EXIT WHEN cur_users%NOTFOUND;
    DBMS_OUTPUT.PUT_LINE(v_row.id || ': ' || v_row.email);
  END LOOP;
  CLOSE cur_users;
END;

%ROWTYPEの3つの利点:①テーブル/カーソルの列変更に自動追従(カラム追加で宣言修正不要)、②個別変数宣言が不要でコード量半減、③v_row.column_nameで構造化アクセスできる。カーソル%ROWTYPE(SELECTした列だけ)を使うとメモリもコンパクト。

レコード単位のINSERT/UPDATE

レコード丸ごとDML(10g+)
DECLARE
  v_user users%ROWTYPE;
BEGIN
  -- レコードを組み立てて
  v_user.id := 100;
  v_user.email := 'test@example.com';
  v_user.created_at := SYSDATE;

  -- レコード単位のINSERT
  INSERT INTO users VALUES v_user;

  -- レコード単位のUPDATE
  v_user.email := 'new@example.com';
  UPDATE users SET ROW = v_user WHERE id = v_user.id;

  COMMIT;
END;

パラメータ付きカーソル:条件を使い回す

パラメータ付きカーソル
DECLARE
  -- 複数の引数可
  CURSOR cur_users(
    p_status   VARCHAR2,
    p_min_age  NUMBER DEFAULT 0   -- デフォルト値も可
  ) IS
    SELECT id, email FROM users
    WHERE status = p_status AND age &gt;= p_min_age;
BEGIN
  -- 条件を変えて何度も使い回せる
  FOR rec IN cur_users('active') LOOP
    DBMS_OUTPUT.PUT_LINE('active: ' || rec.email);
  END LOOP;

  FOR rec IN cur_users('pending', 18) LOOP
    DBMS_OUTPUT.PUT_LINE('pending: ' || rec.email);
  END LOOP;

  FOR rec IN cur_users('suspended') LOOP
    DBMS_OUTPUT.PUT_LINE('suspended: ' || rec.email);
  END LOOP;
END;

パラメータ使い回しの利点

カーソル定義は1回、呼び出し時に引数で条件を変える設計はLibrary Cache HIT率を上げ、パース時間を削減します。バインド変数の役割も果たすため、SQLインジェクション対策にも有効。パッケージ内で共通カーソルとして提供すれば保守性も向上。

FOR UPDATE + WHERE CURRENT OF:行ロック付き更新

取得した行を確実にロックしてから更新/削除する悲観ロックパターン。他のトランザクションに割り込まれず、条件取得→更新を原子的に行えます。

基本パターン
DECLARE
  CURSOR cur_pending IS
    SELECT id, amount FROM orders WHERE status = 'pending'
    FOR UPDATE;   -- 該当行を排他ロック
BEGIN
  FOR rec IN cur_pending LOOP
    -- この時点で rec の行はロック済
    IF rec.amount &gt; 10000 THEN
      UPDATE orders SET status = 'review_required'
      WHERE CURRENT OF cur_pending;   -- 現在のカーソル行を更新
    ELSE
      UPDATE orders SET status = 'approved'
      WHERE CURRENT OF cur_pending;
    END IF;
  END LOOP;

  COMMIT;
END;
オプション:NOWAIT / SKIP LOCKED / WAIT n
-- NOWAIT:ロック競合時に即エラー(ORA-00054)
... FOR UPDATE NOWAIT;

-- SKIP LOCKED:ロック中の行を飛ばす(キュー処理に最適)
... FOR UPDATE SKIP LOCKED;

-- WAIT 5:5秒まで待機
... FOR UPDATE WAIT 5;

-- 特定のテーブルだけロック(JOIN時)
... FOR UPDATE OF orders.status;
キュー型バッチの実例(SKIP LOCKED)
-- 複数ワーカーが同じキューを並列処理する場合
DECLARE
  CURSOR cur_jobs IS
    SELECT id, payload FROM job_queue
    WHERE status = 'pending'
    ORDER BY priority DESC, created_at
    FOR UPDATE SKIP LOCKED;   -- 他ワーカーが処理中の行はスキップ
BEGIN
  FOR rec IN cur_jobs LOOP
    -- 自分専用の行として処理
    process_job(rec.id, rec.payload);
    
    UPDATE job_queue SET status = 'done'
    WHERE CURRENT OF cur_jobs;
  END LOOP;
  COMMIT;
END;

FOR UPDATEの注意点:①COMMIT/ROLLBACKでロック解除→CLOSE必要、②ロック対象行が多い時はlock escalationで全表ロックに昇格する可能性、③Cursor FOR Loop内のCOMMITはロック状態を解除し以降のWHERE CURRENT OFがエラーになるため、分割コミットではLIMIT付きBULK COLLECT+FORALL+ROWIDパターンに変更。

BULK COLLECT LIMIT:大量データの高速処理

基本形:LIMITでメモリ保護
DECLARE
  TYPE t_users IS TABLE OF users%ROWTYPE;
  v_users t_users;

  CURSOR cur IS SELECT * FROM users WHERE status = 'pending';
BEGIN
  OPEN cur;
  LOOP
    FETCH cur BULK COLLECT INTO v_users LIMIT 1000;
    EXIT WHEN v_users.COUNT = 0;

    -- 1000件ずつ処理
    FORALL i IN 1..v_users.COUNT
      UPDATE users SET status = 'active'
      WHERE id = v_users(i).id;

    COMMIT;    -- 1000件ごとにCOMMIT
  END LOOP;
  CLOSE cur;
END;
LIMIT無しの罠
-- ❌ LIMIT無しでBULK COLLECT
FETCH cur BULK COLLECT INTO v_users;
-- → 全件をメモリにロード
-- → 1000万行ならPGAメモリ数GB消費でORA-04030

-- ⭕ LIMIT 1000〜10000で分割
FETCH cur BULK COLLECT INTO v_users LIMIT 1000;
-- → 1回1000行ずつで安定処理

BULK COLLECT LIMITの推奨値は1000〜10000。小さすぎ(100未満)はラウンドトリップが多く効果薄、大きすぎ(10万以上)はPGAメモリ圧迫。テーブル行サイズで調整:1KB/行なら5000、10KB/行なら1000が目安。詳細は【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方参照。

SYS_REFCURSOR:ストアドから結果セットを返す

クライアント(JDBC/.NET/SQL*Plus)やPL/SQLプロシージャ間で結果セットを”ハンドル”として受け渡せるのがカーソル変数(REF CURSOR)。ストアドファンクションから複数行を返却する標準的な手段です。

SYS_REFCURSORで結果を返す
CREATE OR REPLACE PROCEDURE get_users(
  p_status IN  VARCHAR2,
  p_rc     OUT SYS_REFCURSOR
) IS
BEGIN
  OPEN p_rc FOR
    SELECT id, email FROM users WHERE status = p_status;
  -- CLOSEは呼び出し側の責任
END;
/

-- 呼び出し側(PL/SQL)
DECLARE
  v_rc   SYS_REFCURSOR;
  v_id   users.id%TYPE;
  v_mail users.email%TYPE;
BEGIN
  get_users('active', v_rc);
  LOOP
    FETCH v_rc INTO v_id, v_mail;
    EXIT WHEN v_rc%NOTFOUND;
    DBMS_OUTPUT.PUT_LINE(v_id || ': ' || v_mail);
  END LOOP;
  CLOSE v_rc;   -- 閉じる責任
END;

REF CURSORの詳細は別記事で

強い型付けREF CURSOR、動的SQL組み合わせ、複数結果返却などの詳細は【PL/SQL】REF CURSORを使った柔軟なデータ取得方法で完全解説しています。Javaから呼び出す場合はCallableStatement.getObject(JDBC Types.REF_CURSOR)で受け取ります。

例外処理連携:リソースリーク防止

例外安全なCLOSE
DECLARE
  CURSOR cur IS SELECT id FROM users;
  v_id users.id%TYPE;
BEGIN
  OPEN cur;
  LOOP
    FETCH cur INTO v_id;
    EXIT WHEN cur%NOTFOUND;

    -- 例外が出そうな処理
    process_user(v_id);
  END LOOP;
  CLOSE cur;
EXCEPTION
  WHEN OTHERS THEN
    -- EXCEPTION節でもCLOSE確認
    IF cur%ISOPEN THEN
      CLOSE cur;
    END IF;
    log_error(SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
    RAISE;
END;

NO_DATA_FOUND/TOO_MANY_ROWS との関係

暗黙カーソル vs 明示カーソル例外挙動
-- 暗黙カーソル:NO_DATA_FOUND例外が発生する
BEGIN
  SELECT name INTO v_name FROM users WHERE id = 9999;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('対象なし');
END;

-- 明示カーソル:NO_DATA_FOUNDは発生しない
-- 代わりに %NOTFOUND が TRUE になる
DECLARE
  CURSOR cur IS SELECT name FROM users WHERE id = 9999;
  v_name users.name%TYPE;
BEGIN
  OPEN cur;
  FETCH cur INTO v_name;
  IF cur%NOTFOUND THEN
    DBMS_OUTPUT.PUT_LINE('対象なし');
  END IF;
  CLOSE cur;
END;

明示カーソルはNO_DATA_FOUNDを投げないという点に注意。代わりに%NOTFOUND属性で判定します。暗黙カーソル(SELECT INTO)と挙動が違うため混在コードで混乱しやすい。例外処理設計は【PL/SQL】例外処理完全ガイドを参照。

実務パターン10選

①シンプル集計ループ(Cursor FOR Loop)

DECLARE
  v_total NUMBER := 0;
BEGIN
  FOR rec IN (
    SELECT amount FROM orders WHERE status = 'paid'
  ) LOOP
    v_total := v_total + rec.amount;
  END LOOP;
  DBMS_OUTPUT.PUT_LINE('合計: ' || v_total);
END;

②パラメータ付きカーソル+条件再利用

DECLARE
  CURSOR cur_by_dept(p_dept NUMBER) IS
    SELECT id, name FROM employees WHERE dept_id = p_dept;
BEGIN
  FOR rec IN cur_by_dept(10) LOOP
    process_emp(rec.id);
  END LOOP;

  FOR rec IN cur_by_dept(20) LOOP
    process_emp(rec.id);
  END LOOP;
END;

③分割コミットバッチ(BULK COLLECT LIMIT)

DECLARE
  TYPE t_ids IS TABLE OF orders.id%TYPE;
  v_ids t_ids;
  CURSOR cur IS SELECT id FROM orders WHERE status = 'expired';
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
      DELETE FROM orders WHERE id = v_ids(i);
    COMMIT;
  END LOOP;
  CLOSE cur;
END;

④FOR UPDATEで排他処理

BEGIN
  FOR rec IN (
    SELECT id, amount FROM transactions WHERE status = 'pending'
    FOR UPDATE
  ) LOOP
    IF rec.amount &gt; 10000 THEN
      UPDATE transactions SET status = 'hold' WHERE CURRENT OF ???;
      -- ↑ Cursor FOR Loopでは WHERE CURRENT OF 不可
      -- 代わりに id 指定で更新
      UPDATE transactions SET status = 'hold' WHERE id = rec.id;
    END IF;
  END LOOP;
  COMMIT;
END;

⑤SKIP LOCKEDでキュー型並列処理

DECLARE
  CURSOR cur_jobs IS
    SELECT id, payload FROM job_queue
    WHERE status = 'pending'
    ORDER BY priority DESC
    FOR UPDATE SKIP LOCKED;
BEGIN
  FOR rec IN cur_jobs LOOP
    process_job(rec.id, rec.payload);
    UPDATE job_queue SET status = 'done' WHERE CURRENT OF cur_jobs;
  END LOOP;
  COMMIT;
END;

⑥ネストカーソル(親→子テーブル)

BEGIN
  FOR dept IN (SELECT id, name FROM departments) LOOP
    DBMS_OUTPUT.PUT_LINE('部署: ' || dept.name);

    FOR emp IN (
      SELECT id, name FROM employees WHERE dept_id = dept.id
    ) LOOP
      DBMS_OUTPUT.PUT_LINE('  ' || emp.name);
    END LOOP;
  END LOOP;
END;

⑦例外安全なOPEN/CLOSE

DECLARE
  CURSOR cur IS SELECT id FROM users;
BEGIN
  OPEN cur;
  -- 処理
  CLOSE cur;
EXCEPTION
  WHEN OTHERS THEN
    IF cur%ISOPEN THEN CLOSE cur; END IF;
    RAISE;
END;

⑧SYS_REFCURSORで結果セット返却

CREATE OR REPLACE FUNCTION get_top_users RETURN SYS_REFCURSOR IS
  v_rc SYS_REFCURSOR;
BEGIN
  OPEN v_rc FOR
    SELECT id, email, created_at FROM users
    ORDER BY created_at DESC
    FETCH FIRST 10 ROWS ONLY;
  RETURN v_rc;
END;

⑨SELECT…FOR UPDATE+再評価

-- 在庫引当の排他制御
DECLARE
  v_stock products.stock%TYPE;
BEGIN
  SELECT stock INTO v_stock
  FROM products
  WHERE id = p_id
  FOR UPDATE;     -- ここで行ロック取得

  IF v_stock &lt; p_qty THEN
    RAISE_APPLICATION_ERROR(-20001, '在庫不足');
  END IF;

  UPDATE products SET stock = stock - p_qty WHERE id = p_id;
  COMMIT;
END;

⑩カーソル+レコード単位INSERT

BEGIN
  FOR rec IN (
    SELECT * FROM users_staging WHERE imported = 0
  ) LOOP
    INSERT INTO users VALUES rec;   -- レコード単位INSERT
    UPDATE users_staging SET imported = 1 WHERE id = rec.id;
  END LOOP;
  COMMIT;
END;

アンチパターン7選

①CLOSE忘れ。ORA-01000(maximum open cursors exceeded)→アプリ停止。EXCEPTION節でもIF cur%ISOPEN THEN CLOSE cur;でガード必須。

②Cursor FOR Loop内で1行UPDATE連打。1万行で数十秒遅延。BULK COLLECT+FORALLに書き換えで10〜100倍高速化。

③LIMITなしのBULK COLLECT。全件メモリロードでPGA枯渇(ORA-04030)。LIMIT 1000〜10000で分割処理する。

④OPEN前/CLOSE後の属性参照%FOUND%NOTFOUND%ROWCOUNTはFETCH後のみ有効。%ISOPENだけはいつでも参照可能。

⑤明示カーソルでNO_DATA_FOUNDを捕まえようとする。明示カーソルはこの例外を出さず%NOTFOUNDで判定する。暗黙カーソル(SELECT INTO)とは挙動が違う。

⑥Cursor FOR LoopでFOR UPDATE後にCOMMIT。COMMITでロック解除されると以降のWHERE CURRENT OFがエラー。分割コミットが必要ならBULK COLLECT+FORALL+ROWIDパターンに変更。

⑦カーソル変数を複数人で使い回す。OPEN済みSYS_REFCURSORを別プロシージャに渡すのはデバッグ困難。戻り値として返すならCLOSEは呼び出し側の責任を明記しコメント/命名で意図を明示。

よくある質問

Q暗黙カーソルと明示カーソルはどう使い分ける?
A①単一行取得+主キー指定→暗黙カーソル(SELECT INTO、②複数行取得+順次処理→明示カーソル or Cursor FOR Loop、③DML後の行数取得→暗黙カーソル(SQL%ROWCOUNT。基本は「単一行なら暗黙、複数行なら明示」と覚えればOK。
Q明示カーソルとCursor FOR Loopのどちらが推奨?
A実務ではCursor FOR Loopが推奨。OPEN/CLOSE自動、%ROWTYPE自動定義、例外安全、可読性高。明示OPEN/FETCH/CLOSEが必要なのはFOR UPDATE+WHERE CURRENT OFBULK COLLECT LIMITSYS_REFCURSOR返却のみ。詳細は【PL/SQL】カーソルFORループと明示的カーソルの使い分け参照。
QORA-01000(maximum open cursors exceeded)が出た
Aセッション内のOPEN中カーソル数がOPEN_CURSORSパラメータ上限(通常300)を超えた状態。原因の99%はCLOSE忘れ。対策:①EXCEPTION節でIF cur%ISOPEN THEN CLOSE cur; END IF;、②Cursor FOR Loopで自動管理に変更、③一時的にALTER SYSTEM SET OPEN_CURSORS = 1000で緊急回避。
Q%ROWTYPEと%TYPEの違いは?
A%TYPE単一列の型を取得(users.email%TYPE→VARCHAR2等)、%ROWTYPEテーブル/カーソルの全列レコードを取得(users%ROWTYPEv.id, v.email等でアクセス)。用途は変数宣言とカーソルFETCHで両方使われます。
QFOR UPDATEでロックが解除されるタイミング
ACOMMITまたはROLLBACK時に解除されます。CLOSEだけでは解除されません。バッチ中の分割コミットを入れるとWHERE CURRENT OFが使えなくなるので、大量データ+ロック+分割コミットはBULK COLLECT+ROWID+FORALLのパターンへ。
QSKIP LOCKEDはいつ使う?
A複数ワーカーが同じキューテーブルを並列消費する時に最適。他ワーカーがロック中の行をスキップして次の行を取るため、1つのSQLで衝突しないように処理を分担できます。Oracleのジョブキュー・メッセージキュー実装で定番パターン。
Q暗黙カーソルのSQL%FOUNDは何を指す?
A直前に実行したSQL文(SELECT INTO/INSERT/UPDATE/DELETE)の結果を指します。例:UPDATE ...実行直後のSQL%FOUNDは1行以上更新ならTRUE、0行ならFALSE。SELECT INTOで0件ならNO_DATA_FOUND例外が発生するのでSQL%FOUND参照前に例外ハンドリングが必要。
QBULK COLLECT LIMITの推奨値は?
A1000〜10000が実用範囲。1KB/行なら5000、10KB/行なら1000が目安。大きすぎ(10万以上)はPGAメモリ圧迫、小さすぎ(100未満)はラウンドトリップ多発。まずLIMIT 1000から始めて実測で調整するのが現実的。
QCursor FOR LoopのFOR UPDATEでWHERE CURRENT OFが使えない
A使えません。Cursor FOR Loopは内部で暗黙BULK COLLECTしているため、現在行のROWIDが直接参照できない仕組み。回避策:WHERE id = rec.idのように主キーで更新するか、明示カーソル(OPEN/FETCH/CLOSE)に変更します。
QSYS_REFCURSORを返すプロシージャの利点
A①Javaや.NETなどクライアント側で結果セットを受け取れる、②複数結果セットを一つのプロシージャで返却、③動的SQLを内部で組み立てて柔軟な結果を返す、④ストアド層でロジック集約。詳細は【PL/SQL】REF CURSORを使った柔軟なデータ取得方法参照。

関連記事

まとめ

  • カーソルの4フェーズ:DECLARE→OPEN→FETCH→CLOSE(CLOSE忘れ厳禁)
  • 暗黙(SELECT INTO)/明示(CURSOR ... IS)/Cursor FOR Loop/REF CURSORを用途で使い分け
  • 実務最頻出はCursor FOR Loop、特殊要件のみ明示OPEN/FETCH/CLOSE
  • 属性4種:%FOUND%NOTFOUND%ROWCOUNT%ISOPEN(暗黙はSQL%
  • %ROWTYPEで列変更に自動追従するレコード変数
  • パラメータ付きカーソルで同じSELECT形を条件違いで使い回す
  • FOR UPDATE+WHERE CURRENT OFで行ロック+安全更新、キュー型はSKIP LOCKED
  • 大量データはBULK COLLECT LIMIT 1000〜10000でメモリ保護+10〜100倍高速化
  • SYS_REFCURSORでストアドから結果セット返却(CLOSEは呼出側の責任)
  • 明示カーソルはNO_DATA_FOUNDを出さず%NOTFOUNDで判定
  • 例外安全:EXCEPTION WHEN OTHERS THEN IF cur%ISOPEN THEN CLOSE cur; END IF; RAISE;
  • アンチパターン:CLOSE忘れ/1行DML連打/LIMIT無しBULK/属性参照ミス/NO_DATA_FOUND誤用

PL/SQLのカーソルはバッチ処理の根幹。本記事の4フェーズ基本・4属性・%ROWTYPE・FOR UPDATE・BULK COLLECT LIMITを押さえれば、100万行オーダーのデータ処理も安全かつ高速に実装できます。Cursor FOR Loopで基本を素早く書き、特殊要件(行ロック/大量バッチ/結果セット返却)で明示的OPEN/FETCH/CLOSEへ切り替える、というメンタルモデルが現代的なPL/SQL開発のベストプラクティスです。ループはループ処理完全ガイド、例外は例外処理完全ガイド、バルクはFORALL/BULK COLLECT、REF CURSORはREF CURSOR詳解と組み合わせて活用してください。