【PL/SQL】Result Cacheとマテリアライズドビュー併用戦略|鮮度SLA・QUERY REWRITE・監視

【PL/SQL】Result Cacheとマテリアライズドビューを併用した高速化戦略 PL/SQL

Result Cache とマテリアライズドビュー(MV)は、どちらも「同じ計算を何度も繰り返さない」ための高速化手段です。ただし、同じキャッシュという言葉でまとめると設計を誤ります。MVは重いJOIN、GROUP BY、集計、明細からサマリーへの変換をデータベース上に保持する仕組みで、Result Cacheは同じSQL結果や同じ引数のPL/SQL関数結果をSGA上で再利用する仕組みです。

結論から言うと、大きな集合処理はMVで前計算し、頻繁に参照される小さな派生値はResult Cacheで再利用するのが基本です。そのうえで、業務が許容する鮮度、更新頻度、DML負荷、障害時の復旧方法、監視SQLまで決めておくと、本番で壊れにくい高速化になります。単体のResult Cache設計は FUNCTION RESULT_CACHEの使い方、Oracle全体のResult Cacheは Oracle Result Cache完全ガイド もあわせて確認してください。

この記事で整理すること

  • MV、Function Result Cache、Server Result Cache、索引、パーティションの使い分け
  • 鮮度SLAから ON COMMITON DEMAND を決める考え方
  • QUERY REWRITEDBMS_MVIEW.EXPLAIN_MVIEWSTALENESS の確認
  • V$RESULT_CACHE_STATISTICSV$RESULT_CACHE_OBJECTS によるResult Cache監視
  • 本番投入前に避けるべき設計とチェックリスト
スポンサーリンク

最初に結論:MVとResult Cacheは役割を分ける

MVとResult Cacheを併用するときは、先にデータ量と鮮度で役割を切ります。MVはディスク上に結果を保持できるため、何百万行を集計して数千行のサマリーにするような処理に向きます。Result Cacheはメモリ上の再利用なので、戻り値が小さく、引数の種類が絞られ、参照元の更新頻度が低い処理に向きます。

MVに任せる処理日次・時間帯別・顧客別の集計、複数表JOIN、分析用サマリー、画面一覧の母集団など、SQLだけで作れる重い集合処理です。
Function Result Cacheに任せる処理コード値から名称を返す、顧客ランクを返す、設定値を返すなど、同じ引数で何度も呼ばれる小さなPL/SQL関数です。
Server Result Cacheに任せる処理RESULT_CACHE ヒントなどで、同じSELECT結果を短時間に多くのセッションが読むケースです。
索引・パーティションで十分な処理検索条件が明確で、最新値が必須で、集計よりも絞り込みが支配的な処理は、まず索引とパーティションを見直します。

索引やパーティションで解ける問題を、いきなりMVやResult Cacheに寄せると、更新運用と監視が増えるだけになります。大量データ側の切り分けは Oracleパーティションテーブル完全ガイド、PL/SQLの性能改善全体は PL/SQLパフォーマンス改善プレイブック が関連します。

判断基準:先に鮮度SLAを決める

キャッシュ設計で最初に決めるべきなのは「何秒・何分・何時間まで古くてよいか」です。この鮮度SLAが曖昧なままMVやResult Cacheを入れると、速くはなっても業務上の正しさを説明できません。

常に最新が必要MVやキャッシュより、索引、SQL改善、パーティション、実行計画の安定化を優先します。
数秒の遅れなら許容ON COMMIT MVや同期更新を検討できますが、更新トランザクションにリフレッシュ負荷が乗ります。
数分から数十分の遅れでよいON DEMAND MVを DBMS_SCHEDULER で定期更新する設計が扱いやすくなります。
日次・時間次でよい夜間バッチや集計バッチでMVを更新し、画面や帳票はMVを読む構成が向きます。

大事なのは、キャッシュの古さを障害ではなく仕様として説明できることです。たとえば「売上速報は5分遅れまで許容」「請求確定は最新必須」のように、画面や処理ごとに分けて決めます。

MVの基本構成

MVを高速化の中心に置く場合は、ベース表、MVログ、MV本体、リフレッシュ方式、クエリリライト可否をセットで設計します。特にFAST REFRESHを狙う場合、対象SQLの形やMVログの有無で成立条件が変わるため、作ってから速いかどうかを見るのでは遅いです。

MV単体の作成、管理、リフレッシュ方式を先に確認したい場合は Oracleマテリアライズドビュー完全ガイド で基本構文と運用ポイントを押さえてから、この記事の併用設計に進むと整理しやすくなります。

MVログを作成する

mv-log.sql
CREATE MATERIALIZED VIEW LOG ON sales
WITH ROWID, SEQUENCE (customer_id, sales_date, amount)
INCLUDING NEW VALUES;

