忘れかけのIT備忘録

今まで学んできた知識や小技、なるほど!と思ったことをメモするブログです。

Oracleの待機イベント

今回はOracleの待機イベントについて調査、検証しました。

待機イベントとは
プロセス(セッション)が処理できず(CPUを使用できず)、他の処理が完了するのを待っているイベント(出来事、アクション)です。
たとえばプロセスA(セッションA)に対して「db file sequential read」という待機イベントが発生している場合、現在ディスクのランダムアクセスが発生しているため、プロセスA(セッションA)は「db file sequential read」が完了するのを待っているということになります。
プロセス(セッション)に待機が発生した場合、待機イベント情報として待機理由や待機時間、待機回数などが記録されます。

主な待機クラス
待機クラスは10gから導入された概念で、待機イベントを特性ごとにグルーピングしたものです。
AWRレポートなどを使用して性能調査する場合、一般的に先ず待機クラスを見た方が良いと言われています。
待機クラスの状況からシステムの傾向やボトルネックのあたりをつけて調査範囲を絞り込むことができるため、いきなり待機イベントを見るより効率的だと言われています。

主な待機イベント
待機イベントは1000種類以上あると言われていますが、その中でも私が今までの現場で良く見た待機イベントを載せます。

enq: TX - row lock contention

log file sync / log file parallel write

buffer busy waits

library cache lock

free buffer waits

log buffer space

SQL*Net message from client
本待機イベントは「SQL文が発行されていない」という状態のため、通常無視して問題ありません。

SQL*Net message to client

SQL*Net more data from client
SQL*Net message from client」待機イベントとの違いは、クライアントからSQL文が送られてきている途中で送信が滞ってしまったなど。
Oracleはパケットを複数回やり取りすることもあるため、途中で送受信が滞ってしまった場合はこの待機イベントが発生します。

SQL*Net more data to client

db file parallel write

【補足】
データベース・バッファ・キャッシュのバッファ状態
バッファには4つの状態があります。
空き(未使用)
データが読み込まれていない空のバッファ
確保済み(PIN)
読み書き中のバッファ。このバッファにアクセスできるセッションは1つだけであり、別のセッションはアクセスできるまで待機
クリーン
データの内容がデータファイルと一致しているバッファ。CRバッファ(読み取り一貫性)もこの状態
使用済み
データファイルに未だ書き込まれていないバッファ。データの内容がデータファイルと一致していないバッファ。書き込まれたあとは「クリーン」に遷移

db file scattered read
データベース・バッファ・キャッシュ上に単一ブロックが分散的(不連続)に配置されるため、「scattered」となる

db file sequential read
データベース・バッファ・キャッシュ上に複数ブロックが連続に配置されるため、「sequential」となる

direct path read
大規模表(データベース・バッファ・キャッシュにキャッシュされないブロック)を直接PGAに配置することで下記メリットがある
・データベース・バッファ・キャッシュにブロックを配置する処理が省略できる
・大量のブロックがデータベース・バッファ・キャッシュに配置されることで既存のブロックがエージアウトされることを防ぐ

待機イベントを確認できる主なビュー
v$event_name
待機イベント一覧の情報
v$session_wait
セッションごとの現在待機している待機イベント(待機状態)
※10g以降はv$sessionにV$SESSION_WAITの列情報も包含されているため、個別でV$SESSION_WAITを見る必要はなくなった
v$system_event
インスタンス起動後の待機イベント(累積値)
v$session_event
セッションごとの待機イベント(累積値)

待機イベントを発生、確認する検証をしてみました。

■検証環境
OS:Oracle Linux 6.5
DB/GI:Oracle Database 12c Release 1 (12.1.0.2.0) Enterprise Edition
※2ノードRAC(管理者管理型DB)

■検証パターン
①待機イベント(enq: TX - row lock contention)発生

■検証
①待機イベント(enq: TX - row lock contention)発生
複数のセッションで同一の行をUPDATEする場合、一方のセッションがCOMMITするまでもう一方のセッションは待機イベント(enq: TX - row lock contention)が発生するか検証します。

【検証手順】
1. [セッション3 / 管理者ユーザ] 現在発生している待機イベントを確認
2. [セッション1 / アプリケーションユーザ] EMP表のEMPNO = 7369の行をUPDATE(COMMITしない)
3. [セッション3 / 管理者ユーザ] 現在発生している待機イベントを確認
4. [セッション2 / アプリケーションユーザ] EMP表のEMPNO = 7369の行をUPDATE(COMMITしない)
5. [セッション3 / 管理者ユーザ] 現在発生している待機イベントを確認
6. [セッション1 / アプリケーションユーザ] COMMIT
7. [セッション3 / 管理者ユーザ] 現在発生している待機イベントを確認

【作業ログ】

1. [セッション3 / 管理者ユーザ] 現在発生している待機イベントを確認
SQL> select sid, username, event, state from v$session where username in('SCOTT', 'HR');

       SID USERNAME             EVENT                                    STATE
---------- -------------------- ---------------------------------------- ---------------
        15 HR                   SQL*Net message from client              WAITING
       275 SCOTT                SQL*Net message from client              WAITING
★クライアントからリクエスト待ち

2. [セッション1 / アプリケーションユーザ] EMP表のEMPNO=7369の表をUPDATE(COMMITしない)
SQL> update emp set sal = 9999 where empno = 7369;

1 row updated.

3. [セッション3 / 管理者ユーザ] 現在発生している待機イベントを確認
SQL> select sid, username, event, state from v$session where username in('SCOTT', 'HR');

       SID USERNAME             EVENT                                    STATE
---------- -------------------- ---------------------------------------- ---------------
        15 HR                   SQL*Net message from client              WAITING
       275 SCOTT                SQL*Net message from client              WAITING

4. [セッション2 / アプリケーションユーザ] EMP表のEMPNO=7369の表をUPDATE(COMMITしない)
SQL> update SCOTT.emp set sal = 9999 where empno = 7369;
★応答なし

5. [セッション3 / 管理者ユーザ] 現在発生している待機イベントを確認
SQL> select sid, username, event, state from v$session where username in('SCOTT', 'HR');

       SID USERNAME             EVENT                                    STATE
---------- -------------------- ---------------------------------------- ---------------
        15 HR                   enq: TX - row lock contention            WAITING
       275 SCOTT                SQL*Net message from client              WAITING
★HRが「enq: TX - row lock contention」イベントで行ロック取得を待機している

6. [セッション1 / アプリケーションユーザ] UPDATE後のコミット
SQL> commit;

Commit complete.

7. [セッション3 / 管理者ユーザ] 現在発生している待機イベントを確認
SQL> select sid, username, event, state from v$session where username in('SCOTT', 'HR');

       SID USERNAME             EVENT                                    STATE
---------- -------------------- ---------------------------------------- ---------------
        15 HR                   SQL*Net message from client              WAITING
       275 SCOTT                SQL*Net message from client              WAITING
★HRが行ロックを取得できたため、「enq: TX - row lock contention」イベントは解消した

 

■参考資料
Oracle待機イベント
待機イベントの説明
Oracleの現場を効率化する100の技
オラクルマスター教科書 Gold DBA Oracle Database AdministrationⅡ
オラクルマスター教科書 ORACLE MASTER Expert パフォーマンス・チューニング編
データベースの限界性能を引き出す技術
新・門外不出のOracle現場ワザ エキスパートが明かす運用・管理の極意

■おわりに
待機イベントは「待機」というキーワードからネガティブなイメージが持たれやすいですが、SQL文を処理する都合上、必要な待機時間、無視できる待機時間もあります。(もちろん無駄な待機時間もありますが)
無駄な待機時間から待機時間(待機イベント)の原因を特定し、チューニング(SQL文のロジック修正やハードウェアのスケールアップなど)してくのが基本的な対処方法になります。

OCJP Bronze SE / Silver SE11 受験記

この記事は、JPOUG Advent Calendar 2023 16日目の記事です。
15日目は wmo6hash さんの記事 「働き方の新しいスタイル」への仕事道具最適化'23 - wmo6hash::blog でした。

去年はOracle Databaseの従来型監査の検証について書きましたが、今年はOCJPを受験したため、受験した感想や学習で躓いたポイントを書こうと思います。
これから受験を考えている方の何かの参考になれば幸いです。

OCJPとは?
OCJP(Oracle Certified Java Programmer:Oracle認定Javaプログラマ)は日本オラクル社が主催するJava言語に関する知識を問う試験です。
Bronze(未経験者向け)/ Silver(開発初心者向け)/ Gold(開発中上級者向け)の3段階の難易度の試験が用意されています。
各試験の試験時間や出題範囲、受験料など詳細は日本オラクル社の公式サイトをご参照ください。
(参考)
Java SE 11 認定資格 | オラクル認定資格制度 | Oracle University
※参考リンクはSE11の情報です。本記事の執筆中に知ったのですが、2023年12月1日からSE17のSilverの試験がリリースされていました。
(参考)
新着ニュース | Oracle University
 私が勉強を始めた当時はSE11が最新でした。
※元々はSCJP(Sun Certified Java Programmer:Sun認定Javaプログラマ)としてサン・マイクロシステムズ社が主催する試験でしたが、2010年1月の買収に伴い、オラクル社が試験の運営を引き継いでいます。

プログラマー経歴
私は元々アプリケーション側のエンジニアで、プログラマーとしての経験は以下のとおりです。
C言語(約4年)
Java言語(約3年)
現在はOracle DBA業務が中心のため、もう6~7年はプログラムから離れています。(たまにVBAでツール作っている程度です)

なぜ受験したのか?
Oracle DBA業務が中心なのになぜ今ごろJavaの試験を受けたのか?」とツッコミがありそうですが、私が受験した主な理由です。
・元プログラマーとして、資格試験を通じてプログラムの知識を体系的に復習・整理するため
・アプリケーション(Java)のエラーが発生した際、エラー内容が理解できればエラーの発生起因がアプリケーション側なのか基盤側なのか切り分けやすくなると思ったため
・(Goldの範囲ですが)スレッドプログラミングやJDBCの理解を深めるため(特にOLTPではこの辺の知識もあると強みになると思います)
・Goldは会社から報奨金が出るため(これが一番の理由かもしれません(笑))
 →SilverはGold受験の前提資格となるため、先ずSilverまで受験しました。

学習時間
纏まった時間はあまり取れないため、Bronze、Silverともに2ヶ月ぐらい掛かりました。
平日は通勤時間と夜に少々、休日は空いた時間で勉強する程度でした。

学習方法
Bronze、Silverともに紫本と黒本を使用しました。

紫本
・オラクル認定資格教科書 Javaプログラマ Bronze SE(試験番号1Z0-818)
・オラクル認定資格教科書 Javaプログラマ Silver SE11(試験番号1Z0-815)
黒本
・徹底攻略Java SE Bronze問題集[1Z0-818]対応
・徹底攻略Java SE 11 Silver問題集[1Z0-815]対応
現役の方は黒本だけでも十分と巷では言われていますが、私はJava(というかプログラム)から離れてブランクがあるため、紫本からやり直しました。

Bronze、Silverともに最初の1ヶ月間は紫本を1周しました。(Javaを思い出すことが目的のため、章末問題、模擬問題はやりませんでした)
残りの1ヶ月間で黒本(模擬試験含む)を3周しました。
3週目でもスムーズに解答できなかった問題には付箋を貼っておき、試験直前まで付箋が貼られた問題を繰り返し解いていました。
また、自宅の検証用PCにJavaをインストールし、実機検証しながら理解を深めていました。
実際の動きを確認しながら理解を深めることができるため、実機検証は有効な手段だと思います。
JavaはSE21が最新(2023年12月16日時点)ですが、SE11対応の試験ということでSE11をインストールしました。
(参考)Oracle Java SE Supportロードマップ | Oracle 日本

試験結果
Bronzeは95%、Silverは85%で無事合格できました。(Bronzeは60%、Silverは63%が合格ライン)
試験問題については言及できませんが、黒本の類似問題が体感5割ぐらい出題されたと思います。

躓いたポイント
黒本の問題で個人的に躓いたポイントをいくつか紹介します。
全部を紹介するとキリがないため、特に覚えられなかったもの(思い出せなかったもの)だけ紹介します。

【Bronze編】
ポリモーフィズム多態性、多相性)
Javaに限らず、オブジェクト指向プログラミングでは一度は目にするキーワードですね。
サブクラス型のオブジェクトをスーパークラス型で扱い、オブジェクトごとで異なる動作を実現する仕組みです。(スーパークラスの特定のメソッドをサブクラスでオーバーライドするため、is-a関係が成り立つことが前提)
…ということを忘れていました。
たとえば下記のようなコードがあったとします。

実行結果

FugaクラスもPiyoクラスもHogeインタフェースとis-a関係にあるため、ポリモーフィズムが実現可能です。
hoges[0](Fugaインスタンス)のfunc()メソッドは Merry christmas! 2023 を出力します。
hoges[1](Piyoインスタンス)のfunc()メソッドはFugaクラスのfunc()メソッドをオーバーライドしているため、Happy new year! 2024 を出力します。
クラス図は下記のようになります。表現が正しいか不明ですが個人の理解で書いたイメージ図も載せます。

配列の変数宣言
C言語の配列の変数宣言の記憶とごちゃ混ぜになり、なかなか覚えられませんでした。
たとえば下記のうち、配列の宣言、配列インスタンスの生成方法として正しいものはどれでしょうか?

答え
3、5、7

C言語は変数宣言時に要素数を指定しますが、Java言語は変数宣言時に要素数は指定できないなど結構混乱しました。

【Silver編】
switch文の条件式にnullが設定されているString型オブジェクトを指定
バージョン7からswitch文の条件式にString型オブジェクトも指定できるようになったそうです。
私が触っていた当時は確かバージョン5ぐらいで馴染みがないため、なかなか覚えられませんでした。
たとえば下記のようなコードがあったとします。

実行結果

msgはnullが入っているため、default句でmsgに"def"が設定されると予想していました。
switch文の条件式にString型オブジェクトを指定した場合、String型オブジェクトのhashCode()メソッドを実行し、ハッシュコードで分岐する動きになります。
つまり、ここではmsgはnullが入っているため、「null.hashCode()」となり、「NullPointerException」が発生します。
※msgはフィールド変数のため、値を指定しない場合、デフォルト値(null)で初期化されます。

1次元配列と2次元配列のclone()
同じclone()でも1次元配列と2次元配列で動きが異なります。(シャローコピーとディープコピー)
当時もここは躓いていたと思います。

◆1次元配列のclone()
たとえば下記のようなコードがあったとします。

実行結果

検証結果から1次元配列の要素(値)をコピーしていることが分かります。(arr2の1次元目の要素を変えてもコピー元の値は変わりません)
なお、検証では要素がプリミティブ型、参照型どちらも同じ動きをしました。

arr1、arr2で別々の参照を持つのか裏を取るため、配列インスタンスのハッシュコードも表示してみました。

arr1、arr2でそれぞれハッシュコードが異なる(別々の参照を持っている配列インスタンスである)ことが分かります。

◆2次元配列のclone()
たとえば下記のようなコードがあったとします。

実行結果

