catch-img

Oracle SQLチューニング Season2(第17回) 第3章 情報分析による性能改善事例

 今回のSQLチューニング 2nd Season(第17回)は「SQL チューニングのための情報分析方法」の最終回として「情報分析による性能改善事例」について解説していきます。


3.4 情報分析による性能改善事例

 ここまで、SQL性能改善のために必要な情報を把握するための方法について紹介してきました。
第17回となる今回のブログでは、先に紹介したSQLの実行情報を把握する方法、SQLを構成するObjectの構成情報を把握する方法、SQLの業務情報を分析する方法など、性能改善時に必要なすべての情報を簡単な事例を挙げながら解説していこうと思います。

■ 下記のような 日別販売量を抽出するクエリ を例に挙げてみましょう。

[実行SQL]実行

SELECT time_id、
       SUM( amount_sold )
FROM SALES_IN
WHERE time_id >= TO_DATE( :from_date , 'yyyy-mm-dd' )
AND time_id <= TO_DATE( :to_date , 'yyyy-mm-dd' )
GROUP BY time_id;

[SQL Bind値]

From_date = '2011-01-20'
To_date = '2011-01-20'


3.4.1 STEP1.SQL実行情報を分析する

 上記のSQLの性能問題が提起された場合、SQLの実行情報を先に把握する必要があります。
実行計画を抽出する方法には、先に紹介した10046 Trace、DBMS_XPLAN、DBMS_SQLなどがありますが、ここではMS_XPLANを使用することにします。

■ DBMS_XPLAN情報を抽出するために必要なヒント /*+ gather_plan_statistics */ を追加して実行情報を抽出します。

SELECT /*+ gather_plan_statistic */
       time_id,
       SUM( amount_sold )
FROM   SALES_IN
WHERE  time_id >= TO_DATE( :from_date , 'yyyy-mm-dd' )
AND    time_id <= TO_DATE( :to_date , 'yyyy-mm-dd' )
GROUP BY time_id;

SELECT * 
FROM   TABLE( dbms_xplan.display_cursor( NULL , NULL , 'allstats last' ) ); 

------------------------------------------------------------------------------------------
| Id  | Operation           | Name     | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |          |      1 |        |      1 |00:00:00.09 |    4441 |
|   1 |  HASH GROUP BY      |          |      1 |     50 |      1 |00:00:00.09 |    4441 |
|*  2 |   FILTER            |          |      1 |        |    327 |00:00:00.09 |    4441 |
|*  3 |    TABLE ACCESS FULL| SALES_IN |      1 |     50 |    327 |00:00:00.09 |    4441 |
------------------------------------------------------------------------------------------ 
Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter(TO_DATE(:FROM_DATE,'yyyy-mm-dd')<=TO_DATE(:TO_DATE,'yyyy-mm-dd'))
   3 - filter(("TIME_ID">=TO_DATE(:FROM_DATE,'yyyy-mm-dd') AND "TIME_ID"<=TO_DATE(:TO_DATE,'yyyy-mm-dd')))

 実行計画を実行順に見ていくと、ID番号3->2->1->0の順に解釈することが可能です。
 SALES_INテーブルをTABLE FULL SCANを利用してテーブルの総ブロックである4,441 Blockを全て読み込みました。
 その後、全体の抽出件数のうちWhere条件であるWHERE time_id >= TO_DATE( :from_date , ‘yyyy-mm-dd’ ) AND time_id <= TO_DATE( :to_date , ‘yyyy-mm-dd’ )を満たすデータのみFilteringして327件を抽出し、抽出件数327件に対してTime_idでGroupingして最終抽出件数1件をFetchしました。


3.4.2 STEP2. Index構成情報の確認実行計画を見ると、OptimizerはSQLのデータを抽出するために

