Dienstag, Juni 30, 2015

Permanente Deaktivierung einer SQL Plan Directive

Franck Pachot erklärt, wie man dafür sorgt, dass eine SQL Plan Directive dauerhaft deaktiviert bleibt, um den Overhead des Samplings zu vermeiden. Das Problem dabei ist, dass die Direktive nach einer vollständigen Löschung erneut erzeugt werden kann. Um das zu verhindern, muss die Direktive deaktiviert, aber ihre Löschung verhindert werden - die Befehlsfolge dazu lautet:

exec dbms_spd.alter_sql_plan_directive(14130932452940503769,'ENABLED','NO');
exec dbms_spd.alter_sql_plan_directive(14130932452940503769,'AUTO_DROP','NO');

Grundsätzlich sind die Direktiven jedenfalls ein Thema, das man für 12c klar im Blick behalten muss.

Sonntag, Juni 28, 2015

Cardinality Schätzungen für TEMP TABLE TRANSFORMATION

Randolf Geist hat vor einigen Wochen in zwei Artikeln weitere Details zum Verhalten der Cardinality-Schätzungen im Rahmen der TEMP TABLE TRANSFORMATION geliefert - in Ergänzung zu zwei (geringfügig) älteren Artikeln, die ich hier gelegentlich verlinkt habe und in denen vor allem darauf hingewiesen wurde, dass die Transformation nicht kostenbasiert ist, sondern automatisch angewendet wird (sofern die erforderlichen Voraussetzungen erfüllt sind), dass sie zu einer Materialisierung aller Spalten der CTE führt - und nicht nur der tatsächlich relevanten (und somit unnütze Arbeit hervorrufen kann) -, und eben dass sie unerfreuliche Auswirkungen auf die Bestimmung der Cardinalities hat. Der Detaillierung dieses letzten Punktes dienen die beiden folgenden Artikel:
  • Temp Table Transformation Cardinality Estimates - 1: liefert ein relativ einfaches Self-Join-Beispiel, bei dem die Query mit einem Inline-Hint als Hash Join mit zweifacher Verwendung der Basis-Tabelle ausgeführt wird und ohne den Hint als Hash Join der Materialisierung dieser Basistabelle. Der Join erfolgt dabei über einen Ausdruck (id + 1), was die Aufgabe des Optimizers im Fall der Materialisierung offenbar komplizierter macht: während die Inline-Variante die Cardinality 10000 liefert, ergibt sich für den Fall mit Temp Table Transformation eine Cardinality von 1. Tatsächlich ist die 1 laut CBO-Trace eine aufgerundete 0, denn offenbar verliert der Optimizer durch die Transformation diverse statistische Informationen, insbesondere zu den Spalten-Statistiken. Besonders unerfreulich ist dabei, dass sich die Fehler in der Schätzung - zumindest vor 12c - nicht mit Hilfe des opt_estimate Hints korrigieren lassen. In 12c wird der Hint dann nicht mehr übergangen, führt aber zu anderen Merkwürdigkeiten. Für das vorliegende Beispiel lässt sich das Problem durch extended statistics für den Ausdruck id + 1 beheben, aber es bleibt festzuhalten das die Temp Table Transformation massive Auswirkungen auf die Cardinality-Schätzungen haben kann, und dass die Korrektur der dadurch entstehenden Fehler vor 12c nicht mit Hilfe der klassischen Hints durchgeführt werden kann.
  • Temp Table Transformation Cardinality Estimates - 2: erweitert das Beispiel des ersten Artikels um eine zusätzliche Filterung der id-Spalte, der auf beiden Seiten des Joins 99% der Daten zum Opfer fallen. Während die inline-Version des Ausführungsplan dieser Veränderung der Datenmengen Rechnung trägt, wird die Filterung im Fall der Temp Table Transformation komplett ignoriert. Insofern kann die Transformation in vielen Fällen zu einer deutlichen Überschätzung der Cardinalities führen. Da die Filterung im Fall der Transformation nicht beim step des Tabellenzugriffs erfolgt, sondern erst im folgenden view-step, ergeben sich im Exadata-Fall Zugriffe, die eigentlich vermeidbar wären.
Ich werde wohl in Zukunft häufiger als bisher auf den Inline-Hint zugreifen, um zu bestimmen, wann die Temp Table Transformation den Optimizer in die Irre führt.

