web-dev-qa-db-ja.com

クエリのコストが大きく変動する理由とそれを防ぐ方法

私に問題を引き起こしているクエリは大きすぎますが、コアであると思われる部分は非常に単純です。私はそれを試してみます:

クエリの構造は次のとおりです。

_SELECT
    ... a lot of stuff ...
WHERE
    ... lots of complex clauses ...
    AND my_id in ( select my_id from small_table where .. something simple ..)
_

そして、それは数十億のカーディナリティ、約12GBのバイトの読み取りを持っていますと私の一時を爆発させます。

ただし、(常に)7つのレコードを生成する_select my_id from small_table_を実行する場合、これらのレコードを取得してクエリを次のように変更します。

_SELECT
    ... a lot of stuff ...
WHERE
    ... lots of complex clauses ...
    AND my_id in ( 1, 2, 3, 4, 5, 6, 7 )
_

(つまり、値はハードコーディングされています)

コスト、カーディナリティ、読み取りバイト数は劇的に減少し、クエリは数分で実行されます。

今、私はwith句で「小さなクエリ」を分離しようとしましたが、サブクエリではなく結合を使用しようとしましたが、結果は常に同じです。

なぜそれがこのようになり、どうすればそれを防ぐことができるでしょうか?

両方のケース(高速と低速)でクエリのコストがかかる部分は、結合で使用される大きなテーブルの1つのFTSであることを言及する価値があるかもしれません。

また、私はOracle 11gR2を使用しています

[編集]これらは、2つの実行例の説明プランです

悪いもの。私がin ( )を使用していないことに注意してください。_small_table_をfrom句に追加する単純な結合です。

_Plan
SELECT STATEMENT  ALL_ROWSCost: 5,736,441                                   
    22 HASH JOIN RIGHT SEMI  Cost: 5,736,441  Bytes: 52,324,480  Cardinality: 158,080                               
        11 VIEW VIEW VW_NSO_1 Cost: 20  Bytes: 13  Cardinality: 1                           
            10 NESTED LOOPS                         
                8 NESTED LOOPS  Cost: 18  Bytes: 91  Cardinality: 1                     
                    6 NESTED LOOPS  Cost: 4  Bytes: 70  Cardinality: 1                  
                        4 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.SMALL_TABLE Cost: 1  Bytes: 35  Cardinality: 1           
                            3 INDEX UNIQUE SCAN INDEX (UNIQUE) MYUSER.PK_SMALL_TABLE Cost: 0  Cardinality: 1        
                                2 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.SMALL_TABLE Cost: 2  Bytes: 27  Cardinality: 1   
                                    1 INDEX RANGE SCAN INDEX MYUSER.IDX_SMALL_TABLE_1ATUAL Cost: 1  Cardinality: 6  
                        5 TABLE ACCESS FULL TABLE MYUSER.SMALL_TABLE Cost: 3  Bytes: 35  Cardinality: 1             
                    7 INDEX RANGE SCAN INDEX (UNIQUE) MYUSER.PK_MEDIUM_TABLE Cost: 1  Cardinality: 53               
                9 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.MEDIUM_TABLE Cost: 14  Bytes: 420  Cardinality: 20                   
        21 HASH JOIN  Cost: 5,736,193  Bytes: 15,281,925,342  Cardinality: 48,056,369                           
            19 NESTED LOOPS                         
                17 NESTED LOOPS  Cost: 951,151  Bytes: 500,185,440  Cardinality: 1,736,755                      
                    15 NESTED LOOPS  Cost: 3  Bytes: 792  Cardinality: 22               
                        13 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.SMALL_TABLE Cost: 2  Bytes: 27  Cardinality: 1              
                            12 INDEX RANGE SCAN INDEX MYUSER.IDX_SMALL_TABLE_1ATUAL Cost: 1  Cardinality: 6         
                        14 INDEX RANGE SCAN INDEX (UNIQUE) MYUSER.PK_MEDIUM_TABLE Cost: 1  Bytes: 477  Cardinality: 53              
                    16 INDEX RANGE SCAN INDEX MYUSER.IDX_HUGE_TABLE_1 Cost: 18,849  Cardinality: 1,322,763                  
                18 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.HUGE_TABLE Cost: 413,818  Bytes: 19,620,720  Cardinality: 77,860                    
            20 TABLE ACCESS FULL TABLE MYUSER.HUGE_TABLE Cost: 3,958,129  Bytes: 12,063,594,150  Cardinality: 402,119,805  
_

良いもの。ここで行ったのは、_select my_id from small_table where .. something simple .._(7つのレコード)の結果を取得し、my_id in ( 1, 2, 3, 4, 5, 6, 7)を大きなクエリの最後に追加することだけです。説明と同じ:

_Plan
SELECT STATEMENT  ALL_ROWSCost: 4,558,125                                               
    18 HASH JOIN  Cost: 4,558,125  Bytes: 36,100,625  Cardinality: 122,375                                              
        16 NESTED LOOPS                                         
            14 NESTED LOOPS  Cost: 413,809  Bytes: 5,271,671  Cardinality: 122,597                                      
                12 VIEW VIEW VW_NSO_1 Cost: 20  Bytes: 13  Cardinality: 1                               
                    11 HASH UNIQUE  Bytes: 91  Cardinality: 1                           
                        10 NESTED LOOPS                         
                            8 NESTED LOOPS  Cost: 18  Bytes: 91  Cardinality: 1                     
                                6 NESTED LOOPS  Cost: 4  Bytes: 70  Cardinality: 1                  
                                    4 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.SMALL_TABLE Cost: 1  Bytes: 35  Cardinality: 1           
                                        3 INDEX UNIQUE SCAN INDEX (UNIQUE) MYUSER.PK_SMALL_TABLE Cost: 0  Cardinality: 1        
                                            2 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.SMALL_TABLE Cost: 2  Bytes: 27  Cardinality: 1   
                                                1 INDEX RANGE SCAN INDEX MYUSER.IDX_SMALL_TABLE_1ATUAL Cost: 1  Cardinality: 5  
                                    5 TABLE ACCESS FULL TABLE MYUSER.SMALL_TABLE Cost: 3  Bytes: 35  Cardinality: 1             
                                7 INDEX RANGE SCAN INDEX (UNIQUE) MYUSER.PK_MEDIUM_TABLE Cost: 1  Cardinality: 53               
                            9 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.MEDIUM_TABLE Cost: 14  Bytes: 420  Cardinality: 20                   
                13 INDEX RANGE SCAN INDEX MYUSER.IDX_HUGE_TABLE_1 Cost: 18,849  Cardinality: 1,322,763                                  
            15 TABLE ACCESS BY INDEX ROWID TABLE MYUSER.HUGE_TABLE Cost: 413,788  Bytes: 3,677,910  Cardinality: 122,597                                    
        17 TABLE ACCESS FULL TABLE MYUSER.HUGE_TABLE Cost: 3,962,804  Bytes: 3,655,562,652  Cardinality: 14,506,201  
_

それが役に立てば幸い。

3
filippo

ほとんどの場合、テーブルには最新の統計情報がありますが、オプティマイザはカーディナリティの推定を単純化しすぎるために困惑することがあります。

これは動的サンプリングの良い候補のようです。デフォルト値( 10gと11gの2 )では、動的サンプリングのみが使用されます テーブルの1つに統計がない場合 。あなたのケースでは、オプティマイザが統計を収集してより良い計画を構築できるように、その値を変更する必要があります。

DYNAMIC_SAMPLING 単一のクエリのオプティマイザの動作を変更できるヒント。私はサブクエリでテストしましたが、次の構文のいずれかを使用する必要があります。

  • 完全なヒントをクエリの一番上に直接入力すると、すべてのテーブルがサンプリングされます。これは確実に機能しますが、時間がかかりすぎる可能性があります。

    SELECT /*+ DYNAMIC_SAMPLING (10) */ FROM ...
    
  • サブクエリの完全なヒント:

    SELECT
      ... a lot of stuff ...
    WHERE
      ... lots of complex clauses ...
      AND my_id in ( select /*+ DYNAMIC_SAMPLING (10) */ my_id 
                       from small_table 
                      where .. something simple ..)
    
  • クエリブロック名のヒント:

    SELECT /*+ DYNAMIC_SAMPLING (@my_block 10) */
      ... a lot of stuff ...
    WHERE
      ... lots of complex clauses ...
      AND my_id in ( select /*+ QB_NAME(my_block) */ my_id 
                       from small_table 
                      where .. something simple ..)
    

2番目と3番目のオプションは同じ結果を生成するはずです:1つのテーブルでのみサンプリングします。

2
Vincent Malgrat

良い計画と悪い計画の主な違いは、悪い計画は大きなテーブルからほとんど/すべてのデータを返すと予想されるのに対し、良い計画はその約5%を返すと予想されることです。

ハードコードされた値がこの違いを生む理由は、オプティマイザが探している値を明示的に知っているためです。別のテーブルに結合すると、予想される行数がわかりますが、必ずしもそれらの行の値であるとは限りません。

次の例では、RW値が1〜9の小さなテーブルがあり、VAL = 'N'RW = 10VAL = 'Y'があります。大きなテーブルは小さなテーブルにリンクしていますが、行の大部分はRW値10を持っています。大きなテーブルで一般的でないRW値(1〜9)を見つけたい場合は、小さなテーブルに結合できます。これらの行にはVAL = 'N'が含まれるか、数値1〜9が明示的にリストされます。どちらの場合も、大きなテーブルから9行だけが返されることを期待しています。