TABLE FULL SCANを選択しました。
 Optimizerが選択した実行計画が最も効率的なのか、改善できる方向があるかどうかをここで考えてみる必要があります。
 一番最初に考えなければならない部分は、TABLE FULL SCANが果たして効率的な方法であるのかどうか?を考えてみる必要があります。
 当該SQLのWhere節のカラムにIndexが存在する場合、Index ScanとTABLE FULL SCANのどちらがより効率的なのかを調べてみることが必要です。まず、SQLの検索条件カラムにIndexが存在するかどうかについて確認してみましょう。

■ ここでは『 Index構成情報を把握する 』の回で活用したスクリプトを使って確認してみましょう。

SELECT  CASE WHEN column_position = 1 THEN index_name ELSE '' END index_name
     , CASE WHEN column_name like 'SYS%'
            THEN replace(sys.xm_fn_long_to_char(table_owner
                                          , table_name
                                          , index_name
                                          , column_position),'','')
            ELSE column_name END column_name
FROM (
SELECT *
       FROM   dba_ind_columns
       WHERE  table_name  = upper(:v_tname)
       AND    index_owner = upper(:v_own)
       ORDER BY index_name, column_position
     );

INDEX_NAME                       TYPE U COLUMN LIST
-------------------------------- ---- - ------------------------------------------------------
SALES_IN_01                      NORM N CUST_ID
SALES_IN_02                      NORM N PROD_ID

当該テーブルのIndexを見てみると、当該カラムにはIndexは存在していないことを確認することができました。


3.4.3 STEP3.Where節条件カラムの効率確認

 SALES_INテーブルに該当カラムのIndexが存在しないため、OptimizerはTABLE FULL SCANを選択しました。
 もしそうであるならば、そのカラムにIndexが存在する場合、更に効率的なのかどうか?について考えてみましょう。

■ Indexを生成する前にいつもそのカラムの効率を確認する必要があります。

SELECT column_name,
 data_type,
 data_length,
 decode(data_precision  || '/' || data_scale,'/',null,data_precision  || '/' || data_scale) dpds,
 nullable n,
 num_distinct nfv,
 density,
 num_nulls,
 num_buckets,
 sample_size,
 to_char(last_analyzed,'yyyy-mm-dd')  last_anal
FROM   all_tab_columns
WHERE  owner = upper(trim(':schname'))
AND    table_name = upper(trim(':tname'));

COLUMN_NAME      DATA_TYPE    LEN SCAL N        NFV      DENSITY  NUM_NULLS BUCKET SAMPLE_SIZE LAST_ANAL
---------------- ---------- ----- ---- - ---------- ------------ ---------- ------ ----------- ----------
PROD_ID          NUMBER        22      N         72  0.013888889          0      1    918843 2015-06-25
CUST_ID          NUMBER        22      N       7059  0.000141663          0      1    918843 2015-06-25
TIME_ID          DATE           7      N       1460  0.000684932          0      1    918843 2015-06-25
CHANNEL_ID       NUMBER        22      N          4  0.250000000          0      1    918843 2015-06-25
PROMO_ID         NUMBER        22      N          4  0.250000000          0      1    918843 2015-06-25
QUANTITY_SOLD    NUMBER        22 10/2 N          1  1.000000000          0      1    918843 2015-06-25
AMOUNT_SOLD      NUMBER        22 10/2 N       3586  0.000278862          0      1    918843 2015-06-25

 カラムの統計情報を基にカラムの効率を判断した場合、918843/1460 = 約629件でテーブル全体を全て照会するよりは効率的であると判断されます。その場合、そのカラムでIndexを生成することを考慮してみることができます。
 Note. カラムの効率を判断する基準は、テーブル全体の件数/カラムNDV(Number of Distinct Value)でその効率を判断することができます。(本文で計算された値は、1日のデータが照会された場合に限る)。


