【PL/SQL】メモリとカーソルの効率的な管理(カーソルキャッシュの仕組み)

【PL/SQL】メモリとカーソルの効率的な管理(カーソルキャッシュの仕組み) PL/SQL

大量トランザクションや動的SQLが多いPL/SQLでは、「パースをどれだけ避けてカーソルを再利用できるか」と「開いたカーソルを漏らさず閉じられるか」が性能と安定性を左右します。Oracleはライブラリキャッシュとセッションのカーソルキャッシュで再利用を支え、パラメータや書き方次第でソフトパース比率を大きく引き上げられます。この記事では暗黙カーソルと明示カーソル、ハードパースとソフトパース、SESSION_CACHED_CURSORSとOPEN_CURSORSの関係、統計ビューでの可視化、PL/SQLにおける正しいクローズと例外時のリーク防止、動的SQLとバインドの実務パターンまでを体系的に解説します。

カーソルとパースの基礎(ハードパースとソフトパース)

SQLを実行するたびに文脈と実行計画をゼロから生成するのがハードパースで、CPUとラッチ競合を引き起こします。同一の文とバインド値違いで再利用できる場合はソフトパースで済み、ライブラリキャッシュから既存のカーソルを引き当てるため負荷が軽くなります。ハードパースの発生率を下げるためには、プレースホルダを用いたバインド変数化と、文の字面を安定させることが第一歩になります。

暗黙カーソルと明示カーソルの使い分け

SELECT INTOやDMLの実行時にOracleが自動で開閉するのが暗黙カーソルです。PL/SQLのFORループで「FOR r IN (SELECT …) LOOP」の形を取ると、PL/SQLエンジンがフェッチとクローズも自動制御してくれ、明示的にOPEN/CLOSEを書く必要がありません。対して細かな制御や再利用を狙う場合は明示カーソルを使い、OPEN→FETCH→CLOSEを自分で記述します。重要なのは、例外経路でも必ずCLOSEされる構造にしてリークを防ぐことです。

セッションカーソルキャッシュの仕組み(SESSION_CACHED_CURSORS)

同じセッション内で同一SQLを繰り返し実行するとき、パース済みのカーソルを短時間キャッシュしてソフトパースに落とし込むのがセッションカーソルキャッシュです。初期化パラメータSESSION_CACHED_CURSORSを適切に設定すると、統計「session cursor cache hits」が増え、パース負荷が下がります。まずはデフォルトの確認から始め、必要に応じて上げていきます。開発や検証ではALTER SESSIONで一時的に調整し、本番ではワークロードを見ながらシステム既定をチューニングします。

-- 現在セッションのヒット数やオープン数を確認する例
SELECT n.name, s.value
  FROM v$sesstat s JOIN v$statname n ON s.statistic# = n.statistic#
 WHERE s.sid = SYS_CONTEXT('USERENV','SID')
   AND n.name IN ('parse count (total)', 'parse count (hard)', 'session cursor cache hits', 'opened cursors current');

-- 必要に応じたチューニング(検証用途の例)
ALTER SESSION SET session_cached_cursors = 200;

ハードパースが多くヒットが伸びない場合は、バインド不足や文の字面が揺れている可能性が高いため、先にアプリのSQL生成を見直します。

OPEN_CURSORSとオープンカーソル監視(リーク検知)

セッションが同時に開けるカーソル上限はOPEN_CURSORSで制御されます。意図せずOPENだけ行いCLOSEを忘れると、長いバッチ中に上限へ達してORA-01000(maximum open cursors exceeded)が発生します。現状の開放忘れや多重オープンを疑うときはv$open_cursorでSQLごとのオープン状況を調べ、堆積している文を特定します。

-- 現在セッションで開いているカーソルの一覧
SELECT sql_id, parse_call_count, sql_text
  FROM v$open_cursor
 WHERE sid = SYS_CONTEXT('USERENV','SID')
 ORDER BY parse_call_count DESC;

頻繁に出現する同一SQLの多重オープンが見つかったら、明示カーソルのライフサイクルや動的SQLのOPEN場所を点検し、ループ外で準備してループ内ではバインドと実行に徹する形へ改修します。

PL/SQLにおける安全なカーソルの開閉テンプレート

例外時も確実にCLOSEされるよう、OPEN後のブロックをBEGIN…EXCEPTION…ENDで包み、最後にCLOSEを置く構造を徹底します。FETCHのループもEXIT WHENで終端を明示し、途中例外が起きてもCLOSEされるようにします。

DECLARE
  CURSOR c_emp(p_dept NUMBER) IS
    SELECT empno, ename FROM emp WHERE deptno = p_dept;
  v_empno emp.empno%TYPE;
  v_ename emp.ename%TYPE;
BEGIN
  OPEN c_emp(10);
  BEGIN
    LOOP
      FETCH c_emp INTO v_empno, v_ename;
      EXIT WHEN c_emp%NOTFOUND;
      NULL; -- 処理本体
    END LOOP;
  EXCEPTION
    WHEN OTHERS THEN
      -- ここでのログなどを行った上で再スローしても良い
      RAISE;
  END;
  CLOSE c_emp;
