PL/SQLのREF CURSOR(参照カーソル)は、「SELECT結果集合へのハンドル」を変数として受け渡せる唯一の仕組みです。通常のカーソルがブロック内に閉じ込められているのに対し、REF CURSORはOUTパラメータでクライアント(Java/Python/.NET)に結果セットそのものを返せるため、PL/SQL ↔ アプリケーションサーバの標準的なデータ取得APIとして広く使われます。
シンプルに見えるREF CURSORですが、SYS_REFCURSORと強い型付けの使い分け、動的SQLとの統合、パイプライン関数への流し込み、BULK COLLECTとの組み合わせ、クライアント言語別の正しい受け取り方、例外時のリソースリーク防止といった実戦テクニックを押さえないと、本番でメモリリーク・接続枯渇・性能劣化を引き起こします。
この記事ではREF CURSORの実戦活用ガイドとして、通常カーソル/SYS_REFCURSOR/強い型付けの3軸比較、Java/Python/.NETクライアント連携の実装、動的SQL・パイプライン関数・BULK COLLECTとの組み合わせ、複数結果セット返却、ライフサイクル管理、アンチパターン6選、FAQまで2026年版で整理します。
この記事でわかること
- 静的カーソル/SYS_REFCURSOR/強い型付けREF CURSORの本質的な違い
- SYS_REFCURSORを使うべき場面と強い型付けを選ぶ場面の判断基準
- Java/JDBCでREF CURSORを受け取るCallableStatementの書き方
- Python/cx_Oracle・oracledbでの実装例
- .NET/ODP.NETでの実装例
- 動的SQL(OPEN … FOR USING)でセキュアに動的クエリを返す方法
- パイプライン関数の入力としてREF CURSORを流し込む高度な活用
- BULK COLLECT FETCHでREF CURSORを高速に消費する実装
- 複数結果セット返却(注文+明細など複合データ)のパターン
- リソースリーク防止のCLOSE責任と例外時クリーンアップ
- 本番で踏むアンチパターン6選
- 30秒でわかるREF CURSORの結論
- 静的カーソル/SYS_REFCURSOR/強い型付け|3つの違いを正しく理解する
- クライアント言語別の受け取り方|Java/Python/.NETの実装
- 動的SQLとの統合|実行時に決まるクエリで結果集合を返す
- パイプライン関数への流し込み|大量データETLの王道パターン
- BULK COLLECT FETCHで高速消費|PL/SQL内処理の最適化
- 複数結果セット返却|注文+明細など複合データを一度で取得
- ライフサイクル管理|CLOSE責任とリーク防止
- 本番で踏むアンチパターン6選
- よくある質問
- 関連記事で深掘りする
- まとめ|REF CURSORで柔軟なデータ取得層を設計する
30秒でわかるREF CURSORの結論
忙しい読者向けの結論先出しです。
| 結論 | 理由・効果 |
|---|---|
| ① クライアント返却なら原則SYS_REFCURSOR | 列構成が変動しても影響なし、シンプルで実用的 |
| ② PL/SQL内で扱うなら強い型付け | 列構成の不一致をコンパイル時に検出できて安全 |
| ③ 動的SQLはOPEN … FOR … USING | バインド変数で安全+カーソル共有でパース削減 |
| ④ 高速消費はFETCH BULK COLLECT INTO LIMIT | 1行ずつフェッチより数倍〜数十倍速い |
| ⑤ パイプラインへ流すなら関数引数にSYS_REFCURSOR | 並列ETLとREF CURSORを組み合わせて柔軟に流せる |
| ⑥ CLOSEの責任を契約で明示 | 呼出し側/呼出し元のどちらが閉じるか曖昧だとリーク |
| ⑦ 大量データなら列を絞る・ページングする | クライアント遅延でサーバリソースが拘束される時間を短縮 |
静的カーソル/SYS_REFCURSOR/強い型付け|3つの違いを正しく理解する
「カーソル」と一言で呼ばれる仕組みには実は3種類あり、それぞれ役割が異なります。混同するとREF CURSORの強みを活かせません。
静的カーソル(CURSOR cur IS SELECT …)
SQL文を宣言時に固定するカーソル。コンパイル時に解析され、以降の実行は同じSQLで固定されます。PL/SQLブロック内で完結するため、外部に渡すことはできません。カーソル変数のような柔軟性はないが、コンパイル時の検査が効くため内部処理に閉じた使い方では最も安全です。詳細はカーソル完全ガイドを参照してください。
SYS_REFCURSOR(弱い型付けREF CURSOR)
Oracleが標準で提供する汎用REF CURSOR型。結果集合の列構成を宣言時に固定せず、OPEN cur FOR <任意のSELECT>でどんなクエリでも紐付けられます。変数として受け渡せるのが最大の特徴で、OUTパラメータでクライアントに返したり、別関数に渡したりできます。柔軟性最優先の現場では実質これ一択。
強い型付けREF CURSOR(TYPE … IS REF CURSOR RETURN …%ROWTYPE)
列構成を宣言時に固定したREF CURSOR型。RETURN employees%ROWTYPEのように戻り型を縛ると、実装時のSQLが列構成違反していたらコンパイル時にエラーになります。内部の重要な処理で「列構成を絶対に変えてほしくない」場合に有効。柔軟性は犠牲になるので、API契約として明示したい局面だけに絞って使うのが良いです。
-- ① 静的カーソル(PL/SQLブロック内のみ)
DECLARE
CURSOR cur_emp IS SELECT employee_id, last_name FROM employees;
BEGIN
FOR rec IN cur_emp LOOP
DBMS_OUTPUT.PUT_LINE(rec.employee_id || ': ' || rec.last_name);
END LOOP;
END;
/
-- ② SYS_REFCURSOR(柔軟・どんなSELECTでも紐付け可)
CREATE OR REPLACE PROCEDURE get_data(p_cur OUT SYS_REFCURSOR) AS
BEGIN
OPEN p_cur FOR SELECT employee_id, last_name FROM employees;
-- 列構成は呼出し側で受け取って解釈する
END;
/
-- ③ 強い型付けREF CURSOR(コンパイル時の列構成チェック)
CREATE OR REPLACE PACKAGE pkg_types AS
TYPE t_emp_rc IS REF CURSOR RETURN employees%ROWTYPE;
END;
/
CREATE OR REPLACE PROCEDURE get_emps(p_cur OUT pkg_types.t_emp_rc) AS
BEGIN
-- ✅ employees%ROWTYPE と同じ列構成のSELECTでないとコンパイルエラー
OPEN p_cur FOR SELECT * FROM employees;
-- ❌ 一致しないSELECTはコンパイル時に弾かれる
-- OPEN p_cur FOR SELECT employee_id, last_name FROM employees;
END;
/
使い分けの実用判断:①外部APIとして安定インターフェースを提供したい→強い型付け、②動的SQL・複数クエリで使い回したい→SYS_REFCURSOR、③ループ処理など内部完結の処理→静的カーソル。迷ったらSYS_REFCURSORで始め、列構成が揃えにくい段階で強い型付けに切り替えるのが現実解です。
クライアント言語別の受け取り方|Java/Python/.NETの実装
REF CURSORの真価はクライアント側で結果セットとして消費するところにあります。主要言語での実装パターンを示します。いずれもOUTパラメータとして受け取り、ResultSet/Cursor/DataReaderに変換する流れは共通です。
Java/JDBC
CallableStatementでOracleTypes.CURSORをregisterOutParameterに指定し、実行後にgetObject(N)でResultSetとして取得します。
import oracle.jdbc.OracleTypes;
try (Connection conn = DriverManager.getConnection(URL, USER, PASS);
CallableStatement cs = conn.prepareCall(
"BEGIN pkg_customer_rc.open_active(?, ?); END;")) {
cs.setLong(1, 100);
cs.registerOutParameter(2, OracleTypes.CURSOR); // ← REF CURSOR用
cs.execute();
try (ResultSet rs = (ResultSet) cs.getObject(2)) {
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
System.out.println(id + ": " + name);
}
}
}
Python/oracledb(旧cx_Oracle)
cursor.var(oracledb.CURSOR)でカーソル変数を作り、callprocで受け取ります。受け取ったカーソルをそのままfor文でイテレートできるのがPythonの強みです。
import oracledb
with oracledb.connect(user=USER, password=PASS, dsn=DSN) as conn:
with conn.cursor() as cur:
ref_cur = cur.var(oracledb.CURSOR)
cur.callproc("pkg_customer_rc.open_active", [100, ref_cur])
# ref_cur.getvalue() で結果カーソルを取り出す
result_cursor = ref_cur.getvalue()
for row in result_cursor:
print(row) # (id, name, email) のタプル
result_cursor.close()
.NET/Oracle.ManagedDataAccess(ODP.NET)
OracleParameterにOracleDbType.RefCursorを指定し、パラメータのValueをOracleDataReaderにキャストします。
using Oracle.ManagedDataAccess.Client;
using Oracle.ManagedDataAccess.Types;
using (var conn = new OracleConnection(connStr))
{
conn.Open();
using (var cmd = new OracleCommand("pkg_customer_rc.open_active", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("p_min_id", OracleDbType.Int64).Value = 100;
cmd.Parameters.Add("p_cur", OracleDbType.RefCursor)
.Direction = ParameterDirection.Output;
cmd.ExecuteNonQuery();
using (var reader = ((OracleRefCursor)cmd.Parameters["p_cur"].Value)
.GetDataReader())
{
while (reader.Read())
{
Console.WriteLine($"{reader["id"]}: {reader["name"]}");
}
}
}
}
いずれの言語でも「REF CURSORを取得→ResultSet/Cursor/DataReaderに変換→読み取り→閉じる」という3ステップが共通です。using/try-with-resources/with構文で必ず確実にクローズされる構造にしておくと、リソースリークを防げます。
動的SQLとの統合|実行時に決まるクエリで結果集合を返す
「画面の検索条件で動的にWHEREを組み立てる」「列を可変で選択する」といった要件にはOPEN … FOR … USINGを使います。通常のEXECUTE IMMEDIATEと同じくバインド変数を活用でき、カーソル共有でパース削減も効きます。セキュリティの詳細は動的SQLのセキュアな書き方を参照してください。
-- 検索条件を動的に組み立て、結果セットを返す
CREATE OR REPLACE PROCEDURE search_customers(
p_country IN VARCHAR2,
p_min_age IN NUMBER,
p_active IN NUMBER,
p_cur OUT SYS_REFCURSOR
) AS
v_sql VARCHAR2(4000);
BEGIN
v_sql := q'[
SELECT customer_id, customer_name, country, age, active
FROM customers
WHERE 1 = 1
]';
-- 動的にWHEREを追加(バインド変数を活用)
IF p_country IS NOT NULL THEN
v_sql := v_sql || ' AND country = :v_country';
END IF;
IF p_min_age IS NOT NULL THEN
v_sql := v_sql || ' AND age >= :v_min_age';
END IF;
IF p_active IS NOT NULL THEN
v_sql := v_sql || ' AND active = :v_active';
END IF;
-- ❌ 注意: USINGは「実際にバインドされる順番=SQLに登場する順番」
-- 条件によって付与有無が変わるならUSING句も対応する必要がある
-- 実装的にはバインド常時すべて指定する形(NULL許容にする)が安全
v_sql := q'[
SELECT customer_id, customer_name, country, age, active
FROM customers
WHERE (:v_country IS NULL OR country = :v_country)
AND (:v_min_age IS NULL OR age >= :v_min_age)
AND (:v_active IS NULL OR active = :v_active)
]';
OPEN p_cur FOR v_sql
USING p_country, p_country,
p_min_age, p_min_age,
p_active, p_active;
END;
/
動的SQLでREF CURSORを返す場合、列構成も動的に変わるとクライアント側で受け取れません。クライアントは事前に列構成を知っている必要があるため、列リストは固定・WHERE条件だけ動的にするのが実用的。本当に列も動的にするならクライアント側でResultSetMetaDataを見て動的に処理する設計が必要です。
パイプライン関数への流し込み|大量データETLの王道パターン
パイプライン関数の入力にREF CURSORを取ると、SQLの結果集合をストリーミング処理できる強力なETLパターンになります。PARALLEL_ENABLE(PARTITION p_cur BY ANY)を付ければ並列実行も可能。詳細はパイプライン関数完全ガイドを参照してください。
-- 行型の定義
CREATE OR REPLACE TYPE t_sales_row AS OBJECT(
sales_id NUMBER,
product_cd VARCHAR2(20),
amount_inc NUMBER -- 税込加算後
);
/
CREATE OR REPLACE TYPE t_sales_tab IS TABLE OF t_sales_row;
/
-- REF CURSOR入力のパイプライン関数(並列対応)
CREATE OR REPLACE FUNCTION f_apply_tax(
p_cur IN SYS_REFCURSOR
) RETURN t_sales_tab
PIPELINED PARALLEL_ENABLE(PARTITION p_cur BY ANY) AS
rec raw_sales%ROWTYPE;
BEGIN
LOOP
FETCH p_cur INTO rec;
EXIT WHEN p_cur%NOTFOUND;
PIPE ROW(t_sales_row(
rec.sales_id, rec.product_cd, rec.amount * 1.10
));
END LOOP;
CLOSE p_cur;
RETURN;
END;
/
-- 利用:CURSOR(SELECT ...) で REF CURSOR を流し込む
INSERT INTO sales_with_tax
SELECT * FROM TABLE(f_apply_tax(
CURSOR(SELECT * FROM raw_sales)
));
BULK COLLECT FETCHで高速消費|PL/SQL内処理の最適化
REF CURSORを受け取ってPL/SQL内で処理する場合は、1行ずつのFETCHではなくFETCH ... BULK COLLECT INTO ... LIMIT nで一括フェッチするとコンテキストスイッチが減り高速になります。クライアントに渡す前のサーバ側集計や変換処理に有効です。
DECLARE
v_cur SYS_REFCURSOR;
TYPE t_rec IS TABLE OF customers%ROWTYPE;
v_buf t_rec;
C_LIMIT CONSTANT PLS_INTEGER := 1000;
v_total NUMBER := 0;
BEGIN
pkg_customer_rc.open_active(p_min_id => 100, p_cur => v_cur);
LOOP
FETCH v_cur BULK COLLECT INTO v_buf LIMIT C_LIMIT;
EXIT WHEN v_buf.COUNT = 0;
-- まとめて処理(FORALLでDMLしてもOK)
FOR i IN 1 .. v_buf.COUNT LOOP
v_total := v_total + 1;
-- 何か処理
END LOOP;
END LOOP;
CLOSE v_cur;
DBMS_OUTPUT.PUT_LINE('処理件数: ' || v_total);
EXCEPTION
WHEN OTHERS THEN
IF v_cur%ISOPEN THEN CLOSE v_cur; END IF;
RAISE;
END;
/
複数結果セット返却|注文+明細など複合データを一度で取得
「注文ヘッダと明細を一度のAPI呼び出しで取得したい」のような複合データ取得では、OUTパラメータを複数用意してそれぞれにREF CURSORを返します。1回の呼び出しで複数のクエリ結果を効率的にクライアントに渡せます。
CREATE OR REPLACE PROCEDURE get_order_full(
p_order_id IN NUMBER,
p_header OUT SYS_REFCURSOR,
p_items OUT SYS_REFCURSOR
) AS
BEGIN
-- ヘッダ情報
OPEN p_header FOR
SELECT order_id, customer_id, order_date, status, total_amount
FROM orders
WHERE order_id = p_order_id;
-- 明細情報
OPEN p_items FOR
SELECT line_no, product_id, quantity, unit_price, line_amount
FROM order_items
WHERE order_id = p_order_id
ORDER BY line_no;
END;
/
-- Java側はOUTパラメータを2つ受け取って順に処理
-- cs.registerOutParameter(2, OracleTypes.CURSOR);
-- cs.registerOutParameter(3, OracleTypes.CURSOR);
-- ResultSet rs1 = (ResultSet) cs.getObject(2);
-- ResultSet rs2 = (ResultSet) cs.getObject(3);
REST API設計でJSONレスポンスを組み立てる場合、PL/SQL側で複数REF CURSORを返してクライアント側でJSON変換するパターンと、PL/SQL内でJSON文字列を組み立ててCLOBで返すパターンの2通りあります。API応答が頻繁に変わるなら前者、レスポンス構造が安定しているなら後者が実装しやすいです。12c以降のJSON_OBJECT/JSON_ARRAYAGGを使えばPL/SQL内でJSON生成も簡単です。
ライフサイクル管理|CLOSE責任とリーク防止
REF CURSORはサーバ側のリソースを消費するため、CLOSE忘れはカーソル枯渇(ORA-01000)に直結します。「誰が閉じるか」をAPI契約として明確にしておくのが鉄則です。
クライアント返却の場合
OUTパラメータでクライアントに返したREF CURSORはクライアント側がCLOSE責任を持ちます。JDBCならResultSet.close()、Pythonならcursor.close()、.NETならOracleDataReader.Close()。言語側のtry-with-resources等で確実に閉じる構造にしてください。
PL/SQL内消費の場合
受け取ったブロック内で必ずCLOSEします。途中で例外が発生する可能性を考えてEXCEPTION句で%ISOPENを確認してから閉じる「セーフCLOSE」が定石です。
例外時のクリーンアップ
OPEN後に例外が発生した場合、カーソルが開いたまま残ると次の処理に影響します。EXCEPTION句でIF v_cur%ISOPEN THEN CLOSE v_cur; END IF;をパターンとして組み込んでください。
-- ✅ セーフCLOSEパターン
DECLARE
v_cur SYS_REFCURSOR;
TYPE t_rec IS TABLE OF customers%ROWTYPE;
v_buf t_rec;
BEGIN
pkg_customer_rc.open_active(100, v_cur);
LOOP
FETCH v_cur BULK COLLECT INTO v_buf LIMIT 1000;
EXIT WHEN v_buf.COUNT = 0;
-- 処理
END LOOP;
CLOSE v_cur; -- 通常終了でのCLOSE
EXCEPTION
WHEN OTHERS THEN
IF v_cur%ISOPEN THEN
CLOSE v_cur; -- 例外時もCLOSEを保証
END IF;
RAISE;
END;
/
-- ❌ アンチパターン: CLOSE忘れ
DECLARE
v_cur SYS_REFCURSOR;
v_id NUMBER;
BEGIN
pkg_customer_rc.open_active(100, v_cur);
FETCH v_cur INTO v_id;
-- ↓ CLOSE がない!セッション内でカーソルが残り続ける
-- 大量に呼ぶと ORA-01000 (max open cursors exceeded)
END;
/
ORA-01000の原因の多くがREF CURSORのCLOSE忘れです。初期値OPEN_CURSORS=300程度のセッションで300個のREF CURSORを閉じずに開いた瞬間にエラーになります。症状の詳細はORA-01000完全ガイドを参照してください。
本番で踏むアンチパターン6選
① CLOSE忘れによるカーソルリーク
OPENしたREF CURSORを閉じ忘れるとセッション内で蓄積され、ORA-01000でアプリ全体が停止します。言語側のtry-with-resources/with/usingで構造的に必ずCLOSEされるコードを書いてください。
② 動的SQLで列リストまで動的化
列構成が呼び出しごとに変わるとクライアント側で受け取れません。列リストは固定、WHERE句だけ動的に絞ってください。本当に動的列が必要ならResultSetMetaDataで動的に処理する設計が必要です。
③ クライアントが遅いままサーバリソース拘束
REF CURSORを開いたままクライアントの処理が長いとサーバ側のリソースが解放されず、UNDOやスナップショットも保持されます。クライアント側で必要分だけ取得し速やかにCLOSEする設計に。大量取得が必要ならページングに切り替えるのが現実解です。
④ 不要な列を全部返している
SELECT *で全列返す実装が多いですが、クライアントが使わない列はネットワーク負荷とサーバの作業領域を無駄に消費します。必要な列だけ明示するだけで体感が大きく変わります。
⑤ パフォーマンスより柔軟性を取りすぎてSYS_REFCURSOR濫用
「迷ったらSYS_REFCURSOR」自体は正解ですが、API契約として「この列構成を保証したい」場面では強い型付けの方がコンパイル時チェックで事故を防げます。外部公開APIでは強い型付けを優先してください。
⑥ REF CURSORで巨大データを返してメモリ枯渇
REF CURSOR自体はストリーミングですが、クライアント側でList<Map>に全部詰めるとメモリが破綻します。行を処理しながら順次解放する流式実装にするか、それでもダメならページング設計に切り替えてください。
よくある質問
FUNCTION ... RETURN SYS_REFCURSORのように関数の戻り値型としても使えます。OUTパラメータと比べると呼び出し側でv_cur := f();とシンプルに書ける利点があります。PL/SQLからSQLでは関数のREF CURSOR戻り値は使えない(FROM句のTABLE()には使えない)のでPL/SQL内呼び出しに限定されます。OPEN p_cur FOR ... USING ...のバインド変数はオプティマイザに値が伝わり、初回実行時の値で実行計画を立てます。12c以降のAdaptive Cursor Sharingで値の偏りに応じて複数の計画を持つので、極端な選択率の差がある場合も自動最適化されます。計画を確認するにはDBMS_XPLAN.DISPLAY_CURSORを使ってください。OPENし直せます。「複数のクエリを順番に実行して結果を1つずつ受け取る」用途で活用できます。一度CLOSEした後にFETCHしようとするとORA-01001(無効なカーソル)になるので、再OPEN前に必ず確認してください。OPEN p_cur FOR ...を実行する前に例外が発生すると、p_curは未初期化のまま呼出し元に返ります。クライアント側でNULLとして見える挙動になることがあるので、受け取った直後にcs.wasNull()等でチェックする実装が安全です。またEXCEPTION句で「未OPENならOPENで空集合を返す」リカバリも一案です。TABLE()として呼び出してSQLの一部として使いたい場合。REF CURSOR:SELECTの結果集合をクライアントに返したい場合や、PL/SQLからPL/SQLへ結果セットを引き渡したい場合。パイプライン関数の入力として REF CURSOR を渡す合わせ技も4639(パイプライン関数完全ガイド)で紹介しています。FETCH ... BULK COLLECT INTO ... LIMIT nで一部取得してDBMS_OUTPUT.PUT_LINEで表示する形になります。ただし開発環境のデバッグ用途に限定し、本番のREF CURSORを安易にログ出力すると性能に影響するので注意してください。関連記事で深掘りする
REF CURSORに関連する周辺技術もあわせて押さえておきましょう。
- 【PL/SQL】カーソル完全ガイド|4フェーズ・%ROWTYPE・FOR UPDATE+SKIP LOCKED・BULK COLLECT LIMIT・実務10パターン(静的カーソルの詳細)
- 【PL/SQL】カーソルFORループ vs 明示的カーソル完全ガイド(カーソル選択基準)
- 【PL/SQL】パイプライン関数で大量データ処理を勝たせる完全ガイド(REF CURSOR入力で並列ETL)
- 【PL/SQL】動的SQLのセキュアな書き方完全ガイド(OPEN … FOR … USINGの安全実装)
- 【PL/SQL】バルク処理完全ガイド(FETCH BULK COLLECTの詳細)
- 【Oracle】ORA-01000: 最大オープン・カーソル数を超えました完全ガイド(CLOSE忘れによる典型エラー)
- 【PL/SQL】例外処理完全ガイド(カーソル例外の安全な扱い)
- 【PL/SQL】パッケージ設計でコード管理と再利用性を極める(REF CURSORを公開するパッケージAPI設計)
- 【PL/SQL】ストアドプロシージャとファンクションの違いと作り方(REF CURSORを返す関数設計)
- 【PL/SQL】パフォーマンス改善プレイブック(REF CURSOR利用時の性能チューニング)
まとめ|REF CURSORで柔軟なデータ取得層を設計する
REF CURSORは「結果集合を持ち運べるハンドル」として、PL/SQL ↔ クライアントのデータ取得APIで欠かせない仕組みです。使いこなしのポイントは①柔軟性と契約厳密化の使い分け、②動的SQL・パイプライン関数・BULK COLLECTとの統合、③CLOSE責任の明確化、の3点に集約されます。本記事の要点を7つにまとめます。
- SYS_REFCURSORは柔軟性、強い型付けはコンパイル時チェック。用途で使い分ける
- クライアント返却はJava/Python/.NET各言語のOUTパラメータで標準的に受け取れる
- 動的SQLは
OPEN ... FOR ... USINGでバインド変数を活用する - 大量データはパイプライン関数の入力として流し込み並列ETL化
- PL/SQL内消費は
FETCH BULK COLLECT INTO ... LIMITで高速化 - 複数結果セット返却で注文+明細など複合データを1呼び出しで取得
- CLOSE責任を契約で明示し例外時もセーフCLOSEパターンでリーク防止
レガシーシステムで「結果集合を返すために中間テーブルにINSERT→クライアントが読む」ような迂回設計があれば、REF CURSORで直接ストリーミングする実装に置き換えるだけで体感性能が桁違いに改善します。本記事の言語別実装例とテンプレートをベースに自プロジェクトのデータ取得層を見直してみてください。

