Outer Join unterdrückt die Indexnutzung?

7

Ich habe ein Client-Programm, das eine Abfrage für eine Ansicht ausführt, die eine Tabelle mit einer anderen verknüpft. Die Leistung ist schlecht und ich habe versucht, sie durch Hinzufügen des richtigen Index zu optimieren. Die fragliche Abfrage verwendet tatsächlich nur die zweite Tabelle, daher habe ich diese Tabelle direkt getestet.

Ich habe (mehrere) Indizes gefunden, die für die Abfrage für die Tabelle gut funktionierten, aber als ich sie auf die Ansicht umstellte, verwendeten sie keine Indizes mehr und führten stattdessen nur vollständige Scans für beide Tabellen durch. Da diese Tabellen groß sind (jeweils 2-3 Millionen Zeilen), ist dies sehr langsam.

Zum einfachen Testen habe ich die Abfrage geändert, um die zu umgehen und nur den äußeren Join in die Abfrage selbst zu integrieren. Dies reproduzierte das Problem erfolgreich, ließ jedoch das Rätsel offen, warum der äußere Join die Indizes nicht verwenden würde.

Hier ist die Tabelle mit allen Indizes, die ich beim Testen hinzugefügt habe:

  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)) ;

Hier ist die andere Tabelle (die wir für diese Abfrage nicht wirklich verwenden)

  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) ;

Hier ist also zuerst die Abfrage für die einzelne Tabelle direkt, die einen der Indizes erfolgreich verwendet.

-- 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 
;

Hier ist die Abfrage, bei der beide Tabellen mit einem inneren Join verwendet werden. Dies verwendet auch die Indizes und läuft schnell.

-- 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 

Und hier ist dieselbe Abfrage mit einem Left Outer Join, wie sie in der Ansicht geschrieben ist. Dies verwendet KEINEN der Indizes und läuft sehr langsam.

-- 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 
;

Bevor es jemand sagt: Diese Abfrage ist tatsächlich logisch identisch mit der vorherigen. Dies liegt daran, dass die WHERE-Klausel nach Spalten aus der äußeren Tabelle (TD) filtert, wodurch eine äußere Verknüpfung effektiv / logisch in eine innere Verknüpfung umgewandelt wird (aus diesem Grund ist es wichtig, ob Bedingungen in der ON-Klausel gegenüber der WHERE-Klausel auftreten).

Um die Verrücktheit noch zu verstärken, entschied ich mich zu sehen, was passieren würde, wenn ich den Zwang von außen nach innen expliziter machen würde:

-- 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
;

Unglaublich, das hat funktioniert!

Die Frage hier ist also: 1) WARUM findet Oracle das nicht selbst heraus?

Und 2) Gibt es eine Einstellung oder einen Index usw., die ich erstellen kann, damit Oracle dies richtig herausfindet und die Indizes verwendet?

Weitere Überlegungen:

  • Die Ansicht wird von einer Vielzahl anderer Abfragen und Clients verwendet, daher kann ich sie für diese eine Abfrage nicht einfach in einen inneren Join ändern.

  • Der Client generiert die Abfrage, daher ist es schwierig / nahezu unmöglich, die Abfrage unter skurrilen Sonderfallbedingungen wie: " Verwenden Sie diese Ansicht für diese Daten, es sei denn, Sie benötigen nur diese Spalten aus dieser einen Tabelle, und verwenden Sie dann eine andere view "oder" Wenn Sie diese Spalten und nur diese Spalten aus dieser einen Tabelle benötigen, fügen Sie der WHERE-Klausel ein 'IS NOT NULL' hinzu. "

Anregungen oder Erkenntnisse sind willkommen.


UPDATE: Ich habe es gerade auch unter Oracle 11g versucht. Dort erhalte ich genau die gleichen Ergebnisse.


Auf Anfrage ist hier die Ausgabe des Erklärungsplans, zuerst die gute Version, in der Indizes verwendet werden:

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

Und jetzt die schlechte Version:

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
RBarryYoung
quelle
Können Sie nach dem Wechsel der Tabellenreihenfolge in der Abfrage versuchen, wo "es nicht funktioniert"?
Raj
@JoeObbish Ich habe keine Hinweise ausprobiert, weil sie zu hartnäckig sind, um eine Lösung für diese Situation zu finden. Was den Erklärungsplan betrifft, muss ich Vertraulichkeitsprobleme berücksichtigen. Ich habe die Namen in den obigen Skripten geändert. Ein Erklärungsplan wäre schwieriger zu bearbeiten. Ich sehe was ich tun kann.
RBarryYoung
@Raj Meinst du die Reihenfolge der beiden Tabellen ändern und die LINKE in eine RECHTE äußere Verknüpfung ändern? Ich könnte das morgen versuchen ...
RBarryYoung
@ Raj das Umschalten der Tabellenreihenfolge hatte keine Auswirkung.
RBarryYoung
@ JoeObbish Ich habe die Erklärungspläne gepostet.
RBarryYoung

