はじめに
マテリアライズド・ビュー(MV)の活用の一つとしてクエリリライト(Basic Query Rewrite)1というオラクルのオプティマイザの機能があります。これは、SQLの一部または全部が現存するMVで代替可能であるとオプティマイザが判断した場合、クエリトランスフォーメーションにより、実行時にその部分を自動的にMVに置き換えてパフォーマンスを向上させるものです。
クエリリライト自体は古くからある機能2で、クエリコードを一切変更することなく対象クエリのパフォーマンスを飛躍的に向上させることが可能であることから、実行計画の変更等では対処できない場合のパフォーマンスチューニングにおいて非常に有用なものであるといえます。しかし、MVを利用する性質上、更新された最新データへのアクセスに大きな難があり、使用できる場面がかなり限定されていました。
12cR2の新機能であるリアルタイム・マテリアライズド・ビューは、MVでありながらリフレッシュすることなく最新のテーブルデータにアクセスできるため、クエリリライトの活用の幅が広がります。したがって、これまで対処しづらかった場面でのパフォーマンスチューニングに新たな手段を提供してくれると期待されています。
クエリリライトの概要
まずは、クエリクリライトの動きをおさらいしておきます。以下簡単な例ですが、リライトによってSQLが自動的に書き換えられ実行時にテーブルの代わりにMVが使用されること確認します。
まず、リライトが有効であることを確認します。データベースパラメータquery_rewrite_enabled
がTRUE
(デフォルト)かFORCE
であればリライトは有効です。TRUE
の場合リライトされるかどうかはコスト計算により決定されます。FORCE
の場合はコストに関係なくリライト可能であればリライトします。
SQL> show parameter query_rewrite_enabled
NAME TYPE VALUE
------------------------------------ ----------- --------
query_rewrite_enabled string TRUE
次に適当なデータテーブルとリライトオプション(enable query rewrite
)を有効にしたMVを作成します。テーブルサイズが小さすぎるとコスト計算に意図しない影響を与えるので、ここではテーブルにダミー文字列を入れています。
テストデータ
CREATE TABLE test_tbl AS
SELECT LEVEL id,
MOD (LEVEL, 10) grp,
0 val,
RPAD(LEVEL, 500, 'X') s /* ダミー文字列 */
FROM dual
CONNECT BY LEVEL <= 20000;
リライト可能マテリアライズド・ビューを作ります。ここでは、grpごとのvalの合計を求めいています。
CREATE MATERIALIZED VIEW test_tbl_mv
REFRESH ON DEMAND
ENABLE QUERY REWRITE /* リライトオプション */
AS
SELECT grp, SUM (val)
FROM test_tbl
GROUP BY grp;
SELECT staleness FROM dba_mviews WHERE mview_name = 'TEST_TBL_MV';
STALENESS
-------------------
FRESH <-- MVは最新
では、grp毎のvalの合計を求める適当なクエリを実行してみます。以下でオプティマイザは、インラインビュー部分はMVに置換可能と判断し、クエリに記述されたテーブル(TEST_TBL)でなくMV(TEST_TBL_MV)を代替使用してパフォーマンスの向上を図っています。
この例で示した通り、実行するクエリとMVのSQLコードが完全に一致している必要はありません。あくまでデータの代替が可能かどうかが判断基準になります。ただし、代替可能であるかの判断には細かい制限がいろいろあるので(たとえば分析関数を使用している場合はフルテキストマッチのみなど)、詳しくはマニュアルを参照してください。
SELECT MAX (total) max_total
FROM (SELECT grp, SUM (val) total
FROM test_tbl
WHERE grp < 2
GROUP BY grp);
MAX_TOTAL
----------
0
-----------------------------------------------------
| 1 | SORT AGGREGATE | |
|* 2 | MAT_VIEW REWRITE ACCESS FULL| TEST_TBL_MV | <-- 置換されている
-----------------------------------------------------
また、クエリクリライトによる置換を行いたくない場合は、NO_REWRITE
ヒントを使用します。これにより、代替となるMVを使用せず、常にクエリに記述されたテーブルにアクセスします。この他にも、特定の状況でのリライトを制御するNO_MULTIMV_REWRITE
やNO_BASETABLE_MULTIMV_REWRITE
などのヒントがあります。
SELECT MAX (total) max_total
FROM (SELECT /*+ NO_REWRITE */
grp, SUM (val) total
FROM test_tbl
WHERE grp < 2
GROUP BY grp);
-----------------------------------------
| 1 | SORT AGGREGATE | |
| 2 | VIEW | |
| 3 | HASH GROUP BY | |
|* 4 | TABLE ACCESS FULL| TEST_TBL |
-----------------------------------------
テーブル更新時のクエリリライト
クエリリライトはクエリを一切変更することなくMVの追加だけでパフォーマンスを向上できるのが特徴です。しかし、MVを使用する性質上、テーブルが更新された後の最新データの取得に問題があり、使用場面が限定されます。ここでは、テーブル更新後のリライトの動きについてみていきます。
MVが最新の場合のみリライト (ENFORCED)
データベースパラメータquery_rewrite_integrity = enforced
(デフォルト)に設定し、MVの対象テーブルを更新した結果、実行計画がどの様に変わるか確認します。
ALTER SESSION SET query_rewrite_integrity = enforced;
UPDATE test_tbl SET val = 10 WHERE ID = 1;
COMMIT;
select staleness from dba_mviews where mview_name = 'TEST_TBL_MV';
STALENESS
-------------------
NEEDS_COMPILE <--- MVは最新でない(or STALE)
テーブル更新後、オプティマイザはクエリライトを選択せずMVを使用しなくなります。
SELECT MAX (total) max_total
FROM (SELECT grp, SUM (val) total
FROM test_tbl
WHERE grp < 2
GROUP BY grp);
MAX_TOTAL
----------
10 <-- 最新データ
-----------------------------------------
| 1 | SORT AGGREGATE | |
| 2 | VIEW | |
| 3 | HASH GROUP BY | |
|* 4 | TABLE ACCESS FULL| TEST_TBL |
-----------------------------------------
これは更新データがMVに反映されておらず、オプティマイザが「MVは最新データでないので使えない」と判断したためです。結果、テーブルに直接アクセスすることで最新データを取得しています。しかし、MVを使っていないためクエリのパフォーマンスは劣化します。
つまりクエリパフォーマンス向上のためにリライトを有効にしたにもかかわらず、MVの状態によってクエリが早くなったり遅くなったりする状況が発生してしまいます。これでは、パフォーマンスチューニングとしてのクエリリライトは、テーブルの更新がないという前提でしか使用できません。まぁ、逆にそういう状況、たとえば日中更新のない夜間バッチテーブルなどであれば有効ともいえます。
MVの状態に関わらず常にリライト(STALE_TOLERATED)
そこで、オラクルは上記と異なる動きも用意しています。
データベースパラメータ query_rewrite_integrity = stale_tolerated
を設定すると、MVの状態にかかわらず常にMVを使用しパフォーマンスの劣化を防ぎます。しかしながらこの場合、更新された最新のデータには当然アクセスできません。
以下で実行結果が上記と異なっていることを確認してください。
ALTER SESSION SET query_rewrite_integrity = stale_tolerated;
SELECT MAX (total) max_total
FROM (SELECT grp, SUM (val) total
FROM test_tbl
WHERE grp < 2
GROUP BY grp);
MAX_TOTAL
----------
0 <--- 更新前データ
-----------------------------------------------------
| 1 | SORT AGGREGATE | |
|* 2 | MAT_VIEW REWRITE ACCESS FULL| TEST_TBL_MV | <-- 置換
-----------------------------------------------------
リライトされなければエラー(REWRITE_OR_ERROR)
もう一つの異なる動きとして、リライトされなかったらクエリをエラーで終了させることもできます。これは、パラメータでなく、ヒント(REWRITE_OR_ERROR)で制御します。これにより、クエリでエラーが発生した場合は、MVをリフレッシュして再トライする等の動きが可能となります。
ALTER SESSION SET query_rewrite_integrity = enforced;
SELECT /*+ REWRITE_OR_ERROR */ MAX (total) max_total
FROM (SELECT grp, SUM (val) total
FROM test_tbl
WHERE grp < 2
GROUP BY grp);
ORA-30393: a query block in the statement did not rewrite <--- エラー発生
クエリリライトの状況詳細
ちなみに、クエリリライトの状況を詳しく確認するには、DBMS_MVIEW.EXPLAIN_REWRITE というプロシジャが利用できます。リライトが起きないとき原因を見つけるのに便利です。
コレクションを使う方法とテーブルを使う方法があります。詳しくはマニュアル参照のこと。
set serverout ON
DECLARE
msg sys.rewritearraytype := sys.REWRITEARRAYTYPE();
BEGIN
dbms_mview.EXPLAIN_REWRITE(
'SELECT MAX (total) max_total from (select grp, SUM (val) total from test_tbl where grp < 2 group by grp)',
'TEST_TBL_MV',
msg);
FOR i IN 1 .. msg.count LOOP
dbms_output.PUT_LINE(MSG(i).message);
END LOOP;
END;
/
QSM-01150: query did not rewrite
QSM-01106: materialized view, TEST_TBL_MV, is stale with respect to some partition(s) in the base table(s)
QSM-01029: materialized view, TEST_TBL_MV, is stale in ENFORCED integrity mode
QSM-01219: no suitable materialized view found to rewrite this query
問題点
まぁ、結局のところテーブルが更新された後にMVをリフレッシュしていない状況では、パフォーマンスを諦めてテーブルに直接アクセスするか、最新データを無視してパフォーマンスを維持するかの選択しかなく、どちらも痛し痒しといえます。
高速リフレッシュ
ご承知の通りマテリアライズド・ビューにはログを使用した高速リフレッシュ、さらにはON COMMITによる自動高速リフレッシュ機能が備わっており、常にMVを最新データに維持することも可能です。これを使用すれば、常に意図したクエリリライトを得ることができます。
高速リフレッシュのマテリアライズド・ビューにはさまざまな制約が存在します。詳しくはマニュアルを参照してください。
ALTER SESSION SET query_rewrite_integrity = enforced;
DROP MATERIALIZED VIEW test_tbl_mv;
-- マテリアライズド・ビュー・ログ作成
CREATE MATERIALIZED VIEW LOG ON test_tbl
WITH ROWID , SEQUENCE (grp, val)
INCLUDING NEW VALUES;
-- 自動高速リフレッシュMV作成
CREATE MATERIALIZED VIEW test_tbl_mv_fast
REFRESH FAST ON COMMIT
ENABLE QUERY REWRITE
AS SELECT grp,
SUM(val),
COUNT(*), -- 高速リフレッシュ集合制約
COUNT(val) -- 高速リフレッシュ集合制約
FROM test_tbl
GROUP BY grp;
高速リフレッシュMVを作成後、テーブルデータを更新してクエリを実行します。。コミット時に常にMVが更新され、いつでもMV経由で最新のデータにアクセスできます。したがって、データ更新後も問題なくリライトされました。
-- テーブルテータの更新
UPDATE test_tbl SET val = 20 WHERE id = 1;
COMMIT;
-- MVステータスの確認
SELECT staleness FROM dba_mviews WHERE mview_name = 'TEST_TBL_MV_FAST';
STALENESS
-------------------
FRESH <-- コミット時にリフレッシュ済み
SELECT MAX (total) max_total
FROM (SELECT grp, SUM (val) total
FROM test_tbl
WHERE grp < 2
GROUP BY grp);
MAX_TOTAL
----------
20 <-- 最新データ
----------------------------------------------------------
| 1 | SORT AGGREGATE | |
|* 2 | MAT_VIEW REWRITE ACCESS FULL| TEST_TBL_MV_FAST | <-- 置換
----------------------------------------------------------
同一セッション内でコミット前にクエリを実行すると、MVが更新されていないためリライトされません。必ずコミット後に実行します。
問題点
では、これでパフォーマンス問題は解決かと言うとそうでもなくて、自動リフレッシュのMVを作成するとMVの更新が終了するまで対象テーブルへのコミットが終了しないことから、コミットが極端に遅くなるという問題が発生します。つまり更新トランザクションのパフォーマンスを低下させてしまううえ、さらにマルチセッションから頻繁に対象テーブルを更新する環境であれば、リフレッシュ動作の衝突を引き起こす可能性もあります。
ただし、更新トランザクションのパフォーマンスをあまり考慮しなくて良い場合、たとえば更新がシングルバッチ処理の単一コミットという状況などでは問題にならないかもしれません。
また、自動コミットを使わず、毎回クエリの直前に手動高速リフレッシュを実行するという手段もとれますが、クエリの頻度が高い場合、今度は逆にクエリ側でのパフォーマンス低下やリフレッシュの衝突問題が発生します。しかも、コミットのタイミングによっては最新データを取得できない可能性もなきにしもあらずです。
リアルタイム・マテリアライズド・ビュー(12.2)
そこで登場したのが、リアルタイム・マテリアライズド・ビューです。といっても特別なオブジェクトでなく、MVの一つのオプション(enable on query computation
)として提供されています。オプションを有効にするとリライトされたクエリ実行時にマテリアライズド・ビュー・ログを参照してテーブルの最新データをその場で導き出します。これによりMVをリフレッシュすることなく常に最新データにアクセスすることが可能となります。
DROP MATERIALIZED VIEW test_tbl_mv_fast;
CREATE MATERIALIZED VIEW test_tbl_rtmv
REFRESH FAST ON DEMAND
ENABLE QUERY REWRITE
ENABLE ON QUERY COMPUTATION /* リアルタイムオプション */
AS SELECT grp,
SUM(val),
COUNT(*),
COUNT(val)
FROM test_tbl
GROUP BY grp;
UPDATE test_tbl SET val = 30 WHERE id = 1;
COMMIT;
SELECT staleness FROM dba_mviews WHERE mview_name = 'TEST_TBL_RTMV';
STALENESS
-------------------
NEEDS_COMPILE <--- MVは最新でない
SELECT MAX (total) max_total
FROM (SELECT grp, SUM (val) total
FROM test_tbl
WHERE grp < 2
GROUP BY grp);
MAX_TOTAL
----------
30 <-- 最新データ
----------------------------------------------------------------------
| Id | Operation | Name |
----------------------------------------------------------------------
| 1 | SORT AGGREGATE | |
| 2 | VIEW | |
| 3 | UNION-ALL | |
|* 4 | FILTER | |
| 5 | MERGE JOIN OUTER | |
|* 6 | MAT_VIEW ACCESS BY INDEX ROWID | TEST_TBL_RTMV |
| 7 | INDEX FULL SCAN | I_SNAP$_TEST_TBL_RTMV |
|* 8 | SORT JOIN | |
| 9 | VIEW | |
| 10 | HASH GROUP BY | |
|* 11 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
| 12 | VIEW | |
| 13 | UNION-ALL | |
|* 14 | FILTER | |
| 15 | NESTED LOOPS OUTER | |
| 16 | VIEW | |
|* 17 | FILTER | |
| 18 | HASH GROUP BY | |
|* 19 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
|* 20 | INDEX UNIQUE SCAN | I_SNAP$_TEST_TBL_RTMV |
| 21 | MERGE JOIN | |
|* 22 | MAT_VIEW ACCESS BY INDEX ROWID| TEST_TBL_RTMV |
| 23 | INDEX FULL SCAN | I_SNAP$_TEST_TBL_RTMV |
|* 24 | FILTER | |
|* 25 | SORT JOIN | |
| 26 | VIEW | |
| 27 | SORT GROUP BY | |
|* 28 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
----------------------------------------------------------------------
高速リフレッシュ時と同様に、同一セッションの場合必ずコミット後にクエリを実行します。
なにやらいきなり実行計画が煩雑になりましたが、よく見るとマテリアライズド・ビューとマテリアライズド・ビュー・ログ両方にアクセスしていることが確認できます。これでテーブルにアクセスすることなく最新のデータを取得してるわけです。
またMVログにSEQUENCEを含んでいる場合で、かつテーブルの変更がINSERT / DELETEのみであれば、以下の通り結果キャッシュも利用してさらにパフォーマンスを高める努力もしてくれます。ただしなぜかUPDATEでは適用されませんが、オラクルサポートによると仕様(expected behavior)なんだそうです :-)。
-- MVリフレッシュ
exec dbms_mview.refresh('TEST_TBL_RTMV', 'F');
-- テーブルに行を追加
insert into TEST_TBL values (-1, -1, 200, '');
commit;
-- クエリの実行
SELECT MAX (total) max_total from (select grp, SUM (val) total from test_tbl where grp < 2 group by grp);
------------------------------------------------------------------------------
| Id | Operation | Name |
------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | |
| 2 | VIEW | |
| 3 | UNION-ALL | |
| * 4 | FILTER | |
| 5 | MERGE JOIN OUTER | |
| * 6 | MAT_VIEW ACCESS BY INDEX ROWID | TEST_TBL_RTMV |
| 7 | INDEX FULL SCAN | I_SNAP$_TEST_TBL_RTMV |
| * 8 | SORT JOIN | |
| 9 | VIEW | |
| 10 | HASH GROUP BY | |
| 11 | VIEW | |
| 12 | RESULT CACHE | 64aya3zsryhgz6ppm7qjcbccwk | <--- これ
| * 13 | VIEW | |
| 14 | WINDOW SORT | |
| * 15 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
| 16 | VIEW | |
| 17 | UNION-ALL | |
| * 18 | FILTER | |
| 19 | NESTED LOOPS OUTER | |
| 20 | VIEW | |
| * 21 | FILTER | |
| 22 | HASH GROUP BY | |
| * 23 | VIEW | |
| 24 | RESULT CACHE | 64aya3zsryhgz6ppm7qjcbccwk | <--- これ
| * 25 | VIEW | |
| 26 | WINDOW SORT | |
| * 27 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
| * 28 | INDEX UNIQUE SCAN | I_SNAP$_TEST_TBL_RTMV |
|- * 29 | HASH JOIN | |
| 30 | NESTED LOOPS | |
|- 31 | STATISTICS COLLECTOR | |
| 32 | VIEW | |
| 33 | HASH GROUP BY | |
| 34 | VIEW | |
| 35 | RESULT CACHE | 64aya3zsryhgz6ppm7qjcbccwk | <--- これ
| * 36 | VIEW | |
| 37 | WINDOW SORT | |
| * 38 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
| * 39 | MAT_VIEW ACCESS BY INDEX ROWID| TEST_TBL_RTMV |
| * 40 | INDEX UNIQUE SCAN | I_SNAP$_TEST_TBL_RTMV |
|- * 41 | MAT_VIEW ACCESS FULL | TEST_TBL_RTMV |
------------------------------------------------------------------------------
-- 結果キャッシュの確認
SELECT type, status, name, namespace
FROM v$result_cache_objects
WHERE cache_id = '64aya3zsryhgz6ppm7qjcbccwk';
TYPE STATUS NAME NAMESPACE
---------- --------- ------------------------------ ----------
Result Published DMLTYPES:MLOG$_TEST_TBL SQL
まぁ、結果キャッシュが必要であれば、リライトされるクエリ部分にRESULT_CACHEヒントを使用すればいいだけなので、特に問題はないですが、なんにせよ、結果キャッシュはテーブルが更新されれば当然無効化されます。したがってテーブルが頻繁に更新される環境ではあまり意味を成さないでしょう。
問題点
そういうわけで、リアルタイムMVを使用したクエリリライトですべての問題は解決かというと、やはりそんな都合のいいことはなく、更新ログが溜まってくるとMVログを参照して最新データを取得するのに非常に時間がかかってしまうという新たなパフォーマンス問題が発生します。上記のように結果キャッシュを用いてこの問題を緩和していますが、頻繁に更新されるテーブルでは結果キャッシュの効果も限定的です。パフォーマンス劣化対策として、ログを溜めないようバックグランドで適当な間隔で高速リフレッシュを繰り返す、また一度多くの行を更新する時は必ずリフレッシュする等の対処が必要であろうと思われます。
FRESH_MVヒント
リアルタイムMVを使ってMVログからの最新データを即時に取得する機能は、クエリリライト経由でのみで有効となっています。MVに直接アクセスするクエリではそのままでは無効で、有効とするためにはFRESH_MV
ヒントが必要です。このことからもリアルタイムMVは、基本的にクエリリライトのための機能であると推測できます。(まぁバックワードコンパチビリティという理由もあるんでしょうけどね)
-- MVリフレッシュ
exec dbms_mview.refresh('TEST_TBL_RTMV', 'F');
-- テーブルデータの更新
UPDATE test_tbl SET val = 100 WHERE grp = 1;
COMMIT;
-- MVへの直接アクセス
SELECT * FROM test_tbl_rtmv WHERE grp = 1;
GRP SUM(VAL) COUNT(*) COUNT(VAL)
---------- ---------- ---------------- ----------
1 0 2,000 2000 <-- MVから更新前データ取得
----------------------------------------------
| Id | Operation | Name |
----------------------------------------------
|* 1 | MAT_VIEW ACCESS FULL| TEST_TBL_RTMV |
----------------------------------------------
-- リアルタイムMVから最新データを直接取得
SELECT /*+ fresh_mv */ * FROM test_tbl_rtmv WHERE grp = 1
GRP SUM(VAL) COUNT(*) COUNT(VAL)
---------- ---------- ---------------- ----------
1 200000 2,000 2000 <-- 更新後データ
------------------------------------------------------------------------
| Id | Operation | Name |
------------------------------------------------------------------------
| 1 | VIEW | |
| 2 | UNION-ALL | |
| * 3 | FILTER | |
| 4 | MERGE JOIN OUTER | |
| * 5 | MAT_VIEW ACCESS BY INDEX ROWID | TEST_TBL_RTMV |
| 6 | INDEX FULL SCAN | I_SNAP$_TEST_TBL_RTMV |
| * 7 | SORT JOIN | |
| 8 | VIEW | |
| 9 | HASH GROUP BY | |
| * 10 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
| 11 | VIEW | |
| 12 | UNION-ALL | |
| * 13 | FILTER | |
| 14 | NESTED LOOPS OUTER | |
| 15 | VIEW | |
| * 16 | FILTER | |
| 17 | HASH GROUP BY | |
| * 18 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
| * 19 | INDEX UNIQUE SCAN | I_SNAP$_TEST_TBL_RTMV |
|- * 20 | HASH JOIN | |
| 21 | NESTED LOOPS | |
|- 22 | STATISTICS COLLECTOR | |
| 23 | VIEW | |
| 24 | HASH GROUP BY | |
| * 25 | TABLE ACCESS FULL | MLOG$_TEST_TBL |
| * 26 | MAT_VIEW ACCESS BY INDEX ROWID| TEST_TBL_RTMV |
| * 27 | INDEX UNIQUE SCAN | I_SNAP$_TEST_TBL_RTMV |
|- * 28 | MAT_VIEW ACCESS FULL | TEST_TBL_RTMV |
------------------------------------------------------------------------
クエリリライトにおけるMVの使い分け
以上のことからパフォーマンスチューニングとしてクエリリライトを使用する場合、問題に応じた使い分けが必要であると考えられます。以下は個人的な考察による簡単なまとめです。もしオラクル公式の推奨とかベストプラクティスとかありましたら教えてください。ともかくその場に応じた最適な方法を選択することが大切でしょうね。
条件 | 施策 |
---|---|
クエリが最新データを必要としない | query_rewrite_integrity = stale_tolerated |
対象テーブルの更新がない。または時間が決まっている | 完全リフレッシュまたは手動高速リフレッシュ |
更新トランザクションのパフォーマンスはあまり考慮しなくて良い | 自動高速リフレッシュ |
対象テーブルは頻繁に更新されるが一度にたくさんの更新は行われない | リアルタイムMV + 定期高速リフレッシュ |
おわりに
ここまで語っといいてなんですが、リアルタイムMVは公開されてからまだ日が浅く未成熟な印象で、個人的には積極的に使用したいところまで至っていません(それ以前に12.2の環境が少ないですし)。特に、リフレッシュ後にカラム値を同じ値で更新するとリライトが一切効かなくなるという、機能の存在意義さえ疑われかねない致命的なバグとかもあったりします。ま、今後に期待ですね。
追記:上記のバグについて、Patch#28144344がリリースされました。RUのOverlay One-offですが、そのうちRUに追加されるじゃないかと思われます。
以上です。