web-dev-qa-db-ja.com

インデックスの使用を抑制する外部結合?

あるテーブルを別のテーブルに外部結合するビューに対してクエリを実行しているクライアントプログラムがあります。パフォーマンスが悪いので、適切なインデックスを追加してチューニングしようとしています。問題のクエリは実際には2番目のテーブルのみを使用しているため、そのテーブルに対して直接テストしています。

テーブルに対するクエリで正常に機能する(いくつかの)インデックスを見つけましたが、ビューを使用するように切り替えると、インデックスの使用が停止し、代わりに両方のテーブルでフルスキャンが実行されました。これらのテーブルは大きいので(それぞれ200〜300万行)、これは非常に低速です。

単純にテストするために、クエリを変更してをバイパスし、外部結合をクエリ自体に組み込んだだけです。これで問題は再現しましたが、外部結合でインデックスが使用されない理由がわかりませんでした。

以下は、テスト中に追加したすべてのインデックスを含むテーブルです。

  CREATE TABLE TEST_DATA 
   (ID NUMBER(11,0)  PRIMARY KEY, 
    FORMATTED_RESULT VARCHAR2(255 BYTE), 
    F_RESULT NUMBER, 
    IDNUM NUMBER(11,0), 
    IDNUM_DESCRIPTION VARCHAR2(128 BYTE), 
    LAB_NUMBER NUMBER(11,0), 
    SEQ_NUMBER NUMBER(11,0),
    ORDERNO NUMBER(11,0),
    SUPPL_FORMATTED_RESULT VARCHAR2(255 BYTE), 
    SUPPL_IDNUM NUMBER(11,0), 
    SUPPL_IDNUM_DESCRIPTION VARCHAR2(128 BYTE), 
    SUPPL_UNIT VARCHAR2(16 BYTE)
   ) ;

  CREATE UNIQUE INDEX TEST_LN_SQN_ORDER ON TEST_DATA (LAB_NUMBER, SEQ_NUMBER, ORDERNO) ;
  CREATE INDEX TEST_LN_SQN ON TEST_DATA (LAB_NUMBER, SEQ_NUMBER) ;
  CREATE INDEX TD_CUIDD_CUFR ON TEST_DATA (UPPER(COALESCE(SUPPL_IDNUM_DESCRIPTION,IDNUM_DESCRIPTION)), UPPER(COALESCE(SUPPL_FORMATTED_RESULT,FORMATTED_RESULT))) ;
  CREATE INDEX TD_UFR_IDN ON TEST_DATA (UPPER(FORMATTED_RESULT), IDNUM) ;
  CREATE INDEX TD_UIDD_UFR ON TEST_DATA (UPPER(IDNUM_DESCRIPTION), UPPER(FORMATTED_RESULT)) ;
  CREATE INDEX TD_CUFR_CIDN_SN_LN ON TEST_DATA (UPPER(COALESCE(SUPPL_FORMATTED_RESULT,FORMATTED_RESULT)), COALESCE(SUPPL_IDNUM,IDNUM), SEQ_NUMBER, LAB_NUMBER) ;
  CREATE INDEX TD_SN_LN_CUFR_CIDN ON TEST_DATA (SEQ_NUMBER, LAB_NUMBER, UPPER(COALESCE(SUPPL_FORMATTED_RESULT,FORMATTED_RESULT)), COALESCE(SUPPL_IDNUM,IDNUM)) ;
  CREATE INDEX TD_CUFR_CIDN ON TEST_DATA (UPPER(COALESCE(SUPPL_FORMATTED_RESULT,FORMATTED_RESULT)), COALESCE(SUPPL_IDNUM,IDNUM)) ;

これがもう1つのテーブルです(このクエリでは実際に使用しないテーブルです)。

  CREATE TABLE REQUEST_INFO 
   (NUMBER(11,0) PRIMARY KEY, 
    CHARGE_CODE VARCHAR2(32 BYTE), 
    LAB_NUMBER NUMBER(11,0), 
    SEQ_NUMBER NUMBER(11,0)
   ) ;

  CREATE INDEX RI_LN_SN ON REQUEST_INFO (LAB_NUMBER, SEQ_NUMBER) ;
  CREATE INDEX RI_SN_LN ON REQUEST_INFO (SEQ_NUMBER, LAB_NUMBER) ;

したがって、最初に、インデックスの1つを正常に使用する単一のテーブルに対するクエリを直接示します。