Antworten:

2

Dies ist meistens eine teilweise Antwort auf Teil 1 mit einigen Spekulationen. Sie und ich wissen, dass die folgende Abfrage:

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;

Entspricht dieser Abfrage:

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;

Dies bedeutet jedoch nicht, dass Oracle weiß, dass die beiden Abfragen gleichwertig sind. Die Äquivalenz der beiden Abfragen ist erforderlich, damit Oracle den TD_CUFR_CIDN_SN_LNIndex verwenden kann. Was wir hoffen , denn hier ist eine OUTER JOINzu INNER JOINKonvertierung. Ich hatte nicht viel Glück , gute Informationen darüber zu finden. Schauen wir uns also die Erklärungspläne an:

LAB_NUMBER

Das Hinzufügen TD.LAB_NUMBER IS NOT NULLzur WHEREKlausel ist eine sehr direkte Möglichkeit, Oracle darüber zu informieren, dass OUTER JOINeine INNER JOINKonvertierung möglich ist. Wir können sehen, dass es aufgetreten ist, indem wir die hervorgehobene Linie betrachten. Ich denke, dass so ziemlich jede Spalte die Konvertierung zulässt, obwohl die Auswahl der falschen Spalte die Abfrageergebnisse ändern kann.

Wenn wir einen etwas komplizierteren Filter wie diesen versuchen, findet (TD.LAB_NUMBER IS NOT NULL OR TD.SEQ_NUMBER IS NOT NULL)die Join-Konvertierung nicht statt:

Keine Join-Konvertierung

Wir können darauf hinweisen, dass dies OUTER JOINwirklich ein ist, INNER JOINaber der Abfrageoptimierer wurde möglicherweise nicht dafür programmiert. In der ursprünglichen Abfrage haben Sie einen COALESCE()Ausdruck, der wahrscheinlich zu komplex ist, als dass der Abfrageoptimierer die Abfragetransformation anwenden könnte.

Hier ist eine DB-Geige für einige Beispiele.

Bei der zweiten Frage kann ich mir keine Möglichkeit vorstellen, dies zu umgehen. Sie könnten versuchen, die Tabelleneliminierung zu nutzen . Wie Sie sagten, benötigt diese Abfrage nicht einmal die REQUEST_INFOTabelle. Es gibt jedoch einige Einschränkungen:

Derzeit gibt es einige Einschränkungen bei der Tabelleneliminierung:

  • Mehrspaltige Einschränkungen für Primärschlüssel und Fremdschlüssel werden nicht unterstützt.

  • Wenn Sie an anderer Stelle in der Abfrage auf den Join-Schlüssel verweisen, wird die Tabelleneliminierung verhindert. Bei einem inneren Join sind die Join-Schlüssel auf jeder Seite des Joins gleichwertig. Wenn die Abfrage jedoch andere Verweise auf den Join-Schlüssel aus der Tabelle enthält, die andernfalls entfernt werden könnten, wird die Eliminierung verhindert. Eine Problemumgehung besteht darin, die Abfrage neu zu schreiben, um auf den Verknüpfungsschlüssel aus der anderen Tabelle zu verweisen (wir erkennen, dass dies nicht immer möglich ist).

Vielleicht gibt es eine Möglichkeit, dies für dieses Problem zu verwenden, aber ich kann die Einschränkungen nicht umgehen.

Joe Obbish
quelle
Thx, das stimmt ziemlich genau mit dem überein, was ich bei verschiedenen Tests gesehen habe: Bei einem einspaltigen NULL-Test wird es herausgefunden, bei mehreren disjunkten Spalten aus derselben Tabelle (über OR oder COALESCE) jedoch nicht. Die Tabelleneliminierung funktioniert in diesem Fall nicht (mehrspaltige Schlüssel), daher muss ich sie nur drücken, um den Client-Abfragegenerator zu ändern.
RBarryYoung
-1

Ersetzen Sie die Coalesce-Anweisung durch eine ORAnweisung, da beim Hinzufügen einer Funktion auf der linken Seite der Where-Klausel kein Index verwendet wird, es sei denn, die linksseitige Funktion wird mithilfe eines funktionsbasierten Index indiziert. Ändern Sie daher die Abfrage wie folgt. Der separate Index für SUPPL_FORMATTED_RESULTund FORMATTED_RESULTsollte eine upperFunktion für die folgende Abfrage haben , um den Indexzugriff zu verwenden.

Hinweis: Wenn die Daten einen Versatz aufweisen und die Anzahl der Datensätze mit den Werten '491 (10) 376' und 40549 größer ist, wird der Index übersprungen und ein vollständiger Tabellenscan verwendet.

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(TD.SUPPL_FORMATTED_RESULT) ='491(10)376' or   
       UPPER(TD.FORMATTED_RESULT)='491(10)376')
  and  (TD.SUPPL_IDNUM =40549 or TD.IDNUM=40549); 
Oracle Panda
quelle