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フェーズのテンプレ
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版
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)としてメモリに確保されます。
1. 解析済みのSQL文(parsed SQL statement) 2. 結果セットの位置ポインタ(current row pointer) 3. 実行状態(OPEN/CLOSE/属性値) カーソル ≒ プログラムからの「結果セットへのハンドル」
暗黙カーソルと明示カーソルの違い
BEGIN
UPDATE users SET status = 'inactive' WHERE last_login < 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実行
BEGIN
-- OPENで宣言済SQLがパース&実行される
OPEN cur_users;
-- パラメータ付きは引数指定
OPEN cur_by_status('active');
-- この時点ではまだ結果セットの先頭のポインタが立っているだけ
-- 実際のデータはFETCHしないと取れない
NULL;
END;
③FETCH:行取得
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:リソース解放
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
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%)
BEGIN
-- UPDATE実行
UPDATE users SET status = 'inactive'
WHERE last_login < 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例外。必ずFETCH→EXIT WHEN cur%NOTFOUNDの順で使用する。
%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
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 >= 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 > 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:ロック競合時に即エラー(ORA-00054) ... FOR UPDATE NOWAIT; -- SKIP LOCKED:ロック中の行を飛ばす(キュー処理に最適) ... FOR UPDATE SKIP LOCKED; -- WAIT 5:5秒まで待機 ... FOR UPDATE WAIT 5; -- 特定のテーブルだけロック(JOIN時) ... FOR UPDATE OF orders.status;
-- 複数ワーカーが同じキューを並列処理する場合
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:大量データの高速処理
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無しで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)。ストアドファンクションから複数行を返却する標準的な手段です。
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)で受け取ります。
例外処理連携:リソースリーク防止
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 との関係
-- 暗黙カーソル: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 > 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 < 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は呼び出し側の責任を明記しコメント/命名で意図を明示。
よくある質問
SELECT INTO)、②複数行取得+順次処理→明示カーソル or Cursor FOR Loop、③DML後の行数取得→暗黙カーソル(SQL%ROWCOUNT)。基本は「単一行なら暗黙、複数行なら明示」と覚えればOK。OPEN_CURSORSパラメータ上限(通常300)を超えた状態。原因の99%はCLOSE忘れ。対策:①EXCEPTION節でIF cur%ISOPEN THEN CLOSE cur; END IF;、②Cursor FOR Loopで自動管理に変更、③一時的にALTER SYSTEM SET OPEN_CURSORS = 1000で緊急回避。%TYPEは単一列の型を取得(users.email%TYPE→VARCHAR2等)、%ROWTYPEはテーブル/カーソルの全列レコードを取得(users%ROWTYPE→v.id, v.email等でアクセス)。用途は変数宣言とカーソルFETCHで両方使われます。COMMITまたはROLLBACK時に解除されます。CLOSEだけでは解除されません。バッチ中の分割コミットを入れるとWHERE CURRENT OFが使えなくなるので、大量データ+ロック+分割コミットはBULK COLLECT+ROWID+FORALLのパターンへ。SQL%FOUNDは何を指す?UPDATE ...実行直後のSQL%FOUNDは1行以上更新ならTRUE、0行ならFALSE。SELECT INTOで0件ならNO_DATA_FOUND例外が発生するのでSQL%FOUND参照前に例外ハンドリングが必要。LIMIT 1000から始めて実測で調整するのが現実的。WHERE CURRENT OFが使えないWHERE id = rec.idのように主キーで更新するか、明示カーソル(OPEN/FETCH/CLOSE)に変更します。関連記事
- 【PL/SQL】IF文完全ガイド — カーソルループ内のIF分岐
- 【PL/SQL】例外処理完全ガイド — カーソル例外安全なCLOSE
- 【PL/SQL】ループ処理完全ガイド — 4種ループとCursor FOR Loop
- 【PL/SQL】変数・定数の使い方 — %TYPEと%ROWTYPE
- 【PL/SQL】初心者でもわかる基本構文とブロック構造の書き方 — PL/SQLブロック構造
- 【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方 — BULK COLLECT LIMIT詳解
- 【PL/SQL】カーソルFORループと明示的カーソルの使い分け — 性能と可読性比較
- 【PL/SQL】REF CURSORを使った柔軟なデータ取得方法 — SYS_REFCURSOR詳解
- 【PL/SQL】コレクション(配列・ネスト表)の基本と活用例 — BULK COLLECT先のコレクション
- 【PL/SQL】FORALLとSAVE EXCEPTIONSでバルクDMLのエラーを個別処理する方法 — バルクエラーハンドリング
まとめ
- カーソルの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詳解と組み合わせて活用してください。