Nachtrag 02.08.2015: Jonathan Lewis hat zuletzt zwei Artikel veröffentlicht, in denen er auch noch einmal darauf hinweist, dass die TEMP TABLE TRANSFORMATION nicht kostenbasiert, sondern auf Basis einer Heuristik erfolgt. Er kommt darin auch (wieder einmal) zu dem Schluss, dass CTEs, die aus Gründen der Lesbarkeit eingeführt werden, manchmal die Query-Performance beeinträchtigen.

Donnerstag, Juni 25, 2015

Leere Index-Blocks nach einem Rollback

Im OTN-Forum wurde vor einigen Tagen die Frage gestellt, wie es dazu kommen kann, dass ein Index nach einer abgebrochenen großen Update-Operation zahlreiche vollständig leere Blocks enthält. Dazu gab es im Forum ein paar kluge Antworten - und eine weniger kluge von mir, die zwar das gundsätzliche Problem benannte (nämlich die Tatsache, dass Index-Einträge nicht direkt gelöscht werden, sondern als gelöscht markiert), aber in der Erläuterung so unvollständig blieb, dass ich selbst nicht mehr sagen kann, worauf ich eigentlich hinaus wollte. Stefan Koehler lieferte ein schönes Test-Beispiel, das den Fall exakt reproduziert, aber darüber hinaus hat Richard Foote den Fall zum Anlass genommen, ein paar neue Artikel in seinem Blog zu veröffentlichen:
  • Empty Leaf Blocks After Rollback Part I (Empty Spaces): erläutert das grundsätzliche Problem: im Zusammenhang mit Indizes setzt sich ein Update aus einem Delete und einem Insert zusammen. Die ursprünglichen Einträge werden als gelöscht markiert und die neuen Einträge an anderer Stelle im Index eingefügt, was zur Entstehung neuer Index Leaf Blocks führen kann. Ein Rollback stellt dann die ursprünglichen Einträge wieder her, während die neuen Einträge als gelöscht markiert werden: es handelt sich also um eine logische Wiederherstellung, die Änderungen der Index-Struktur - wie Block Splits - nicht zurücksetzt. Dazu gibt es dann ein paar Beispiele: zunächst wird das Verhalten eines non-unique Index untersucht, bei dem alle Id-Werte um 1 erhöht werden. Dieser Fall führt zu sehr vielen 50:50-Splits und dazu, dass der Index auf die doppelte Größe wächst, wobei alle Blocks nur noch zur Hälfte gefüllt sind. Ein folgendes Update der Id-Werte um 10000 führt dazu, dass neue Werte jenseits des Bereichs der bisher verwendeten Einträge entstehen (durch 90:10-Splits) - nach dem folgenden Rollback sind diese neuen Blocks dann wieder völlig leer, bleiben aber in der Index-Struktur. Für folgende Zugriffe kann das ein Problem sein - insbesondere für die üblicherweise sehr effizienten INDEX FULL SCAN (MIN/MAX) Operationen, die all diese leeren Blocks überprüfen müssen, um den tatsächlichen Maximalwert zu finden. Ein ALTER INDEX ... COALESCE entfernt die leeren Leaf-Blöcke aus der Struktur.
  • Empty Leaf Blocks After Rollback Part II (Editions of You): erklärt zunächst, dass sich für die beiden Beispiele des vorangehenden Artikel ähnliche Effekte ergeben hätten, wenn statt des Rollbacks ein Commit erfolgt wäre: auch dann wäre die Index-Struktur durch das Update deutlich gewachsen. Es folgt eine Darstellung des Verhaltens beim Einsatz eines unique index. Zunächst zeigt sich, dass der unique index mehr Einträge pro Block unterbringen kann (was damit zusammenhängt, dass Oracle im Fall des nonunique Index die rowid als zusätzliches Attribut im Index speichert, um die Einträge intern eindeutig zu machen). Dann wird das Verhalten beim Update der Id um den Wert 1 beleuchtet: da Einträge im Fall des eindeutigen Index wiederverwendet werden können, wächst dieser Index im Rahmen des Updates nicht, sondern behält seine ursprüngliche Größe. Anders verhält es sich beim Update um 10000, da in diesem Fall keine Wiederverwendung existierender Einträge erfolgen kann.
Damit hätte sich das OTN-Forum mal wieder nützlich gemacht.

Samstag, Juni 20, 2015

Grundlagen des kostenbasierten Optimizers