-- GOOD, Uses index : TD_CUFR_CIDN_SN_LN
select td.LAB_NUMBER 
from test_DATA td 
where UPPER(COALESCE(SUPPL_FORMATTED_RESULT,FORMATTED_RESULT))='491(10)376'
  and COALESCE(TD.SUPPL_IDNUM, TD.IDNUM)=40549 
;

これがinner結合で両方のテーブルを使用するクエリです。これもインデックスを使用し、高速に実行されます。

-- GOOD, Uses indexes : TD_CUFR_CIDN_SN_LN AND RI_SN_LN
select TD.LAB_NUMBER  
from REQUEST_INFO RI 
JOIN TEST_DATA TD ON  TD.LAB_NUMBER = RI.LAB_NUMBER AND TD.SEQ_NUMBER = RI.SEQ_NUMBER 
where UPPER(COALESCE(TD.SUPPL_FORMATTED_RESULT,TD.FORMATTED_RESULT))='491(10)376'
  and COALESCE(TD.SUPPL_IDNUM, TD.IDNUM)=40549 

そして、ビューに書き込まれている、左外部結合を使用したsameクエリを次に示します。これはインデックスを使用せず、実行速度が非常に遅くなります。

-- BAD, does not use indexes
select TD.LAB_NUMBER 
from REQUEST_INFO RI 
LEFT JOIN TEST_DATA TD ON  TD.LAB_NUMBER = RI.LAB_NUMBER AND TD.SEQ_NUMBER = RI.SEQ_NUMBER 
where UPPER(COALESCE(TD.SUPPL_FORMATTED_RESULT,TD.FORMATTED_RESULT))='491(10)376'
  and COALESCE(TD.SUPPL_IDNUM, TD.IDNUM)=40549 
;

さて、誰もが言う前に、このクエリは実際にはlogically前のものと同じです。これは、WHERE句が外部テーブル(TD)の列をフィルタリングしているためです。これにより、効果的/論理的に外部結合が内部結合に変わります(これが、条件がON句とWHERE句のどちらで発生するかが問題になる理由です)。

ここで、奇妙さを増すために、外部から内部への強制をより明確にした場合にどうなるかを確認することにしました。

-- GOOD, Uses indexes : TD_CUFR_CIDN_SN_LN AND RI_SN_LN
select TD.LAB_NUMBER 
from REQUEST_INFO RI 
LEFT JOIN TEST_DATA TD ON  TD.LAB_NUMBER = RI.LAB_NUMBER AND TD.SEQ_NUMBER = RI.SEQ_NUMBER 
where UPPER(COALESCE(TD.SUPPL_FORMATTED_RESULT,TD.FORMATTED_RESULT))='491(10)376'
  and COALESCE(TD.SUPPL_IDNUM, TD.IDNUM)=40549 
and TD.LAB_NUMBER IS NOT NULL
;

信じられないほど、これはうまくいきました!

したがって、ここでの質問は次のとおりです。1)Oracleがこれを理解できないのはなぜですか。

2)Oracleがこれを正しく理解してインデックスを使用できるようにするために作成できる設定やインデックスなどはありますか?

その他の考慮事項:

  • ビューは他のさまざまなクエリやクライアントで使用されているため、この1つのクエリの内部結合に変更することはできません。

  • クライアントがクエリを生成しているため、次のような風変わりな特殊なケースの条件でクエリを変更することは困難/ほとんど不可能です: "このデータにこのビューを使用してください。これらの列と、この1つのテーブルからのこれらの列のみが必要な場合は、1つのテーブルから別のビュー "または"を使用し、WHERE句に「IS NOT NULL」を追加します。

どんな提案や洞察も歓迎します。


PDATE: Oracle 11gでも試したところ、まったく同じ結果が得られました。


リクエストごとに、ここではExplain Planの出力を示します。最初は、インデックスを使用する適切なバージョンです。

Rows      Plan                                       COST    Predicates
        3 SELECT STATEMENT                                 8 
        3  HASH JOIN                                       8 Access:TD.LAB_NUMBER=RI.LAB_NUMBER AND TD.SEQ_NUMBER=RI.SEQ_NUMBER
        3   NESTED LOOPS                                   8 
             STATISTICS COLLECTOR
        3     INDEX RANGE SCAN TD_CUFR_CIDN_SN_LN          4 Access:UPPER(COALESCE(SUPPL_FORMATTED_RESULT,FORMATTED_RESULT))='491(10)376' AND COALESCE(SUPPL_IDNUM,IDNUM)=40549, Filter:TD.LAB_NUMBER IS NOT NULL
        1    INDEX RANGE SCAN RI_SN_LN                     2 Access:TD.SEQ_NUMBER=RI.SEQ_NUMBER AND TD.LAB_NUMBER=RI.LAB_NUMBER
        1   INDEX FAST FULL SCAN RI_SN_LN                  2

