Heute geht mit dem Thema effizientes SQL - aus der Sicht des Entwicklers - weiter:
Im letzten Post habe ich den Performance-Effekt der Verwendung von Bindevariablen vorgestellt. Wie das Testprogramm zeigte, ist dieser Effekt ganz erheblich - wenn eine Applikation viele "gleichartige" SQL-Statements absetzt, hat die Verwendung (oder Nicht-Verwendung) von Bindevariablen signifikante Auswirkungen auf die Performamce.
Heute möchte ich auf die Frage eingehen, ob man wirklich immer Bindevariablen nutzen sollte (Ihr könnt es euch denken: Natürlich nicht). Das Thema Bind Variable Peeking wird dabei ein wichtiger Schwerpunkt sein. Zunächst jedoch nochmal ein paar Worte zur Abarbeitung von SQL-Kommandos in der Datenbank:
- Parse: Das Statement wird syntaktisch und semantisch geprüft und es wird ein Ausführungsplan erstellt
- Bind: Die Platzhalter der Bindevariablen werden durch die konkreten Werte ersetzt
- Execute: Das Statement wird ausgeführt.
Der Ausführungsplan wird also während des "Parse" im ersten Schritt erstellt. Der Optimizer schaut dazu nach ...
- ... ob Indizes da sind ...
- ... wieviele Zeilen da sind ...
- ... ob die Query selektiv ist oder nicht ...
Doch wie kann der Optimizer die "Selektivität" bestimmen, wenn er die Werte der Bindevariablen gar nicht kennt ... ?
Naja, zunächst ist diese Frage nur dann relevant, wenn es ein extremes Ungleichgewicht in der Verteilung der Daten der jeweiligen Spalte gibt (wie im folgenden Beispiel). Bei Gleichverteilung ist die optimale Strategie vom konkreten Wert der Bindevariable unabhängig. Zunächst erstellen wir mit folgendem Skript eine Tabelle basierend auf der Tabelle CUSTOMERS im Schema SH. Dieser Tabelle wird eine neue Spalte STATUS hinzugefügt. 1% der Kunden werden den Status "A" und alle anderen die Status "B" oder "C" erhalten.
drop table customers / create table customers as select * from sh.customers where rownum < 5000 / alter table customers add (status char(1)) / declare type number_array is table of number index by binary_integer; type char_array is table of varchar2(1) index by binary_integer; v_id_array number_array; v_status_array char_array; v_random_val number; begin select cust_id bulk collect into v_id_array from customers; for i in v_id_array.first..v_id_array.last loop v_random_val := dbms_random.value(0,100); if v_random_val < 1 then v_status_array(i) := 'A'; elsif v_random_val > 1 and v_random_val < 10 then v_status_array(i) := 'B'; else v_status_array(i) := 'C'; end if; end loop; forall i in v_id_array.first..v_id_array.last update customers set status = v_status_array(i) where cust_id = v_id_array(i); end; / sho err
Nun wird die neue Spalte STATUS indiziert (IDX_STATUS). Schließlich werden die Statistiken erneuert (DBMS_STATS.GATHER_SCHEMA_STATS). Dabei stellt der Parameter method_opt sicher, dass ein Histogramm für die Spalte STATUS gebildet wird - mit einem solchen Histogramm "weiss" der Optimizer um die (ungleiche) Verteilung der Daten.
create index idx_status on customers (status) / exec dbms_stats.gather_schema_stats(ownname => user, method_opt=>'FOR ALL COLUMNS SIZE SKEWONLY');
Wie geht die Datenbank nun vor, wenn eine SQL Abfrage mit der Spalte STATUS in der WHERE-Klausel hereinkommt...?
select * from customers where status = 'A'
Wenn keine Bindevariablen verwendet werden, "sieht" die Datenbank den Wert "A" sofort und sie kann anhand der Histogramme nachschauen, wieviele Zeilen die Abfrage zurückliefern wird. Die Entscheidung, ob Index oder Full Table Scan nun besser sind, ist dann ein Leichtes. Im letzten Beispiel haben wir jedoch gesehen, dass man besser Bindevariablen verwenden sollte - was passiert dann ...?
select * from customers where status = :1
Die Datenbank "sieht" den Wert nun zur Parse-Zeit noch nicht. Allerdings muss der Optimizer einen Ausführungsplan erstellen - zur Lösung des Problems zieht er das Binding gewissermaßen vor (Bind Variable Peeking). Dieses Bind Variable Peeking wird pro SQL-Kommando aber nur einmal durchgeführt - wenn das gleiche SQL-Kommando erneut hereinkommt, wird der beim ersten Mal erstellte Ausführungsplan wiederverwendet. Und dies kann zu "seltsamen" Ergebnissen führen ... dazu zurück zum Beispiel:
Also: Die Tabelle wurde wie oben beschrieben erstellt und die Applikation nutzt Bindevariablen. Nach dem Hochfahren der Datenbank kommt also das SQL herein ...
select * from customers where status = :1
Wie gerade beschrieben, führt die Datenbank das Bind Variable Peeking durch; wir gehen davon aus, dass in diesem Fall nach dem Status "A" gefragt wird. Anhand des Histogramms stellt der Optimizer fest, dass nur 1% der Zeilen selektiert werden und er entscheidet sich für ...
SQL_ID cfrfax3f40swx, child number 0 ------------------------------------- select * from customers where status = :1 Plan hash value: 2427356980 ------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 40 (100)| | | 1 | SORT AGGREGATE | | 1 | 6 | | | | 2 | TABLE ACCESS BY INDEX ROWID| CUSTOMERS | 38 | 228 | 40 (0)| 00:00:01 | |* 3 | INDEX RANGE SCAN | IDX_STATUS | 38 | | 10 (0)| 00:00:01 | ------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("STATUS"=:1)
... den Indexzugriff - und das ist gut so. Nun geht es weiter. Über die Applikation sucht der nächste Endanwender nach allen Kunden mit Status "C". Als SQL kommt wieder herein ...
select * from customers where status = :1
Nun kennt die Datenbank das SQL-Statement aber bereits - es wurde ja schonmal ausgeführt. Also findet kein Bind Variable Peeking statt - der beim erstem Mal erstellte Ausführungsplan wird wiederverwendet - die Datenbank macht für dieses SQL-Statement nun immer den Indexzugriff. Aber eigentlich wäre der Full Table Scan besser ...
Anderes Szenario: Die Datenbank wird durchgestartet und der erste Endanwender, der über die Anwendung auf die Datenbank kommt, sucht diesmal nach Kunden mit Status "C". Der Optimizer kennt das Statement noch nicht, also macht er ein Bind Variable Peeking - und kommt zu folgendem Ausführungsplan:
SQL_ID bz2rfvv5145f5, child number 0 ------------------------------------- select * from customers where status = :1 Plan hash value: 296924608 -------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 40 (100)| | | 1 | SORT AGGREGATE | | 1 | 6 | | | |* 2 | TABLE ACCESS FULL| CUSTOMERS | 4494 | 26964 | 40 (0)| 00:00:01 | -------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("STATUS"=:1)
"Full Table Scan" (logisch - schließlich haben 90% der Kunden den Status "C"). Nun kommt der nächste Anwender und sucht nach den Kunden mit Status "A" ... und Ihr könnt euch den Rest denken: Das SQL ist ja nun bekannt, die Datenbank verwendet den bereits erstellten Ausführungsplan wieder und wird nun alle diese SQL-Abfragen mit Full Table Scans ausführen ... nicht so gut ...
Das Problem ist hier, dass wir eigentlich gerne je nach abgefragtem Status unterschiedliche Ausführungspläne hätten. Durch die Verwendung der Bindevariable ist das SQL-Statement textlich jedoch stets dasselbe. Mit Bind Variable Peeking wird für die erste Abfrage ein Ausführungsplan berechnet und dann beibehalten. Für ein SQL-Statement (der SQL-Text ist entscheidend) gibt es im SQL-Cache der Datenbank nur einen aktiven Ausführungsplan.
Dies ist also ein Fall, in dem es besser ist, das SQL per Stringverkettung zusammenzusetzen und keine(!) Bindevariablen zu verwenden. Denn dann liegen in der Datenbank für die verschiedenen Status textlich verschiedene SQL-Abfragen (mit unterschiedlichen Ausführungsplänen) vor. Bindevariablen sind zwar in den meisten Fällen, aber eben nicht "per-se" die beste Lösung:
- Bindevariablen sollten nicht genutzt werden, wenn je nach Inhalt der Bindevariablen unterschiedliche Ausführungspläne erwartet werden
- Bindevariablen sollten genutzt werden, wenn der Ausführungsplan von den Inhalten der Bindevariablen unabhängig ist und ohnehin stets gleich berechnet würde.
Und wer Oracle11g nutzt, liest am besten gleich im nächsten Posting weiter, denn 11g bringt Lösungen für genau dieses Problem mit.