3.4.4 STEP4. Bind Pattern分析

 Index生成時に効率的であることをSALES_INテーブルの統計情報照会で確認しました。
 では、TIME_IDカラムで照会する場合、常にIndex Scanが効率的なのでしょうか? 
 この質問に対する答えは、Bind Patternを把握した場合のみ正確な答えが可能です。
 Bind値が入力されるTime_id条件に1日ではなく1ヶ月あるいはそれ以上のデータが照会されると、先に計算したカラムの効率性かどうかに大きなエラーが発生する可能性があるからです。
 現在実行されたバインド値は、1日範囲のデータ値を抽出するものですが、常に1日間のデータだけを照会しているかどうか?については、バインド値のパターンを確認しなければ正確に知ることができません。

日付区間の検索条件は業務担当者が一番よく知っていますが、Oracleはdba_hist_sqlbind、dba_hist_snapshotを照会すると、特定期間中のBind Patternを照会することができます。

SELECT b.datatype_string 、
        b.name 、
        b.value_string 、
        b.last_captured
FROM dba_hist_sqlbind b 、
       dba_hist_snapshot sn
WHERE sn.snap_id = b.snap_id
AND sql_id = :sql_id
ORDER BY b. last_captured DESC

DATATYPE NAME VALUE_STRING LAST_CAPTURED
--------------- ----------- ------------ ------------------------
VARCHAR2(2000) :FROM_DATE 20140120 2015-06-25 1:42:49 午後 1:42:49
VARCHAR2(2000) :TO_DATE	20140120 2015-06-25 1:42:49 午後 1:42:49
...省略
VARCHAR2(2000) :FROM_DATE 20140101 2015-06-25 午後 1:42:49
VARCHAR2(2000) :TO_DATE 20140131 2015-06-25 午後 1:42:49

 Bind Patternを見ると、日照会だけでなく月照会も行われ、合計2種類のBind Patternが存在することが分かりました。
 月照会が実行された場合の効率は、先に日照会の効率を計算した値に約30を掛ける必要があります。
 その値は約18,870で、Index生成に関して決して効率的ではない数値であることが分かりました。


3.4.5 STEP5.月次照会のFTS効率確認

 Bind Patternから抽出された情報を通じて、日または月ごとに照会されるSQLであることを確認しました。
 日別照会はIndexを生成することで解決が可能ですが、月別照会はIndex Scanの時に非常に非効率的だと思います。月単位の抽出件数が多く、それだけtable random accessの回数も増えるからです。

 上のSQLで1ヶ月に該当する件数が14,248件であることが確認できます。
 Indexを活用する場合、Index Read Blockを除いてもTable Random Accessの回数は最大14,248 Block(Clustering factorが非常に悪い場合)を読むことができます。
 そうすると、月次照会である場合には、Index ScanよりTable Full Scanが性能上有利になります。
 先ほど、Table Full Scan時の総Block Read量は4,441ということで比較が可能です。
 したがって、日照会の場合はIndex Scanで、月照会の場合は既存のAccess方式であるTable Full Scanで実行されるようにSQLを分岐することが効率的でしょう。(※この部分については、Step9で詳しく説明します。)
 月次照会が行われる場合、Table Full Scanが効率的であると判断されたからといってすぐに適用するよりも、Segment Sizeのトピックで言及したように、Table Full Scanを実行する場合にSegment Sizeの増加量を確認する手順が必要となります。
 現時点では、Table Full Scanを実行することが有利ですが、該当SegmentのSizeの増加傾向が徐々に増加したり、特定の業務状況に応じて指数関数的に増加する可能性がある場合、近い将来にパフォーマンス問題を引き起こす主な原因として作用する可能性があるからです。

■ それでは、テーブルのSegmentのSizeの増加傾向を見てみましょう。

SELECT TO_CHAR( begin_interval_time , 'yyyy' ) gatharing_date ,
       object_name  ,
       Round(MAX(space_used_total/1024/1024),1) MB
FROM   dba_hist_seg_stat s ,
       dba_hist_seg_stat_obj o ,
       dba_hist_snapshot sn