検証結果から1次元配列の要素(参照)をコピーしていることが分かります。(arr2の2次元目の要素を変えるとコピー元の値も変わります)
なお、検証では要素がプリミティブ型、参照型どちらも同じ動きをしました。

こちらも配列インスタンスのハッシュコードも表示してみました。

arr1、arr2でそれぞれハッシュコードが異なる(別々の参照を持っている2次元配列インスタンスである)ことが分かります。
しかし、arr1[0]とarr2[0]、arr1[1]とarr2[1]はそれぞれハッシュコードが同じ(同じ参照を持っている1次元配列インスタンスである)ことが分かります。
なお、arr3はarr1およびarr2の1次元配列インスタンスとハッシュコードが異なる(別々の参照を持っている1次元配列インスタンスである)ため、arr2の2次元目の要素を変えてもarr3の要素は変わりません。

具体例を書く時間が取れなかったのですが、他にもラムダ式やモジュール・システムなどの新機能も馴染みが無いため、なかなか覚えられず苦労しました。

引っ掛かったポイント
黒本の問題で個人的に引っ掛かったポイントをいくつか紹介します。
こちらも全部を紹介するとキリがないため、特に引っ掛かったものだけ紹介します。

【Bronze編】
if文に中括弧がない
たとえば下記のようなコードがあったとします。

実行結果

良く見るとif文に中括弧がありません。
msgがnullのため、if文には入りません。
if文の中括弧を省略した場合、true時の処理として最初の1文だけ実行されます。(true/falseに関わらず、7行目の"Hello, Java!"は出力されます)

switch文のcase句にbreakがない
たとえば下記のようなコードがあったとします。

実行結果

良く見ると「case 'b':」にbreakがありません。
breakがないと次のcase句の処理も実行されます。

【Silver編】
初期化されていない変数の使用
たとえば下記のようなコードがあったとします。

実行結果

5行目のif文がfalseだった場合、iが不定値の状態で11行目で出力されてしまうため、コンパイルでエラーになります。

上記のように注意して読まないと見落としてしまうようなものが他にもありました。

受験する際の注意事項
・試験予約は早めにした方が良い(特にSilver)
受験日の1か月前にSilverの試験予約をしようとしたのですが、最寄りのテストセンターは空いていませんでした。(最短で1.5か月先とか)
Silverは試験時間が180分あるため、180分の席を確保できる日があまり無かったのではと思います。
受験日を先延ばししたくないという方には、あらかじめ試験予約だけしておくことをお勧めします。
・トイレは済ませておく
試験中も退室できますが、試験時間は止まりません。
・計算用紙(ホワイトボード)はなるべく余白を残しながら使う
計算用紙としてホワイトボードを渡されますが、書いたら消せません。
ホワイトボードは交換してもらえますが、待っている間は手元に計算用紙がない状態になってしまいます。
・Bronzeは試験時間が鬼門
Bronzeは試験時間65分に対し、試験問題60問です。(1問につき約1分で解くペースになる)
・Silverも試験時間が鬼門
Silverは試験時間180分に対し、試験問題80問です。(1問につき約2分半で解くペースになる)
Bronzeに比べ、時間的に余裕ありそうと錯覚してしまいますが、見直しを含めると私は余裕が無かったです。
・分からない問題は消去法
試験時間には限りがあります。時間を掛け過ぎる前に分からない問題は消去法で選択肢を絞る方法も有効だと思います。

参考資料
徹底攻略Java SE Bronze問題集[1Z0-818]対応
徹底攻略Java SE 11 Silver問題集[1Z0-815]対応

さいごに
今年は業務ではAWRレポート(Oracle Databaseの性能レポート)を読んで性能分析する機会も多い年でした。
読めば読むほど知らないことが多いという現実を突きつけられるため、とても勉強になる年でもありました。
来年はOCJPのGold取得やOGGOracle GoldenGate)、RAT(Real Application Testing)にも手を出せたらいいなと思います。
最後までお読みいただきありがとうございました。
17日目の記事もお楽しみに!

OracleのAWR(概要)

今回はOracleのAWRについて調査、検証しました。

AWR(Automatic Workload Repository:自動ワークロードリポジトリ)とは
Oracle 10gから登場した機能で、データベースの統計情報とワークロード(処理の負荷)情報をスナップショットとして定期的(デフォルト60分間隔)に自動で収集、管理する機能です。
データベースのCPU使用率やREDOログ生成量、実行SQLなどが収集されます。
収集した情報は性能分析やチューニング、障害発生時の原因調査などに使用できます。
AWRを使用する場合、Enterprise EditionのOracle Diagnostics Packライセンスが必要です。
類似機能としてSTATSPACKという機能がありますが、こちらは無償で使用できる機能です。
AWRに比べ、機能が少ないですがStandard EditionなどEnterprise Edition以外のエディションで稼働統計情報を収集、管理する場合に有効です。

AWRスナップショット(スナップショット)
AWRで収集された特定の時点の統計情報とワークロード情報です。
AWRレポートやADDM、アドバイザなどで使用されます。
SYSAUX表領域に格納されます。
スナップショットは自動で収集する方法と手動で収集する方法があります。
自動で収集する場合、デフォルト60分間隔で収集され、8日間保存されます。(保存期間を過ぎたスナップショットは自動削除)
手動で収集する場合、DBMS_WORKLOAD_REPOSITORYパッケージのCREATE_SNAPSHOT(スナップショット取得)を使用します。

【補足】
ADDM(Automatic Database Diagnostic Monitor:自動データベース診断モニター)
直近の2点のスナップショット間の稼働統計情報を基にデータベース全体の分析(自己診断)を実施し、問題の特定や解決のための推奨事項を提示する機能
スナップショットが取得される都度(デフォルト60分間隔)、自動的に実行され、分析結果はAWRに格納される
たとえば下記のような問題、推奨事項が提示される
・H/W関連:CPU追加、ストレージ構成変更
・DB関連:初期化パラメータ設定変更
スキーマ関連:表、索引のハッシュパーティション化、手動セグメント管理方式から自動セグメント管理方式への変更
・アプリケーション関連:シーケンスのキャッシュの使用、バインド変数の使用
・その他:負荷の高いSQLに対するSQLチューニング・アドバイザの実行、頻繁に変更が加えられるオブジェクトに対するセグメント・アドバイザの実行
分析結果はEnterprise Managerなどから確認できる
なお、自動で実行する方法と手動で実行する方法があり、手動で実行する場合は下記のような方法がある
・Enterprise Manager
・$ORACLE_HOME/rdbms/admin/addmrpt.sql
DBMS_ADDMパッケージ

AWRレポート
特定の2点のスナップショット間の稼働統計情報をファイル(HTML形式またはテキスト形式)としてレポートに出力したものです。
ただし、指定した2点のスナップショット間にインスタンス再起動している場合、稼働統計情報がリセットされるため、AWRレポートは出力できません。(ORA-20200: The instance was shutdown between snapshots <開始SNAP_ID> and <終了SNAP_ID> が発生します)
なお、AWRレポートの出力方法は下記のような方法があります。
・Enterprise Manager
・$ORACLE_HOME/rdbms/admin/awrrpt.sql
DBMS_WORKLOAD_REPOSITORYパッケージ

AWR期間比較レポート
特定の2点のスナップショット間と別の期間の特定の2点のスナップショット間の稼働統計情報を比較してレポート出力したものです。
たとえば異常(性能劣化など)が発生した場合、異常時のAWRレポートだけ見ても問題の特定が難しい可能性が高いです。(出力されている数値の妥当性の判断が難しい)
そのような場合、平常時のスナップショットと異常時のスナップショットの比較が有効です。
なお、AWR期間比較レポートの出力方法は下記のような方法があります。
・Enterprise Manager
・$ORACLE_HOME/rdbms/admin/awrddrpt.sql

AWRベースライン
特定の時点のスナップショットのペアに名前を付けて保存したもの(スナップショットセット)です。
AWRベースラインのスナップショットは自動パージ対象外のため、保存期間を経過してもスナップショットの比較に使用できます。
たとえば正常時のスナップショットをベースラインとして登録しておき、通常のスナップショットの保存期間を過ぎたあとに異常(性能劣化など)が発生しても正常時と異常時のスナップショットの比較に使用できます。
なお、正常時に取得したAWRベースラインを適応しきい値として設定しておけばデータベースの状態に異常が発生した場合(ベースライン値と現在の値が大きく異なる場合)、アラートを受け取ることもできます。
ベースラインには3種類あります。
・固定ベースライン
過去の特定の期間のスナップショットから作成するベースライン
・変動ウィンドウベースライン
スナップショットの保存期間内で自動で作成・保存されるベースライン(デフォルトSYSTEM_MOVING_WINDOW)
・ベースラインテンプレート
過去ではなく、将来の任意の期間で作成されるベースライン
たとえば「1年を通して毎週月曜日の朝に自動でベースラインを作成する」という設定ができる
なお、AWRベースラインの取得方法は下記のような方法があります。
・Enterprise Manager
DBMS_WORKLOAD_REPOSITORYパッケージ

私が今まで経験した現場でもAWRの使用、AWRレポート出力はどの現場でも必ず要件になっていました。
手動のAWRレポート出力は経験がありますが、手動のAWR期間比較レポートやADDMは経験が無いため検証してみました。

■前提
・CONTROL_MANAGEMENT_PACK_ACCESS初期化パラメータはDIAGNOSTIC+TUNING
・STATISTICS_LEVEL初期化パラメータ初期化パラメータはTYPICAL
・スナップショットの取得間隔は10分、保存期間は8日間(デフォルト) ※検証用に取得間隔を変更
・2ノードRACのうち、レポートはインスタンス1分のみ出力

■検証環境
OS:Oracle Linux 6.5
DB/GI:Oracle Database 12c Release 1 (12.1.0.2.0) Enterprise Edition
※2ノードRAC(管理者管理型DB)

■検証パターン
①AWRレポート出力
②AWRレポート期間比較
③ADDMレポート出力
④AWRベースライン作成

■検証
①AWRレポート出力
手動によるAWRレポート出力(スクリプト使用)を検証します。

【検証手順】
1. AWRスナップショット確認
2. AWRレポート出力スクリプト実行
3. AWRレポート確認

【作業ログ】

1. AWRスナップショット確認
SQL> SELECT SNAP_ID, INSTANCE_NUMBER, TO_CHAR(BEGIN_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') BEGIN_TM, TO_CHAR(END_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') END_TM FROM DBA_HIST_SNAPSHOT ORDER BY SNAP_ID, INSTANCE_NUMBER;

   SNAP_ID INSTANCE_NUMBER BEGIN_TM                       END_TM
---------- --------------- ------------------------------ ------------------------------
        45               1 2023/04/07 09:43               2023/04/07 09:53
(略)
        47               1 2023/04/07 10:40               2023/04/07 10:50
        47               2 2023/04/07 10:40               2023/04/07 10:50
        48               1 2023/04/07 10:50               2023/04/07 11:00
        48               2 2023/04/07 10:50               2023/04/07 11:00
(略)
        50               2 2023/04/07 11:10               2023/04/07 11:20
★今回はSNAP_ID47と48のAWRレポートを出力します
★2ノードRACのため同一SNAP_IDが2つありますが、今回はインスタンス1のAWRレポートを出力します

2. AWRレポート出力スクリプト実行
SQL> --レポート出力前
SQL> !ls -l /home/oracle
合計 4
drwxrwx--- 6 oracle oinstall 4096  1月  3 10:41 2023 tmp

SQL> --レポート出力 「★」はユーザが入力する箇所です
SQL> @?/rdbms/admin/awrrpt.sql

Current Instance
~~~~~~~~~~~~~~~~

   DB Id    DB Name      Inst Num Instance
----------- ------------ -------- ------------
 1652947528 ORCL                1 orcl1


Specify the Report Type
~~~~~~~~~~~~~~~~~~~~~~~
AWR reports can be generated in the following formats.  Please enter the
name of the format at the prompt.  Default value is 'html'.

'html'          HTML format (default)
'text'          Text format
'active-html'   Includes Performance Hub active report

report_typeに値を入力してください: html ★レポートの出力形式(HTML/TEXT)

Type Specified:                                  html


Instances in this Workload Repository schema
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

   DB Id     Inst Num DB Name      Instance     Host
------------ -------- ------------ ------------ ------------
  1652947528        2 ORCL         orcl2        node2.oracle
                                                12c.jp
* 1652947528        1 ORCL         orcl1        node1.oracle
                                                12c.jp

Using 1652947528 for database Id
Using          1 for instance number


Specify the number of days of snapshots to choose from
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Entering the number of days (n) will result in the most recent
(n) days of snapshots being listed.  Pressing <return> without
specifying a number lists all completed snapshots.


num_daysに値を入力してください: 1 ★出力するスナップショット日数(1の場合、現在から過去1日分出力)

Listing the last day's Completed Snapshots

                                                        Snap
Instance     DB Name        Snap Id    Snap Started    Level
------------ ------------ --------- ------------------ -----
orcl1        ORCL                45 07 4月  2023 09:53     1
                                 46 07 4月  2023 10:40     1
                                 47 07 4月  2023 10:50     1
                                 48 07 4月  2023 11:00     1
                                 49 07 4月  2023 11:10     1

 

Specify the Begin and End Snapshot Ids
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
begin_snapに値を入力してください: 47 ★開始SNAP_ID
Begin Snapshot Id specified: 47

end_snapに値を入力してください: 48 ★終了SNAP_ID
End   Snapshot Id specified: 48

 

Specify the Report Name
~~~~~~~~~~~~~~~~~~~~~~~
The default report file name is awrrpt_1_47_48.html.  To use this name,
press <return> to continue, otherwise enter an alternative.

report_nameに値を入力してください: test1_awrrpt_47_48.html ★レポート名(省略時はデフォルトのレポート名)

Using the report name test1_awrrpt.html

<html lang="en"><head><title>AWR Report for DB: ORCL, Inst: orcl1, Snaps: 47-48</title>
(略)
End of Report
</body></html>
Report written to test1_awrrpt.html

SQL> --レポート出力後
SQL> !ls -l /home/oracle
合計 660
-rw-r--r-- 1 oracle oinstall 669931  4月  7 11:18 2023 test1_awrrpt_47_48.html
drwxrwx--- 6 oracle oinstall   4096  1月  3 10:41 2023 tmp

 

3. AWRレポート確認
・HTML形式

・TEXT形式

RAC(全ノード分を取得する)の場合、$ORACLE_HOME/rdbms/admin/awrgrpt.sqlを使用します

②AWRレポート期間比較
出力した2つのAWRレポートを使用して、AWRレポート期間比較(スクリプト使用)を検証します。

【検証手順】
1. AWRスナップショット確認
2. AWR期間比較レポート出力スクリプト実行
3. AWR期間比較レポート確認

【作業ログ】

1. AWRスナップショット確認
SQL> SELECT SNAP_ID, INSTANCE_NUMBER, TO_CHAR(BEGIN_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') BEGIN_TM, TO_CHAR(END_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') END_TM FROM DBA_HIST_SNAPSHOT ORDER BY SNAP_ID, INSTANCE_NUMBER;

   SNAP_ID INSTANCE_NUMBER BEGIN_TM                       END_TM
