【PL/SQL】REF CURSORを使った柔軟なデータ取得方法

【PL/SQL】REF CURSORを使った柔軟なデータ取得方法 PL/SQL

REF CURSOR(参照カーソル)は、SELECTの結果集合への“ハンドル”を変数として受け渡せる仕組みです。固定の列構成に縛られず、呼び出し元へストリーミング的にデータを渡せるため、画面やレポートなど多様な取得要件に柔軟に対応できます。ここではSYS_REFCURSORを中心に、強く型付けしたREF CURSOR、OUT引数での返却、動的SQLとの組み合わせ、クライアント側での受け取り、ライフサイクルと注意点まで実務視点で解説します。

基本:SYS_REFCURSORで結果集合を返す

PL/SQLでは最も手軽に使える汎用型としてSYS_REFCURSORが用意されています。次の例は検索条件を受け取り、その結果集合を呼び出し元へ返します。

CREATE OR REPLACE PACKAGE pkg_customer_rc AS
  PROCEDURE open_active(p_min_id IN NUMBER, p_cur OUT SYS_REFCURSOR);
END pkg_customer_rc;
/
CREATE OR REPLACE PACKAGE BODY pkg_customer_rc AS
  PROCEDURE open_active(p_min_id IN NUMBER, p_cur OUT SYS_REFCURSOR) IS
  BEGIN
    OPEN p_cur FOR
      SELECT id, name, email
        FROM customers
       WHERE active = 1
         AND id >= p_min_id
       ORDER BY id;
  END;
END pkg_customer_rc;
/

OPEN … FOR 句でSELECTを結び付けると、カーソル変数が結果集合を指し示すようになります。大きなデータでも一括ではなく逐次フェッチできるため、メモリ効率に優れます。

クライアント(SQL*Plus/SQLcl)での受け取り方

クライアントによってAPIは異なりますが、SQL*Plus/SQLclならREF CURSOR型のバインド変数に受け取り、その場で出力できます。

VAR rc REFCURSOR
EXEC pkg_customer_rc.open_active(100, :rc)
PRINT rc

アプリケーションコード(JDBC/ODP.NET/ODBC)ではOUTパラメータとして受領し、ResultSet/DataReaderとして順次読み取ります。

強く型付けしたREF CURSORでインターフェースを厳密化する

列レイアウトを固定したい場合は、RETURN 句で型を縛ったREF CURSOR型を定義します。これにより列構成の不一致をコンパイル時に検出できます。

CREATE OR REPLACE PACKAGE types_pkg AS
  TYPE t_emp_rc IS REF CURSOR RETURN employees%ROWTYPE;
END;
/
CREATE OR REPLACE PACKAGE pkg_emp AS
  PROCEDURE open_all(p_cur OUT types_pkg.t_emp_rc);
END;
/
CREATE OR REPLACE PACKAGE BODY pkg_emp AS
  PROCEDURE open_all(p_cur OUT types_pkg.t_emp_rc) IS
  BEGIN
    OPEN p_cur FOR SELECT * FROM employees;
  END;
END;
/

柔軟性(どんなSELECTでも流せる)を重視するならSYS_REFCURSOR、互換性チェックを重視するなら強い型付け、と役割で使い分けます。

動的SQLとREF CURSORの組み合わせ

列や条件が実行時に変わる場合は、OPEN p_cur FOR 動的SQL USING … の形でEXECUTE IMMEDIATE相当の書き方をします。バインド変数を使えばパース回数やセキュリティ面でも有利です。

CREATE OR REPLACE PROCEDURE open_dynamic(
  p_cols   IN VARCHAR2,       -- 例: 'id, name, email'
  p_active IN NUMBER,
  p_cur    OUT SYS_REFCURSOR
) AS
  v_sql CLOB;
BEGIN
  v_sql := 'SELECT ' || p_cols || ' FROM customers WHERE active = :b1 ORDER BY id';
  OPEN p_cur FOR v_sql USING p_active;
END;
/

カラム名のようにバインドできない要素はテンプレート化とホワイトリスト検証で安全性を確保します。

REF CURSORのフェッチをPL/SQL側で行う場合

通常はクライアントに渡しますが、PL/SQL内で受け取って処理を続けたいケースもあります。FETCH … INTO で1行ずつ、LOOPとEXIT WHENで終端まで読み取ります。

DECLARE
  c   SYS_REFCURSOR;
  v_id   customers.id%TYPE;
  v_name customers.name%TYPE;
BEGIN
  pkg_customer_rc.open_active(100, c);
  LOOP
    FETCH c INTO v_id, v_name, /* 他の列は省略可 */;
    EXIT WHEN c%NOTFOUND;
    DBMS_OUTPUT.put_line(v_id || ':' || v_name);
  END LOOP;
  CLOSE c;
END;
/

フェッチが終わったら必ずCLOSEでクローズします。閉じ忘れはリソースリークの原因です。

複数の結果集合を返す設計

Oracleは単一ステートメントから複数の結果セットを一度に返す標準構文を持たないため、OUTパラメータを複数用意して個別にOPENします。クライアント側で複数のカーソルを順に読み取る実装にします。

CREATE OR REPLACE PROCEDURE open_two_sets(
  p_orders OUT SYS_REFCURSOR,
  p_items  OUT SYS_REFCURSOR
) AS
BEGIN
  OPEN p_orders FOR SELECT * FROM orders WHERE order_date > SYSDATE - 7;
  OPEN p_items  FOR SELECT * FROM items  WHERE active = 1;
END;
/

パフォーマンスとメモリの勘所

REF CURSORは「逐次フェッチ」の強みを活かす設計が適しています。大量データを一度にPL/SQLコレクションへ吸い上げる場合は、REF CURSORでなくBULK COLLECTを検討します。クライアントからの読み取りが遅いとサーバー側でリソースが長時間保持されるため、必要な列だけを選択し、並び替えや集約は本当に必要なものに限ります。ネットワーク越しの往復を抑えるため、画面のページサイズに合わせたサーバー側の絞り込みも効果的です。

例外とクリーンアップ

OPEN後に例外が発生した場合、未【PL/SQL】REF CURSORを使った柔軟なデータ取得方法クローズのカーソルが残らないように、例外節でCLOSEを試みるか、呼び出し契約として「受け取った側が必ずCLOSEする」責務を明確にします。

PROCEDURE safe_open(p_cur OUT SYS_REFCURSOR) IS
BEGIN
  OPEN p_cur FOR SELECT * FROM customers;
EXCEPTION
  WHEN OTHERS THEN
    -- ここではOPENに失敗していればp_curはNULLの想定
    RAISE;
END;

クローズの責務はアプリケーション全体で統一し、接続プールや例外時の再試行戦略と一緒に設計すると安定します。

テストとデバッグのコツ

SQL*Plus/SQLclのPRINTで内容をすばやく確認できます。アプリから受け取る場合は、最初の数行だけ読み取って構造をログに出すと不整合に早く気付けます。仕様変更に強くするには、パッケージ仕様に「返す列の契約(ドキュメント)」を明記し、強い型付けのREF CURSORを併用するのが有効です。

まとめ

REF CURSORは、結果集合を“持ち運べる”ポインタとして、画面・帳票・APIなど多様な取得要件をPL/SQLで柔軟に解決する手段です。汎用性重視ならSYS_REFCURSOR、契約厳密化なら強い型付け、動的要件にはOPEN … FOR+USINGを組み合わせます。クローズの責務と最小列選択、逐次フェッチの前提を押さえれば、拡張性と性能を両立したデータ取得レイヤーを実装できます。