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