---------- --------------- ------------------------------ ------------------------------
        45               1 2023/04/07 09:43               2023/04/07 09:53
(略)
        47               1 2023/04/07 10:40               2023/04/07 10:50
        47               2 2023/04/07 10:40               2023/04/07 10:50
        48               1 2023/04/07 10:50               2023/04/07 11:00
        48               2 2023/04/07 10:50               2023/04/07 11:00
(略)
        50               1 2023/04/07 11:10               2023/04/07 11:20
        50               2 2023/04/07 11:10               2023/04/07 11:20
        51               1 2023/04/07 11:20               2023/04/07 11:30
        51               2 2023/04/07 11:20               2023/04/07 11:30
(略)
        52               2 2023/04/07 11:30               2023/04/07 11:40
★今回はSNAP_ID47~48と50~51のスナップショットを比較します

2. AWR期間比較レポート出力スクリプト($ORACLE_HOME/rdbms/admin/awrddrpt.sql)実行
SQL> --レポート出力前
SQL> !ls -l /home/oracle
合計 1524
-rw-r--r-- 1 oracle oinstall 669931  4月  7 11:18 2023 test1_awrrpt_47_48.html
-rw-r--r-- 1 oracle oinstall 235701  4月  7 11:30 2023 test1_awrrpt_47_48.txt
drwxrwx--- 6 oracle oinstall   4096  1月  3 10:41 2023 tmp

SQL> --レポート出力 「★」はユーザが入力する箇所です
SQL> @?/rdbms/admin/awrddrpt.sql

Current Instance
~~~~~~~~~~~~~~~~

   DB Id       DB Id    DB Name      Inst Num Inst Num Instance
----------- ----------- ------------ -------- -------- ------------
 1652947528  1652947528 ORCL                1        1 orcl1


Specify the Report Type
~~~~~~~~~~~~~~~~~~~~~~~
Would you like an HTML report, or a plain text report?
Enter 'html' for an HTML report, or 'text' for plain text
Defaults to 'html'
report_typeに値を入力してください: html ★レポートの出力形式(HTML/TEXT)

Type Specified:                                  html


Instances in this Workload Repository schema
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

   DB Id     Inst Num DB Name      Instance     Host
------------ -------- ------------ ------------ ------------
  1652947528        2 ORCL         orcl2        node2.oracle
                                                12c.jp
* 1652947528        1 ORCL         orcl1        node1.oracle
                                                12c.jp

Database Id and Instance Number for the First Pair of Snapshots
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Using 1652947528 for Database Id for the first pair of snapshots
Using          1 for Instance Number for the first pair of snapshots


Specify the number of days of snapshots to choose from
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Entering the number of days (n) will result in the most recent
(n) days of snapshots being listed.  Pressing <return> without
specifying a number lists all completed snapshots.


num_daysに値を入力してください: 1 ★出力するスナップショット日数(比較元)

Listing the last day's Completed Snapshots

                                                        Snap
Instance     DB Name        Snap Id    Snap Started    Level
------------ ------------ --------- ------------------ -----
orcl1        ORCL                45 07 4月  2023 09:53     1
                                 46 07 4月  2023 10:40     1
                                 47 07 4月  2023 10:50     1
                                 48 07 4月  2023 11:00     1
                                 49 07 4月  2023 11:10     1
                                 50 07 4月  2023 11:20     1
                                 51 07 4月  2023 11:30     1
                                 52 07 4月  2023 11:40     1

 

Specify the First Pair of Begin and End Snapshot Ids
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
begin_snapに値を入力してください: 47 ★開始SNAP_ID(比較元)
First Begin Snapshot Id specified: 47

end_snapに値を入力してください: 48 ★終了SNAP_ID(比較元)
First End   Snapshot Id specified: 48

 


Instances in this Workload Repository schema
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

   DB Id     Inst Num DB Name      Instance     Host
------------ -------- ------------ ------------ ------------
  1652947528        2 ORCL         orcl2        node2.oracle
                                                12c.jp
* 1652947528        1 ORCL         orcl1        node1.oracle
                                                12c.jp

 


Database Id and Instance Number for the Second Pair of Snapshots
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Using 1652947528 for Database Id for the second pair of snapshots
Using          1 for Instance Number for the second pair of snapshots


Specify the number of days of snapshots to choose from
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Entering the number of days (n) will result in the most recent
(n) days of snapshots being listed.  Pressing <return> without
specifying a number lists all completed snapshots.


num_days2に値を入力してください: 1 ★出力するスナップショット日数(比較先)

Listing the last day's Completed Snapshots

                                                        Snap
Instance     DB Name        Snap Id    Snap Started    Level
------------ ------------ --------- ------------------ -----
orcl1        ORCL                45 07 4月  2023 09:53     1
                                 46 07 4月  2023 10:40     1
                                 47 07 4月  2023 10:50     1
                                 48 07 4月  2023 11:00     1
                                 49 07 4月  2023 11:10     1
                                 50 07 4月  2023 11:20     1
                                 51 07 4月  2023 11:30     1
                                 52 07 4月  2023 11:40     1

 

Specify the Second Pair of Begin and End Snapshot Ids
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
begin_snap2に値を入力してください: 50 ★開始SNAP_ID(比較先)
Second Begin Snapshot Id specified: 50

end_snap2に値を入力してください: 51 ★終了SNAP_ID(比較先)
Second End   Snapshot Id specified: 51

 

Specify the Report Name
~~~~~~~~~~~~~~~~~~~~~~~
The default report file name is awrdiff_1_47_1_50.html  To use this name,
press <return> to continue, otherwise enter an alternative.

report_nameに値を入力してください: test2_awrdiff_47_48vs50_51.html ★レポート名(省略時はデフォルトのレポート名)

Using the report name test2_awrdiff_47_48vs50_51.html


<html lang="en"><head><title>AWR Compare Period Report for (1) DB: ORCL, Inst: orcl1, Snaps: 47-48 (2) DB: ORCL, Inst: orcl1, Snaps: 50-51</title>
(略)
<p />
</body></html>
Report written to test2_awrdiff_47_48vs50_51.html

SQL> --レポート出力後
SQL> !ls -l /home/oracle
合計 2832
-rw-r--r-- 1 oracle oinstall  669931  4月  7 11:18 2023 test1_awrrpt_47_48.html
-rw-r--r-- 1 oracle oinstall  235701  4月  7 11:30 2023 test1_awrrpt_47_48.txt
-rw-r--r-- 1 oracle oinstall 1338043  4月  7 12:31 2023 test2_awrdiff_47_48vs50_51.html
drwxrwx--- 6 oracle oinstall    4096  1月  3 10:41 2023 tmp

 

3. AWR期間比較レポート確認

※AWR期間比較レポートのサンプルはHTML形式のみ
RAC(全ノード分を取得する)の場合、$ORACLE_HOME/rdbms/admin/awrgdrpt.sqlを使用します

③ADDMレポート出力
手動によるADDMレポート出力(スクリプト使用)を検証します。

【検証手順】
1. AWRスナップショット確認
2. ADDMレポート出力スクリプト実行
3. ADDMレポート確認

【作業ログ】

1. AWRスナップショット確認
SQL> SELECT SNAP_ID, INSTANCE_NUMBER, TO_CHAR(BEGIN_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') BEGIN_TM, TO_CHAR(END_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') END_TM FROM DBA_HIST_SNAPSHOT ORDER BY SNAP_ID, INSTANCE_NUMBER;

   SNAP_ID INSTANCE_NUMBER BEGIN_TM                       END_TM
---------- --------------- ------------------------------ ------------------------------
        45               1 2023/04/07 09:43               2023/04/07 09:53
(略)
        47               1 2023/04/07 10:40               2023/04/07 10:50
        47               2 2023/04/07 10:40               2023/04/07 10:50
        48               1 2023/04/07 10:50               2023/04/07 11:00
        48               2 2023/04/07 10:50               2023/04/07 11:00
(略)
        52               2 2023/04/07 11:30               2023/04/07 11:40
★今回はSNAP_ID47と48のADDMレポートを出力します

2. ADDMレポート出力スクリプト実行
SQL> --レポート出力前
SQL> !ls -l /home/oracle
合計 4236
-rw-r--r-- 1 oracle oinstall  669931  4月  7 11:18 2023 test1_awrrpt_47_48.html
-rw-r--r-- 1 oracle oinstall  235701  4月  7 11:30 2023 test1_awrrpt_47_48.txt
-rw-r--r-- 1 oracle oinstall 1338043  4月  7 12:31 2023 test2_awrdiff_47_48vs50_51.html
drwxrwx--- 6 oracle oinstall    4096  1月  3 10:41 2023 tmp

SQL> --レポート出力 「★」はユーザが入力する箇所です
SQL> @?/rdbms/admin/addmrpt.sql

Current Instance
~~~~~~~~~~~~~~~~

   DB Id    DB Name      Inst Num Instance
----------- ------------ -------- ------------
 1652947528 ORCL                1 orcl1


Instances in this Workload Repository schema
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

   DB Id     Inst Num DB Name      Instance     Host
------------ -------- ------------ ------------ ------------
  1652947528        2 ORCL         orcl2        node2.oracle
                                                12c.jp
* 1652947528        1 ORCL         orcl1        node1.oracle
                                                12c.jp

Using 1652947528 for database Id
Using          1 for instance number


Specify the number of days of snapshots to choose from
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Entering the number of days (n) will result in the most recent
(n) days of snapshots being listed.  Pressing <return> without
specifying a number lists all completed snapshots.

 

Listing the last 3 days of Completed Snapshots

                                                        Snap
Instance     DB Name        Snap Id    Snap Started    Level
------------ ------------ --------- ------------------ -----
orcl1        ORCL                45 07 4月  2023 09:53     1
                                 46 07 4月  2023 10:40     1
                                 47 07 4月  2023 10:50     1
                                 48 07 4月  2023 11:00     1
                                 49 07 4月  2023 11:10     1
                                 50 07 4月  2023 11:20     1
                                 51 07 4月  2023 11:30     1
                                 52 07 4月  2023 11:40     1

 

Specify the Begin and End Snapshot Ids
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
begin_snapに値を入力してください: 47 ★開始SNAP_ID
Begin Snapshot Id specified: 47

end_snapに値を入力してください: 48 ★終了SNAP_ID
End   Snapshot Id specified: 48

 

Specify the Report Name
~~~~~~~~~~~~~~~~~~~~~~~
The default report file name is addmrpt_1_47_48.txt.  To use this name,
press <return> to continue, otherwise enter an alternative.

report_nameに値を入力してください: test3_addmrpt_47_48.txt ★レポート名(省略時はデフォルトのレポート名)

Using the report name test3_addmrpt_47_48.txt


Running the ADDM analysis on the specified pair of snapshots ...
(略)
End of Report
Report written to test3_addmrpt_47_48.txt

SQL> --レポート出力後
SQL> !ls -l /home/oracle
合計 4240
-rw-r--r-- 1 oracle oinstall  669931  4月  7 11:18 2023 test1_awrrpt_47_48.html
-rw-r--r-- 1 oracle oinstall  235701  4月  7 11:30 2023 test1_awrrpt_47_48.txt
-rw-r--r-- 1 oracle oinstall 1338043  4月  7 12:31 2023 test2_awrdiff_47_48vs50_51.html
-rw-r--r-- 1 oracle oinstall    1312  4月  7 14:06 2023 test3_addmrpt_47_48.txt
drwxrwx--- 6 oracle oinstall    4096  1月  3 10:41 2023 tmp

 

3. ADDMレポート確認

※ADDMレポートはHTML形式は指定できませんでした

④AWRベースライン作成
手動によるAWRベースライン作成を検証します。

【検証手順】
1. AWRスナップショット確認
2. AWRベースライン確認(ベースライン作成前)
3. AWRベースライン作成
4. AWRベースライン確認(ベースライン作成後)
5. AWRベースライン確認(保存期間経過後)
6. AWRスナップショット確認(保存期間経過後)

【作業ログ】

1. AWRスナップショット確認
SQL> SELECT SNAP_ID, INSTANCE_NUMBER, TO_CHAR(BEGIN_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') BEGIN_TM, TO_CHAR(END_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') END_TM FROM DBA_HIST_SNAPSHOT ORDER BY SNAP_ID, INSTANCE_NUMBER;

   SNAP_ID INSTANCE_NUMBER BEGIN_TM                       END_TM
---------- --------------- ------------------------------ ------------------------------
        45               1 2023/04/07 09:43               2023/04/07 09:53
        45               2 2023/04/07 09:42               2023/04/07 09:53
        46               1 2023/04/07 09:53               2023/04/07 10:40
(略)
        51               2 2023/04/07 11:20               2023/04/07 11:30
        52               1 2023/04/07 11:30               2023/04/07 11:40
        52               2 2023/04/07 11:30               2023/04/07 11:40

2. AWRベースライン確認(ベースライン作成前)
SQL> SELECT BASELINE_ID, BASELINE_NAME, BASELINE_TYPE, START_SNAP_ID, END_SNAP_ID, CREATION_TIME FROM DBA_HIST_BASELINE ORDER BY BASELINE_ID;

BASELINE_ID BASELINE_NAME                  BASELINE_TYPE        START_SNAP_ID END_SNAP_ID CREATION_TIME
----------- ------------------------------ -------------------- ------------- ----------- -------------------
          0 SYSTEM_MOVING_WINDOW           MOVING_WINDOW                   45          58 2023-01-03 18:19:59
※MOVING_WINDOW:Oracleで自動で取得されている開始SNAP_IDと終了SNAP_IDのスナップショットを含むベースライン

3. AWRベースライン作成
SQL> BEGIN
  2    DBMS_WORKLOAD_REPOSITORY.CREATE_BASELINE (start_snap_id => 47,
  3                                              end_snap_id   => 48,
  4                                              baseline_name => 'TEST_BASELINE_47_48');
  5  END;
  6  /

PL/SQLプロシージャが正常に完了しました。

4. AWRベースライン確認(ベースライン作成後)
SQL> SELECT BASELINE_ID, BASELINE_NAME, BASELINE_TYPE, START_SNAP_ID, END_SNAP_ID, CREATION_TIME FROM DBA_HIST_BASELINE ORDER BY BASELINE_ID;

BASELINE_ID BASELINE_NAME                  BASELINE_TYPE        START_SNAP_ID END_SNAP_ID CREATION_TIME
----------- ------------------------------ -------------------- ------------- ----------- -------------------
          0 SYSTEM_MOVING_WINDOW           MOVING_WINDOW                   45          58 2023-01-03 18:19:59
          1 TEST_BASELINE_47_48            STATIC                          47          48 2023-04-07 14:25:53
※STATIC:ユーザが手動で作成したベースライン

5. AWRベースライン確認(保存期間経過後)
ベースライン作成日:2023/4/7
ベースライン確認日:2023/4/30

SQL> SELECT BASELINE_ID, BASELINE_NAME, BASELINE_TYPE, START_SNAP_ID, END_SNAP_ID, CREATION_TIME FROM DBA_HIST_BASELINE ORDER BY BASELINE_ID;