WHERE  o.owner = :owner
AND    s.obj# = o.obj#
AND    sn.snap_id = s.snap_id
AND    object_name = :obj_name
GROUP BY  TO_CHAR( begin_interval_time , 'yyyy’ ) , object_name
ORDER  BY gatharing_date  asc

GATHARING_DATE	OBJECT_NAME        MB
---------------    ------------ ---------
2011       	   SALES_IN     15.1M  
2012       	   SALES_IN     20.2M  
2013       	   SALES_IN     27.1M  
2014       	   SALES_IN     36.6M  

 SegmentのSizeの増加傾向を確認した結果、テーブルSizeがどんどん増加することが確認できます。
 このような場合、今は性能問題が発生しないかもしれませんが、時間が経つほどテーブルSizeが増加するため、性能問題が発生する可能性があります。
 Note. Index Clustering Factorとは? テーブルにデータがロードされている順序がIndexのソートされた順序とどれだけ一致するかを示す数値です。Indexとテーブルのソート順序が非常に良い場合、Index Clustering FactorはBlock数とほぼ一致し、最悪の場合はテーブルの全体件数とほぼ一致します。Index Clustering Factorが良いほど、範囲が広いIndex Range Scanをする場合、Index Lookupを通じて発生するI/O量が大きく減少します。


3.4.6 STEP6.てーぶるじょうほうかくにん月単位での照会が行われるので、Time_IDカラムでPartition Tableへの移行を検討する必要があります。

 Partition Tableに転換すれば、全体Segmentを読む必要がなく、該当するPartition Table(Bin3.4.6 STEP6.てーぶるじょうほうかくにんすることができるからです。
 その前にSALES_INテーブルがパーティションテーブルかどうかを確認する手順が先行されなければならないでしょう。テーブルのパーティションかどうか確認してみましょう。

TABLE_NAME OWNER TAB_S LREAD PREAD LR_PR_RATE PART_TYPE PART_COL PART_CNT
------------ ---------- ----- ---------- ---------- ---------- --------- --------
SALES SH 36.9M 958394839 854954     

確認の結果、パーティションは構成されていないことがわかりました。


3.4.7 STEP7.アクセスパターン情報の確認

 STEP6.段階を通じてSALES_INテーブルはNon-Partition Tableとして確認されました。

 Partition Tableへの転換を検討する際、そのテーブルを照会するSQL照会パターン、つまりAccess Patternを確認しなければなりません。
 Partition移行時、Partition Keyとして使用するカラムTime_idがSALES_INテーブルを照会するすべての業務で条件として使用されるかどうかが非常に重要となってくるからです。
 これが重要となる理由としては、Time_id条件がない場合において、生成された全てのPartition TableをAccessしなければならない非効率が発生するからです。


SALES_IN テーブルの Access Pattern 照会結果


 Access Patternを確認した結果、SALES_INテーブルの照会パターンは3つのタイプに分類されたことがわかります。
 1つ目はPROD_ID、TIME_ID、2つ目はCUST_ID、TIME_ID、3つ目はTIME_IDで照会するパターンです。
 SALES_INテーブルを照会するすべてのSQLはTIME_IDで照会されることを確認しました。
したがって、TIME_IDカラムでパーティションキーに選定することが大きな無理がないことをAccess Patternを通じて結論付けることができます。また、既存のIndexにTIME_IDカラムを追加して結合Indexで構成することも検討することができます。
 SALES_IN_01とSALES_IN_02 IndexにTIME_IDを追加して結合Indexを生成すれば、Indexの効率が上がるからです。


3.4.8 STEP8.Partition構成とIndex作成

 Access Patternを確認後、更にはIndexとパーティション構成についても確認した上で、該当するテーブルをパーティションテーブルに変更してから、以下のようなIndexを生成します。

CREATE TABLE SALES_IN 
(
PROD_ID                        NUMBER ,                                                                                    
CUST_ID                        NUMBER ,                                                                                    
TIME_ID                        DATE   ,                                                                                    
CHANNEL_ID                     NUMBER ,                                                                                    
PROMO_ID                       NUMBER ,                                                                                    
QUANTITY_SOLD                  NUMBER ,                                                                                    
AMOUNT_SOLD                    NUMBER
)
 PARTITION BY RANGE( time_id )(
 PARTITION sales_2011_1 VALUES less than (to_date('2011-02-01','YYYY-MM-DD')),
 PARTITION sales_2011_2 VALUES less than (to_date('2011-03-01','YYYY-MM-DD')),
 PARTITION sales_2011_3 VALUES less than (to_date('2011-04-01','YYYY-MM-DD')) 
 -- 省略
 PARTITION sales_2015_1 VALUES less than (to_date('2015-02-01','YYYY-MM-DD')),
 PARTITION sales_2015_2 VALUES less than (to_date('2015-03-01','YYYY-MM-DD')),
 PARTITION sales_2015_3 VALUES less than (to_date('2015-04-01','YYYY-MM-DD')), 
 PARTITION sales_2015_4 VALUES less than (to_date('2015-05-01','YYYY-MM-DD')),
 PARTITION sales_2015_5 VALUES less than (to_date('2015-06-01','YYYY-MM-DD')),
 PARTITION sales_2015_6 VALUES less than (to_date('2015-07-01','YYYY-MM-DD'))  
 );

CREATE INDEX sales_time_idx01 ON SALES_IN (time_id) LOCAL;
CREATE INDEX sales_cust_idx01 ON SALES_IN (cust_id, time_id) LOCAL;
CREATE INDEX sales_prod_idx01 ON SALES_IN (prod_id, time_id) LOCAL;


3.4.9 STEP9.SQLブランチによる該当SQLパフォーマンスの確認

 パーティションの構成とIndexの生成が完了したら、前述したように日別、月別照会パターン別にSQLを分岐して照会するようにしなければなりません。日照会パターンである場合には、Indexを活用し、月照会パターンである場合には、その区間のパーティションだけを全体照会するようにSQLを作成するようにしましょう。

 なぜ上記のSQLを分岐するのか?についての核心は、to_dateからfrom_dateのBind変数を引いて抽出された結果が28より小さい場合は日照会、28より大きいか同じ場合は月照会に分けることができます。

 SQL実行時、一番最初に実行される部分がBind変数を計算することなるため、日照会の場合には、日照会に該当するSQLのみを実行し、月照会を抽出する部分はFalseなので、該当するSQLを実行せずに終了します。
 もちろん、逆の場合も同様です。
 このように照会パターン別にSQLを分岐して照会すれば、日、月照会ともに最適な実行計画でデータを照会することができます。
 今回のテーマを通じて、単純なSQLの性能改善をする過程においても、こんなにも多くの考慮事項と知っておくべき情報が多いことをここでは理解してほしいと思います。
 短文SQLのチューニングは、単純にテーブル情報、Index情報など最小限の情報だけでも性能改善が可能ですが、深く考えれば考えるほど、もっと把握しなければならない情報が存在します。
 結局、完璧な性能改善のためにはそれだけの時間と労力が必要となってくるのです。
 この章の締めくくりとして、以下の文章を掲載しておきます。
 「SQLの意味を把握し、それによって様々な情報を収集して性能改善作業に取り組む人の結果と、単に実行計画だけを見て独自のノウハウで性能改善作業をする人の結果は、どちらが信頼できるでしょうか?」
→ この答えはこれまでのブログを読んでいただければ、自ずとお分かりいただけると思います。

 さて、4回シリーズでお送りしてまいりました「SQL チューニングのための情報分析方法」は今回で終了です。
 次回からは新シリーズとして「SQL TuningとHINTの関係」をテーマに8回シリーズでお送りしていきます。





    次回のSQLチューニングブログは

    新シリーズ

    「 SQL TuningとHINTの関係 」(1/8)について

    CONTACT

    他社に頼らず自社でデータベースを監視・運用をしませんか?
    MaxGaugeがサポートします

    お役立ち資料は
    こちらから

    不明点がある方は、
    こちらからお問い合わせください

    お電話でのお問い合わせはこちら

    平日 10時~18時

    人気記事ランキング

    タグ一覧