そして今、悪いバージョン:

Rows      Plan                                       COST    Predicates
 31939030 SELECT STATEMENT                            910972
           FILTER                                             Filter:UPPER(COALESCE(SUPPL_FORMATTED_RESULT,FORMATTED_RESULT))='491(10)376' AND COALESCE(SUPPL_IDNUM,IDNUM)=40549
 31939030   HASH JOIN OUTER                           910972 Access:TD.LAB_NUMBER(+)=RI.LAB_NUMBER AND TD.SEQ_NUMBER(+)=RI.SEQ_NUMBER
  6213479    TABLE ACCESS FULL REQUEST_INFO            58276
 56276228    TABLE ACCESS FULL TEST_DATA              409612
7
RBarryYoung

これは主にパート1に対する部分的な回答であり、いくつかの推測が含まれています。あなたと私は次のクエリを知っています:

_select TD.LAB_NUMBER 
from REQUEST_INFO RI 
LEFT JOIN TEST_DATA TD ON  TD.LAB_NUMBER = RI.LAB_NUMBER AND TD.SEQ_NUMBER = RI.SEQ_NUMBER 
where UPPER(COALESCE(TD.SUPPL_FORMATTED_RESULT,TD.FORMATTED_RESULT))='491(10)376'
  and COALESCE(TD.SUPPL_IDNUM, TD.IDNUM)=40549;
_

このクエリと同等です:

_select TD.LAB_NUMBER 
from REQUEST_INFO RI 
INNER JOIN TEST_DATA TD ON 
TD.LAB_NUMBER = RI.LAB_NUMBER 
AND TD.SEQ_NUMBER = RI.SEQ_NUMBER 
AND UPPER(COALESCE(TD.SUPPL_FORMATTED_RESULT,TD.FORMATTED_RESULT))='491(10)376'
and COALESCE(TD.SUPPL_IDNUM, TD.IDNUM)=40549;
_

ただし、これは、2つのクエリが同等であることをOracleが認識しているという意味ではありません。 Oracleが_TD_CUFR_CIDN_SN_LN_インデックスを使用できるようにするには、2つのクエリの同等性が必要です。ここで期待しているのは、_OUTER JOIN_から_INNER JOIN_への変換です。私はあまり運が良くなかった これに関する良い情報を見つける なので、説明計画を見てみましょう:

LAB_NUMBER

_TD.LAB_NUMBER IS NOT NULL_をWHERE句に追加することは、_OUTER JOIN_から_INNER JOIN_への変換が可能であることをOracleに通知する非常に直接的な方法です。強調表示された行を見ると、それが発生したことがわかります。間違った列を選択するとクエリ結果が変わる可能性がありますが、ほとんどの列で変換が可能になると思います。

_(TD.LAB_NUMBER IS NOT NULL OR TD.SEQ_NUMBER IS NOT NULL)_などのもう少し複雑なフィルターを試した場合、結合変換は行われません。

no join conversion

_OUTER JOIN_は実際には_INNER JOIN_であると考えられますが、クエリオプティマイザーはそのようにプログラムされていない可能性があります。元のクエリには、おそらくクエリオプティマイザーがクエリ変換を適用するには複雑すぎるCOALESCE()式があります。

以下は db fiddle の例です。

2番目の質問については、これを回避する方法を考えることができません。 table elimination を利用してみてください。あなたが言ったように、このクエリは_REQUEST_INFO_テーブルを必要としません。ただし、いくつかの制限があります。

現在、テーブルの削除にはいくつかの制限があります。

  • 複数列の主キー-外部キー制約はサポートされていません。

  • クエリの他の場所で結合キーを参照すると、テーブルが削除されなくなります。内部結合の場合、結合の両側の結合キーは同等ですが、クエリに、テーブルからの結合キーへの他の参照が含まれていて、それ以外の方法では削除できない場合、削除できなくなります。回避策は、他のテーブルの結合キーを参照するようにクエリを書き直すことです(これが常に可能であるとは限りません)。

おそらく、この問題にそれを使用する方法はありますが、制限を回避することはできません。

2
Joe Obbish