結合された場合、オプティマイザはSMALL_TABに対するクエリが9行を返すことを認識しますが、これらがLARGE_TABの値にどのようにリンクしているかを正確に判断できません。その結果、これは、9行のインデックス範囲スキャンではなく、予想される10,000行に対して全テーブルスキャンを実行します。

create table small_tab as
select rownum rw, decode(rownum, 10, 'Y', 'N') val
from   dual
connect by level <= 10;

create table large_tab as
select case when rownum >= 10 then 10 else rownum end rw, --9990 rows have same value
        dbms_random.string('x', 20) filler
from   dual
connect by level <= 10000;

create index sm_i on small_tab(rw);
create index lg_i on large_tab(rw);

--calc stats, SIZE 10 to ensure we get histograms on the large table
exec dbms_stats.gather_table_stats(user, 'small_tab', cascade => true);
exec dbms_stats.gather_table_stats(user, 'large_tab', method_opt => 'FOR ALL COLUMNS SIZE 10', cascade => true);

explain plan for 
SELECT * FROM large_tab l
where  l.rw in (select s.rw from small_tab s where val = 'N');

SELECT * FROM table(dbms_xplan.display(NULL, NULL, 'BASIC +ROWS'));
-- the optimizer doesn't know how the SMALL_TAB.RW values returned by the
-- subquery match back to LARGE_TAB.RW, so assumes we need to full scan the whole table
--------------------------------------------------
| Id  | Operation            | Name      | Rows  |
--------------------------------------------------
|   0 | SELECT STATEMENT     |           |  5000 |
|   1 |  HASH JOIN RIGHT SEMI|           |  5000 |
|   2 |   TABLE ACCESS FULL  | SMALL_TAB |     5 |
|   3 |   TABLE ACCESS FULL  | LARGE_TAB | 10000 |
--------------------------------------------------

explain plan for 
SELECT * FROM large_tab
where  rw IN (1, 2, 3, 4, 5, 6, 7, 8, 9);

SELECT * FROM table(dbms_xplan.display(NULL, NULL, 'BASIC +ROWS'));
--because we've explicity asked for values, these can be directly compared 
--against the stats information and the optimizer knows we should expect 9 rows.
---------------------------------------------------------- 
| Id  | Operation                    | Name      | Rows  | 
---------------------------------------------------------- 
|   0 | SELECT STATEMENT             |           |     9 | 
|   1 |  INLIST ITERATOR             |           |       | 
|   2 |   TABLE ACCESS BY INDEX ROWID| LARGE_TAB |     9 | 
|   3 |    INDEX RANGE SCAN          | LG_I      |     9 | 
---------------------------------------------------------- 

これを修正するために、クエリが静的で(変更されない)、チューニングパックのライセンスを取得している場合は、SQLチューニングアドバイザーを確認することをお勧めします。これにより、クエリを適切な計画にロックするSQLプロファイルを作成できます。うまくいけば、アドバイザーが自動的に適切なプランを見つけてくれますが、そうでない場合は手動で作成する必要があります。いくつかのリンク:

チューニングアドバイザーパッケージの使用: http://www.Oracle-base.com/articles/10g/automatic-sql-tuning-10g.php SQLプロファイルを手動で作成: http:/ /kerryosborne.Oracle-guy.com/2010/07/sqlt-coe_xfr_sql_profilesql/

チューニングパックのライセンスを取得していない場合は、クエリを再構築し、インデックスを追加して、ヒントをいじる必要があると思います。どの変更がメリットをもたらすかを見つけることは、少し試行錯誤することになります。実際のデータセットにアクセスせずに何をすべきかを正確に述べることは困難です。

1
Chris Saxon

私が働いている場所では、最大のデータベースでCBOにさまざまな問題が発生しています。 11gR1を使用していますが、R2でもオプティマイザは非常に似ていると思いますが、そのバージョンの他のデータベースには目立った問題はないようです。

私はこれを行う/推奨するのが嫌いですが(必要に応じて数回実行しました)、クエリに「ルール」ヒントを追加して、それがどのように機能するかを確認しましたか?

あなたは次のようなことをするでしょう:select/* + rule */...たくさんそしてたくさんのもの;

ただし、CBOで問題が発生しないように、そのクエリを作成する別の方法がおそらくあります。この方法は、ヒントを使用するよりも望ましいでしょう。少なくともヒントを試してみると、それが障害のあるオプティマイザーであるかどうかを確認でき、別の質問をするポイントがあるように見えます。

まだ確認していませんが、R1にはいくつかのオプティマイザの問題を修正するパッチがあり、R2にも同様のパッチが利用できる可能性があります。メタリンクのログイン情報が私のマシンにあります。

0
Brian Efting