BASELINE_ID BASELINE_NAME                  BASELINE_TYPE        START_SNAP_ID END_SNAP_ID CREATION_TIME
----------- ------------------------------ -------------------- ------------- ----------- -------------------
          0 SYSTEM_MOVING_WINDOW           MOVING_WINDOW                   60          60 2023-01-03 18:19:59
          1 TEST_BASELINE_47_48            STATIC                          47          48 2023-04-07 14:25:53
★保存期間経過後も作成したベースライン(TEST_BASELINE_47_48)は残っている

6. AWRスナップショット確認(保存期間経過後)
SQL> SELECT SNAP_ID, INSTANCE_NUMBER, TO_CHAR(BEGIN_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') BEGIN_TM, TO_CHAR(END_INTERVAL_TIME, 'YYYY/MM/DD HH24:MI') END_TM FROM DBA_HIST_SNAPSHOT ORDER BY SNAP_ID, INSTANCE_NUMBER;

   SNAP_ID INSTANCE_NUMBER BEGIN_TM                       END_TM
---------- --------------- ------------------------------ ------------------------------
        47               1 2023/04/07 10:40               2023/04/07 10:50
        47               2 2023/04/07 10:40               2023/04/07 10:50
        48               1 2023/04/07 10:50               2023/04/07 11:00
        48               2 2023/04/07 10:50               2023/04/07 11:00
★保存期間経過後もベースラインに登録したスナップショット(開始SNAP_ID47、終了SNAP_ID48)は残っている

 

■参考資料
AWRを基本からおさらいしよう - Speaker Deck
オラクルマスター教科書 Gold DBA Oracle Database AdministrationⅡ
オラクルマスター教科書 ORACLE MASTER Expert パフォーマンス・チューニング編
ORACLE MASTER Oracle Database 11g Gold(Gold DBA11g)(試験番号:1Z0-053)完全詳解+精選問題集(オラクルマスタースタディガイド)
Oracleの現場を効率化する100の技

■おわりに
性能問題が発生した場合、AWRレポートを確認するというのが定石の1つだと思いますが、AWRレポートでは調査が難しい状況もあります。
たとえば下記のような状況です。
・AWRレポートは定期的に取得される稼働統計情報をもとに過去の性能を分析するためのものであり、現在のデータベースの性能状況は確認できない
・AWRレポートは2点の差分をもとにデータをサマリして出力するため、「1分程度の間パフォーマンスが悪かった」、「数十秒の間、CPUが高騰していた」など細かい情報はAWRレポートに目立った数値として現れない可能性がある
スナップショット間隔を短くするという対応も考えられますが、スナップショット取得の負荷の増加やAWRスナップショット量が増加(SYSAUX表領域の肥大化)するなど影響があるため、お勧めできません。
このような細かい情報を確認する場合、ASH(Active Session History)が有効です。(Oracle Diagnostics Packライセンスが必要)
ASHは1秒間隔でアクティブなセッションに関わる稼働統計情報を自動で取得します。
ASHやAWRレポートの詳しい読み方は別の機会に調査してみようと思います。

Oracleの実行計画(SPM)

今回はOracleの実行計画のSPMについて調査、検証しました。

SPM(SQL Plan Management:SQL計画管理)とは
Oracle 11gから登場した機能で、SQLごとに実行計画を固定化したり、統計情報の変化によって新たに生成された最適な実行計画を候補として登録したりなど実行計画を管理します。
オプティマイザが最適と判断した実行計画が登録されており、SQLのパフォーマンスに応じて使用する実行計画を制御できるメリットがあります。
なお、類似の機能としてプラン・スタビリティという機能がありますが、こちらは実行計画を固定化する機能で、11gから非推奨となりました。

【補足】
SQL管理ベース(SQL Management Base)
SQLごとのSQL計画履歴を保管する領域
SYSAUX表領域に存在する

SQL計画ベースライン
SQL計画履歴の(1つ以上の)複数の実行計画のうち、承認され使用できる実行計画
SQLSQL計画ベースラインに登録されている実行計画のみ使用できる(DBAが承認した実行計画のみ使用できる)
SQLごとに存在する

SQL計画履歴
SQLに対して生成されたすべての実行計画
SQL計画ベースラインは承認された実行計画のみ格納されているが、SQL計画履歴には承認済み/未承認の実行計画が格納されている
自動収集された実行計画もSQL計画履歴に格納されるが、承認されるまで使用できない
SQLごとに存在する

プラン・スタビリティ
実行計画を固定化する機能
SQLの実行計画をストアド・アウトライン(実行計画を再現するためのオプティマイザヒントのセット)としてディクショナリ(OUTLNスキーマ)に格納される
プラン・スタビリティが有効にされたセッションでストアド・アウトラインが作成されたSQLを実行した場合、格納されたオプティマイザヒントが取り出され、SQLに適用される
実行計画を誘導したいがパッケージ製品のSQLのためSQLが修正できない(ヒント句が付与できない)状況で有効
※プラン・スタビリティを検証するため、書籍やWebサイトの手順を参考にしましたが実行計画の固定化はできませんでした
 私の検証環境が12cR1 Enterprise Editionだったのが原因かもしれません(Enterprise Editionの場合、SPMが使用できるためあえてプラン・スタビリティは使えないようになっている?)
 Standard Editionが手に入ったら別途検証してみようと思います
 プラン・スタビリティについて参考資料だけ載せておきます
(参考)
新・門外不出のOracle現場ワザ エキスパートが明かす運用・管理の極意(151~154ページ)
【Oracle Database】実行計画の固定化方法まとめ | アシスト

SPMを使用した実行計画の登録方法
実行登録の方法は2種類(自動登録、手動登録)あります

SPM展開アドバイザ
12cから自動メンテナンスタスクに追加された新機能(SYS_AUTO_SPM_EVOLVE_TASK)で、SQL計画履歴に追加された実行計画を展開(未承認の実行計画の優位性の検証と承認)します。
11gまでは新たに生成された実行計画は1つ目はSQL計画ベースラインとして登録されますが、2つ目以降は未承認の実行計画としてSQL計画履歴に登録されます(管理者に承認される前に使用されるとパフォーマンス低下に繋がる可能性があるため、あえて未承認の状態にしています)
実行計画を承認するためには管理者が手動(DBMS_SPM.EVOLVE_SQL_PLAN_BASELINE()など)でSQL計画ベースラインに登録する必要がありました。
よって、新たに追加された実行計画が最適な実行計画でも管理者が承認しない限り、すぐに使用できませんでした。
SPM展開アドバイザは、新たに追加された実行計画が現行の実行計画(SQL計画ベースライン)より優位性が高いと判断された場合、自動的に実行計画が承認されます
なお、19c以降はAWRにレポートされる遅いSQLについてより良い実行計画が見つかった場合、優位性を検証し、有効と判断した場合、自動的に承認(SQL計画ベースラインに登録)されます
(参考)SQL計画ベースラインの管理

私の経験上、実行計画を固定化するためにSPMを使用する現場が多かったです。(DBバージョンアップ後にSQLのパフォーマンスが悪くなり、バージョンアップ前の実行計画に固定するという要件が多かった)
今回は実行計画を固定化する方法のうち、今までの現場で最も多く使用していた方法「手動で共有SQL領域(カーソル)から実行計画をロードする方法」を検証しました。

■前提
OPTIMIZER_CAPTURE_SQL_PLAN_BASELINES BOOLEAN初期化パラメータはFALSE

■検証環境
OS:Oracle Linux 6.5
DB/GI:Oracle Database 12c Release 1 (12.1.0.2.0) Enterprise Edition
※2ノードRAC(管理者管理型DB)

■検証パターン
①実行計画の固定化(共有SQL領域(カーソル)から実行計画をロード)
②固定化した実行計画を削除

■検証
①実行計画の固定化(共有SQL領域(カーソル)から実行計画をロード)
性能の悪いSQLの実行計画を性能の良いSQLの実行計画で固定化した場合、性能の悪いSQLを実行しても固定化した実行計画で実行されるかどうか検証します

【検証手順】
1. [セッション1 / アプリケーションユーザ] 性能の悪いSQLを実行
2. [セッション2 / 管理者ユーザ] 性能の悪いSQLSQL識別子、シグネチャを取得
3. [セッション2 / 管理者ユーザ] 性能の悪いSQLの実行計画を取得
4. [セッション1 / アプリケーションユーザ] 性能の良いSQLを実行
5. [セッション2 / 管理者ユーザ] 性能の良いSQLSQL識別子、シグネチャを取得
6. [セッション2 / 管理者ユーザ] 性能の良いSQLの実行計画を取得
7. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(性能の悪いSQLの実行計画ロード前)
8. [セッション2 / 管理者ユーザ] 性能の悪いSQLの実行計画をSQL計画ベースラインとして登録
9. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(性能の悪いSQLの実行計画ロード後)
10. [セッション2 / 管理者ユーザ] 性能の悪いSQLSQL計画ベースラインの属性変更(現在の実行計画の無効化)
11. [セッション2 / 管理者ユーザ] 属性変更したSQL計画ベースラインの確認(属性変更後)
12. [セッション2 / 管理者ユーザ] 性能の悪いSQLSQL計画ベースラインのSQLハンドルと性能の良いSQLのSQLID、実行計画のセットを使用して新しい実行計画を登録
13. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(属性変更前)
14. [セッション2 / 管理者ユーザ] 新たに登録したSQLSQL計画ベースラインの属性変更(自動パージ無効化)
15. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(属性変更後)
16. [セッション1 / アプリケーションユーザ] 性能の悪いSQLの実行計画を取得

【作業ログ】

1. [セッション1 / アプリケーションユーザ] 性能の悪いSQLを実行
SQL> SELECT /* BAD_XPLAN */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO;

     EMPNO ENAME               DEPTNO DNAME
---------- --------------- ---------- ---------------
      7782 CLARK                   10 ACCOUNTING
      7839 KING                    10 ACCOUNTING
      7934 MILLER                  10 ACCOUNTING
(略)
      7900 JAMES                   30 SALES
      7698 BLAKE                   30 SALES
      7654 MARTIN                  30 SALES

14 rows selected.

2. [セッション2 / 管理者ユーザ] 性能の悪いSQLSQL識別子、シグネチャを取得
SQL> SELECT SQL_ID, PLAN_HASH_VALUE, SQL_TEXT, EXACT_MATCHING_SIGNATURE, FIRST_LOAD_TIME FROM V$SQL WHERE SQL_TEXT like '%BAD_XPLAN%' order by FIRST_LOAD_TIME desc;

SQL_ID        PLAN_HASH_VALUE SQL_TEXT                                                                             EXACT_MATCHING_SIGNATURE FIRST_LOAD_TIME
------------- --------------- -------------------------------------------------------------------------------- ---------------------------- --------------------
8phh3bbfzfb9p      2836784050 SELECT SQL_ID, PLAN_HASH_VALUE, SQL_TEXT, EXACT_MATCHING_SIGNATURE, FIRST_LOAD_T    1,096,901,067,240,622,581 2023-03-24/10:06:33
                              IME FROM V$SQL WHERE SQL_TEXT like '%BAD_XPLAN%' order by FIRST_LOAD_TIME desc

chyjw3v56383h       844388907 SELECT /* BAD_XPLAN */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WH    7,132,992,821,986,115,812 2023-03-24/10:06:06
                              ERE D.DEPTNO = E.DEPTNO

SQL_ID:SQL識別子(ライブラリ・キャッシュ)
※PLAN_HASH_VALUE:実行計画ID(数値)
SQL_TEXT:SQL文(最初の1000文字)
※EXACT_MATCHING_SIGNATURE:正規化(SQL文の空白や改行の削除、非リテラル文字列の大文字化)されたSQLの識別子(ハッシュ値
※FIRST_LOAD_TIME:SQL(親カーソル)が共有プール(ライブラリ・キャッシュ)にロードされた時刻

3. [セッション2 / 管理者ユーザ] 性能の悪いSQLの実行計画を取得
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('chyjw3v56383h'));

PLAN_TABLE_OUTPUT
-------------------------------------
SQL_ID  chyjw3v56383h, child number 0
-------------------------------------
SELECT /* BAD_XPLAN */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D,
EMP E WHERE D.DEPTNO = E.DEPTNO

Plan hash value: 844388907

----------------------------------------------------------------------------------------
| Id  | Operation                    | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |       |       |    38 (100)|          |
|   1 |  MERGE JOIN                  |         |    14 |   364 |    38   (3)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPT    |     4 |    52 |     2   (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN           | PK_DEPT |     4 |       |     1   (0)| 00:00:01 |
|*  4 |   SORT JOIN                  |         |    14 |   182 |    36   (3)| 00:00:01 |
|   5 |    TABLE ACCESS FULL         | EMP     |    14 |   182 |    35   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("D"."DEPTNO"="E"."DEPTNO")
       filter("D"."DEPTNO"="E"."DEPTNO")
★ソートマージ結合になっている→ハッシュ結合に変更する

4. [セッション1 / アプリケーションユーザ] 性能の良いSQLを実行
SQL> SELECT /* GOOD_XPLAN */ /*+ USE_HASH(E) LEADING(D) */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO;

     EMPNO ENAME               DEPTNO DNAME
---------- --------------- ---------- ---------------
      7782 CLARK                   10 ACCOUNTING
      7839 KING                    10 ACCOUNTING
      7934 MILLER                  10 ACCOUNTING
(略)
      7900 JAMES                   30 SALES
      7698 BLAKE                   30 SALES
      7654 MARTIN                  30 SALES

14 rows selected.

5. [セッション2 / 管理者ユーザ] 性能の良いSQLSQL識別子、シグネチャを取得
SQL> SELECT SQL_ID, PLAN_HASH_VALUE, SQL_TEXT, EXACT_MATCHING_SIGNATURE, FIRST_LOAD_TIME FROM V$SQL WHERE SQL_TEXT like '%GOOD_XPLAN%' order by FIRST_LOAD_TIME desc;

SQL_ID        PLAN_HASH_VALUE SQL_TEXT                                                                             EXACT_MATCHING_SIGNATURE FIRST_LOAD_TIME
------------- --------------- -------------------------------------------------------------------------------- ---------------------------- --------------------
70kmzfpgs4sqr      2836784050 SELECT SQL_ID, PLAN_HASH_VALUE, SQL_TEXT, EXACT_MATCHING_SIGNATURE, FIRST_LOAD_T    2,437,342,059,016,891,273 2023-03-24/10:10:58
                              IME FROM V$SQL WHERE SQL_TEXT like '%GOOD_XPLAN%' order by FIRST_LOAD_TIME desc

gxc9mvqxknma1       615168685 SELECT /* GOOD_XPLAN */ /*+ USE_HASH(E) LEADING(D) */ E.EMPNO, E.ENAME, D.DEPTNO      956,405,267,997,147,471 2023-03-24/10:10:42
                              , D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO

6. [セッション2 / 管理者ユーザ] 性能の良いSQLの実行計画を取得
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('gxc9mvqxknma1'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  gxc9mvqxknma1, child number 0
-------------------------------------
SELECT /* GOOD_XPLAN */ /*+ USE_HASH(E) LEADING(D) */ E.EMPNO, E.ENAME,
D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO

Plan hash value: 615168685

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |       |       |    70 (100)|          |
|*  1 |  HASH JOIN         |      |    14 |   364 |    70   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| DEPT |     4 |    52 |    35   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| EMP  |    14 |   182 |    35   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("D"."DEPTNO"="E"."DEPTNO")

7. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(性能の悪いSQLの実行計画ロード前)
SQL> SELECT SQL_HANDLE, PLAN_NAME, to_char(CREATED, 'YYYY-MM-DD HH24:MI:SS') as CREATED, to_char(LAST_EXECUTED, 'YYYY-MM-DD HH24:MI:SS') as LAST_EXECUTED, ENABLED, ACCEPTED, FIXED, AUTOPURGE FROM DBA_SQL_PLAN_BASELINES WHERE SIGNATURE IN (SELECT EXACT_MATCHING_SIGNATURE FROM V$SQL WHERE SQL_ID = 'chyjw3v56383h') ORDER BY CREATED DESC;

no rows selected

SQL_HANDLE:SPMで使用するSQL識別子。V$SQLSQL_IDと基本的に意味は同じだが、SQL_HANDLEは正規化されたSQLの識別子となる
※PLAN_NAME:SPMで使用する実行計画の識別子
※CREATED:SQL計画ベースライン/SQL計画履歴に実行計画が作成された時刻
※LAST_EXECUTED:SQL計画ベースラインの実行計画が最後に実行された時刻
※ENABLED:SQL計画ベースラインの実行計画が使用できるか否か(YES:使用可能 / NO:使用不可)
※ACCEPTED:SQL計画ベースラインの実行計画が承認されているか否か(YES:承認済み。SQL計画ベースラインに移動 / NO:未承認。SQL計画管理に残る)
※FIXED:SQL計画ベースラインの実行計画が固定されているか否か(YES:固定。ハード解析後も新たな実行計画はSQL計画管理に登録されない / NO:固定解除。ハード解析後は新たな実行計画はSQL計画管理に登録される)
※AUTOPURGE:SQL計画ベースラインの実行計画が自動削除されるか否か(YES:一定期間(デフォルト53週)経過後、自動削除される / NO:自動削除されない)
※SIGNATURE:正規化されたSQLの識別子(ハッシュ値)。V$SQLのSIGNATUREと同じ値

8. [セッション2 / 管理者ユーザ] 性能の悪いSQLの実行計画をSQL計画ベースラインとして登録
SQL> set serveroutput on
SQL> DECLARE
  2    ret number;
  3  BEGIN
  4    ret := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
  5             sql_id          => 'chyjw3v56383h',
  6             plan_hash_value => '844388907');
  7    DBMS_OUTPUT.PUT_LINE('LOAD PLANS:' || ret);
  8  END;
  9  /
LOAD PLANS:1

PL/SQL procedure successfully completed.
★「LOAD PLANS:」はSQL計画ベースラインにロードした実行計画の個数

9. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(性能の悪いSQLの実行計画ロード後)
SQL> SELECT SQL_HANDLE, PLAN_NAME, to_char(CREATED, 'YYYY-MM-DD HH24:MI:SS') as CREATED, to_char(LAST_EXECUTED, 'YYYY-MM-DD HH24:MI:SS') as LAST_EXECUTED, ENABLED, ACCEPTED, FIXED, AUTOPURGE FROM DBA_SQL_PLAN_BASELINES WHERE SIGNATURE IN (SELECT EXACT_MATCHING_SIGNATURE FROM V$SQL WHERE SQL_ID = 'chyjw3v56383h') ORDER BY CREATED DESC;

SQL_HANDLE                     PLAN_NAME                      CREATED              LAST_EXECUTED        ENABLED    ACCEPTED     FIXED      AUTOPURGE
------------------------------ ------------------------------ -------------------- -------------------- ---------- ------------ ---------- ----------
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h745ac47e2d 2023-03-24 10:44:14                       YES        YES          NO         YES

10. [セッション2 / 管理者ユーザ] 性能の悪いSQLSQL計画ベースラインの属性変更(現在の実行計画の無効化)
SQL> DECLARE
  2    ret  number;
  3    ret1 number;
  4    ret2 number;
  5    ret3 number;
  6  BEGIN
  7    ret1 := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
  8             sql_handle      => 'SQL_62fd7b2debe4c0e4',
  9             plan_name       => 'SQL_PLAN_65zbv5rpy9h745ac47e2d',
 10             attribute_name  => 'ENABLED',
 11             attribute_value => 'NO');
 12    ret2 := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
 13             sql_handle      => 'SQL_62fd7b2debe4c0e4',
 14             plan_name       => 'SQL_PLAN_65zbv5rpy9h745ac47e2d',
 15             attribute_name  => 'FIXED',
 16             attribute_value => 'NO');
 17    ret3 := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
 18             sql_handle      => 'SQL_62fd7b2debe4c0e4',
 19             plan_name       => 'SQL_PLAN_65zbv5rpy9h745ac47e2d',
 20             attribute_name  => 'AUTOPURGE',
 21             attribute_value => 'NO');
 22    ret := ret1 + ret2 + ret3;
 23    DBMS_OUTPUT.PUT_LINE('ALTER PLANS:' || ret);
 24  END;
 25  /
