【Oracle】明示的カーソル(Explicit CURSOR)完全ガイド|OPEN/FETCH/CLOSE・カーソルFOR LOOP・パラメータ・BULK COLLECTまで実例で解説

【Oracle】明示的カーソル(Explicit CURSOR)完全ガイド|OPEN/FETCH/CLOSE・カーソルFOR LOOP・パラメータ・BULK COLLECTまで実例で解説 Oracle

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 が起きやすい 件数に依存しない安全な設計が可能
SELECT INTO で ORA-01403(NO_DATA_FOUND)ORA-01422(TOO_MANY_ROWS) に悩んでいる場合は、明示的カーソルへの書き換えを検討するのがベストプラクティスです。SELECT INTO エラーの詳細と対処法も参照してください。

明示的カーソルの基本サイクル:DECLARE → OPEN → FETCH → CLOSE

明示的カーソルは4つのステップで動作します。

  1. DECLARE: カーソルに名前と SELECT 文を定義する
  2. OPEN: SELECT を実行してカーソルを開く(結果セットを確定)
  3. FETCH: 現在行のデータを変数に取り出し、ポインタを次行へ進める
  4. CLOSE: カーソルを閉じてリソースを解放する
明示的カーソルの基本サイクル(OPEN/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;
/
CLOSE を忘れるとリソースリークが起きる
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 されるため安全です。

カーソル FOR LOOP(推奨パターン)
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;
/
カーソル FOR LOOP はシンプルで安全ですが、%ROWCOUNT などの属性にアクセスできないという制限があります。件数カウントや途中脱出など細かい制御が必要な場合は、明示的な OPEN/FETCH/CLOSE を使います。

BULK COLLECT INTO:大量データを高速処理する

通常の FETCH はコンテキストスイッチ(PL/SQL エンジン ⇔ SQL エンジン)が行ごとに発生するため、大量データの処理ではパフォーマンスのボトルネックになります。BULK COLLECT INTO を使うと複数行を一括で配列に取り込み、コンテキストスイッチを大幅に削減できます。

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;
/
BULK COLLECT + LIMIT でメモリを制御する(推奨)
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;
/
BULK COLLECT LIMIT の目安
LIMIT を設定しない場合、対象行が全メモリに収まる量であれば問題ありませんが、大量データでは UGA(User Global Area)を枯渇させる可能性があります。実務では 100〜1000 件を LIMIT として設定し、バッチ単位で COMMIT するパターンが一般的です。

FORALL:BULK COLLECT とセットで使う一括 DML

FORALL は BULK COLLECT で取り込んだ配列を使って、一括 INSERT/UPDATE/DELETE を行う構文です。行ごとの DML に比べてコンテキストスイッチが大幅に減り、処理が高速化されます。

FORALL による一括 UPDATE
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 を使ったプロシージャ(結果セットの返却)
-- 弱い 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 完全ガイドを、カーソルを使ったストアドプロシージャの書き方はストアドプロシージャ・ファンクションの作成完全ガイドも参照してください。