END;
/

FOR r IN (…) ループが使える場面では暗黙カーソルに任せるのが簡潔で安全です。FETCHやCLOSEを自動化することで漏れの余地を減らせます。

動的SQLの再利用とバインド(EXECUTE IMMEDIATEとDBMS_SQL)

WHERE句やターゲット表が可変の処理では動的SQLが必要になります。EXECUTE IMMEDIATEを多用する場合でも、文字列連結で値を埋め込まず必ずバインドを使えば、同一SQLの字面を保てるためカーソル再利用が効きます。列数やデータ型まで動的に変わる高可変なケースはDBMS_SQLが有利ですが、パフォーマンスの要所では「文の骨格を固定し、値だけをバインド」で設計するのが定石です。

DECLARE
  v_sql  VARCHAR2(4000) := 'UPDATE orders SET amount = :1 WHERE order_id = :2';
BEGIN
  -- 値は必ずバインドして字面を安定化させる
  EXECUTE IMMEDIATE v_sql USING :new_amount, :order_id;
END;
/

バインドを避けて値を直接連結すると、注文ごとに別文としてパースされ、ライブラリキャッシュが汚れてソフトパースも効かなくなります。

フェッチ戦略とPGAの使い方(メモリ効率の勘所)

大量行の読み取りでは、FOR ループの暗黙フェッチでも十分実用ですが、ネットワーク境界をまたぐケースやPL/SQL内での集計前処理が重い場合はBULK COLLECT+LIMITでチャンクフェッチに切り替えるとPGAのスパイクを抑えられます。LIMITを付けずに全件取り込みを行うと、巨大な配列が一時的にPGAを圧迫するため注意が必要です。

DECLARE
  TYPE t_emp IS TABLE OF emp%ROWTYPE;
  v_tab t_emp;
BEGIN
  LOOP
    FETCH cur BULK COLLECT INTO v_tab LIMIT 1000;
    EXIT WHEN v_tab.COUNT = 0;
    -- ここでv_tabを処理
  END LOOP;
END;
/

サーバ側でのカーソル再利用はあくまで「同じ文」であることが前提のため、フェッチ戦略と合わせてSQLの字面を安定させることが重要です。

カーソル無効化と依存関係(リコンパイルの影響を理解する)

参照する表やビュー、インデックスのDDL、依存するパッケージの再コンパイルなどが起きると、既存カーソルは無効化され再解析が必要になります。夜間バッチや高頻度処理の直前に大規模なDDLを入れない、リリース時はプランのウォームアップを兼ねたプレ実行を入れる、といった運用の工夫でハードパースのスパイクを平滑化できます。

監視とチューニングの実務手順(ソフトパース率を上げる)

最初にv$sesstatやAWRで「parse count (hard)」「parse count (total)」「session cursor cache hits」を確認し、ハード比率が高ければバインドとSQL生成の見直しを行います。次にv$open_cursorでリークや多重オープンの兆候を探り、必要ならコード上でOPEN/CLOSEの境界を短縮します。ワークロードに応じてSESSION_CACHED_CURSORSを引き上げ、OPEN_CURSORSのヘッドルームも確保した上で、ピーク時の「opened cursors current」が安全域に収まっているかを観察します。SQLごとの再利用状況はv$sqlareaのexecutionsとloads、child_numberの分岐数から把握できます。

実務テンプレート:動的UPDATEを高再利用で回す

値だけが変わるUPDATEを高頻度で繰り返すユースケースでは、文の固定化、セッションカーソルキャッシュの活用、ループ外準備・ループ内実行の三点を守ると安定します。

DECLARE
  v_sql CONSTANT VARCHAR2(200) := 'UPDATE orders SET status = :1, updated_at = SYSTIMESTAMP WHERE order_id = :2';
BEGIN
  FOR r IN (SELECT order_id, new_status FROM orders_delta ORDER BY order_id) LOOP
    EXECUTE IMMEDIATE v_sql USING r.new_status, r.order_id; -- 値のみバインド
    IF MOD(r.order_id, 50000) = 0 THEN COMMIT; END IF;      -- チャンク確定(任意)
  END LOOP;
  COMMIT;
END;
/

この形ならカーソルは同一の字面で再利用され、セッションカーソルキャッシュのヒットも伸びやすくなります。加えてorders(status)の選択性が低いなら補助インデックスやヒント検討も合わせて行います。

まとめ

カーソル管理の要点は、同じSQLの字面を維持してバインド変数で値だけを変えること、セッションカーソルキャッシュとOPEN_CURSORSの余裕を用意すること、例外時も確実にCLOSEするライフサイクルを徹底することの三つです。暗黙カーソルを使える場面では任せ、必要に応じて明示カーソルで再利用と制御を高め、動的SQLでは「骨格固定+バインド」を貫く。v$系ビューでパースとオープン状況を可視化しながら調整すれば、ハードパースのスパイクを抑え、メモリもカーソルも効率的に使い切る堅牢なPL/SQL実行基盤を構築できます。