Einführung Testfall
Transcription
Einführung Testfall
Betrifft Optimizer Autor Urs Meier ([email protected]) Art der Info Technical Info (Februar 2002) Quelle Aus unserer Projekterfahrung und “Forschung” Einführung Mit jedem Oracle Release nimmt die Anzahl Features zu. Nicht nur für uns ist es schwierig die Übersicht zu behalten, auch der Oracle Optimizer muss mit immer mehr komplexen Fällen fertig werden. Man denke hier zum Beispiel nur an Object-Tables, Index Organized Tables oder Datawarehouse-Features wie Materialized Views mit Query-Rewrite. Umso erfreulicher ist die Tatsache, dass mit Oracle9i einige grundlegende Verbesserungen eingeführt wurden. Dieser Artikel beleuchtet kurz eine der Schwächen des Oracle Optimizers, die SubQueries, und zeigt, was mit Oracle9i besser geworden ist. Der Test basiert auf dem Vorschlag von Hermann Sauer, dessen Buch (Relationale Datenbanken, Theorie und Praxis, Addision-Wesley) ich bestens empfehlen kann. Wer sich mit dem Oracle Optimizer genauer auseinander setzen möchte, dem empfehle ich den Trivadis Workshop „Oracle Optimizer“. Testfall Wir erstellen zwei Tabellen. Die erste Tabelle „klein“ hat 10 Records, die zweite Tabelle „gross“ hat 1'000'000 Records. Danach joinen wir die beiden Tabellen, das Resultat wird immer der kleineren Tabelle entsprechen, also 10 Rows. create table klein (k integer); create table gross (g integer); -- create 10 records insert into klein(k) insert into klein(k) insert into klein(k) insert into klein(k) insert into klein(k) insert into klein(k) insert into klein(k) insert into klein(k) insert into klein(k) insert into klein(k) 0..9 values(0); values(1); values(2); values(3); values(4); values(5); values(6); values(7); values(8); values(9); -- create 1000000 records 0..999999 insert into gross(g) select k1.k*100000+ k2.k*10000+ k3.k*1000+ k4.k*100+ k5.k*10+ k6.k from klein klein klein klein klein klein k1, k2, k3, k4, k5, k6; -- index both tables create index i_klein on klein(k); create index i_gross on gross(g); -- get statistics for the optimizer BEGIN dbms_stats.gather_table_stats ( ownname => user ,tabname => 'KLEIN' ,cascade => true ); END; / BEGIN dbms_stats.gather_table_stats ( ownname => user ,tabname => 'GROSS' ,cascade => true ); END; / Nun kommen die Joins, welche der Oracle Cost-Based Optimizer schon seit einigen Versionen immer richtig optimiert (der alte Rule-Based Optimizer hätte hier übrigens schon Probleme). Die einzelnen Statements unterscheiden sich in der Reihenfolge im FROM und der Join-Attribute. SELECT * FROM gross,klein WHERE g=k; SELECT * FROM gross,klein WHERE k=g; SELECT * FROM klein,gross WHERE g=k; SELECT * FROM klein,gross WHERE k=g; Resultate mit Joins Der Optimizer erkennt diesen einfachen Fall und optimiert alle 4 Varianten identisch und korrekt. Driving Table (oder Index) über die Tabelle „KLEIN“ und Join-Methode Nested Loop für kleines Join-Resultat (10 Rows) sowie Index-Zugriff auf die Tabelle „GROSS“ im inneren Loop: SQL> SELECT * 2 FROM klein,gross 3 WHERE k=g; K G ---------- ---------0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 rows selected. Execution Plan ---------------------------------------------------------SELECT STATEMENT Optimizer=CHOOSE (Cost=12 Card=10 Bytes=60) NESTED LOOPS (Cost=12 Card=10 Bytes=60) INDEX (FULL SCAN) OF 'I_KLEIN' (NON-UNIQUE) INDEX (RANGE SCAN) OF 'I_GROSS' (NON-UNIQUE) Nun kommen die Varianten mit den Sub-Queries. SELECT * FROM gross WHERE g IN (SELECT k FROM klein); SELECT * FROM gross WHERE EXISTS (SELECT * FROM klein WHERE k = g); SELECT * FROM klein WHERE k IN (SELECT g FROM gross); SELECT * FROM klein WHERE EXISTS (SELECT * FROM gross WHERE k = g); Resultate mit Sub-Queries Natürlich erwartet man für die Sub-Queries den gleichen Execution-Plan wie für die Joins. Dies ist leider nicht der Fall. Bis und mit Oracle8i wurden die Sub-Queries leicht anders optimiert und 2 der 4 Queries waren katastrophal falsch (Antwortzeit rund 40 Sekunden oder beinahe 100x langsamer). Oracle9i geniert hier zwei unterschiedliche Execution Plans. Einerseits die „normale“ Oracle Sub-Query Optimierung, wie schon in den Vorgänger Versionen, z.B. SQL> SELECT * 2 FROM gross 3 WHERE EXISTS (SELECT * 4 FROM klein 5 WHERE k = g); G ---------0 1 2 3 4 5 6 7 8 9 10 rows selected. Execution Plan ---------------------------------------------------------SELECT STATEMENT Optimizer=CHOOSE (Cost=15 Card=10 Bytes=60) NESTED LOOPS (Cost=15 Card=10 Bytes=60) SORT (UNIQUE) INDEX (FULL SCAN) OF 'I_KLEIN' (NON-UNIQUE) (Cost=1 Card=10 INDEX (RANGE SCAN) OF 'I_GROSS' (NON-UNIQUE) Bei den Oracle8i Problemfällen stellt Oracle9i automatisch auf den Semi-Join um, z.B: SQL> SELECT * 2 FROM klein 3 WHERE k IN (SELECT g 4 FROM gross); K ---------0 1 2 3 4 5 6 7 8 9 10 rows selected. Execution Plan ---------------------------------------------------------SELECT STATEMENT Optimizer=CHOOSE (Cost=12 Card=10 Bytes=60) NESTED LOOPS (SEMI) (Cost=12 Card=10 Bytes=60) INDEX (FULL SCAN) OF 'I_KLEIN' (NON-UNIQUE) (Cost=1 Card=10 Bytes=20) INDEX (RANGE SCAN) OF 'I_GROSS' (NON-UNIQUE) (Cost=2 Card=1000000 Oracle9i optimiert also auch alle 4 Sub-Queries korrekt, wenn auch mit einem anderen Execution Plan als bei den Joins. Die ehemals katastrophal langsamen Sub-Queries werden mit dem SEMI-Join optimiert (ein SEMI-Join eliminiert „Wiederholungen“ im Join-Resultat). Wer sich mit den undokumentierten INIT.ORA Parameter auskennt (und das sollte jeder, der den Oracle Optimizer etwas “kitzeln” will), weiss, dass die SEMI-Joins bereits mit Oracle8i manuell forciert werden können. Fazit Oracle9i hat einige undokumentierte Änderungen von Oracle8i nun permanent implementiert, dazu gehört die Sub-Query Optimierung durch Semi-Joins. Die im Artikel gezeigten Tests wurden mit den Default Oracle9i Parametern vorgenommen. Oracle9i optimiert Sub-Queries jedoch noch immer nicht identisch wie die Joins. Die hier gezeigte Fallstudie ist natürlich keine Herausforderung für einen Optimizer. Oracle kann das Problem aber erst mit der jüngsten Version lösen. Bei komplexeren SubQueries stösst man noch immer auf Probleme. Die Empfehlung lautet deshalb nach wie vor: a) Wo immer möglich JOINs und nicht Sub-Queries verwenden b) Correlated Sub-Queries (normalerweise EXISTS/NOT EXISTS) müssen in der Haupt-Query eine gute Restriktion (WHERE-Klausel aufweisen) c) Uncorrelated Sub-Queries (normalerweise IN) müssen in der Sub-Query eine gute Restriktion aufweisen Mit optimalen Grüssen Urs Meier Trivadis AG Kanalstrasse 5 8152 Glattbrugg Mail: [email protected] Tel: +41 1 808 70 18 Fax: +41 1 808 70 21 Internet: http://www.trivadis.com