ALTER PLANS:3

PL/SQL procedure successfully completed.
★「ALTER PLANS:」はSQL計画ベースラインの属性変更した実行計画の個数

11. [セッション2 / 管理者ユーザ] 属性変更したSQL計画ベースラインの確認(属性変更後)
SQL> SELECT SQL_HANDLE, PLAN_NAME, to_char(CREATED, 'YYYY-MM-DD HH24:MI:SS') as CREATED, to_char(LAST_EXECUTED, 'YYYY-MM-DD HH24:MI:SS') as LAST_EXECUTED, ENABLED, ACCEPTED, FIXED, AUTOPURGE FROM DBA_SQL_PLAN_BASELINES WHERE SQL_HANDLE = 'SQL_62fd7b2debe4c0e4' ORDER BY CREATED DESC;

SQL_HANDLE                     PLAN_NAME                      CREATED              LAST_EXECUTED        ENABLED    ACCEPTED     FIXED      AUTOPURGE
------------------------------ ------------------------------ -------------------- -------------------- ---------- ------------ ---------- ----------
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h745ac47e2d 2023-03-24 10:44:14                       NO         YES          NO         NO

12. [セッション2 / 管理者ユーザ] 性能の悪いSQLSQL計画ベースラインのSQLハンドルと性能の良いSQLのSQLID、実行計画のセットを使用して新しい実行計画を登録
SQL> DECLARE
  2    ret number;
  3  BEGIN
  4    ret := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
  5             sql_id          => 'gxc9mvqxknma1',        --性能の良いSQLのSQLID
  6             plan_hash_value => '615168685',            --性能の良いSQLの実行計画
  7             sql_handle      => 'SQL_62fd7b2debe4c0e4', --性能の悪いSQLSQLハンドル
  8             enabled         => 'YES',                  --実行計画を有効化
  9             fixed           => 'YES');                 --実行計画を固定化
 10    DBMS_OUTPUT.PUT_LINE('LOAD PLANS:' || ret);
 11  END;
 12  /
LOAD PLANS:1

PL/SQL procedure successfully completed.

13. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(属性変更前)
SQL> SELECT SQL_HANDLE, PLAN_NAME, to_char(CREATED, 'YYYY-MM-DD HH24:MI:SS') as CREATED, to_char(LAST_EXECUTED, 'YYYY-MM-DD HH24:MI:SS') as LAST_EXECUTED, ENABLED, ACCEPTED, FIXED, AUTOPURGE FROM DBA_SQL_PLAN_BASELINES WHERE SQL_HANDLE = 'SQL_62fd7b2debe4c0e4' ORDER BY CREATED DESC;

SQL_HANDLE                     PLAN_NAME                      CREATED              LAST_EXECUTED        ENABLED    ACCEPTED     FIXED      AUTOPURGE
------------------------------ ------------------------------ -------------------- -------------------- ---------- ------------ ---------- ----------
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h748447c07a 2023-03-24 10:47:00                       YES        YES          YES        YES
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h745ac47e2d 2023-03-24 10:44:14                       NO         YES          NO         NO
★1行目が新たにロードした実行計画(実行計画は性能の良いSQLの実行計画)

14. [セッション2 / 管理者ユーザ] 新たに登録したSQLSQL計画ベースラインの属性変更(自動パージ無効化)
SQL> DECLARE
  2    ret number;
  3  BEGIN
  4    RET := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
  5             sql_handle      => 'SQL_62fd7b2debe4c0e4',           --性能の悪いSQLSQLハンドル
  6             plan_name       => 'SQL_PLAN_65zbv5rpy9h748447c07a', --新たにロードした実行計画
  7             attribute_name  => 'AUTOPURGE',                      --属性名(自動パージ)
  8             attribute_value => 'NO');                            --無効化
  9    DBMS_OUTPUT.PUT_LINE('ALTER PLANS:' || ret);
 10  END;
 11  /
ALTER PLANS:1

PL/SQL procedure successfully completed.

15. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(属性変更後)
SQL> SELECT SQL_HANDLE, PLAN_NAME, to_char(CREATED, 'YYYY-MM-DD HH24:MI:SS') as CREATED, to_char(LAST_EXECUTED, 'YYYY-MM-DD HH24:MI:SS') as LAST_EXECUTED, ENABLED, ACCEPTED, FIXED, AUTOPURGE FROM DBA_SQL_PLAN_BASELINES WHERE SQL_HANDLE = 'SQL_62fd7b2debe4c0e4' ORDER BY CREATED DESC;

SQL_HANDLE                     PLAN_NAME                      CREATED              LAST_EXECUTED        ENABLED    ACCEPTED     FIXED      AUTOPURGE
------------------------------ ------------------------------ -------------------- -------------------- ---------- ------------ ---------- ----------
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h748447c07a 2023-03-24 10:47:00                       YES        YES          YES        NO
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h745ac47e2d 2023-03-24 10:44:14                       NO         YES          NO         NO

16. [セッション1 / アプリケーションユーザ] 性能の悪いSQLの実行計画を取得
SQL> EXPLAIN PLAN FOR SELECT /* BAD_XPLAN */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO;

Explained.

SQL> @?/rdbms/admin/utlxpls.sql

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 615168685

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |    14 |   364 |    70   (0)| 00:00:01 |
|*  1 |  HASH JOIN         |      |    14 |   364 |    70   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| DEPT |     4 |    52 |    35   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| EMP  |    14 |   182 |    35   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("D"."DEPTNO"="E"."DEPTNO")

Note
-----
   - SQL plan baseline "SQL_PLAN_65zbv5rpy9h748447c07a" used for this statement
★新たに登録した実行計画「SQL_PLAN_65zbv5rpy9h748447c07a(ハッシュ結合)」を使用して実行された
★EXPLAIN PLAN文は実行計画の出力のみで実行はされない

 

②固定化した実行計画を削除
固定化した実行計画をSQL計画ベースラインから削除した場合、性能の悪いSQLを実行して固定化する前の実行計画でSQLが実行されるか検証します

【検証手順】
1. [セッション1 / アプリケーションユーザ] 実行計画を固定化したSQLの実行計画を取得
2. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(新たにロードした実行計画の削除前)
3. [セッション2 / 管理者ユーザ] 固定化した実行計画をSQL計画ベースラインから削除
4. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(新たにロードした実行計画の削除後)
5. [セッション1 / アプリケーションユーザ] 実行計画を固定化解除したSQLの実行計画を取得

【作業ログ】

1. [セッション1 / アプリケーションユーザ] 実行計画を固定化したSQLの実行計画を取得
SQL> EXPLAIN PLAN FOR SELECT /* BAD_XPLAN */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO;

Explained.

SQL> @?/rdbms/admin/utlxpls.sql

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 615168685

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |    14 |   364 |    70   (0)| 00:00:01 |
|*  1 |  HASH JOIN         |      |    14 |   364 |    70   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| DEPT |     4 |    52 |    35   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| EMP  |    14 |   182 |    35   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("D"."DEPTNO"="E"."DEPTNO")

Note
-----
   - SQL plan baseline "SQL_PLAN_65zbv5rpy9h748447c07a" used for this statement

2. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(新たにロードした実行計画の削除前)
SQL> SELECT SQL_HANDLE, PLAN_NAME, to_char(CREATED, 'YYYY-MM-DD HH24:MI:SS') as CREATED, to_char(LAST_EXECUTED, 'YYYY-MM-DD HH24:MI:SS') as LAST_EXECUTED, ENABLED, ACCEPTED, FIXED, AUTOPURGE FROM DBA_SQL_PLAN_BASELINES WHERE SQL_HANDLE = 'SQL_62fd7b2debe4c0e4' ORDER BY CREATED DESC;

SQL_HANDLE                     PLAN_NAME                      CREATED              LAST_EXECUTED        ENABLED    ACCEPTED     FIXED      AUTOPURGE
------------------------------ ------------------------------ -------------------- -------------------- ---------- ------------ ---------- ----------
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h748447c07a 2023-03-24 10:47:00                       YES        YES          YES        NO
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h745ac47e2d 2023-03-24 10:44:14                       NO         YES          NO         NO

3. [セッション2 / 管理者ユーザ] 固定化した実行計画をSQL計画ベースラインから削除
SQL> DECLARE
  2    ret number;
  3  BEGIN
  4    ret := DBMS_SPM.DROP_SQL_PLAN_BASELINE(
  5             sql_handle => 'SQL_62fd7b2debe4c0e4',
  6             plan_name  => 'SQL_PLAN_65zbv5rpy9h748447c07a');
  7    DBMS_OUTPUT.PUT_LINE('DROP PLANS:' || ret);
  8  END;
  9  /
DROP PLANS:1

PL/SQL procedure successfully completed.
★「DROP PLANS:」はSQL計画ベースラインから削除した実行計画の個数

4. [セッション2 / 管理者ユーザ] SQL計画ベースラインの取得(新たにロードした実行計画の削除後)
SQL> SELECT SQL_HANDLE, PLAN_NAME, to_char(CREATED, 'YYYY-MM-DD HH24:MI:SS') as CREATED, to_char(LAST_EXECUTED, 'YYYY-MM-DD HH24:MI:SS') as LAST_EXECUTED, ENABLED, ACCEPTED, FIXED, AUTOPURGE FROM DBA_SQL_PLAN_BASELINES WHERE SQL_HANDLE = 'SQL_62fd7b2debe4c0e4' ORDER BY CREATED DESC;

SQL_HANDLE                     PLAN_NAME                      CREATED              LAST_EXECUTED        ENABLED    ACCEPTED     FIXED      AUTOPURGE
------------------------------ ------------------------------ -------------------- -------------------- ---------- ------------ ---------- ----------
SQL_62fd7b2debe4c0e4           SQL_PLAN_65zbv5rpy9h745ac47e2d 2023-03-24 10:44:14                       NO         YES          NO         NO