Jonathan Lewis hat für AllThingsOracle eine neue Serie Basics of the Cost Based Optimizer gestartet und liefert damit vermutlich eine neue Standardreferenz zum Thema. Allerdings weist der Autor darauf hin, dass die Serie eher einführenden Charakter besitzt und nicht allzu sehr auf die technischen Details eingeht, weshalb ich meine Zusammenfassungen eher knapp zu halten gedenke:
  • Basics of the Cost Based Optimizer – Part 1: beschreibt die Grundlagen der Arbeit des Optimizers: um einen möglichst effektiven Zugriff zu ermöglichen erstellt er ein numerisches Modell basierend auf Informationen über den Daten (= Objektstatistiken), die Leistungsfähigkeit des Rechners (= Systemstatistiken) und die verfügbareb Ressourcen auf dem Rechner (= Konfiguration der Datenbank/Instanz). Dieses Modell ist natürlich nicht immer eine exakte Abbildung der realen Gegebenheiten und aus diesen Differenzen ergeben sich dann die Fehlentscheidungen des Optimizers. Dazu gibt es dann ein umfangreiches Beispiel eines Joins zweier Tabellen, die unter Verwendung unterschiedlicher Strategien miteinander gejoint werden können.
  • Basics of the Cost Based Optimizer – Part 2: liefert Daten zu den in Teil 1 vorgestellten Objektdefinitionen (DDL) und zeigt, dass das Standardmodell relativ leicht zu Fehleinschätzungen führt, und wie man diese durch Konfigurationsänderungen korrigieren kann. Dabei enthalten die Daten eine Ungleichverteilung der Werte (skew). Im Beispiel werden die Optimizer-Kosten der Zugriffe sehr detailliert mit den zugehörigen I/O-Operationen auf Indizes und Tabellen verglichen. Zudem wird der durch ASM verzerrte Clustering Factor eines Index erläutert. Nach Korrektur des CF (durch Verwendung eines angemessenen table_cached_blocks-Parameters) verzichtet der Optimizer auf einen zunächst vorgesehenen Index Hash Join und verwendet stattdessen einen einfachen Index Range Scan mit folgendem Tabellenzugriff.
  • Basics of the Cost Based Optimizer – Part 3: erläutert die Arbeitsweise und das Costing von Hash Joins und Nested Loops Joins. Im NL-Beispiel treten ausschließlich single block I/O Operationen auf, so dass die Reads im Plan mit rowsource statistics theoretisch exakt der cost Angabe entsprechen sollten: und das tun sie auch bei einer der beiden möglichen Reihenfolgen beim Tabellenzugriff, aber nicht bei der anderen, was mit dem Clustering der Daten zu tun hat (und der Effizienz des Cachings). Auch für den Hash Join ergibt sich eine sehr akkurate Cost-Angabe: in zwei der drei Test-Fälle ist das Modell also recht nah an der Realität. Aber im Fall von Nested Loops Joins kann der Optimizer die Caching Effekte nicht vorhersehen und neigt deshalb dazu, die Kosten für physical I/O zu überschätzen (was sich übrigens nicht solide durch die überholten Parameter optimizer_index_caching und optimizer_index_cost_adj korrigieren lässt).
  • Basics of the Cost Based Optimizer – Part 4: erläutert die Rolle von Query Blocks und führt vor, welche Transformationen für ein Beispiel einer korrelierten Subquery möglich sind. Insbesondere weist er darauf hin, dass der Optimizer nicht immer die geeignetste Transformation auswählt, weshalb es wichtig ist, Einfluss auf die Umwandlungen nehmen zu können.
  • Basics of the Cost Based Optimizer – Part 5: liefert zunächst eine knappe Zusammenfassung der älteren Artikel, da zwischen Teil 4 und Teil 5 der Serie ein gewisser Abstand lag. Im Anschluss werden die Begriffe cardinality (Anzahl Datensätze) und Selectivity (Anteil der Datensätze) eingeführt. Eine Hauptursache für ineffiziente Pläne ist das der Optimizer bei der Bestimmung der Selectivity oft keine präzise Informationsgrundlage hat - unter bestimmten Umständen kann er nur raten. Entsprechende Fälle sind:
    • function(column) = constant: geschätzte Selectivity 1%. Korrigierbar mit virtual column.
    • function(column) > constant: geschätzte Selectivity 5%. Beim range mit between wird daraus 5% * 5% = 0,25%. Korrigierbar mit virtual column.
    • column = column: berücksichtigt die Anzahl distinkter Werte in beiden Spalten (1/greatest(num_distinct(col1), num_distinct(col2)). Korrigierbar mit dynamic sampling.
    • combining columns: multipliziert die Selektivitäten, da der Optimizer unabhängige Werte erwartet. Korrigierbar mit extended statistics (column groups).
  • Basics of the Cost Based Optimizer – part 6: beschäftigt sich mit vier Fällen, die in Teil 5 erwähnt, aber noch nicht erklärt worden waren. Ein erster Fall sind 5% Schätzungen für die Cardinality von Subqueries (mit einem "größer als" Prädikat), die man unter Umständen durch cardinality/opt_estimate Hints korrigieren kann. Ein zweiter Fall betrifft die Verwendung veralteter Statistiken, die einen unzutreffenden high_value etwa für Zeitangaben liefern, was durch häufiges Aktualisierung der Statistiken oder durch Setzen der high_values korrigiert werden kann. Die beiden letzten Fälle betreffen die Verwendung von Bind Variablen im Zusammenhang mit data skew, wobei einerseits die Begrenzung der maximalen Bucket-Anzahl auf 254 (in 11g) und die Verwendung von bind peeking eine Rolle spielen. Grundsätzlich gilt immer noch, dass Bind Variablen und Histogramme nicht gut miteinander harmonieren (weshalb der Einsatz von Literalen manchmal gar keine schlechte Idee ist).
Ich gehe davon aus, dass in der Serie noch weitere Artikel folgen werden, und diese werde ich dann voraussichtlich hier ergänzen.

Donnerstag, Juni 18, 2015

Typen des Dynamic Sampling Hint

Jonathan Lewis weist - erneut - darauf hin, dass der dynamic sampling Hint in zwei Versionen existiert: als cursor level Hint und als statement level Hint, und dass die Anzahl der gesampelten Blocks von der Version abhängt: Level 4 auf cursor level verwendet 64 Blocks als Sample - sofern bestimmte Voraussetzungen gegeben sind, während Level 4 auf table level 256 Blocks verwendet, und das unabhängig von weiteren Voraussetzungen. Deshalb plädiert der Autor dafür, den Typ explizit zu erwähnen, wenn über dynamic sampling gesprochen wird. Darüber hinaus liefert er einen kleinen PL/SQL Block, der ein CBO Trace (Event 10053) erzeugt, um die unterschiedlichen Sample-Größen der beiden Verfahren anzuzeigen. Die Resultate zeigen, dass curosr level sampling in fast jedem Fall ein kleineres Sample verwendet und auf Level 1 nur unter ganz bestimmten Voraussetzungen überhaupt durchgeführt wird.

Darüber hinaus verweist Jonathan noch auf einen Artikel von Mohamed Houri, der sich mit der Unterscheidung von Fällen, in denen kein Sample erzeugt wurde, und Fällen, in denen das Sample ignoriert wurde, beschäftigt. Anlass für beide Artikel war dabei ein OTN-Thread, in dem eine recht intensive Diskussion des Sampling-Verhaltens erfolgte (unter Teilnahme von Mohamed, Jonathan, Randolf Geist, Dom Brooks, Stefan Koehler), und der schon für sich gesehen die Leküre lohnt.

Sonntag, Juni 14, 2015

Hoher Parse-Overhead durch Dynamic Statistics in der Standard Edition 12c

Nicht mein bester Titel für einen Blog-Artikel; aber wahrscheinlich auch nicht der schlechteste... Worum es geht, ist Folgendes: Franck Pachot hat im DBI Blog vor kurzem ein recht unerfreuliches Problem angesprochen, das sich auswirkt, wenn man in 12c Dynamic Statistics (vormals: Dynamic Sampling) in einem Standard Edition System verwendet. Grundsätzlich reduziert Oracle in 12c den Overhead beim Parsing, der sich durch das Sampling ergibt, dadurch, dass die Queries, die für die Erzeugung der Dynamic Statistics verwendet werden, mit einem result_cache Hint versehen sind, der dafür sorgt, dass die Ergebnisse 3600 Sekunden lang im Result-Cache gespeichert bleiben, was die Zugriffskosten natürlich dramatisch reduziert. Leider gibt es in der Standard Edition 12c aber keinen Result-Cache, so dass jede Sampling Query eine zusätzliche Belastung darstellt. Für eine Applikation, die auf Bindevariablen großzügig verzichtet, könnte sich dieses Verhalten in der SE recht unerfreulich auswirken.

Samstag, Juni 13, 2015

INDEX FULL SCAN (MIN/MAX) und partitionierte Tabellen

Eine Frage, die ich mir gelegentlich schon einmal gestellt hatte, und dachte, die Antwort hier im Blog bereits notiert zu haben, lautet: ist der INDEX FULL SCAN (MIN/MAX) als Zugriffsoption auch für partitionierte Tabellen möglich? Da diese Antwort aber auf Anhieb unauffindbar zu sein scheint und womöglich von mir nie protokolliert worden ist, schreibe ich sie (noch einmal?) auf:

drop table t;
create table t
( id number
, startdate date
)
   partition by range (startdate)
   interval (numtoyminterval(1,'MONTH'))
  (partition p1 values less than ( to_date('01.07.2015','dd.mm.yyyy'))
  );

-- Daten einfügen
insert into t
select rownum
     , trunc(sysdate) + mod(rownum, 365)
  from dual 
connect by level <= 100000;

create index t_idx on t(startdate) local;

explain plan for
select min(startdate) from t;

-----------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name  | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |       |     1 |     9 |     2   (0)| 00:00:01 |       |       |
|   1 |  PARTITION RANGE ALL MIN/MAX|       |     1 |     9 |            |          |     1 |1048575|
|   2 |   SORT AGGREGATE            |       |     1 |     9 |            |          |       |       |
|   3 |    INDEX FULL SCAN (MIN/MAX)| T_IDX |     1 |     9 |     2   (0)| 00:00:01 |     1 |1048575|
-----------------------------------------------------------------------------------------------------

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

select /*+ gather_plan_statistics */ max(startdate) from t;

--------------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
--------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |       |      1 |        |      1 |00:00:00.01 |       1 |      1 |
|   1 |  PARTITION RANGE ALL MIN/MAX|       |      1 |      1 |      1 |00:00:00.01 |       1 |      1 |
|   2 |   SORT AGGREGATE            |       |      1 |      1 |      1 |00:00:00.01 |       1 |      1 |
|   3 |    INDEX FULL SCAN (MIN/MAX)| T_IDX |      1 |      1 |      1 |00:00:00.01 |       1 |      1 |
--------------------------------------------------------------------------------------------------------

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

Ich will nicht behaupten, dass da eine große Überraschung im Spiel ist: jeder einzelne lokale Index ist ein Segment und für jedes dieser Segmente ist ein INDEX FULL SCAN (MIN/MAX) möglich. Während das einfache Explain Plan die für interval partitions übliche - eher weniger hilfreiche - Bereichsangabe 1-1048575 für Pstart und Pstop liefert, zeigt der Plan mit rowsource statistics recht deutlich, dass tatsächlich nur auf ein Segment zugegriffen wird, denn Buffers = 1 wäre für einen Zugriff auf mehrere lokale Indizes schwer zu erklären. Auch das ist zunächst keine Überraschung, denn startdate ist schließlich der partition key, so dass hier partition pruning möglich sein sollte und anschließend dann der INDEX FULL SCAN (MIN/MAX) auf der (zeitlich) letzten Partition. Überraschender ist da schon die PARTITION RANGE ALL MIN/MAX Angabe, denn eigentlich ist hier kein Scan aller Partitionen erforderlich und findet laut rowsource statistics auch nicht statt. Um die Annahme, dass hier tatsächlich partition pruning im Spiel (und der Execution Plan nicht so ganz plausibel) ist, zu überprüfen, habe ich - den Erläuterungen von Christoph Bohl folgend - noch ein Trace mit Event 10128 erzeugt und darin Folgendes gefunden:

Partition Iterator Information:
  partition level = PARTITION
  call time = RUN
  order = DESCENDING
  Partition iterator for level 1:
   iterator = RANGE [0, 4]
   index = 4
  current partition: part# = 4, subp# = 1048576, abs# = 4

An dieser Stelle bin ich mir der Semantik nicht so ganz sicher: einerseits scheint die Iterator-Angabe einen Zugriff auf alle fünf Partitionen anzugeben, andererseits verweist current partition nur auf die (zeitlich) letzte Partition. Da mir die zweite Aussage besser in den Kram passt, halte ich sie zunächst mal für die korrekte Interpretation (zumal sie auch den Aussagen in der manuell erzeugten Hilfstabelle kkpap_pruning entspricht) - versuche aber noch, weitere Meinungen einzuholen.

P.S.: die geringe Anzahl der Partitionen ergibt sich übrigens daraus, dass meine Datenbank ein wenig in der Vergangenheit lebt und ihr sysdate im Oktober 2014 sieht.

Donnerstag, Juni 11, 2015

Fehlende Bloom-Filter im Ausführungsplan

Mauro Pagano zeigt in seinem jüngsten Artikel einen Fall, in dem ein Bloom-Filter in den Filter-Prädikaten eines Execution Plans erscheint, aber im Plan selbst nicht aufgeführt wird. Seine Analyse zeigt, dass es sich dabei um ein Problem der Darstellung in dbms_xplan handelt: in 11.2 fehlt der Filter, aber in 12.1 wird er korrekt angezeigt. Interessant ist dabei vor allem die Analysestrategie: zunächst bestimmt er mit Hilfe von Event 10128, dass ein weiterer im Plan aufgeführter Bloom Filter zum partition pruning herangezogen wurde. Dann überprüft er die Existenz und Verwendung des zweiten Filters mit Hilfe von rowsource statistics. Einmal mehr zeigt sich, dass dbms_xplan ein extrem nützliches Werkzeug ist, aber im Rendering der Informationen immer wieder kleine Fehler enthält.

Dienstag, Juni 09, 2015

Merge für überlappende Intervalle

Ein Thema, das in SQL regelmäßig viel Freude bereitet, ist die Logik der Verschmelzung von Intervallen. Da ist es sehr nützlich, wenn sich jemand wie Stew Ashton der Fragestellung annimmt, den Tom Kyte in seinen Oracle-Magazin-Artikeln schon wiederholt anerkennend erwähnt hat. In seinem aktuellen Artikel Merging Overlapping Date Ranges stellt der Autor zunächst die 13 möglichen Fälle der Intervall Agebra von James Allen vor und liefert dann eine überraschend kompakte SQL-Lösung dazu (die letztlich eine Tabibitosan-Variante darstellt). Es wäre nützlich, wenn ich mich an diesen Link erinnern würde, wenn ich mich mal wieder mit Intervallen auseinandersetzen sollte.

Sonntag, Juni 07, 2015

Reihenfolge der Prädikatauswertung

In seinem aktuellen Artikel Predicate Order erläutert und modelliert Jonathan Lewis ein Problem, das vor kurzem in einem OTN Thread vorgestellt wurde: abhängig von der Verwendung von IN oder NOT IN läuft eine Query in einen Fehler "ORA-01722: invalid number" oder wird erfolgreich verarbeitet. Das grundsätzliche Problem ist dabei - und das ist zunächst einmal keine Überraschung - die Wahl ungeeigneter Datentypen: in dem per IN oder NOT IN eingeschränkten Attribut finden sich nicht-numerische Werte, aber die Bedingung vergleicht mit numerischen Angaben. Der ORA-01722 tritt demnach in Abhängigkeit davon auf, ob die Datensätze mit den nicht numerischen Werten bereits durch andere Filterpredikate ausgeschlossen wurden oder nicht. Im OTN-Thread und in Jonathans Modellierung existiert neben dem IN/NOT IN-Prädikat noch eine Einschränkung auf einen Datums-Range, der diese vorangehende Filterung hervorrufen kann: im Fall des IN-Prädikats erwartet der Optimizer, dass dessen Einschränkung deutlich selektiver ist als die der Datums-Prädikate und wendet es deshalb als erstes an, was zum invalid-number-Fehler führt. Das NOT IN-Prädikat hält der Optimizer für deutlich weniger selektiv und verwendet es daher erst nach den Datums-Einschränkungen, wodurch die Query erfolgreich ablaufen kann. Explizit sichtbar ist diese Reihenfolge in der Predicate-Section des Execution Plan, denn die Filter-Prädikate eines Steps erscheinen dort in der Reihenfolge ihrer Anwendung. Im Beispiel des Artikels ist es durch geringfügige Änderungen der Datumseinschränkungen möglich, die Reihenfolge der Prädikatauswertung zu ändern, so dass alle sechs möglichen Reihenfolgen der Prädikate auftreten können, von denen jeweils die Hälfte zu einer erfolgreichen Ausführung und die andere Hälfte zu einem Fehler führen. Bis zu Release 10g war es möglich, die Auswertungsreihenfolge mit Hilfe des Hints ordered_predicates zu beeinflussen, aber dieser ist inzwischen deprecated. Das eigentliche Problem in diesem Zusammenhang ist natürlich die unpassende Definition von Datentypen, aber eine unglückliche Wahl der Reihenfolge der Prädikatauswertung kann wohl auch in Fällen mit weniger deutlichen Modellierungsfehlern auftreten.