CREATE MATERIALIZED VIEW LOG ON customers
WITH ROWID, SEQUENCE (customer_id, customer_rank)
INCLUDING NEW VALUES;

MVログはFAST REFRESHの差分更新に使われます。ログを作るとDML時の記録コストも増えるため、更新頻度が高い表では書き込み側の性能も測定します。

集計MVを作成する

sales-mv.sql
CREATE MATERIALIZED VIEW mv_sales_daily_rank
BUILD IMMEDIATE
REFRESH FAST ON DEMAND
ENABLE QUERY REWRITE
AS
SELECT c.customer_rank,
       TRUNC(s.sales_date) AS sales_day,
       COUNT(*)            AS order_count,
       SUM(s.amount)       AS total_amount
FROM sales s
JOIN customers c
  ON c.customer_id = s.customer_id
GROUP BY c.customer_rank,
         TRUNC(s.sales_date);

ENABLE QUERY REWRITE を付けると、条件がそろった場合にOracleが元SQLをMV参照へ書き換えられます。ただし、MVが古い、制約や統計情報が不足している、クエリリライト設定が無効、SQLの形が合わない、といった理由で使われないことがあります。

リフレッシュ方式を選ぶ

MVのリフレッシュは、鮮度と更新負荷のトレードオフです。ON COMMIT は最新性を保ちやすい反面、ベース表のCOMMITに追加処理が乗ります。ON DEMAND は遅延を許容する代わりに、更新タイミングをジョブで制御しやすくなります。

ON COMMIT更新直後の参照鮮度を重視する小さめのMV向きです。大量DMLや高頻度更新表ではCOMMIT遅延を測ります。
ON DEMAND数分から日次の遅れを許容できる集計向きです。ジョブ失敗時の再実行と通知を設計しやすいです。
FAST REFRESH差分更新で済む可能性がありますが、MV定義やログの条件を満たす必要があります。
COMPLETE REFRESH全作り直しです。単純ですが、データ量が増えたときの時間とロック影響を見積もります。
mv-refresh.sql
BEGIN
  DBMS_MVIEW.REFRESH(
    list           => 'MV_SALES_DAILY_RANK',
    method         => 'F',
    atomic_refresh => TRUE
  );
END;
/

BEGIN
  DBMS_MVIEW.REFRESH(
    list           => 'MV_SALES_DAILY_RANK',
    method         => 'C',
    atomic_refresh => FALSE
  );
END;
/

atomic_refresh の扱いは、参照中の一貫性、リフレッシュ時間、領域使用量、メンテナンス時間帯によって決めます。本番ではFASTで失敗したときにCOMPLETEへ自動で逃がすのか、停止して通知するのかも決めておきます。

QUERY REWRITEを確認する

MVを作っただけでは、既存SQLが必ずMVを使うとは限りません。クエリリライトを狙うなら、パラメータ、権限、制約、統計情報、MVの鮮度、SQLの形を確認します。実行計画の読み方は DBMS_XPLAN完全ガイドEXPLAIN PLAN完全ガイド が関連します。

EXPLAIN_MVIEWで成立条件を見る

explain-mview.sql
BEGIN
  DBMS_MVIEW.EXPLAIN_MVIEW('MV_SALES_DAILY_RANK');
END;
/

SELECT capability_name,
       possible,
       related_text,
       msgtxt
FROM mv_capabilities_table
WHERE mvname = 'MV_SALES_DAILY_RANK'
ORDER BY capability_name;

DBMS_MVIEW.EXPLAIN_MVIEW は、FAST REFRESHやQUERY REWRITEが可能かを確認する入口になります。できない理由が MSGTXT に出るため、MVログ不足、集計式、JOIN条件、制約不足などを切り分けます。

リライトされているかを見る

query-rewrite-check.sql
EXPLAIN PLAN FOR
SELECT c.customer_rank,
       TRUNC(s.sales_date) AS sales_day,
       SUM(s.amount)       AS total_amount
FROM sales s
JOIN customers c
  ON c.customer_id = s.customer_id
WHERE s.sales_date >= DATE '2026-01-01'
GROUP BY c.customer_rank,
         TRUNC(s.sales_date);

SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, NULL, 'BASIC +NOTE'));

実行計画やNOTEにMV利用の痕跡が出るかを確認します。SQLヒントで無理に寄せる前に、統計情報、クエリリライト設定、MVの STALENESS、制約の信頼性を確認します。ヒント句の使い方は Oracle SQLヒント句完全ガイド も参考になります。

MVの鮮度と状態を監視する

MVは作成して終わりではありません。運用では、最後にいつリフレッシュされたか、古くなっていないか、リフレッシュに失敗していないか、参照SQLが期待どおりMVを使っているかを確認します。

