
Oracle SQLチューニング(第36回)「任意のデータを活用した効率的なSQLの作成」(2/2回)
今回は、「任意のデータを活用した効率的なSQLの作成」二回目になります。早速、始めましょう。
9.2.1 データ変換時CONNECT BY LEVELを使う
以下のSQLを実行すると、抽出結果は1行で表されます。 しかし、読みやすさを高めるために横列を縦列に変換して抽出したい場合があるでしょう。 このような場合、SQLをどのように書くべきですか? まず、水平列のデータを抽出するSQLを見てみましょう。
上記のSQLで行データ(横列)を列データ(縦列)に置き換えるには、Connet By LevelとDECODEを使用すると処理できます。 SQL作成の鍵は、結果件数だけLEVELを指定しなければならないという点と、結果件数だけDECODEを使用して作成しなければならないということです。 したがって、水平に出力されるデータを縦に出力できるように作成するSQLは、以下のように作成できます。
9.2.2 データ重複照会の除去
今回は任意のデータを生成して、同一データに対する重複照会を除去する方法に対して調べてみるようにします。 テストに先立ち先にテスト スクリプトを実行することにしましょう。
Script. テスト データ生成用
< CONNECT_BY_LEVEL_SUM >
■テーブル生成
以下のSQL[1]は、月(系)と日(系)データをUNION ALLを用いて、同一データを2回読み込み、UNION ALLの上段には月(系)を、下段には日(系)を抽出する SQLです。 ここで問題となる部分は、同じデータを2回読み取らなければならないという点です。
9.2.2.1 SQL[1]. 同一データ重複照会による非効率発生SQL
前述のSQL [1]で同じデータを2回読み取る非効率性を排除することはできませんか?
SQL の 2 つのテーブルが結合すると、結合条件が存在しない場合に Cartesian Product が発生します。 このときデータがM*Mに複製されますが、このような点を利用して、元のデータを所望だけ複製し、同じデータを何度も繰り返し読み取ることなく処理することができます。
上記の説明がわかりにくい可能性があるため、Cartesian Productが発生した場合にデータがどのように複製されるかを簡単に調べてみましょう。
テーブルAにはXという列のデータが21、22、23存在し、BテーブルにはY列のデータが1、2存在するとする。 Cartesian Productが発生すると、3×2で合計6件のデータが抽出されます。
結合後の結果を見ると、Y値(Bテーブルの値)に基づいてX値(Aテーブルの値)の全体が複製されたことがわかります。 したがって、Y値が1のものについては月(計)を求め、Y値が2のものについては日(計)を求めればテーブルを一度だけ読み込んでも処理できるようになります。
Connet By Levelが存在しない当時は、Cartesian Productを発生させてデータを複製するために別々のCOPYテーブルを作成したり、ディクショナリビューなどを用いて作成しました。 しかし、Oracle 10g以降からはCOPYテーブルの代わりに、以下のようにConnect By Levelを利用すれば追加のI/Oなしで処理できます。
9.2.2.2 SQL[2]. 同一データ重複照会を除去した効率的なSQL
9.2.3 インデックス処理の負荷が多い場合
9.2.3.1 任意の数字データ生成後使用する
任意の数字データを生成して、性能を改善したもう一つの事例を通じて問題点を把握して改善案を導き出してみることにしましょう。
[性能問題SQL]
[性能問題SQL]が本当非効率が発生するのか、非効率が発生するならばどの程度なのか調べてみるためにV$SQLAREAで実行内訳を問い合わせてみましょう。
[SQL実行内訳[HASH VALUE:450651061]]
テーブル性格:パーテーション テーブル
パーテーション キー:part_id
パーテーション キー値:00 ~ 99 (合計100個、パーテーション個数は固定)
インデックス構成情報:TB_SLP_IX1 - part_id,status
[性能問題SQL]はSQL実行内訳のうちROWS(総抽出件数)/EXEC(総実行回数)を計算すれば33,311/9,318 = 3.57件で平均4件未満のデータを抽出したことが分かりました。
もし、インデックスが存在せず非効率が発生したわけではないことを確認するために実行計画を確認してみると、Full Table ScanではなくIndex Range Scanで行われており、特に問題がないように見えます。 しかし、[性能問題SQL]の実行履歴結果を見ると、平均4件未満を抽出するために発生する平均I/O処理量(BUFFER GET(1))が140,056ブロックで、抽出件数に比べて非常に多くなります。
性能問題SQL]の実行内訳と関連情報を分析した結果、非効率はインデックス構成と照会条件との関係による問題であることを分かることができました。
それならなぜこのような非効率が発生したのか調べてみましょう。 Where節のPART_ID >=:B0 AND PART_ID <=:B1条件はパーテーション テーブルのパーテーション範囲を現わすことであり、STATUS IN (‘I’,’T’)はデータの状態をチェックする条件です。
ところがSTATUS条件に対するデータがごく少数であるが、インデックスの先頭カラムであるPART_IDが範囲条件で行われ、STATUS条件がIndex Access predicate条件として使用されず、Index Filter Predicateで行われました。 したがって、少ないデータを抽出するにもかかわらず、大量のI/O処理を行うことになったのです。
まとめると、索引から PART_ID 列を照会条件で範囲検索を行った後、 PART_ID 条件で抽出された多くのデータについて、表アクセスをする前の索引の末尾列にある STATUS 列に対する条件を Index Filter Predicate で行い、 ほぼほとんどのデータをろ過していたのです。
PART_IDカラムに値を付与した後SQLのトレース結果を確認してみることにしましょう。
上記のようにインデックスの先頭列を範囲照会する場合、インデックス構成列のうちデータを効果的に減少させるSTATUS条件はIndex Filter Predicateで処理することになります。 このように処理され抽出されたデータは1件ですが、I/Oは561,442ブロックも処理しました。 つまり、インデックスの先頭列であるPART_ID列の条件で抽出された多くのデータでインデックススキャンを行い、抽出件数に比べて多すぎるI/Oを処理するようになったのです。
したがって、SQLのパフォーマンス改善のためには、データを削減できるSTATUS条件がIndex Access Predicateで使用できるようにインデックスの構成を変更したり、範囲検索を行うPART_ID条件に対する照会方法をEQUALやIN方式に変えてパフォーマンスを 改善する方案を考えることができます。
- 改善策 1. インデックス構成情報を変更 ( PART_ID, STATUS STATUS, PART_ID )
- 改善方法 2. インデックスフィルタにならないように範囲検索をINまたは=(EQUAL)照会に変更
改善案1のような方法が可能であれば最も効率的な改善案となるでしょうが、運用業務環境ではインデックス変更が不可能な場合があります。 例えば、PART_ID列だけで照会される場合があり、必然的にPART_IDが先頭列であるインデックスがなければならない場合と、テーブルサイズが大きすぎてインデックスを変更することが負担になる場合などです。
この場合、上記の改善策のうちの2番目の方法を導き出して適用することを検討する必要があります。 PART_IDに対する範囲条件の値を、任意のテーブルにデータを格納した後、それを結合として処理すると、既存のインデックスを使用するより効率的でしょう。 つまり、範囲条件の値をConnect By Levelを使ってデータを生成した後、Where句のPART_IDに対する範囲検索の代わりに結合で処理方式を変えれば、後続のカラムがIndex Access Predicateで行われ、既存の非効率を解消することができます。
例えば、PART_IDの抽出対象データが1〜4までの範囲であれば、Connect By Levelを用いて対応する値1、2、3、4を抽出して結合に変更します。ただし、PART_IDが整数値のみ存在するという仮定が必要です。
それでは[性能問題SQL]の性能を改善してみましょう。
PART_IDの範囲条件値は以下のように生成することができます。
[改善後SQL]は、前述のPART_ID範囲条件値を抽出して結合に変更したSQLです。 SQLのトレース結果を見ると、改善前に比べて多くの性能改善効果があることがわかります。
[改善後SQL]
9.3.2.2 任意の日データ生成後使用する
前述のConnect By Levelを使用して任意の数を作成し、SQLのパフォーマンスを向上させる方法について学びました。 今回は、ランダムな日付データを生成してパフォーマンスを向上させる方法を学びましょう。 テストする前に、まずテストスクリプトを最初に実行しましょう。
Script. テスト データ生成用
■テーブル生成
■データ生成
■各カラムにインデックス生成および統計情報収集
[比較テスト(BETWEEN vs. CONNECT BY LEVEL活用)]
上記のSQLの結果値は、C3列のデータ分布図を意味します。 つまり、C3値の種類は20110101〜20110105までの5つで、それぞれ100,000件ずつ存在します。 そして、DUMMY_ ABLE_DATEテーブルのインデックスには、C3 + C2の順になった結合インデックスがあります。 次に、DUMMY_TABLE_ DATEテーブルにインデックスの先頭列であるC3列をBETWEENで照会するときとConnect By Levelを活用して結合として処理する場合のパフォーマンスの違いを比較してみましょう。
テスト[1]. BETWEENビュー
前述の任意の数を生成してパフォーマンスを改善した[パフォーマンス問題SQL]と同様に、テスト[1]もインデックス先頭列C3の条件をBETWEENで比較するよりも結合で処理すればパフォーマンスを改善することができるでしょう。 そのためには、まずConnect By Levelを使用して任意の日付データを次のように生成する必要があります。
任意の日データを生成して、これを以下のテスト[2]とともに結合で処理するならば性能を改善することができます。
テスト[2]. CONNECT BY LEVELを活用した照会
上記のようにSQLを変更すると、既存の2,799ブロックから1,366ブロックに約2倍ほどの性能改善効果があることが分かりました。
以下は、任意の日付データをConnect By Levelで生成し、改善した事例を紹介するようにします。
[パフォーマンス問題SQL]
Note. [性能問題SQL]はWhere節条件の日期間は常に1日で、TB_CDテーブルはトランザクション(UPDATE,DELETE)が頻繁です。
[WHERE節インデックス構成情報]
前述の[パフォーマンス問題SQL]は、インデックスの先頭列がBETWEENと照会され、インデックス構成列のうち先頭列を除いた残りの列条件がIndex Filter Predicateで行われるため、非効率が発生した場合です。 トレース結果を見るとインデックスから抽出されたデータが7,980件であるが、インデックスを読み取ったブロック量は12,928ブロックで抽出結果より多いことが分かります。
このテーブルは、頻繁なトランザクション(UPDATE、DELETE)のためにすでに非効率的にインデックスブロックが増えており、SEQ列がIndex Filter Predicateで実行され、多くのブロックを読み取ることになっています。 INDEX REBUILD を実行して不要なインデックスブロックを整理してくれれば、非効率が一時的に減少するが、ある程度時間が流れれば、また再び性能問題が発生するでしょう。
したがって、BETWEENで照会された日付列を任意のデータを生成した後に結合に置き換え、SEQ列の条件をIndex Access Predicateで処理できるようにしてパフォーマンスを向上させる必要があります。 日付データを持つ列のデータ型がCHAR、VARCHAR2であり、列のデータが年月日データ(YYYYMMDD)の形であれば、BETWEEN条件に対して任意の日付データを生成してテーブル結合に置き換えて処理することができます。
次の[改善後SQL]は、Connect By Levelで任意のデータを生成した後、日付の範囲条件をなくし、任意のデータを生成したインラインビューと結合で処理するように変更することでパフォーマンスが大幅に改善されました。
[改善後SQL]
これまでWhere句にある条件のうちインデックス末尾カラム条件が効率的であるにもかかわらず、先頭カラムが範囲条件で実行され、非効率が発生した場合に任意のデータを抽出した後、これを結合に置き換えてパフォーマンスを改善する方法について調べてみました。
追加のI / O(10gから)を必要としないDUALテーブルとConnect By Levelを使用して任意のデータを抽出してパフォーマンスを向上させる方法は、さまざまな状況で便利に使用できるため、必ず理解して積極的に活用して効率的なSQLを作成 するのに役立つことを願っています。
二回にわってお送りしました「任意のデータを活用した効率的なSQLの作成」は、いかがでしたでしょうか?
次回からは、「カラム変形によるSQL性能問題の理解する」と題しまして、二回シリーズでお送りします。ご期待ください。では、See You ^^