あるテーブルを別のテーブルに外部結合するビューに対してクエリを実行しているクライアントプログラムがあります。パフォーマンスが悪いので、適切なインデックスを追加してチューニングしようとしています。問題のクエリは実際には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
これは主にパート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
_への変換です。私はあまり運が良くなかった これに関する良い情報を見つける なので、説明計画を見てみましょう:
_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)
_などのもう少し複雑なフィルターを試した場合、結合変換は行われません。
_OUTER JOIN
_は実際には_INNER JOIN
_であると考えられますが、クエリオプティマイザーはそのようにプログラムされていない可能性があります。元のクエリには、おそらくクエリオプティマイザーがクエリ変換を適用するには複雑すぎるCOALESCE()
式があります。
以下は db fiddle の例です。
2番目の質問については、これを回避する方法を考えることができません。 table elimination を利用してみてください。あなたが言ったように、このクエリは_REQUEST_INFO
_テーブルを必要としません。ただし、いくつかの制限があります。
現在、テーブルの削除にはいくつかの制限があります。
複数列の主キー-外部キー制約はサポートされていません。
クエリの他の場所で結合キーを参照すると、テーブルが削除されなくなります。内部結合の場合、結合の両側の結合キーは同等ですが、クエリに、テーブルからの結合キーへの他の参照が含まれていて、それ以外の方法では削除できない場合、削除できなくなります。回避策は、他のテーブルの結合キーを参照するようにクエリを書き直すことです(これが常に可能であるとは限りません)。
おそらく、この問題にそれを使用する方法はありますが、制限を回避することはできません。