mv-monitor.sql
SELECT mview_name,
       refresh_mode,
       refresh_method,
       staleness,
       last_refresh_type,
       last_refresh_date
FROM user_mviews
WHERE mview_name = 'MV_SALES_DAILY_RANK';

SELECT name,
       last_refresh
FROM user_mview_refresh_times
WHERE name = 'MV_SALES_DAILY_RANK';

SELECT master,
       log_table
FROM user_mview_logs
ORDER BY master;

STALENESS は、MVが最新か、古いか、利用不能に近い状態かを見る重要な列です。リフレッシュジョブの監視とあわせて、古い状態が業務SLAを超えたら通知する仕組みを用意します。ジョブ管理は DBMS_SCHEDULER完全ガイドPL/SQLのDBMS_SCHEDULER運用 が関連します。

Result Cacheを重ねる場所

MVで集計結果を作ったあと、さらにPL/SQL関数で表示名、閾値、設定値、ランク判定を返す場合があります。この関数が同じ引数で大量に呼ばれるなら、Function Result Cacheを重ねる余地があります。逆に、毎回違う引数、戻り値が大きい、セッション状態に依存する、頻繁に更新される表を読む場合は向きません。

MVを読むResult Cache関数

result-cache-function.sql
CREATE OR REPLACE FUNCTION get_rank_sales_total(
  p_customer_rank IN VARCHAR2,
  p_sales_day     IN DATE
) RETURN NUMBER
RESULT_CACHE
IS
  l_total NUMBER;
BEGIN
  SELECT total_amount
  INTO l_total
  FROM mv_sales_daily_rank
  WHERE customer_rank = p_customer_rank
    AND sales_day = TRUNC(p_sales_day);

  RETURN l_total;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RETURN 0;
END;
/

古いサンプルでは RESULT_CACHE RELIES_ON(...) を見かけますが、現行設計では依存オブジェクトの自動検出に任せる方針が基本です。ただし、外部状態、セッション状態、NLS、アプリケーションコンテキスト、権限差で結果が変わる関数はキャッシュ対象から外します。依存オブジェクトとINVALIDの扱いは 依存オブジェクトとINVALID再コンパイル が近いテーマです。

Result Cacheの状態を確認する

Result Cacheは入れたら速くなる、ではなく、ヒットしているか、メモリを圧迫していないか、無効化が多すぎないかを見ます。キャッシュサイズが小さすぎる、引数の種類が多すぎる、更新が頻繁すぎる場合は、キャッシュの管理コストが勝ちます。

result-cache-monitor.sql
SELECT name,
       value
FROM v$result_cache_statistics
ORDER BY name;

SELECT type,
       status,
       name,
       namespace,
       creation_timestamp
FROM v$result_cache_objects
ORDER BY creation_timestamp DESC;

BEGIN
  DBMS_RESULT_CACHE.MEMORY_REPORT;
END;
/

リリース直後やMVリフレッシュ直後はキャッシュが無効化されることがあります。計測するときは、初回実行、2回目以降、リフレッシュ直後、ピーク時間帯を分けます。本番の遅いSQL特定は 遅いSQLを特定する方法、AWR/ASHは AWR・ASH完全ガイドAWRレポート完全ガイド が役立ちます。

スケジューラでMV更新を管理する

ON DEMAND のMVは、手動実行ではなくスケジューラで管理します。ジョブ名、実行間隔、失敗時通知、最大実行時間、再実行方針、業務カレンダーを運用設計に入れます。

scheduler-refresh.sql
BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name        => 'JOB_REFRESH_MV_SALES_DAILY_RANK',
    job_type        => 'PLSQL_BLOCK',
    job_action      => q'[
      BEGIN
        DBMS_MVIEW.REFRESH(
          list           => 'MV_SALES_DAILY_RANK',
          method         => 'F',
          atomic_refresh => TRUE
        );
      END;
    ]',
    start_date      => SYSTIMESTAMP,
    repeat_interval => 'FREQ=MINUTELY;INTERVAL=5',
    enabled         => TRUE,
    comments        => 'Refresh sales summary materialized view every 5 minutes'
  );
END;
/

リフレッシュジョブは、成功したかどうかだけでなく、実行時間が伸びていないかも見ます。5分間隔のジョブが4分50秒かかる状態は、次のデータ増加で破綻しやすいです。観測性の設計は PL/SQLインストゥルメンテーション設計 を組み合わせると運用しやすくなります。

ON COMMIT MVの注意点

ON COMMIT は便利ですが、COMMITが軽いままである保証はありません。ベース表に大量INSERT、UPDATE、DELETEが入る処理では、更新トランザクションのレスポンスが悪化したり、ロック保持時間が伸びたりします。