5. [セッション1 / アプリケーションユーザ] 実行計画を固定化解除したSQLの実行計画を取得
SQL> EXPLAIN PLAN FOR SELECT /* BAD_XPLAN */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO;

Explained.

SQL> @?/rdbms/admin/utlxpls.sql

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 844388907

----------------------------------------------------------------------------------------
| Id  | Operation                    | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |    14 |   364 |    38   (3)| 00:00:01 |
|   1 |  MERGE JOIN                  |         |    14 |   364 |    38   (3)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPT    |     4 |    52 |     2   (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN           | PK_DEPT |     4 |       |     1   (0)| 00:00:01 |
|*  4 |   SORT JOIN                  |         |    14 |   182 |    36   (3)| 00:00:01 |
|   5 |    TABLE ACCESS FULL         | EMP     |    14 |   182 |    35   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("D"."DEPTNO"="E"."DEPTNO")
       filter("D"."DEPTNO"="E"."DEPTNO")
★実行計画の固定化前(ソートマージ結合)に戻った

 

■参考資料
自動データベース・メンテナンス・タスクの管理
SQL計画ベースラインの管理
SQL計画ベースラインの管理
https://blogs.oracle.com/otnjp/post/tsushima-hakushi-75
オラクルマスター教科書 ORACLE MASTER Expert パフォーマンス・チューニング編
Oracleの現場を効率化する100の技

■おわりに
下記のような状況はSPMが有効です。
・実行計画の変動が多くパフォーマンスが安定しない
・アプリケーション(SQL)にヒント句を追加したいが修正できない
 →実行計画を固定化
・適切な実行計画が選択されずある日突然パフォーマンスが低下してしまった
・過去の性能の良い実行計画に戻したい

 →過去の実行計画に戻す
SQL計画ベースラインを進化させたい(より良い実行計画が選択されるようにしたい)
 →SQL計画ベースラインのFIXED属性をNOにする
なお、SPMで実行計画を固定化した場合でも、下記のような場合は実行計画が再現できないため注意が必要です。
・索引を削除したなどオブジェクトに変更があった場合
 →索引を利用した実行計画をSPMに登録した状態で索引を削除すると索引を使用した実行計画が再現できない
・トリガーを無効にした場合
 →トリガーを有効にした場合と無効にした場合で実行計画が一致しない場合がある
・外部キー制約を無効化した場合
 →外部キー制約はハード解析時に最適化されるが、制約を無効化すると最適化されない場合がある

Oracleの実行計画(表結合)

今回はOracleの実行計画のうち、テーブルの結合について調査、検証しました。

テーブルの結合方法は4種類(ネステッドループ結合、ソートマージ結合、ハッシュ結合、直積結合)あり、それぞれ下記の図のような特徴があります。
なお、3つ以上の表を結合する場合、先ず2つの表を結合し、その結果と3つ目の表を結合します(すべての表が結合されて結果が生成されるまでこれを繰り返す)

■前提
DBMS_XPLAN.DISPLAY_CURSOR()のformatパラメータに「'ALL ALLSTATS LAST'」は指定せずに実行計画を取得

■検証環境
OS:Oracle Linux 6.5
DB/GI:Oracle Database 12c Release 1 (12.1.0.2.0) Enterprise Edition
※2ノードRAC(管理者管理型DB)

■検証パターン
①ネステッドループ結合(NESTED LOOPS)
②ハッシュ結合(HASH JOIN)
③ソートマージ結合(SORT JOIN / MERGE JOIN)
④直積結合(デカルト積)

■検証
①ネステッドループ結合(NESTED LOOPS)
外部表(駆動表)から検索条件に一致するキー値を1行ずつ取得し、取得したキー値を内部表のキー値と突き合わせてキー値が同じ場合、データを結合する
外部表から取得するキー値が少なければ内部表に対するループ回数も減るため、検索条件で取得するキー値の行数が少ない方を外部表に指定するのがセオリーとなっている(結合列のカーディナリティが低い方を外部表に指定する)
最初の1件を返すのが速いため、OLTP向き
少量データの結合に採用される可能性が高い

※プログラムのfor文やwhile文の多重ループのイメージ
while { ※外部表ループ
  外部表から検索条件に一致するキー値を1行取得
  →1行もなければループから抜ける
  while { ※内部表ループ
    外部表から取得したキー値に一致する内部表のデータを1行取得
    →1行もなければループから抜ける
  }
}

【発生条件】
・外部表、内部表の検索条件に一致するデータが比較的少量
・外部表、内部表の検索条件に指定する列に索引がある

【ヒント句】
/*+ USE_NL(内部表別名) LEADING(外部表別名) */
※USE_NL:指定した内部表を使用してネステッドループ結合する
※LEADING:表を結合する順序

【検証手順】
1. SELECT(ネステッドループ結合)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(ネステッドループ結合)を実行
SQL> SELECT /* XPLAN_TEST1 */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO AND D.DEPTNO = 10;

     EMPNO ENAME               DEPTNO DNAME
---------- --------------- ---------- ---------------
      7782 CLARK                   10 ACCOUNTING
      7839 KING                    10 ACCOUNTING
      7934 MILLER                  10 ACCOUNTING

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('05wg5gppgxj6w'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  05wg5gppgxj6w, child number 0
-------------------------------------
SELECT /* XPLAN_TEST1 */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT
D, EMP E WHERE D.DEPTNO = E.DEPTNO AND D.DEPTNO = 10

Plan hash value: 1561127466

-------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |          |       |       |     1 (100)|          |
|   1 |  NESTED LOOPS                        |          |     3 |   165 |     1   (0)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID        | DEPT     |     1 |    22 |     1   (0)| 00:00:01 |
|*  3 |    INDEX UNIQUE SCAN                 | PK_DEPT  |     1 |       |     1   (0)| 00:00:01 |
|   4 |   TABLE ACCESS BY INDEX ROWID BATCHED| EMP      |     3 |    99 |     0   (0)|          |
|*  5 |    INDEX RANGE SCAN                  | IDX1_EMP |     3 |       |     0   (0)|          |
--------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("D"."DEPTNO"=10)
   5 - access("E"."DEPTNO"=10)

 

【実行順序】
3→2→5→4→1→0

②ハッシュ結合(HASH JOIN)
結合対象の一方の表をメモリ(PGA)上に展開後、ハッシュ演算を適用してハッシュ表を作成し、もう一方の表のキー値(ハッシュ関数を適用したキー値)と突き合わせてキー値(ハッシュ値)が同じ場合、データを結合する
ハッシュ値をもとに結合するため、結合条件は等価結合('=')になる
PGAに作成されるハッシュ表が小さくなり、結合処理が効率的になるため、カーディナリティが低い方の表を先に処理するのがセオリーとなっている
最初にハッシュ表を生成するためフルスキャンが発生するが、以降の結合処理はPGAで行われ非常に高速なため、OLAP向き(大量のレコードを扱うバッチ処理や帳票処理、DWHなど)
大量データの結合に採用される可能性が高い
PGAが不足した場合、一時表領域が使用される(ディスクI/Oによる性能劣化が発生する可能性がある)

【発生条件】
・各表の検索条件に一致するデータが比較的大量
・各表の検索条件に指定する列に索引がない
・各表の検索条件が等価条件

【ヒント句】
/*+ USE_HASH(内側の表別名) LEADING(外側の表別名) */
※USE_HASH:指定した(内側の)表を使用してハッシュ結合する
※ハッシュ結合やソートマージ結合は外部表、内部表という呼び方をしないため、あえて外部表に相当する表を「外側の表」、内部表に相当する表を「内側の表」と書いています

【検証手順】
1. SELECT(ハッシュ結合)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(ハッシュ結合)を実行
SQL> SELECT /* XPLAN_TEST2 */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO = E.DEPTNO;

     EMPNO ENAME               DEPTNO DNAME
---------- --------------- ---------- ---------------
      7369 SMITH                   20 RESEARCH
      7499 ALLEN                   30 SALES
      7521 WARD                    30 SALES
      7566 JONES                   20 RESEARCH
      7654 MARTIN                  30 SALES
      7698 BLAKE                   30 SALES
      7782 CLARK                   10 ACCOUNTING
      7788 SCOTT                   20 RESEARCH
      7839 KING                    10 ACCOUNTING
      7844 TURNER                  30 SALES
      7876 ADAMS                   20 RESEARCH
      7900 JAMES                   30 SALES
      7902 FORD                    20 RESEARCH
      7934 MILLER                  10 ACCOUNTING

14行が選択されました。

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('3kbj95gsq0ftk'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  3kbj95gsq0ftk, child number 0
-------------------------------------
SELECT /* XPLAN_TEST2 */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT
D, EMP E WHERE D.DEPTNO = E.DEPTNO

Plan hash value: 615168685

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |       |       |     4 (100)|          |
|*  1 |  HASH JOIN         |      |    14 |   770 |     4   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| DEPT |     4 |    88 |     2   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| EMP  |    14 |   462 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("D"."DEPTNO"="E"."DEPTNO")

 

【実行順序】
2→3→1→0

③ソートマージ結合(SORT JOIN / MERGE JOIN)
結合対象の各表をメモリ(PGA)上に展開後、各表の結合列のキー値でソートし、ソート結果をPGAでマージしてデータを結合する
オペレーションの「SORT JOIN」はソート処理、「MERGE JOIN」はマージ処理
ソートマージ結合も大量データの結合に適しているが、結合条件に非等価結合('>'、'<'、BETWEENなど)を使用した場合、採用される可能性が高い
PGAが不足した場合、一時表領域が使用される(ディスクI/Oによる性能劣化が発生する可能性がある)

【発生条件】
・各表の検索条件に一致するデータが比較的大量
・各表の検索条件に指定する列に索引がない
・各表の検索条件が非等価条件

【ヒント句】
/*+ USE_MERGE(内側の表別名) LEADING(外側の表別名) */
※USE_MERGE:指定した表を使用してソートマージ結合する
※ハッシュ結合やソートマージ結合は外部表、内部表という呼び方をしないため、あえて外部表に相当する表を「外側の表」、内部表に相当する表を「内側の表」と書いています

【検証手順】
※発生条件のとおり検証しましたが再現しなかったため、ヒント句を使用して検証しました
1. SELECT(ソートマージ結合)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(ソートマージ結合)を実行
SQL> SELECT /* XPLAN_TEST3 */ /*+ USE_MERGE(e) LEADING(d) */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO < E.DEPTNO;

     EMPNO ENAME               DEPTNO DNAME
---------- --------------- ---------- ---------------
      7566 JONES                   10 ACCOUNTING
      7902 FORD                    10 ACCOUNTING
      7876 ADAMS                   10 ACCOUNTING
      7369 SMITH                   10 ACCOUNTING
      7788 SCOTT                   10 ACCOUNTING
      7521 WARD                    10 ACCOUNTING
      7844 TURNER                  10 ACCOUNTING
      7499 ALLEN                   10 ACCOUNTING
      7900 JAMES                   10 ACCOUNTING
      7698 BLAKE                   10 ACCOUNTING
      7654 MARTIN                  10 ACCOUNTING
      7521 WARD                    20 RESEARCH
      7844 TURNER                  20 RESEARCH
      7499 ALLEN                   20 RESEARCH
      7900 JAMES                   20 RESEARCH
      7698 BLAKE                   20 RESEARCH
      7654 MARTIN                  20 RESEARCH

17行が選択されました。

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('73bv800j2mbhd'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  73bv800j2mbhd, child number 0
-------------------------------------
SELECT /* XPLAN_TEST3 */ /*+ USE_MERGE(e) LEADING(d) */ E.EMPNO,
E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E WHERE D.DEPTNO < E.DEPTNO

Plan hash value: 1407029907

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |       |       |     6 (100)|          |
|   1 |  MERGE JOIN         |      |     3 |   165 |     6  (34)| 00:00:01 |
|   2 |   SORT JOIN         |      |     4 |    88 |     3  (34)| 00:00:01 |
|   3 |    TABLE ACCESS FULL| DEPT |     4 |    88 |     2   (0)| 00:00:01 |
|*  4 |   SORT JOIN         |      |    14 |   462 |     3  (34)| 00:00:01 |
|   5 |    TABLE ACCESS FULL| EMP  |    14 |   462 |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("D"."DEPTNO"<"E"."DEPTNO")
       filter("D"."DEPTNO"<"E"."DEPTNO")

 

【実行順序】
3→2→5→4→1→0

④直積結合(デカルト積)
結合対象の各表をメモリ(PGA)上に展開後、直積(各表の行数の掛け算)で各表のデータを結合する
結合条件を指定しない場合、採用される結合方法
オペレーションの「MERGE JOIN CARTESIAN」は結合条件を指定しないで直積を取得する処理、「BUFFER SORT」は内部表の行をあらかじめソートしておく処理

【発生条件】
・各表を結合するための検索条件(結合条件)を指定しない

【ヒント句】
なし

【検証手順】
1. SELECT(直積結合)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(直積結合)を実行
SQL> SELECT /* XPLAN_TEST4 */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT D, EMP E;

     EMPNO ENAME               DEPTNO DNAME
---------- --------------- ---------- ---------------
      7369 SMITH                   10 ACCOUNTING
      7499 ALLEN                   10 ACCOUNTING
      7521 WARD                    10 ACCOUNTING
(略)
      7900 JAMES                   40 OPERATIONS
      7902 FORD                    40 OPERATIONS
      7934 MILLER                  40 OPERATIONS

56行が選択されました。

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('g86znfr5hwvcn'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  g86znfr5hwvcn, child number 0
-------------------------------------
SELECT /* XPLAN_TEST4 */ E.EMPNO, E.ENAME, D.DEPTNO, D.DNAME FROM DEPT
D, EMP E

Plan hash value: 2034389985

-----------------------------------------------------------------------------
| Id  | Operation            | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |      |       |       |     5 (100)|          |
|   1 |  MERGE JOIN CARTESIAN|      |    56 |  2352 |     5   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL  | DEPT |     4 |    88 |     2   (0)| 00:00:01 |
|   3 |   BUFFER SORT        |      |    14 |   280 |     3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL | EMP  |    14 |   280 |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

 

【実行順序】
2→4→3→1→0

■参考資料
コメント
https://www.oracle.com/jp/a/tech/docs/technical-resources/100811-sql-tuning.pdf
NESTED LOOPS – Oracle SQL実行計画 | 技術情報 | 株式会社コーソル
HASH JOIN – Oracle SQL実行計画 | 技術情報 | 株式会社コーソル
SORT JOIN – Oracle SQL実行計画 | 技術情報 | 株式会社コーソル
MERGE JOIN – Oracle SQL実行計画 | 技術情報 | 株式会社コーソル
MERGE JOIN CARTESIAN – Oracle SQL実行計画 | 技術情報 | 株式会社コーソル
パフォーマンス改善と事前対策に役立つ Oracle SQLチューニング 本気で学ぶ実践的な考え方とテクニック

新・門外不出のOracle現場ワザ エキスパートが明かす運用・管理の極意
オラクルマスター教科書 ORACLE MASTER Expert パフォーマンス・チューニング編
Oracleの現場を効率化する100の技

■おわりに
記事内の各種オペレーションの発生条件はあくまでも書籍やサイト、実機検証で確認した条件ですが、必ず該当オペレーションが発生するわけではありません(どのオペレーションを採用するかは最終的にオプティマイザが判断します)
ヒント句を使用すれば採用されるオペレーションを狙いやすくなります。

Oracleの実行計画(全表スキャンと索引スキャン)

今回はOracleの実行計画について調査、検証しました。

実行計画とは
SQLの実行手順(データへのアクセスパス、テーブルの結合方法、テーブルの結合順序の組み合わせから構成される)
オプティマイザ(CBO)が実行コストを基に作成する

【補足】
CBO(コストベースオプティマイザ)
統計情報(オプティマイザ統計)のデータを元にSQLの実行コストが最も低くなるように実行計画を作成する機能(アルゴリズム)
SQLの実行手順が複数存在する場合、複数の実行コストを比較して最適な実行手順を選択する
※統計情報が古いと適切な実行計画が作成・選択されない可能性があるため、統計情報は定期的に取得することをお勧めします
※実行コスト:処理に必要な見積もり時間、見積もりリソース使用量など
なお、10gまではRBO(ルールベースオプティマイザ)が標準でした
私は未経験ですが、RBOはあらかじめ設定されているルールを元にSQLの実行手順を作成・選択する機能(アルゴリズム)です
あらかじめアクセスパスをランキング形式で用意し、アクセスパスが複数候補に挙がった場合、ランクの高い方を採用する方法です
詳細はコチラ(https://www.oracle.com/jp/a/tech/docs/technical-resources/index-tuning1.pdf)※22ページ参照

アクセスパス
データベースからデータを取得するルート
・表の全レコードを取得する場合(フルスキャン)
・索引からレコードを特定して、レコードを取得する場合(索引スキャン)
など

テーブルの結合方法
複数の表を結合する方法
主に4種類ある
ネステッドループ結合、ソートマージ結合、ハッシュ結合、直積結合

テーブルの結合順序
複数の表を結合するときの表の結合順序
3つ以上の表を結合する場合、先ず2つの表を結合し、その結果と3つ目の表を結合する
すべての表が結合されて結果が生成されるまでこれを繰り返す

実行計画を読んでみる
実行計画はツリー構造になっており、各行には親子関係があります。
ステップの一番上から親ステップ→子ステップへ遷移し、子ステップの処理の実行が完了したら親ステップへ遷移し、親ステップの処理を実行する、というのが基本動作になります。
ただし、同じインデントの子ステップが複数ある場合は注意が必要です。
とりあえずサンプルを読んでみます。

SQLに付与される識別子。13桁の文字列で管理される。MD5で算出されたハッシュ値の末尾8バイト
② 選択された子カーソル(実行計画)番号
③ 実行SQL
④ 実行計画の識別子
⑤ 実行手順。1行1行をステップと呼ぶ(赤枠)。ステップには親子関係がある
   Id:オペレーションID
   Operation:オペレーション内容(行ソース操作)
   Name:オペレーション対象のオブジェクト名
   Rows:オペレーションでアクセスされる行数
   Bytes:オペレーションでアクセスされるサイズ(バイト数)
   Cost (%CPU):オペレーションに対するオプティマイザが見積もった実行コスト
   Time:オペレーションに要するオプティマイザが見積もった時間
⑥ 述語(補足情報)。オペレーションで適用された検索条件など

サンプルだと実行順序は「3 → 2 → 5 → 4 → 1 → 0」となります。
実行順序① Id3(INDEX UNIQUE SCAN):検索条件(DEPT表のDEPTNO=10)にヒットする1行を特定
実行順序② Id2(TABLE ACCESS BY INDEX ROWID):インデックス(PK_DEPT)で特定した列データを基にテーブルからデータを取得
実行順序③ Id5(INDEX RANGE SCAN):検索条件(EMP表のDEPTNO=10)にヒットする複数行(サンプルでは3行)を特定
実行順序④ Id4(TABLE ACCESS BY INDEX ROWID):インデックス(IDX1_EMP_DEPTNO)で特定した列データを基にテーブルからデータをまとめて取得
実行順序⑤ Id1(NESTED LOOPS):DEPT表とEMP表から取得したデータを結合
実行順序⑥ Id0(SELECT STATEMENT):検索結果を返す

整理すると・・・
ポイント① 親ステップに同じインデントの子ステップが複数ある場合、一番上の子ステップへ遷移する(サンプルだとId2とId4が同じインデントの子ステップに該当するが、一番上のId2へ遷移する)
ポイント② その子ステップにさらに子ステップがある場合、現在到達している子ステップが親ステップになり、子ステップへ遷移する(サンプルだとId2が親ステップになり、Id3が子ステップになる。Id2へ遷移する)
ポイント③ 遷移した子ステップに子ステップがない場合(一番下の子ステップに到達した場合)、処理を実行する(サンプルだとId3)
ポイント④ 子ステップの処理完了後、親ステップへ遷移し、親ステップの処理を実行する(サンプルだとId2)
ポイント⑤ 親ステップの処理完了後、他に子ステップがある場合、その子ステップへ遷移後、③へ(Id2に他の子ステップがある場合、その子ステップへ遷移するがサンプルだとId2に他の子ステップはない)
           他に子ステップがない場合、その親ステップへ遷移する(サンプルだとId2からId1へ遷移する)
           他に子ステップがないが同じインデントの子ステップが残っている場合、その子ステップへ遷移後、②へ(サンプルだとId4へ遷移し、さらに子ステップのId5へ遷移・実行)
           他に子ステップがない・同じインデントの子ステップが残っていない場合、その親ステップへ遷移し、処理を実行する。これをツリーの一番上まで繰り返す(サンプルだとId1実行後、Id0実行)

【補足】
サンプルの実行計画はDBMS_XPLAN.DISPLAY_CURSOR()で出力したため、Rows、Bytes、Cost (%CPU)、Timeは実績ではなく、オプティマイザが見積もった統計値
DBMS_XPLAN.DISPLAY_CURSOR()で実行計画を出力した場合、実行計画は過去に実際に使用されたものが出力されるが、各統計値はオプティマイザが見積もったものになる
DBMS_XPLAN.DISPLAY_CURSOR()のformatパラメータに「'ALL ALLSTATS LAST'」を指定した場合、実行計画は実績、統計値は見積もりと実績の両方が出力される
※STATISTICS_LEVEL初期化パラメータを「ALL」に設定する必要がある
(例)'ALL ALLSTATS LAST'ありの場合

データへアクセスする際、よく見るオペレーションのパターンを検証しました。

■検証環境
OS:Oracle Linux 6.5
DB/GI:Oracle Database 12c Release 1 (12.1.0.2.0) Enterprise Edition
※2ノードRAC(管理者管理型DB)

■前提
・索引はBtreeインデックス
・実行計画はDBMS_XPLAN.DISPLAY_CURSOR()のformatパラメータに「'ALL ALLSTATS LAST'」は指定せずに取得

■検証パターン
①全表スキャン(TABLE ACCESS FULL)
②ROWIDスキャン(TABLE ACCESS BY USER ROWID)
③索引一意スキャン(INDEX UNIQUE SCAN)
④索引範囲スキャン(INDEX RANGE SCAN)
⑤索引スキップスキャン(INDEX SKIP SCAN)
⑥索引フルスキャン(INDEX FULL SCAN)
⑦索引高速フルスキャン(INDEX FAST FULL SCAN)

■検証
①全表スキャン(TABLE ACCESS FULL)
索引を使用せず、表(表ブロック)全体を読み込む(表フルスキャン)
表を読み込む際、HWM(High Water Mark)まで読み込む
db_file_multiblock_read_count初期化パラメータで設定された値のブロックをまとめて読み込む(マルチブロックリード
※表がパーティション化されている場合、オペレーションが「TABLE ACCESS FULL」となる場合があるが表全体ではなく、特定のパーティションを読み込んでいる可能性もある

【発生条件】
・検索条件を指定しない
・検索条件で指定する列に索引がない

【ヒント句】
/*+ FULL(表別名) */

【検証手順】
1. SELECT(全表スキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(全表スキャン)を実行
SQL> SELECT /* XPLAN_TEST1 */ * FROM EMP WHERE JOB = 'CLERK';

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
      7876 ADAMS           CLERK                 7788 1987-05-23 00:00:00       1100                    20
      7900 JAMES           CLERK                 7698 1981-12-03 00:00:00        950                    30
      7934 MILLER          CLERK                 7782 1982-01-23 00:00:00       1300                    10

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('1gq2u2fah1sgg'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  1gq2u2fah1sgg, child number 0
-------------------------------------
SELECT /* XPLAN_TEST1 */ * FROM EMP WHERE JOB = 'CLERK'

Plan hash value: 3956160932

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |       |       |     2 (100)|          |
|*  1 |  TABLE ACCESS FULL| EMP  |     4 |   348 |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("JOB"='CLERK')

 

【実行順序】
1→0

②ROWIDスキャン(TABLE ACCESS BY USER ROWID)
索引は使用せず、レコードのROWIDを指定して、1件のレコードを読み込む
ROWIDを指定することでデータファイル、ブロック、レコードの位置が分かるため、1件のレコードを最も高速に読み込める

【発生条件】
・検索条件でROWIDを指定する

【ヒント句】
なし

【検証手順】
1. SELECT(ROWIDスキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(ROWIDスキャン)を実行
SQL> --各レコードのROWID確認
SQL> SELECT /* XPLAN_TEST2 */ ROWID, EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO FROM EMP;

ROWID                   EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
------------------ ---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
AAAW4+AABAAAYTpAAA       7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
AAAW4+AABAAAYTpAAB       7499 ALLEN           SALESMAN              7698 1981-02-20 00:00:00       1600        300         30
AAAW4+AABAAAYTpAAC       7521 WARD            SALESMAN              7698 1981-02-22 00:00:00       1250        500         30
AAAW4+AABAAAYTpAAD       7566 JONES           MANAGER               7839 1981-04-02 00:00:00       2975                    20
AAAW4+AABAAAYTpAAE       7654 MARTIN          SALESMAN              7698 1981-09-28 00:00:00       1250       1400         30
AAAW4+AABAAAYTpAAF       7698 BLAKE           MANAGER               7839 1981-05-01 00:00:00       2850                    30
AAAW4+AABAAAYTpAAG       7782 CLARK           MANAGER               7839 1981-06-09 00:00:00       2450                    10
AAAW4+AABAAAYTpAAH       7788 SCOTT           ANALYST               7566 1987-04-19 00:00:00       3000                    20
AAAW4+AABAAAYTpAAI       7839 KING            PRESIDENT                  1981-11-17 00:00:00       5000                    10
AAAW4+AABAAAYTpAAJ       7844 TURNER          SALESMAN              7698 1981-09-08 00:00:00       1500          0         30
AAAW4+AABAAAYTpAAK       7876 ADAMS           CLERK                 7788 1987-05-23 00:00:00       1100                    20
AAAW4+AABAAAYTpAAL       7900 JAMES           CLERK                 7698 1981-12-03 00:00:00        950                    30
AAAW4+AABAAAYTpAAM       7902 FORD            ANALYST               7566 1981-12-03 00:00:00       3000                    20
AAAW4+AABAAAYTpAAN       7934 MILLER          CLERK                 7782 1982-01-23 00:00:00       1300                    10

14行が選択されました。

SQL> --ROWIDスキャン
SQL> SELECT /* XPLAN_TEST2 */ * FROM EMP where rowid ='AAAW4+AABAAAYTpAAA';

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('c1f0h5kfg9vgx'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  c1f0h5kfg9vgx, child number 0
-------------------------------------
SELECT /* XPLAN_TEST2 */ * FROM EMP where rowid ='AAAW4+AABAAAYTpAAA'

Plan hash value: 1116584662

-----------------------------------------------------------------------------------
| Id  | Operation                  | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |      |       |       |     1 (100)|          |
|   1 |  TABLE ACCESS BY USER ROWID| EMP  |     1 |    99 |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

 

【実行順序】
1→0

③索引一意スキャン(INDEX UNIQUE SCAN)
検索条件に指定した列に一意制約または主キー制約が設定された索引を使用して、データを読み込む
リーフブロック内の1つのROWIDをもとにデータを参照するため、検索結果は常に「検索結果 ≦ 1 」になる(検索条件に該当する1or0エントリを返す)
なお、SQLが索引列のみ参照している場合、表データは参照せず、リーフブロック内の列データを返す場合もある(カバーリングインデックス)

【補足】
カバーリングインデックス
インデックスに参照する列データがすべて含まれている状態。インデックスブロック内で情報取得が完結するため、テーブルアクセス不要

【発生条件】
・検索条件で指定する列に索引(一意制約または主キー制約)が設定されている

【ヒント句】
なし
/*+ INDEX(表別名 索引名) */ で索引を指定しても良いと思います

【検証手順】
1. SELECT(索引一意スキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引一意スキャン)を実行
SQL> SELECT /* XPLAN_TEST3 */ * FROM EMP WHERE EMPNO = 7369;

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('91kz6rf00jf72'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  91kz6rf00jf72, child number 0
-------------------------------------
SELECT /* XPLAN_TEST3 */ * FROM EMP WHERE EMPNO = 7369

Plan hash value: 2949544139

--------------------------------------------------------------------------------------
| Id  | Operation                   | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |        |       |       |     1 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID| EMP    |     1 |    87 |     1   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | PK_EMP |     1 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("EMPNO"=7369)

※TABLE ACCESS BY INDEX ROWIDはテーブルのROWIDを使用して表のレコードへアクセスするオペレーション

 

【実行順序】
2→1→0

④索引範囲スキャン(INDEX RANGE SCAN)
検索条件に指定した列に一意制約または主キー制約が設定されていない索引を使用して、データを読み込む
リーフブロック内の1つ以上のROWIDをもとにデータを参照するため、検索結果は常に「検索結果 ≧ 0 」になる(キー値の範囲でリーフブロックをスキャンして、検索条件に該当する複数エントリを返す)
③同様、SQLが索引列のみ参照している場合、表データは参照せず、リーフブロック内の列データを返す場合もある

【発生条件】
・検索条件で指定する列に索引(一意制約または主キー制約以外)が設定されている

【ヒント句】
なし
/*+ INDEX(表別名 索引名) */ でインデックスを指定しても良いと思います

【検証手順】
1. SELECT(索引範囲スキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引範囲スキャン)を実行
SQL> SELECT /* XPLAN_TEST4 */ * FROM EMP WHERE DEPTNO = 10;

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7782 CLARK           MANAGER               7839 1981-06-09 00:00:00       2450                    10
      7839 KING            PRESIDENT                  1981-11-17 00:00:00       5000                    10
      7934 MILLER          CLERK                 7782 1982-01-23 00:00:00       1300                    10

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('dmpqvjxhsp6y2'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  dmpqvjxhsp6y2, child number 0
-------------------------------------
SELECT /* XPLAN_TEST4 */ * FROM EMP WHERE DEPTNO = 10

Plan hash value: 746399115

----------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |              |       |       |     1 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| EMP          |     3 |   261 |     1   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | IDX1_EMP_DNO |     3 |       |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("DEPTNO"=10)

※TABLE ACCESS BY INDEX ROWID BATCHEDはテーブルのROWIDを使用して表のレコードへアクセスする際、複数ブロックをまとめて読み込むオペレーション(12c~)
読み込む複数ブロックが連接している場合:マルチブロックリード(db file scattered read)
読み込む複数ブロックが連接していない:パラレルリード(db file parallel read)

 

【実行順序】
2→1→0

⑤索引スキップスキャン(INDEX SKIP SCAN)
検索条件に指定した列に複合索引(コンポジット索引)が設定されているが第1キー以外の索引を使用して、データを読み込む
複合索引の第1キーのNDVが小さい(第1列は「男性」・「女性」の2種類など)場合、採用される可能性が高くなる
Oracle内部では第1キーの使用をスキップした上で第2キー以降を使用する(INDEX RANGE SCAN)となるため、最初からINDEX RANGE SCANを使用するパターンと比べると効率が悪いそうです。

【発生条件】
・検索条件で指定する列に複合索引が設定されている(WHERE句は第1キー以外の列を指定する)
・検索条件で指定する列に複合索引の第1キーの列が表の先頭列にない

【ヒント句】
/*+ INDEX_SS(表別名) */
/*+ INDEX_SS(表別名 索引名) */

【検証手順】
1. SELECT(索引スキップスキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引スキップスキャン)を実行
SQL> SELECT /* XPLAN_TEST5 */ * FROM EMP WHERE EMPNO = 7369;

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('4vtjmw4fv7q32'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  4vtjmw4fv7q32, child number 0
-------------------------------------
SELECT /* XPLAN_TEST5 */ * FROM EMP WHERE EMPNO = 7369

Plan hash value: 178841609

-----------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |               |       |       |     2 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| EMP           |     3 |   114 |     2   (0)| 00:00:01 |
|*  2 |   INDEX SKIP SCAN                   | IDX1_EMP_COMP |     3 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("EMPNO"=7369)
       filter("EMPNO"=7369)

 

【実行順序】
2→1→0

⑥索引フルスキャン(INDEX FULL SCAN)
リーフブロック全体をフルスキャン(シングルブロックリード)して索引の順番どおりに読み込む
SQLで参照する列がすべて索引に含まれている必要があるが、検索条件に索引列を指定する必要はない
SQLで参照する列がすべて索引列でソート処理(ORDER BY句など)が必要な場合、採用される可能性が高くなる
リーフブロック内の索引キー値でソートされた順にエントリを返すため、索引ブロックへのアクセスだけで要件が満たせる(SQLで参照する列が取得できる)場合、表ブロックアクセスおよびソート処理は省略できる可能性がある

【発生条件】
SQLで参照される列がすべて索引に含まれている
SQLで参照されるいずれかの列にNOT NULL制約がある
・ソート処理(ORDER BY句など)のキーにNOT NULL制約がある列を指定している

【ヒント句】
なし
/*+ INDEX(表別名 索引名) */ でインデックスを指定しても良いと思います

【検証手順】
1. SELECT(索引フルスキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引フルスキャン)を実行
SQL> SELECT /* XPLAN_TEST6 */ EMPNO, DEPTNO FROM EMP order by EMPNO, DEPTNO;

     EMPNO     DEPTNO
---------- ----------
      7369         20
      7499         30
      7521         30
      7566         20
      7654         30
      7698         30
      7782         10
      7788         20
      7839         10
      7844         30
      7876         20
      7900         30
      7902         20
      7934         10

14行が選択されました。

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('9kxmsjcmgrvc3'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  9kxmsjcmgrvc3, child number 0
-------------------------------------
SELECT /* XPLAN_TEST6 */ EMPNO, DEPTNO FROM EMP order by EMPNO, DEPTNO

Plan hash value: 150391907

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |       |       |     3 (100)|          |
|   1 |  SORT ORDER BY     |      |    14 |   364 |     3  (34)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP  |    14 |   364 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------

 

【実行順序】
1→0

⑦索引高速フルスキャン(INDEX FAST FULL SCAN)
索引ブロック全体をフルスキャン(マルチブロックリード)してディスクに保存されている順番どおりに読み込む
SQLで参照する列がすべて索引に含まれている必要があるが、検索条件に索引列を指定する必要はない(索引フルスキャンと同じ)
索引フルスキャンと似ているが、索引高速フルスキャンはソート処理を回避できない
索引フルスキャンは索引キーがソートされた状態のリーフブロックをシングルブロックリードするためソート処理を回避できるが、索引高速フルスキャンはツリー構造を意識せず索引ブロックをセグメントヘッダからマルチブロックリードするため、取得するデータを索引キーでソートできない
索引高速フルスキャンはマルチブロックリードやパラレル処理も可能なため、一般的に索引フルスキャンに比べて高速

【発生条件】
SQLで参照される列がすべて索引に含まれている
SQLで参照されるいずれかの列にNOT NULL制約がある

【ヒント句】
/*+ INDEX_FFS(表別名 索引名) */

【検証手順】
1. SELECT(索引高速フルスキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引高速フルスキャン)を実行
SQL> SELECT /* XPLAN_TEST7 */ EMPNO FROM EMP;

     EMPNO
----------
      7369
      7369
      7369
(略)
      7934
      7934
      7934

1400行が選択されました。

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('can7v3mzd0sqd'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  can7v3mzd0sqd, child number 0
-------------------------------------
SELECT /* XPLAN_TEST7 */ EMPNO FROM EMP

Plan hash value: 3993716296

---------------------------------------------------------------------------------
| Id  | Operation            | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |          |       |       |     4 (100)|          |
|   1 |  INDEX FAST FULL SCAN| IDX1_EMP |  1400 | 18200 |     4   (0)| 00:00:01 |
---------------------------------------------------------------------------------

 

【実行順序】
1→0

■参考資料
実行計画の生成と表示
オプティマイザのアクセス・パス
コメント
https://blogs.oracle.com/otnjp/post/tsushima-hakushi-4
https://blogs.oracle.com/otnjp/post/tsushima-hakushi-21
https://www.oracle.com/webfolder/technetwork/jp/ondemand/ddd2013/A-1.pdf
https://www.oracle.com/jp/a/tech/docs/technical-resources/100811-sql-tuning.pdf
https://www.oracle.com/jp/a/tech/docs/technical-resources/index-tuning1.pdf
Oracle SQL実行計画の読み方 | コーソルDatabaseエンジニアのBlog
パフォーマンス改善と事前対策に役立つ Oracle SQLチューニング 本気で学ぶ実践的な考え方とテクニック
オラクルマスター教科書 ORACLE MASTER Expert パフォーマンス・チューニング編

■おわりに
記事内の各種オペレーションの発生条件はあくまでも書籍やサイト、実機検証で確認した条件ですが、必ず該当オペレーションが発生するわけではありません(どのオペレーションを採用するかは最終的にオプティマイザが判断します)
ヒント句を使用すれば採用されるオペレーションを狙いやすくなりますが、今回はヒント句を使用しない状態で検証したため、索引スキップスキャンや索引フルスキャン、索引高速スキャンが出たり出なかったりで苦労しました。

OracleのSELECT文の評価順序

今回はOracleのSELECT文の評価順序について調査、検証しました。

SELECT文は列別名を指定できる句がありますが、一部の句(WHERE句、GROUP BY句など)では列別名を指定できません。
理由はSELECT文で指定する句の評価順序があるためです。
SELECT文で指定する句は下記の順序で評価されます。

1. FROM
2. WHERE
3. GROUP BY
4. HAVING
5. SELECT
6. ORDER BY

列別名を定義するのはSELECT句であるため、SELECT句より前に評価される句では列別名が使用できません。
評価順序のルールを守らないと解析エラーが発生する、というのを検証して実際の動きを見てみます。

■検証環境
OS:Oracle Linux 6.5
DB/GI:Oracle Database 12c Release 1 (12.1.0.2.0) Enterprise Edition
※2ノードRAC(管理者管理型DB)

■前提
下記2テーブルを検証に使用します

SQL> select * from emp;

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
      7499 ALLEN           SALESMAN              7698 1981-02-20 00:00:00       1600        300         30
      7521 WARD            SALESMAN              7698 1981-02-22 00:00:00       1250        500         30
      7566 JONES           MANAGER               7839 1981-04-02 00:00:00       2975                    20
      7654 MARTIN          SALESMAN              7698 1981-09-28 00:00:00       1250       1400         30
      7698 BLAKE           MANAGER               7839 1981-05-01 00:00:00       2850                    30
      7782 CLARK           MANAGER               7839 1981-06-09 00:00:00       2450                    10
      7839 KING            PRESIDENT                  1981-11-17 00:00:00       5000                    10
      7844 TURNER          SALESMAN              7698 1981-09-08 00:00:00       1500          0         30
      7900 JAMES           CLERK                 7698 1981-12-03 00:00:00        950                    30
      7902 FORD            ANALYST               7566 1981-12-03 00:00:00       3000                    20
      7934 MILLER          CLERK                 7782 1982-01-23 00:00:00       1300                    10

SQL> select * from dept;

    DEPTNO DNAME           LOC
---------- --------------- ---------------
        10 ACCOUNTING      NEW YORK
        20 RESEARCH        DALLAS
        30 SALES           CHICAGO
        40 OPERATIONS      BOSTON

 

■検証パターン
①WHERE句に列別名を指定
②ORDER BY句に列別名を指定
③GROUP BY句に列別名を指定
④HAVING句に列別名を指定
⑤(番外編1)ON句に列別名を指定
⑥(番外編2)WHERE句に集計関数を指定

■検証
①WHERE句に列別名を指定
WHERE句に列別名を指定した状態でSELECT文を実行した場合の動きを検証します

【検証手順】
1. WHERE句に列別名を指定した状態でSELECT文を実行

【作業ログ】

1. WHERE句に列別名を指定した状態でSELECT文を実行
SQL> select empno as eno, ename, deptno from emp where eno = 7369;
select empno as eno, ename, deptno from emp where eno = 7369
                                                  *
行1でエラーが発生しました。:
ORA-00904: "ENO": 無効な識別子です。

SQL> --改善案
SQL> select empno as eno, ename, deptno from emp where empno = 7369;

       ENO ENAME               DEPTNO
---------- --------------- ----------
      7369 SMITH                   20

 

②ORDER BY句に列別名を指定
ORDER BY句に列別名を指定した状態でSELECT文を実行した場合の動きを検証します

【検証手順】
1. ORDER BY句に列別名を指定した状態でSELECT文を実行

【作業ログ】

1. ORDER BY句に列別名を指定した状態でSELECT文を実行
SQL> select empno as eno, ename, deptno from emp order by eno;

       ENO ENAME               DEPTNO
---------- --------------- ----------
      7369 SMITH                   20
      7499 ALLEN                   30
      7521 WARD                    30
      7566 JONES                   20
      7654 MARTIN                  30
      7698 BLAKE                   30
      7782 CLARK                   10
      7839 KING                    10
      7844 TURNER                  30
      7900 JAMES                   30
      7902 FORD                    20
      7934 MILLER                  10
★列別名は指定できる

 

③GROUP BY句に列別名を指定
GROUP BY句に列別名を指定した状態でSELECT文を実行した場合の動きを検証します

【検証手順】
1. GROUP BY句に列別名を指定した状態でSELECT文を実行

【作業ログ】

1. GROUP BY句に列別名を指定した状態でSELECT文を実行
SQL> select deptno as dno, count(*) from emp group by dno;
select deptno as dno, count(*) from emp group by dno
                                                 *
行1でエラーが発生しました。:
ORA-00904: "DNO": 無効な識別子です。

SQL> --改善案
SQL> select deptno as dno, count(*) from emp group by deptno;

       DNO   COUNT(*)
---------- ----------
        30          6
        20          3
        10          3

 

④HAVING句に列別名を指定
HAVING句に列別名を指定した状態でSELECT文を実行した場合の動きを検証します

【検証手順】
1. HAVING句に列別名を指定した状態でSELECT文を実行

【作業ログ】

1. HAVING句に列別名を指定した状態でSELECT文を実行
SQL> select deptno as dno, count(*) as dcnt from emp group by deptno having dcnt > 3;
select deptno as dno, count(*) as dcnt from emp group by deptno having dcnt > 3
                                                                       *
行1でエラーが発生しました。:
ORA-00904: "DCNT": 無効な識別子です。

SQL> --改善案
SQL> select deptno as dno, count(*) as dcnt from emp group by deptno having count(*) > 3;

       DNO       DCNT
---------- ----------
        30          6

 

⑤(番外編1)ON句に列別名を指定
検証の対象の句には上げていませんでしたが、表の結合でON句もよく使用するため、ON句に列別名を指定した状態でSELECT文を実行した場合の動きも検証しようと思います

【検証手順】
1. ON句に列別名を指定した状態でSELECT文を実行

【作業ログ】

1. ON句に列別名を指定した状態でSELECT文を実行
SQL> select e.empno as eno, e.ename, d.deptno as dno, d.dname from emp e inner join dept d on e.eno = d.dno;
select e.empno as eno, e.ename, d.deptno as dno, d.dname from emp e inner join dept d on e.eno = d.dno
                                                                                                 *
行1でエラーが発生しました。:
ORA-00904: "D"."DNO": 無効な識別子です。
★ON句も列別名は使用できない。SELECT句より前に評価されることが分かる(実際はFROM句とWHERE句の間で評価されるようです)

SQL> --改善案
SQL> select e.empno as eno, e.ename, d.deptno as dno, d.dname from emp e inner join dept d on e.deptno = d.deptno;

       ENO ENAME                  DNO DNAME
---------- --------------- ---------- ---------------
      7369 SMITH                   20 RESEARCH
      7499 ALLEN                   30 SALES
      7521 WARD                    30 SALES
      7566 JONES                   20 RESEARCH
      7654 MARTIN                  30 SALES
      7698 BLAKE                   30 SALES
      7782 CLARK                   10 ACCOUNTING
      7839 KING                    10 ACCOUNTING
      7844 TURNER                  30 SALES
      7900 JAMES                   30 SALES
      7902 FORD                    20 RESEARCH
      7934 MILLER                  10 ACCOUNTING

 

⑥(番外編2)WHERE句に集計関数を指定
こちらは列別名を指定するパターンではありませんが、条件を指定する句であるWHERE句とHAVING句のうち、なぜWHERE句では集計関数が指定できないのかも検証しようと思います

【検証手順】
1. WHERE句に集計関数を指定した状態でSELECT文を実行

【作業ログ】

1. WHERE句に集計関数を指定した状態でSELECT文を実行
SQL> select empno as eno, ename, deptno from emp where count(*) > 3;

select empno as eno, ename, deptno from emp where count(*) > 3
                                                  *
行1でエラーが発生しました。:
ORA-00934: ここではグループ関数は使用できません。
★WHERE句が評価される順番はGROUP BY句より前のため、WHERE句では集計関数を使用できない

 

■おわりに
整理すると下記のようになります。

ORACLE MASTERSQLの勉強などで「列別名を使用できる句、できない句が覚えられない」と言われることが結構ありますが、SELECT文の評価順序を意識すると覚えやすいかと思います。