Oracle PL/SQL の明示的カーソル(Explicit CURSOR)は、複数行を返す SELECT 結果を1行ずつ処理するための仕組みです。暗黙カーソル(SELECT INTO)が1行しか扱えないのに対し、明示的カーソルは任意の件数の結果セットを順番に処理できます。
- 暗黙カーソルと明示的カーソルの違いと使い分け
- DECLARE / OPEN / FETCH / CLOSE の基本サイクル
- カーソル属性(%FOUND / %NOTFOUND / %ROWCOUNT / %ISOPEN)の使い方
- パラメータ付きカーソルで再利用性を高める方法
- カーソル FOR LOOP による簡潔な書き方
- BULK COLLECT INTO でパフォーマンスを改善する方法
- REF CURSOR(動的カーソル)の基本
暗黙カーソルと明示的カーソルの違い
| 観点 | 暗黙カーソル(SELECT INTO) | 明示的カーソル |
|---|---|---|
| 返せる行数 | 必ず1行(0行・2行以上はエラー) | 0行〜何行でも処理可能 |
| 記述量 | 短い | DECLARE〜CLOSE が必要 |
| 主な用途 | 主キー検索・集計値の取得など確実に1行の場合 | ループ処理・バッチ更新など複数行 |
| エラーリスク | NO_DATA_FOUND・TOO_MANY_ROWS が起きやすい | 件数に依存しない安全な設計が可能 |
ORA-01403(NO_DATA_FOUND) や ORA-01422(TOO_MANY_ROWS) に悩んでいる場合は、明示的カーソルへの書き換えを検討するのがベストプラクティスです。SELECT INTO エラーの詳細と対処法も参照してください。
明示的カーソルの基本サイクル:DECLARE → OPEN → FETCH → CLOSE
明示的カーソルは4つのステップで動作します。
- DECLARE: カーソルに名前と SELECT 文を定義する
- OPEN: SELECT を実行してカーソルを開く(結果セットを確定)
- FETCH: 現在行のデータを変数に取り出し、ポインタを次行へ進める
- CLOSE: カーソルを閉じてリソースを解放する
DECLARE
-- ① DECLARE: カーソルの定義(SELECT 文を名前に関連付ける)
CURSOR c_emp IS
SELECT employee_id, last_name, salary
FROM employees
WHERE department_id = 50
ORDER BY salary DESC;
-- FETCH で受け取る変数(%ROWTYPE でカーソルの行型を自動定義)
v_rec c_emp%ROWTYPE;
BEGIN
-- ② OPEN: SELECT を実行してカーソルを開く
OPEN c_emp;
-- ③ FETCH: 1行ずつ取り出す(ループ)
LOOP
FETCH c_emp INTO v_rec;
EXIT WHEN c_emp%NOTFOUND; -- データがなくなったら終了
DBMS_OUTPUT.PUT_LINE(
'ID=' || v_rec.employee_id ||
' 名前=' || v_rec.last_name ||
' 給与=' || v_rec.salary
);
END LOOP;
-- ④ CLOSE: リソースを解放する(必ず実行する)
CLOSE c_emp;
END;
/
OPEN したカーソルを CLOSE しないと、セッションあたりのオープンカーソル数の上限(
OPEN_CURSORS パラメータ)に達し、ORA-01000: 最大オープン・カーソル数を超えました が発生します。例外が発生しても CLOSE できるよう、EXCEPTION ブロックに CLOSE を書くか、カーソル FOR LOOP を使うことを推奨します。
カーソル属性(%FOUND / %NOTFOUND / %ROWCOUNT / %ISOPEN)
| 属性 | 型 | 説明 |
|---|---|---|
%FOUND |
BOOLEAN | 直前の FETCH で行を取得できた場合 TRUE。OPEN 直後・FETCH 前は NULL |
%NOTFOUND |
BOOLEAN | 直前の FETCH でデータがなかった場合 TRUE。ループ終了条件に使う |
%ROWCOUNT |
NUMBER | OPEN 以降に FETCH した累計件数 |
%ISOPEN |
BOOLEAN | カーソルが現在 OPEN 状態なら TRUE |
DECLARE
CURSOR c_emp IS
SELECT employee_id, last_name, salary
FROM employees
WHERE department_id = 50;
v_rec c_emp%ROWTYPE;
BEGIN
-- %ISOPEN でオープン状態を確認してから OPEN
IF NOT c_emp%ISOPEN THEN
OPEN c_emp;
END IF;
LOOP
FETCH c_emp INTO v_rec;
EXIT WHEN c_emp%NOTFOUND; -- データなし → ループ終了
-- %ROWCOUNT: 何行目かをログに出力
IF c_emp%ROWCOUNT <= 5 THEN
DBMS_OUTPUT.PUT_LINE(
c_emp%ROWCOUNT || '件目: ' || v_rec.last_name
);
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE('合計処理件数: ' || c_emp%ROWCOUNT);
CLOSE c_emp;
EXCEPTION
WHEN OTHERS THEN
-- 例外発生時も CLOSE でリソースを解放する
IF c_emp%ISOPEN THEN
CLOSE c_emp;
END IF;
RAISE;
END;
/
パラメータ付きカーソル:再利用性を高める
カーソルにパラメータを定義すると、OPEN するたびに異なる条件で結果セットを取得できます。同じ処理を複数の部門や期間に対して繰り返す場合に便利です。
DECLARE
-- パラメータ付きカーソル: CURSOR 名 (パラメータ名 型) IS SELECT ...
CURSOR c_dept_emp (p_dept_id NUMBER, p_min_salary NUMBER := 0) IS
SELECT employee_id, last_name, salary
FROM employees
WHERE department_id = p_dept_id
AND salary >= p_min_salary
ORDER BY salary DESC;
v_rec c_dept_emp%ROWTYPE;
BEGIN
-- 部門 10 を処理
DBMS_OUTPUT.PUT_LINE('--- 部門 10 ---');
OPEN c_dept_emp(10);
LOOP
FETCH c_dept_emp INTO v_rec;
EXIT WHEN c_dept_emp%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_rec.last_name || ': ' || v_rec.salary);
END LOOP;
CLOSE c_dept_emp;
-- 同じカーソルを部門 50、給与 30万以上 で再オープン
DBMS_OUTPUT.PUT_LINE('--- 部門 50(30万以上)---');
OPEN c_dept_emp(50, 300000);
LOOP
FETCH c_dept_emp INTO v_rec;
EXIT WHEN c_dept_emp%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_rec.last_name || ': ' || v_rec.salary);
END LOOP;
CLOSE c_dept_emp;
END;
/
カーソル FOR LOOP:最もシンプルな書き方
カーソル FOR LOOP を使うと、OPEN・FETCH・CLOSE をすべて暗黙的に行ってくれます。通常の OPEN/FETCH/CLOSE に比べてコードが大幅に短くなり、例外が発生しても自動的に CLOSE されるため安全です。
BEGIN
-- カーソル FOR LOOP: OPEN/FETCH/CLOSE を自動処理
-- v_rec は暗黙的に宣言される(明示的な DECLARE 不要)
FOR v_rec IN (
SELECT employee_id, last_name, salary
FROM employees
WHERE department_id = 50
ORDER BY salary DESC
) LOOP
DBMS_OUTPUT.PUT_LINE(
v_rec.last_name || ': ' || v_rec.salary
);
END LOOP;
-- カーソルは自動的に CLOSE される
END;
/
-- 名前付きカーソルを使うバリエーション
DECLARE
CURSOR c_emp IS
SELECT employee_id, last_name, salary
FROM employees
WHERE department_id = 50;
BEGIN
FOR v_rec IN c_emp LOOP
DBMS_OUTPUT.PUT_LINE(v_rec.last_name || ': ' || v_rec.salary);
END LOOP;
END;
/
BULK COLLECT INTO:大量データを高速処理する
通常の FETCH はコンテキストスイッチ(PL/SQL エンジン ⇔ SQL エンジン)が行ごとに発生するため、大量データの処理ではパフォーマンスのボトルネックになります。BULK COLLECT INTO を使うと複数行を一括で配列に取り込み、コンテキストスイッチを大幅に削減できます。
DECLARE
-- コレクション型(配列)の定義
TYPE t_emp_id IS TABLE OF employees.employee_id%TYPE;
TYPE t_last_name IS TABLE OF employees.last_name%TYPE;
TYPE t_salary IS TABLE OF employees.salary%TYPE;
v_ids t_emp_id;
v_names t_last_name;
v_sals t_salary;
BEGIN
-- BULK COLLECT INTO: 一括取得(全件)
SELECT employee_id, last_name, salary
BULK COLLECT INTO v_ids, v_names, v_sals
FROM employees
WHERE department_id = 50
ORDER BY salary DESC;
-- 取得した配列をループ処理
FOR i IN 1 .. v_ids.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(
i || ': ' || v_names(i) || ' = ' || v_sals(i)
);
END LOOP;
DBMS_OUTPUT.PUT_LINE('合計: ' || v_ids.COUNT || '件');
END;
/
DECLARE
CURSOR c_emp IS
SELECT employee_id, last_name, salary
FROM employees
WHERE department_id = 50
ORDER BY employee_id;
-- %ROWTYPE の TABLE 型でまとめて受け取る
TYPE t_emp_tbl IS TABLE OF c_emp%ROWTYPE;
v_batch t_emp_tbl;
c_batch_size CONSTANT PLS_INTEGER := 100; -- 一度に取得する行数
v_total PLS_INTEGER := 0;
BEGIN
OPEN c_emp;
LOOP
-- LIMIT で一度に取り込む件数を制限してメモリを抑える
FETCH c_emp BULK COLLECT INTO v_batch LIMIT c_batch_size;
EXIT WHEN v_batch.COUNT = 0;
-- バッチ内の処理
FOR i IN 1 .. v_batch.COUNT LOOP
-- 例: 給与を 5% 上げて更新
UPDATE employees
SET salary = v_batch(i).salary * 1.05
WHERE employee_id = v_batch(i).employee_id;
END LOOP;
v_total := v_total + v_batch.COUNT;
COMMIT; -- バッチ単位でコミット
EXIT WHEN v_batch.COUNT < c_batch_size; -- 最後のバッチ
END LOOP;
CLOSE c_emp;
DBMS_OUTPUT.PUT_LINE('更新完了: ' || v_total || '件');
END;
/
LIMIT を設定しない場合、対象行が全メモリに収まる量であれば問題ありませんが、大量データでは UGA(User Global Area)を枯渇させる可能性があります。実務では 100〜1000 件を LIMIT として設定し、バッチ単位で COMMIT するパターンが一般的です。
FORALL:BULK COLLECT とセットで使う一括 DML
FORALL は BULK COLLECT で取り込んだ配列を使って、一括 INSERT/UPDATE/DELETE を行う構文です。行ごとの DML に比べてコンテキストスイッチが大幅に減り、処理が高速化されます。
DECLARE
TYPE t_emp_id IS TABLE OF employees.employee_id%TYPE;
TYPE t_salary IS TABLE OF employees.salary%TYPE;
v_ids t_emp_id;
v_sals t_salary;
BEGIN
-- 対象データを一括取得
SELECT employee_id, salary
BULK COLLECT INTO v_ids, v_sals
FROM employees
WHERE department_id = 50;
-- FORALL で一括 UPDATE(コンテキストスイッチを最小化)
FORALL i IN 1 .. v_ids.COUNT
UPDATE employees
SET salary = v_sals(i) * 1.03
WHERE employee_id = v_ids(i);
DBMS_OUTPUT.PUT_LINE('更新件数: ' || SQL%ROWCOUNT || '件');
COMMIT;
END;
/
REF CURSOR:動的なカーソルの受け渡し
REF CURSOR(カーソル変数)はカーソルへの参照を変数として扱えます。プロシージャの OUT パラメータとして結果セットを返したり、実行時に動的に SQL を変えたりするケースで活用されます。
-- 弱い REF CURSOR 型の定義(パッケージや型定義で共有することが多い)
-- Oracle 組み込みの SYS_REFCURSOR を使うのが一般的
-- プロシージャ: 部門IDを受け取り、従業員リストを REF CURSOR で返す
CREATE OR REPLACE PROCEDURE get_dept_employees (
p_dept_id IN employees.department_id%TYPE,
p_cursor OUT SYS_REFCURSOR
)
AS
BEGIN
OPEN p_cursor FOR
SELECT employee_id, last_name, salary
FROM employees
WHERE department_id = p_dept_id
ORDER BY salary DESC;
-- CLOSE は呼び出し側で行う
END get_dept_employees;
/
-- 呼び出し側: REF CURSOR を受け取って処理
DECLARE
v_cursor SYS_REFCURSOR;
v_id employees.employee_id%TYPE;
v_name employees.last_name%TYPE;
v_salary employees.salary%TYPE;
BEGIN
get_dept_employees(50, v_cursor);
LOOP
FETCH v_cursor INTO v_id, v_name, v_salary;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_name || ': ' || v_salary);
END LOOP;
CLOSE v_cursor;
END;
/
カーソル処理パターン別のパフォーマンス比較
| パターン | コンテキストスイッチ | メモリ | 主な用途 |
|---|---|---|---|
| カーソル FOR LOOP | 行ごと(多い) | 少ない | 数千件まで、可読性重視 |
| BULK COLLECT(全件) | 1回 | 多い(全件メモリに展開) | 件数が少なく確実にメモリに収まる場合 |
| BULK COLLECT + LIMIT | LIMIT 件ごと | 制御可能 | 大量データの安全なバッチ処理 |
| FORALL | 1回(DML 一括) | 配列分 | 大量 INSERT/UPDATE/DELETE の高速化 |
- 数千件以下で可読性重視 → カーソル FOR LOOP
- 大量データのバッチ処理 → BULK COLLECT INTO … LIMIT 100 + FORALL
- 結果セットを呼び出し元に返したい → REF CURSOR(SYS_REFCURSOR)
まとめ
- 明示的カーソルは DECLARE → OPEN → FETCH → CLOSE の4ステップで動作する
%NOTFOUNDでループ終了を判定し、%ROWCOUNTで処理件数を確認できる- シンプルなループ処理には カーソル FOR LOOP(OPEN/CLOSE 自動)が推奨
- 大量データの高速処理には BULK COLLECT INTO … LIMIT でバッチ取得 + FORALL で一括 DML
- 結果セットをプロシージャの外に渡すには SYS_REFCURSOR(REF CURSOR)を使う
SELECT INTO で ORA-01403・ORA-01422 が発生するケースについてはORA-01403・ORA-01422 完全ガイドを、カーソルを使ったストアドプロシージャの書き方はストアドプロシージャ・ファンクションの作成完全ガイドも参照してください。