DML側の性能も測る参照SQLだけでなく、登録・更新・削除処理の時間、待機イベント、UNDO、REDOを測定します。
バッチ投入と相性を見る大量ロード時は一時的にCOMPLETE更新や夜間更新へ寄せた方が単純な場合があります。
障害時の戻し方を決めるMVログ肥大、リフレッシュ失敗、定義変更、統計情報不足が起きたときの手順を用意します。
COMMIT頻度を見直す大量処理ではCOMMIT粒度とUNDO設計も関係します。

大量データ処理のCOMMIT設計は 大量データ処理のコミット頻度とUNDO最適化、トランザクション全体の考え方は PL/SQLトランザクション設計パターン も関連します。

併用パターン

MVとResult Cacheを併用するなら、役割を重ねすぎないことが重要です。MVで大きく削り、Result Cacheで小さく再利用する、という二段構えにします。

分析ダッシュボード売上・件数・平均などをMVで集計し、表示用ラベルや権限別の補助値をResult Cache関数で返します。
マスタ参照が多い一覧画面一覧の母集団はMVで作り、ステータス名や区分名の変換をFunction Result Cacheで軽くします。
帳票出力帳票対象をMVで固定し、テンプレート設定やしきい値判定をResult Cacheで再利用します。
API参照高頻度参照の集計値をMVで保持し、同じキーで何度も引かれる派生値だけPL/SQL関数に寄せます。

SQL内でPL/SQL関数を呼びすぎると、キャッシュされても呼び出しコストが残ります。SQL側で完結できる処理はSQLに寄せ、関数化した方が保守しやすい処理だけ切り出します。SQL内関数の扱いは WITH FUNCTION句の使い方、集計SQLの整理は 分析関数を活用した集計ロジック が関連します。

避けたい設計

キャッシュ系の高速化は、成功すると大きく効きます。一方で、間違えると「速いが古い」「速いがたまに間違う」「更新側が遅い」「障害時に直せない」という状態になります。

鮮度SLAなしで導入する古いデータを見せてよい範囲が決まっていないと、障害なのか仕様なのか判断できません。
更新頻度が高い表を安易にキャッシュするDMLのたびに無効化やリフレッシュが走り、効果が出にくくなります。
セッション依存値をResult Cacheに入れるユーザー、ロール、NLS、アプリケーションコンテキストで結果が変わる関数は危険です。
MVを作って実行計画を見ないQUERY REWRITEされていない、または想定外の計画になっている可能性があります。
リフレッシュ失敗を監視しない古いMVを読み続ける事故につながります。ジョブ履歴とMV状態を両方見ます。

本番投入前チェックリスト

  • 対象SQLの現状実行時間、実行回数、論理読取、待機イベントを測定している
  • 最新必須、数秒遅延可、数分遅延可、日次可のどれかを業務側と合意している
  • MVで前計算する範囲と、Result Cacheで再利用する範囲を分けている
  • MVログのDML負荷を登録・更新・削除処理で測定している
  • DBMS_MVIEW.EXPLAIN_MVIEW でFAST REFRESHとQUERY REWRITEの成立条件を確認している
  • USER_MVIEWS.STALENESS とリフレッシュジョブ履歴を監視対象にしている
  • V$RESULT_CACHE_STATISTICS でヒット率、メモリ、無効化傾向を確認している
  • MVリフレッシュ失敗時の再実行、通知、COMPLETE更新、縮退運用を決めている
  • リリース後にAWR/ASH/DBMS_XPLANで参照側と更新側の両方を再測定する
  • セッション依存値、外部状態、副作用のある関数をResult Cache対象から外している
production-check.sql
-- MV state
SELECT mview_name,
       staleness,
       last_refresh_type,
       last_refresh_date
FROM user_mviews
WHERE mview_name = 'MV_SALES_DAILY_RANK';

-- Result cache statistics
SELECT name,
       value
FROM v$result_cache_statistics
ORDER BY name;

-- Cached objects
SELECT type,
       status,
       name
FROM v$result_cache_objects
WHERE name LIKE '%GET_RANK_SALES_TOTAL%'
   OR name LIKE '%MV_SALES_DAILY_RANK%'
ORDER BY type, status, name;

まとめ

Result Cacheとマテリアライズドビューを併用する目的は、単にキャッシュを増やすことではありません。重い集合処理をMVで前計算し、同じ引数で何度も呼ばれる小さな派生値をResult Cacheで再利用し、鮮度SLAと監視で本番運用できる形にすることです。

設計順は、現状計測、鮮度SLA、MV化するSQL、リフレッシュ方式、QUERY REWRITE確認、Result Cache対象の選定、監視、リリース後再測定です。この順番を守ると、速さだけでなく、古さ、更新負荷、障害時の戻し方まで説明できる高速化になります。