Montag, Dezember 28, 2015

Langsamer Zugriff auf v$sql

Ein ganz wohlfeiler Trick, um in diesem Jahr noch auf 94 Blog-Einträge zu kommen - aber keine Angst: 100 werden es diesmal nicht mehr. Kurz vor Weihnachten habe ich im OTN-Forum General Database einen Fall vorgestellt, in dem der Zugriff auf eine eigentlich sehr übersichtliche dynamische Performance-View v$sql (mit kaum mehr als 2000 Einträgen) stabil 25 Sekunden dauert. Dabei sind anscheinend keine Waits im Spiel, sondern es wird fast ausschließlich CPU verbrannt. Trotz zahlreicher sachdienlicher Hinweise von Jonathan Lewis, Stefan Koehler und anderen Beiträgern ist bisher noch keine endgültige Erklärung in Sicht, aber großartig war es, mal wieder zu sehen, wie viele nützliche Antworten man auf OTN bekommt, wenn man eine höfliche Frage stellt. Und wenn ich schon dabei bin, mich bei den OTN-Kommentatoren zu bedanken, dann kann ich gleich auch noch einen Dank an meine treuen Leser und gute Wünsche fürs Folgejahr ergänzen.

Sonntag, Dezember 20, 2015

Table Expansion Bug mit Interval Partitioning

Als das Interval Partitioning in 11g eingeführt wurde, schien mir das eine der besten Ideen gewesen zu sein, die Oracle seit Einführung der Partitionierung eingefallen waren. Leider hat sich im Verlauf der Zeit herausgestellt, dass die Implementierung eine ziemliche große Zahl von Problemen hervorgerufen hat, von denen mir erstaunlich viele im Rahmen meiner eigenen Arbeit begegnet sind. Einen weiteren bizarren Bug, der in diesem Zusammenhang auftreten kann, hat Jonathan Lewis vor kurzem beschrieben: in 12c ergeben sich unter Umständen falsche Ergebnisse, wenn man Table Expansion - also die Möglichkeit, verschiedene Partitionen einer Tabelle mit unterschiedlichen Zugriffsstrategien abzufragen - in Verbindung mit Interval Partitioning verwendet. Im Beispiel wird - via Hint expand_table - ein Index-Zugriff auf eine Partition hervorgerufen, während die übrigen Partitionen per Full Table Scan gelesen werden. Der daraus resultierende Plan enthält zunächst die erwarteten Elemente: das UNION ALL, einen step PARTITION RANGE SINGLE für den Index-Zugriff und einen step PARTITION RANGE ITERATOR mit dem FULL TABLE SCAN für die Partitionen 2 bis 4. Seltsamerweise folgt dann aber noch ein step PARTITION RANGE INLIST ohne Partitionsangaben. Das könnte noch ein Darstellungsfehler im Plan sein, aber die rowsource Statistiken zeigen, dass tatsächlich die doppelte Anzahl von Datensätzen zurückgegeben wird. Zur Eingrenzung des Problems wurden noch folgende Prüfungen durchgeführt:
  • in 11.2.0.4 wird die Table Expansion beim Zugriff auf eine intervallpartitionierte Tabelle auch beim Einsatz des Hints expand_table nicht verwendet.
  • in 11g und 12c ergibt sich das Problem nicht, wenn keine Intervallpartitionierung im Spiel ist.
Klar ist an dieser Stelle zunächst nur, dass hier ein Bug im Spiel ist und dass 12c offenbar Table Expansion für intervallpartitionierte Tabellen in Fällen zulässt, in denen das in 11g noch nicht vorgesehen war. Doppelt schade, denn Table Expansion halte ich im Grunde für ein ähnlich interessantes Feature wie Interval Partitioning.

Dienstag, Dezember 15, 2015

AWR Snapshots vergleichen

Wieder nur ein Link: diesmal auf einen Artikel von Kyle Hailey, der erläutert, wie zwei AWR-Snapshots miteinander vergleicht. Wenn beide Snapshots im im gleichen AWR-Repository liegen, ist dafür nur ein Aufruf der table function dbms_workload_repository.awr_diff_report_text erforderlich (bzw. von dbms_workload_repository.awr_diff_report_html, wenn man die HTML-Version vorzieht). Darüber hinaus hat der Herr Hailey aber noch ein Shell-Skript awrdiff.sh definiert, das es erlaubt textuelle AWR-Reports aus unterschiedlichen Quellen zu vergleichen.

Montag, Dezember 07, 2015

Views für die Postgres Performance Analyse

Nur ein Link auf eine sehr nützliche Grafik, in der Alexey Lesovsky zeigt, welche interne View jeweils verwendet werden kann um bestimmte Performance-Fragestellungen zu beantworten. Sollte ich mir irgendwo an die Wand hängen.

Sonntag, Dezember 06, 2015

Fünf wichtige Optimizer Hints

Jonathan Lewis hat in seinem Blog eine Zusammenfassung der wichtigsten Punkte seiner DOAG-Präsentation Five Hints for Optimising SQL veröffentlicht (was ich sehr erfreulich finde, da ich den Vortrag natürlich nicht gehört habe, so wie alle Vorträge, die in den letzten zehn Jahren bei nationalen und internationalen Konferenzen gehalten wurden). Dabei beginnt er mit dem üblichen Hinweis, dass Hints normalerweise im Produktivsystem zu vermeiden und als Ultima Ratio zu betrachten sind. Und dass von den 332 Hints, die in 12.1.0.2 zur Verfügung stehen, nur eine Handvoll tatsächlich eingesetzt werden sollten, wenn es sich nicht vermeiden lässt. Aus dieser Menge nimmt er dann noch einmal fünf Hints heraus, die er als relativ nützliche Hilfsmittel ansieht, um den Optimizer bei seiner Entscheidungsfindung zu unterstützen. Zu diesen fünf Hints liefert er dann jeweils eine längere Erklärung, die im Rahmen eines komplexeren Beispiels erscheint. Ich versuche, hier hier die initiale knappe Definition mit den folgenden Erklärungen zusammenzuführen. Wie übliche werde ich dabei zweifellos zentrale Punkte unterschlagen...
  • Merge / no_merge — "Whether to use complex view merging". Der Hint kann aber auch verwendet werden, um einzelne Abschnitte einer komplexen Abfrage voneinander zu isolieren. Dadurch kann man einerseits den Suchraum des Optimizers reduzieren und andererseits manchmal auch verhindern, dass der Optimizer in einem bestimmten Bereich der Query eine ungeeignete Strategie wählt. Im Beispiel wird neben den no_merge Hint noch ein cardinality Hint gestellt, der zwar nicht vollständig dokumentiert ist, aber dazu verwendet werden kann, die cardinality eines Query Blocks anzugeben. Ein Einsatzgebiet des Hint-Paars ist auch der Wechsel zwischen den Möglichkeiten "join then aggregate" und "aggregate then join".
  • Push_pred / no_push_pred — "What to do with join predicates to non-merged views". Mit Hilfe dieser Hints kann man festlegen, ob eine Join-Bedingung in eine (nicht im Rahmen der Transformation in einen Join umgewandelte) Subquery propagiert werden soll. Das kann sinnvoll sein, wenn sich dadurch eine effektivere Zugriffsstrategie ergibt (etwa durch einen indizierten Zugriff).
  • Unnest / no_unnest — "Whether or not to unnest subqueries". Normalerweise wandelt der Optimizer Subqueries gerne in Joins um (Semi-Join für exists; Anti-Join für not exists). Da eine Subquery in solchen Fällen oft besser zu verstehen ist, kann ein Hint an dieser Stelle dafür sorgen, dass der Optimizer auch dann die Join-Variante wählt, wenn das (fehleranfällige) Costing nicht für diese Strategie sprechen würde. Im Rahmen des Beispiels weist der Autor darauf hin, dass das Costing in seinem Beispiel (ohne Unnest) tatsächlich die Anzahl der Ausführungen einer Subquery berücksichtigt (Anzahl Departments), was eine jüngere Errungenschaft des CBOs ist.
  • Push_subq / no_push_subq — "When to handle a subquery that has not been unnested". Über diesen Hint kann man beeinflussen, wann eine Subquery ausgeführt wird - traditionell wurde sie erst gegen Ende der Verarbeitung berücksichtigt. Das Pushing einer Subquery bedeutet, sie im Plan nach unten zu schieben, also früher auszuführen.
  • Driving_site — "Where to execute a distributed query". Dieser Hint hat keine NO-Variante, weil er nur angibt, auf welcher Seite einer verteilten Query die Ausführung koordiniert wird und welche Seite als remote fungiert. Der Hint fünktioniert übrigens nicht für CTAS und Insert as Select Queries, was nicht unbedingt nachvollziehbar ist. In solchen Fällen kann die Verwendung einer pipelined table function sinnvoll sein.
Mal wieder eine sehr nützliche Zusammenfassung, die ich in Zukunft vermutlich häufiger als Referenz verwenden werde.

Dienstag, Dezember 01, 2015

Nutzlose und weniger nutzlose METHOD_OPT-Angaben

Da ich hier zuletzt fast nur noch Links kommentiert habe, zur Abwechslung noch mal ein bisschen was Praktisches. Im OTN-Forum wurde heute die Frage gestellt, wieso dbms_stats.gather_table_stats auf den nicht dokumentierten Parameter-Wert "FOR ALL INDEXES" nicht mit einem Fehler reagiert. Meine Antwort darauf lautet: keine Ahnung, aber er ist noch gefährlicher als "FOR ALL INDEXED COLUMNS":

drop table t;
create table t
as
select rownum id
     , mod(rownum, 2) col1
     , mod(rownum, 5) col2
     , mod(rownum, 10) col3
  from dual
 connect by level <= 10000;  
 
create index t_idx1 on t(id);

exec dbms_stats.delete_table_stats(user, 't')
exec dbms_stats.gather_table_stats(user, 't', method_opt=>'FOR ALL INDEXES FOR ALL INDEXED COLUMNS')

select column_name, num_distinct, last_analyzed, histogram from user_tab_cols where table_name = 'T' order by 1;

COLUMN_NAME                    NUM_DISTINCT LAST_ANA HISTOGRAM
------------------------------ ------------ -------- ---------------
COL1                                                 NONE
COL2                                                 NONE
COL3                                                 NONE
ID                                    10000 01.12.15 HEIGHT BALANCED

--> create column statistics (and histograms) just for indexed columns

exec dbms_stats.delete_table_stats(user, 't')
exec dbms_stats.gather_table_stats(user, 't', method_opt=>'FOR ALL INDEXES')

select column_name, num_distinct, last_analyzed, histogram from user_tab_cols where table_name = 'T' order by 1;

COLUMN_NAME                    NUM_DISTINCT LAST_ANA HISTOGRAM
------------------------------ ------------ -------- ---------------
COL1                                                 NONE
COL2                                                 NONE
COL3                                                 NONE
ID                                                   NONE

--> creates no column statistics

exec dbms_stats.delete_table_stats(user, 't')
exec dbms_stats.gather_table_stats(user, 't', method_opt=>'FOR ALL COLUMNS')

select column_name, num_distinct, last_analyzed, histogram from user_tab_cols where table_name = 'T' order by 1;

COLUMN_NAME                    NUM_DISTINCT LAST_ANA HISTOGRAM
------------------------------ ------------ -------- ---------------
COL1                                      2 01.12.15 FREQUENCY
COL2                                      5 01.12.15 FREQUENCY
COL3                                     10 01.12.15 FREQUENCY
ID                                    10000 01.12.15 HEIGHT BALANCED

--> creates column statistics (and histograms) for all columns

exec dbms_stats.delete_table_stats(user, 't')
exec dbms_stats.gather_table_stats(user, 't', method_opt=>'FOR ALL COLUMNS SIZE 1 FOR COLUMNS COL3 SIZE 254')

select column_name, num_distinct, last_analyzed, histogram from user_tab_cols where table_name = 'T' order by 1;

COLUMN_NAME                    NUM_DISTINCT LAST_ANA HISTOGRAM
------------------------------ ------------ -------- ---------------
COL1                                      2 01.12.15 NONE
COL2                                      5 01.12.15 NONE
COL3                                     10 01.12.15 FREQUENCY
ID                                    10000 01.12.15 NONE

--> creates column statistics for all columns and a histogram for COL3

Warum gefährlicher als "FOR ALL INDEXED COLUMNS"? Weil man damit tatsächlich gar keine Spalten-Statistiken erhält, so dass der Optimizer bei der Bestimmung der Cardinalities für alle Spalten auf Schätzungen zurückgehen muss. Ganz ohne Statistiken hätte man da noch dynamic sampling (und dadurch brauchbare Cardinalities), aber wenn Tabellen-Statistiken vorliegen, geht der Optimizer davon aus, dass er auch mit den Angaben zu den Spalten etwas anfangen kann:

SQL> select count(*) from t where col2 = 1;

  COUNT(*)
----------
      2000

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |    13 |     9   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |    13 |            |          |
|*  2 |   TABLE ACCESS FULL| T    |   100 |  1300 |     9   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("COL2"=1)

SQL> exec dbms_stats.delete_table_stats(user, 't')

PL/SQL-Prozedur erfolgreich abgeschlossen.

SQL> select count(*) from t where col2 = 1;

  COUNT(*)
----------
      2000

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |    13 |     9   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |    13 |            |          |
|*  2 |   TABLE ACCESS FULL| T    |  2000 | 26000 |     9   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("COL2"=1)

Note
-----
   - dynamic sampling used for this statement (level=2)

Donnerstag, November 26, 2015

Korrigierte Histogramm-Statistiken im SQL Server anlegen

Nachdem ich viele Jahre lang Tom Kytes Mantra wiederholt habe, dass alle RDBMS unterschiedlich sind und man die Unterschiede kennen muss, um sinnvoll mit den Systemen umzugehen, behaupte ich in jüngerer Vergangenheit immer häufiger, dass die relationalen Datenbanken im Prinzip alle ziemlich ähnlich sind und sich in mancher Hinsicht immer ähnlicher werden. In jedem Fall bin ich immer wieder froh, wenn ich neue Gemeinsamkeiten feststelle, so etwa auch diese hier: im immer wieder lesenswerten SQL Performance.com Blog erläutert Dan Holmes anhand eines umfangreichen Beispiels, wie man mit Hilfe der (nicht supporteten) Option STATS_STREAM des UPDATE STATISTICS Kommandos Optimizer-Statistiken exportieren und importieren kann, um auf diese Weise ein passenderes Histogram einer ungleichen Datenverteilung zu erstellen, als das durch die WITH SAMPLE Option von UPDATE STATISTICS erzeugte. Im Oracle-Universum ist diese Strategie nicht unbekannt (und wird dort sogar offiziell unterstützt) - ein entsprechendes Beispiel liefert (wie üblich) Jonathan Lewis.

Montag, November 23, 2015

Ein griffiges Beispiel für Lateral Inline Views

Auf den Lateral Join, der in 12c eingeführt wurde, habe ich hier schon vor geraumer Zeit hingewiesen - aber was damals fehlte, war ein griffiges Beispiel dafür, was man damit eigentlich anstellen kann. Das hat nun Oren Nakdimon in Teil 7 seiner überaus lesenswerten Serie Write Less with More ergänzt. In seinem Beispiel wird eine Projekt-Tabelle mit einer last_days_to_show_in_reports Spalte vorgestellt, in der für die einzelnen Einträge unterschiedliche Zeiträume angegeben sind (als Anzahl von Tagen). Will man an diese Tabelle eine der last_days_to_show_in_reports Angabe entsprechende Anzahl von Datensätzen joinen, so könnte man vor 12c einen Join mit einer Generator-Query und einer geeigneten ">=" Bedingung definieren (oder alternativ eine table function mit Collection Unnesting, was für mich aber deutlich komplizierter aussieht). In 12c lässt sich dazu in der FROM Klausel eine sehr übersichtliche korrelierte Subquery einsetzen, die nach dem Schlüsselwort LATERAL erscheint. Na gut: ich gebe zu, dass das bei wiederholtem Lesen auch auch nicht selbsterklärend klingt - aber wenn man sich das Code-Beispiel im Artikel ansieht, wird der Fall deutlich klarer.

Donnerstag, November 19, 2015

Ein Wait Interface für Postgres

Ein schöner Hinweis im Artikel Wait interface in PostgreSQL, dessen Autor ich leider nicht ohne weitere Recherche namentlich benennen kann: für postgres 9.4 gibt es eine von Ildus Kurbangaliev entwickelte Erweiterung pg_stat_wait, die möglicherweise in 9.6 Teil des Standards werden wird (aktuell aber noch ein Recompile von postgres und eine Einbindung via shared_preload_libraries erfordert). Nach der Installation stehen diverse Funktionen und Views zur Verfügung, die Informationen zu Events unterschiedlicher Typen bereitstellen (LWLocks, Storage, Locks, Network, Latch, CPU). Zu den Wait Events werden Angaben zur Häufigkeit des Auftretens und zur Dauer der Wartesituationen erfasst. Dabei ist die Erfassung Sample-basiert (wodurch sich nur ein geringer Overhead ergibt), aber man kann auch ein explizites Tracing der Session aktivieren (pg_start_trace), das ein lückenlose Erfassung der Waits ermöglicht. Insgesamt eine Ergänzung, die meine Begeisterung für die Analyse von Performance-Fragestellungen in postgres deutlich erhöhen würde, und die aus meiner Sicht unbedingt in den Standard gehört.

Dienstag, November 17, 2015

Wait Event Analyse mit Flame Graphs

Luca Canali hat im Databases at CERN Blog wieder einmal eine detaillierte Analyse vorgestellt, in der er Flame Graphs mit Extended Stack Profiling verbindet und damit die Aussagen des Oracle Wait Interfaces näher beleuchtet. Der eigentliche Anlass der Untersuchung ist dabei die Beobachtung des Auftretens von Fällen, in denen die DB time größer ist als die Summe von CPU time und Wait time, was a) nicht vorkommen sollte und b) mit dem Einsatz moderner Storage zu tun haben kann. Eine Nacherzählung der umfangreichen Analyseschritte erscheint mir eher witzlos - mir genügt hier der Vermerk, dass das verwendete Instrumentarium sehr genaue Aussagen über das Systemverhalten erlaubt. Wenn ich gelegentlich wieder mal mit komplexen und OS-nahen Problemen zu tun habe, sollte ich mich hier bedienen. Nützlich ist dabei auch, dass der Herr Canali seine älteren Artikel zu ähnlichen Themen und die verwendeten Tools verlinkt hat.

Dienstag, November 10, 2015

Falsche Ergebnisse in 12c mit ROWSETS

Gerne würde ich an dieser Stelle erklären, was ein ROWSET (außerhalb von Java) in Oracle 12c eigentlich ist, aber dazu findet man nicht allzu viele Erklärungen. Aber zumindest kann man offenbar mit einiger Sicherheit sagen, dass es sich nicht um ein besonders ausgereiftes neues Feature handelt...

In einem aktuellen Scratchpad-Artikel beschreibt Jonathan Lewis einen Fall, in dem die Auswahl unterschiedlicher Arraysize-Angaben dazu führt, dass die gleiche Query (auf der gleichen Datenbasis) bei mehrfacher Ausführung eine unterschiedliche Anzahl von Ergebniszeilen zurückliefert. Nun sollten Queries bei unveränderter Datenbasis grundsätzlich die gleichen Ergebnisse liefern - und die Arrayssize, die nur bestimmt, wie viele Datensätze im Rahmen einer Fetch-Operation an den Client geschickt werden, sollte ganz gewiß keine Ergebnisänderung hervorrufen. Die Ursache der unterschiedlichen Ergebnisse zeigt dbms_xplan.display_cursor mit aktivierter Anzeige der Projection. In dieser findet sich in runden Klammern eine Angabe (rowset=200). Zur Bedeutung des Features sagt der Herr Lewis nur: "This is reporting a feature new in 12c (and not to be confused with Oracle Java Rowsets) that should improve the performance of some queries." Und viel mehr habe ich dazu auch an anderer Stelle dazu nicht gefunden. Der Hinweis auf die mögliche Rolle der Rowset verdankte sich dabei übrigens Stefan Koehler, der vor einigen Wochen in den Kreis der Oak Table aufgenommen wurde - was sicherlich eine sehr plausible Ergänzung dieser Tafelrunde darstellt.

Basierend auf dem Scratchpad-Artikel (und weiteren bekannten Bugs) hat inzwischen Mike Dietrich die offizielle Empfehlung ausgesprochen, vorläufig auf die Verwendung von rowsets zu verzichten.

Nachtrag 17.11.2015: Mike Dietrich hat inzwischen in einem weiteren Artikel zusätzliche Details zum Thema geliefert. Einerseits nennt er die Ursache des Problems: "When a hash join operation receives rowsets from its right input but then produces one row at a time as output. This explains why one of the bugs had as potential workaround hash_join_enabled=false (and please don't use this as a w/a!!!)." Zusätzlich liefert er neben der globalen Deaktivierung des Features noch zwei weitere Workarounds: das Einspielen des zugehörigen Bug-Fixes (der aber zum Zeitpunkt der Veröffentlichung des Artikels noch nicht verfügbar war) und die Verwendung eines speziellen Events im spfile, das die rowset Verwendung nur im angesprochenen Problemfall deaktiviert.

Mittwoch, November 04, 2015

Clustering Factor in RAC-Systemen

Da ich seit einigen Monaten wieder verstärkt mit RAC-Datenbanken (und überhaupt mit Oracle) zu tun habe, ist der folgende Hinweis von Jonathan Lewis für mich recht interessant: in einem RAC-System besitzt jede Instanz im Rahmen des ASSM-Speichermanagements ihren eigenen level 1 (L1) bitmap Block und schreibt neue Datensätze insofern auch in ihr zugeordnete Datenblöcke. Wenn konkurrierende Inserts über mehrere Instanzen hinweg erfolgen, landen die neusten Einträge daher in vielen unterschiedlichen Blocks, denn neben der Anzahl der Instanzen spielt dabei auch die Strategie von ASSM eine Rolle, Inserts auch in einer einzelnen Instanz auf 16 unterschiedliche Blocks zu verteilen. Der Clustering Factor eines Index würde unter diesen Umständen in nahezu jedem Fall extrem schlecht aussehen, wenn es nicht die Möglichkeit gäbe, mit Hilfe des Parameters table_cached_blocks dafür zu sorgen, dass Oracle ein gewisses Erinnerungsvermögen zeigt, wenn bestimmt wird, wie stark die Daten in der Tabelle im Hinblick auf einen Index geordnet sind. Ursprünglich wurde hier nur gezählt, wie oft sich die data block Adresse ändert, wenn man den sortiereten rowid-Verweisen der Index-Struktur folgt - was in Abwesenheit von RAC und ASSM immer noch eine plausible Strategie ist. Ausgehend von diesen Überlegungen schlägt der Herr Lewis als Ausgangswert für table_cached_blocks in einem RAC-System 16 * Anzahl Instanzen vor - und das ist angesichts dieser Überlegungen nachvollziehbar.

Freitag, Oktober 30, 2015

SQL Monitoring in 12c (Präsentation von Tanel Poder)

In seiner OOW Präsentation SQL Monitoring in Oracle Database 12c liefert Tanel Poder eine schöne Übersicht zum SQL Monitoring in 12c, was in Anbetracht der Überschrift womöglich nicht allzu sehr überrascht. Ich spare mir eine komplette Nacherzählung (die bei Folien ohnehin einen gewissen Anteil an Interpretation enthalten würde), sondern beschränke mich auf die Auflistung von Punkten, die mir erinnerungswürdig erscheinen - was vielleicht nur eine freundlichere Umschreibung dafür ist, dass ich sie vergessen hatte:
  • das Monitoring erfolgt in 12c:
    • wenn der MONITOR Hint verwendet wird
    • bei parallel queries
    • nach 5 Sekunden CPU/IO für serielle Queries (gesteuert über _sqlmon_threshold)
  • es unterscheidet zwischen "duration" (= wall-clock time from the execution start) und "db time" (= amount of database time your session (and its PX slaves, if any) spent executing this SQL).
  • die "Other" Spalte im HTML-Report enthält in 12c wichtige Zusatzinformationen (HASH JOIN row source, JOIN FILTER CREATE row source, Exadata Details).
  • Bindewerte sind im Report erfasst (ebenso wie in der zugrunde liegenden Spalte v§sql_monitor.bind_xml.
  • für häufig ausgeführte OLTP queries ist die Verwendung des MONITOR-Hints keine gute Idee, da die runtime metrics für jede Ausführung in einer eigenen Struktur in der SGA gespeichert werden müssen.
  • natürlich hat der Herr Poder ein Skript zur Repräsentation der Informationen in sqlplus - nämlich asqlmon.sql.
  • in 12c gibt es AWR-Tabellen zur Persistierung dieser Daten (DBA_HIST_REPORTS, DBA_HIST_REPORTS_DETAILS).
  • große Pläne werden per default nicht erfasst (_sqlmon_max_planlines=300). 
Keine besonders aufsehenerregenden Änderungen, aber doch ein paar nette Ergänzungen - insbesondere die AWR-Integration finde ich nützlich.

Dienstag, Oktober 27, 2015

Oracle Tools: pathfinder, SQLd360, eDB360

Ein paar Links von Mauro Pagano und Carlos Sierra:
Zu den ersten beiden Links ist nicht viel zu sagen, das sind kurze Einführungs-Videos zu den jeweiligen Tools. Der dritte Artikel stellt das Pathfinder Tool vor, das eine recht brachiale Form der Suche nach besseren Ausführungsplänen verwendet: es führt eine gegebene Query mit jedem in der Datenbank möglichen CBO Parameter- und fix_control-Setting durch. Als Ergbnis wird ein html file generiert, das die Resourcen-Nutzung der unterschiedlichen Ausführungen angibt. Interessant ist die Nutzung des Tools sicher nur für Queries mit einer niedrigen Gesamtlaufzeit. Was auch immer man sonst zur Vorgehensweise sagen will: das Verfahren dürfte ziemlich lückenlos ausprobieren, was über eine solche Parametersetzung ausprobiert werden kann.

Mittwoch, Oktober 21, 2015

Lesekonsistenzprobleme mit PL/SQL und Result Cache

Jonathan Lewis hat sich in den letzten Wochen mit einigen Fällen beschäftigt, bei denen die Verwendung des Result Caches oder von mehreren SQL Queries innerhalb einer PL/SQL-Operation Probleme mit der Lesekonsistenz hervorrufen kann:
  • Result Cache: liefert das Fazit "Do not mix the pl/sql result cache with database queries. The cache is public but (unlike the buffer cache) it is not guaranteed to give you read-consistency." Im zugehörigen Beispiel wird eine Temporary Table verwendet, für die der Result Cache laut Dokumentation nicht vorgesehen ist, aber die Probleme scheinen nicht auf diesen Fall begrenzt zu sein. Offenbar gibt es in diesem Zusammenhang eine ganze Reihe von Problemen und Bugs - einige entsprechende Fälle hat Sayan Malakshinov in seinem Kommentar verknüpft.
  • Result Cache 2: behandelt ein Konsistenzproblem, das sich ergeben kann, wenn man PL/SQL-Funktionen in skalaren Subqueries verwendet. Das vorgestellte Beispiel ist ziemlich komplex und betrifft eine Reporting-Fragestellung und zu den möglichen Workarounds gehören die explizite Setzung einer Tabelle oder eines Tablespaces auf read-only, das Einfrieren der SCN für die Transaktion (read-only) und die Verwendung von deterministischen Funktionen. Ich spare mir die detailliertere Wiedergabe der Ausführungen, aber das Fazit lautet: "The moment you create a PL/SQL function that uses the result cache or deterministic option you have to ensure that nobody uses that function without ensuring that their code has handled the consistency threat properly. It’s too easy to forget, with the passing of time, that certain procedures have to be adopted when particular coding strategies are used."
  • Read Consistency: befasst sich mit dem grundsätzlicheren Problem "Any time you execute more than one SQL statement in a PL/SQL procedure the results of executing that procedure may not be self-consistent unless you have explicitly locked your session SCN to a fixed value!!!!!", dass Jonathan Lewis mehrere Ausrufezeichen wert ist - und die sieht man bei ihm sonst sehr selten!! Oracles Standard-Konsistenz-Versprechen ist auf Statement-Ebene beschränkt: wer mehr Konsistenz benötigt - sei es innerhalb einer PL/SQL-Prozedur oder innerhalb eines Berichts auf Basis mehrerer SQL-Queries - muss das passende Isolation-Level wählen. Dazu gibt es ein Code-Beispiel, aber der Fall ist eigentlich auch ohne dieses ziemlich eindeutig.
Ich erhebe keinen Anspruch darauf, die Artikel halbwegs plausibel zusammengefasst zu haben - wie man sieht, habe ich mir sogar das Übersetzen der Kernpunkte weitgehend gespart: mir ging es nur darum die Links zu erfassen, um sie gelegentlich wiederfinden zu können.

Freitag, Oktober 16, 2015

Spalten als Zeilen ausgeben mit SQLcl

Ein Feature, das ich in psql für postgres sehr schätze, ist die Möglichkeit, durch das Kommando \x die "erweiterte Anzeige" zu aktivieren, die Datensätze in einer Record-Darstellung präsentiert, also:

select * from t;

a | b | c
---+---+---
1 | 2 | 3
4 | 5 | 6

--> Switch display with \x

select * from t;

-[ RECORD 1 ]
a | 1
b | 2
c | 3
-[ RECORD 2 ]
a | 4
b | 5
c | 6

Tom Kyte hat für diesen Zweck vor vielen Jahren die Funktion print_table bereitgestellt, die als Parameter den Query-Text erhält - und in der Vergangenheit habe ich damit oft gearbeitet. Allerdings hätte ich lieber eine Lösung, die ohne die Anlage eines Objekts in der Datenbank auskommt, und daher habe ich vor einiger Zeit die Idee A simple output pivot option for sqlplus (or SQLcl) bei OTN untergebracht - vor allem in der Hoffnung, dass da gelegentlich jemand vom SQL Developer Team hinein schauen könnte. Das ist jetzt geschehen und Kris Rice hat eine Lösung mit einem JS File für SQLcl ergänzt. Das ist zwar noch nicht die builtin-Funktion, die ich gerne hätte, aber schon eine richtig gute Lösung. Danke dafür.

Montag, Oktober 12, 2015

Oracle-Package für HTTP/HTTPS

Sayan Malakshinov hat zuletzt zwei Artikel veröffentlicht, in denen er sein GitHub Package XT_HTTP vorstellt, mit dessen Hilfe man auf HTTP- bzw. HTTPS-Seiten zugreifen kann, ohne Zertifikate importieren zu müssen. Die aktuelle Version enthält einen ergänzenden Timeout-Parameter, eine Suchoption auf Basis von regulären Ausdrücken (PCRE) und den Support für plsqldoc (ein Hilfsmittel zur automatischen Generierung von Dokumentation; ähnlich wie javadoc). Sicher sehr nützlich für entsprechende Fragestellungen.

Der Artikel hat noch den zusätzlichen Pluspunkt, dass er im Beispiel belegt, dass ich meiner moralischen Verpflichtung zur Teilnahme am Oracle Database Developer Choice Award nachgekommen bin und tatsächlich auch zu den Up-Votern gehörte. Den Hintergrund zu dieser Bemerkung kann man bei Tim Hall finden - bzw. bei den bei ihm verlinkten Artikeln; und grundsätzlich geht es darum, dass die Teilnahme an der Abstimmung eine sehr kostengünstige Möglichkeit ist, sich bei den nominierten Mitgliedern der Oracle-Community zu bedanken. Wenn ich daran denke, wie viele gute Ideen ich mir im Laufe der Zeit bei Adrian Billington, Stew Ashton, Matthias Rogel und eben auch dem Herrn Malakshinov ausgeborgt habe (um nur einige zu nennen), dann ist ein Dank da durchaus nicht unangemessen...

Dienstag, Oktober 06, 2015

Zusammenspiel von log writer und foreground Prozessen beim Commit

Wenn Frits Hoogland, dessen Beiträge in der Regel ziemlich komplexe Analysen beinhalten, seinen Artikel How the log writer and foreground processes work together on commit mit den Worten beginnen lässt "warning: this is a rather detailed technical post on the internal working of the Oracle database’s commit interactions between the committing foreground processes and the log writer", dann sollte ich so einen Hinweis vielleicht lieber ernst nehmen. Nach der wiederholten Lektüre des Textes bin ich zum Ergebnis gekommen, dass ich in diesem Fall nicht dazu in der Lage bin, ein halbwegs plausibles Exzerpt zu schreiben, weil der Artikel dafür zu viele technische Details beinhaltet, die ich nicht aus dem Kontext reißen mag und auch nicht ausreichend klar erläutern könnte, ohne den Artikel komplett zu übersetzen - und dazu bin ich, Gott sei Dank, nicht verpflichtet. Sollte ich aber jemals in eine Situation kommen, in der das Wissen um die exakten Zusammenhänge bei der Ermittlung der SCN im Kontext von private redo strands eine Rolle spielt und die Auswirkungen der Verwendung der Funktion kcscur3() relevant sind, dann wüsste ich, wo ich nachzuschauen hätte. Und das gleiche gilt für den Fall, dass ich mir ausgefeilte Analysestrategien für die Untersuchung interner Oracle-Operationen ausdenken müsste. Der Artikel kann als vertiefende Ergänzung zu Jonathan Lewis' Buch Oracle Core betrachtet werden - und viel tiefer geht es nicht...

Samstag, Oktober 03, 2015

Estimate_Percent und Histogramme

Dass die Sample-Größe bei der Statistikerfassung via dbms_stats ein schwieriges Thema ist, habe ich wohl schon gelegentlich erwähnt - bzw. die Artikel anderer Autoren nacherzählt, die darüber geschrieben haben. Und ein besonders heikler Teilbereich dieses Themas sind die Histogramme. Und wahrscheinlich gehe ich auf den neuen Scratchpad-Artikel von Jonathan Lewis hier vor allem deshalb noch einmal intensiver ein, weil der Herr Lewis im zugehörigen OTN-Thread meine Einschätzung des gegebenen Falls bestätigt hat - und lobende Erwähnungen von Jonathan Lewis heben meine Stimmung ganz beträchtlich.

Im Artikel geht es um Folgendes: für eine relativ große Tabelle werden täglich neue Statistiken auf Basis einer Sample-Größe von einem Prozent erstellt. In der Folge ergeben sich für eine auf die Tabelle zugreifende einfache Query zwei Pläne: ein effektiver Plan mit Full Table Scan und ein ineffektiver Plan mit Index Range Scan, bei dem die Cardinality der einschränkenden Bedingungen offenbar massiv unterschätzt wird. Im Fall des Index Range Scans erfolgt dabei ein Access über eine Id-Spalte (mit order_id = 0, was ja oft ein häufig erscheinender Sonderfall ist) und eine anschließende Filterung über zwei weitere Spalten (bucket_type = 'P' und sec_id > 0). Auffällig ist dabei noch, dass der Filter Step nur noch eine geringe Reduzierung der Cardinality mit sich bringt, was darauf hin deutet, dass zumindest für bucket_type ein Histogram existiert, dass dem Optimizer mitteilt, dass diese Bedingung nicht selektiv ist. Um das Verhalten zu überprüfen, wird ein Test erstellt, der ein Datenmuster erzeugt, dass zu solchen Effekten führen könnte. Entscheidend ist dabei, dass für die order_id eine massive Ungleichverteilung definiert wird: 5% der Werte enthalten den Wert 0 und sind am Ende der Tabelle geclustert. Anschließend werden die Statistiken einmal mit estimate_percent => 1 und einmal mit mit der default auto_sample_size erzeugt. Wie im OTN-Fall ergeben sich zwei Pläne: bei Verwendung des expliziten Prozentwertes ergibt sich ein ein Index Range Scan und bei Verwendung von auto_sample_size ein Full Table Scan. Ursache ist, dass der Optimizer im Fall des 1% samples nicht erkannte, dass hier ein Histogramm nützlich sein könnte und daher eine Gleichverteilung annahm - und damit die Cardinality für den prädominaten Fall natürlich unterschätzt; und das, obwohl das 1% sample für die Histogrammerstellung eine größere Anzahl von Datensätzen überprüft als die auto_sample_size - nämlich 10000 gegenüber 5500 (diesen unerfreulich niedrigen und nicht anpassbaren Standardwert habe ich hier schon häufiger erwähnt). Warum die beiden Verfahren zu unterschiedlichen Einschätzung kommen, kann auch Jonathan Lewis auf Anhieb nicht erklären: zwar gibt es kleinere Unterschiede zwischen den sql traces der beiden Strategien, aber die erklären das unterschiedliche Verhalten hinsichtlich der Histogramme nicht - und auch die Dokumentation schweigt dazu. Aber entscheidend ist an dieser Stelle zunächst, dass das 1% sample die Gefahr mit sich bringt, data skew für stark geclusterte Werte zu übersehen, was die Instabilität der Pläne in der OTN-Frage erklärt.

Mittwoch, September 30, 2015

Master Notes auf MOS

Jonathan Lewis hat in seinem Blog eine Liste mit Master Notes erstellt, die als Dokumente in der "Knowledge Base" des Oracle-Supports (MOS, die Älteren kennen es noch als Metalink) zu finden sind. Dabei handelt es sich um konzeptionelle Erläuterungen zu zentralen Mechanismen der Datenbank - enthalten sind solche Themen wie "Overview of Data Dictionary (1500058.1)", "Privileges And Roles (1347470.1)", "Overview of Oracle Background Processes (1503146.1)" etc. Dazu gibt er noch den folgenden Tipp:
If you search MoS with either of the expressions “overview of” or “Master Note” and a word describing the feature you’re researching then there’s a fair chance that you’ll find a document that catalogues a number of articles about the topic you’re interested in.
Zum besseren Verständnis zentraler Systemkomponenten sind diese Master Notes sicher ziemlich nützlich.

Freitag, September 25, 2015

Wissenswertes zum Umgang mit ORM-Tools

Sigrid Keydana hat für das diesjährige Trivadis TechEvent eine sehr schöne Präsentation zu den Möglichkeiten und Schwierigkeiten des Einsatzes von Object Relational Mapping (ORM) Tools erstellt, in der sie unter anderem erklärt:
  • warum Entwickler überhaupt auf die Idee kommen, dergleichen einzusetzen.
  • wo die Schwierigkeiten bei der Abbildung von Klassen auf Datenbankobjekte liegen ("Object-Relational Impedance Mismatch").
  • welche Vorgehensweisen beim data fetching zur Verfügung stehen und worauf man dabei zu achten hat (Lazy vs. Eager Fetch, das "n+1 SELECTs" Problem, Prefetching etc.).
Die Präsentation enthält neben ein paar Links zu grundlegenden Artikeln, konzeptionelle Erläuterungen und praktische Beispiele und fasst das Thema aus meiner Sicht sehr nachvollziehbar zusammen.

Insbesondere unter DBAs gelten ORM-Tools ja bisweilen als Teufelswerk, aber- so banal das auch klingt - hier gilt mal wieder: für jedes Werkzeug gibt es den geeigneten Anwendungsbereich und die geeignete Verwendung. Zumindest habe ich in der Vergangenheit auch Anwendungen gesehen, die über Hibernate völlig plausibles SQL erzeugten; neben solchen, denen das nicht so ganz uneingeschränkt gelang... Dass die Generierung von SQL-Code anhand von Regelwerken kein triviales Problem ist, hat man allerdings auch schon bei Tools wie dem Oracle Warehouse Builder (OWB) gesehen, dessen generierter Code auch manchmal suboptimal ausgefallen ist.

    Samstag, September 19, 2015

    Überflüssige Indizes anhand von Statistiken erkennen

    Bereits vor einigen Wochen hat Jonathan Lewis einen Beitrag veröffentlicht, in dem er zeigt, wie man anhand der Statistiken in v$segstat (bzw. v$segment_statistics) bestimmen kann, ob die Lesezugriffe für einen Index nur für dessen Maintenance erforderlich, oder ob auch Zugriffe über den Index erfolgt sind. Ganz grob gilt dabei, dass ein Index mit blevel = 2 dann vermutlich nicht für Zugriffe verwendet wird, wenn die Anzahl der db block changes etwa ein Drittel der logical reads beträgt. Natürlich weist der Herr Lewis explizit darauf hin, dass sich auf der Basis dieser Beobachtung keine exakten Aussagen treffen lassen - man kann nur Kandidaten ausfindig machen, die dann genauer zu untersuchen wären. Außerdem werden die Zahlen massiv fragwürdig, wenn ein Commit im Test ergänzt wird: aber dadurch ergeben sich keine false positives in der Richtung, dass ein für Zugriffe verwendeter Index als Lösch-Kandidat vorgeschlagen werden würde. Insgesamt ist das Verfahren insofern cum grano salis zu nehmen, aber trotzdem ein interessanter Ansatz, der insbesondere in unübersichtlichen Landschaften mit vielen "historisch begründeten" Indizes einen Einstieg liefern kann. Natürlich umfasst der Artikel ein umfangreiches Beispiel, das ich hier (wie üblich) nicht wiedergebe.

    Freitag, September 18, 2015

    Index Advanced Compression und mehrspaltige Indizes

    Vor einem knappen Jahr hatte Richard Foote in seinem Blog mehrere Artikel zum Thema Index Advanced Compression veröffentlicht ("Advanced Index Compression" hätte mir besser gefallen). Jetzt hat der Autor offenbar eine neue Artikelserie begonnen, die sich mit der Wirkung des Features für mehrspaltige Indizes beschäftigt. Damit ich es nicht später vergesse, hier noch mal der explizite Hinweis, dass die Index Advanced Compression eine Lizenzierung der Advanced Compression Option erfordert. Wie üblich werde ich versuchen, alle Folgeartikel zu erfassen, kann aber nichts versprechen:
    • Index Advanced Compression: Multi-Column Index Part I (There There): wie üblich gibt es ein Beispiel und in diesem Beispiel wird eine Tabelle angelegt, deren erste Spalte unique ist, während die zweite nur 10 distinkte Werte enthält und die dritte immer den gleichen (erstaunlicherweise den String "David Bowie"; ich frage mich, wie der Herr Foote seine Passwörter auswählt). Dazu wird ein Index angelegt, dessen führendes Attribut die zweite Tabellen-Spalte ist (also die mit 10 unterschiedlichen Werten), der dann die eindeutige Id als zweite Spalte folgt. Zunächst wird gezeigt, dass eine Compression über beide Spalten den Index größer werden lässt, als er es ohne Komprimierung wäre. Wird die Compression auf die erste Spalte beschränkt, so ergibt sich eine Größenreduzierung für den Index. Diese Entscheidung für den passenden Komprimierungsgrad wird durch die Index Advanced Compression hinfällig, da dabei die Bestimmung des optimalen Verfahrens automatisch erfolgt. In einem zweiten Schritt wird das Beispiel dadurch modifiziert, dass in der eindeutigen Id-Spalte für einen größeren Bereich (10%) die Werte auf einen einheitlichen Wert gesetzt werden. Mit der manuell gesetzten Compression über eine bzw. zwei Spalten ergeben sich ähnliche Werte wie für den initialen Fall. Durch die Index Advanced Compression ist für diesen Fall aber eine Verbesserung möglich, da das Feature dafür sorgt, dass die dafür geeigneten Abschnitte des Index über beide Spalten komprimiert werden, während für den Rest der Struktur nur die erste Spalte komprimiert (soll heißen: dedupliziert) wird. Also zur Abwechslung mal eine automatische Option bei Oracle, die eine deutliche Verbesserung gegenüber der manuellen Vorgehensweise darstellt (das klingt jetzt vielleicht abfälliger, als es gemeint ist, aber ich hatte zuletzt mit AMM zu tun).
    • Index Advanced Compression: Multi-Column Index Part II (Blow Out): weist darauf hin, dass die Spaltenreihenfolge auch mit der Index Advanced Compression relevant bleibt: eine führende Spalte mit wenigen Wiederholungen ist weiterhin kein geeigneter Kandidat für Komprimierung. Gut komprimierbar ist mit dem neuen Verfahren hingegen ein Index, bei dem in einem bestimmten Bereich viele Wiederholungen für die führende Spalte erscheinen, während in der Regel kaum Wiederholungen auftreten. In diesem Fall kann Oracle den gut komprimierbaren Bereich komprimieren und den Rest des Index unkomprimiert lassen.
    Wie gesagt: ich versuche den oder die Folgeartikel zu ergänzen.

    Freitag, September 11, 2015

    Lokale und globale Hints

    Es gibt Blog-Einträge, deren Inhalt man in einem Satz zusammenfassen kann. Bei den Artikeln von Stefan Koehler in seinem SAP on Oracle Blog ist das in der Regel nicht der Fall, was vermutlich einer der Gründe dafür ist, dass ich mir mit dem Exzerpieren seiner Beiträge in der Regel relastiv viel Zeit lasse - manchmal so lange, dass sie rechtzeitig wieder aus meinem Blog-Reader verschwunden sind, ehe ich sie berücksichtige. Vor drei Wochen erschien der Artikel Insights into SQL hints - Embedded global and local hints and how to use them, der zunächst auf grundlegende Artikel (einen eigenen und einen von Jonathan Lewis) verweist und die Verwendung von Mauro Paganos SQLd360 empfieht. Nach dem Hinweis, dass Hints nur in Ausnahmefällen als "last resort" verwendet werden sollten (dem ich vorbehaltlos zustimme) bildet die Erläuterung, was denn eigentlich ein "embedded hint" ist, den eigentlichen Eigenstieg in den Artikel: "embedded hints" sind solche, die in SQL Statements verwendet werden - und insofern zu unterscheiden von "outline hints", die in outlines und baselines verwendet werden. Ich schenke mir diese terminologische Genauigkeit und rede in der Folge nur noch von Hints.

    Die nächste definitorische Aussage unterscheidet zwischen globalen und lokalen Hints:
    • local hint: gelten für den query block, in dem sie erscheinen. Es folgen Informationen zu ihrem Auftreten im SAP-Kontext, aber SAP ist nicht mein Thema.
    • global hint: werden mit einem vorangestellten @-Symbol vor der Angabe des query-Blocks, auf den sie sich beziehen, ausgezeichnet (und die Hints in outlines sind immer globale Hints): sie erscheinen also an einer zentralen Stelle (zu Beginn der Query) und sind nicht über den Text der Query verteilt. Sie sind insbesondere bei der Definition von Queries, die auf Views zugreifen, relevant, da einer Ergänzung der Hints im View-Text in der Regel keine Option ist.
    Es folgt ein umfangreiches (SAP-)Beispiel, das zeigt, dass ein FULL-Hint für eine Query mit View-Zugriff kalt lächelnd ignoriert wird, weil er aufgrund einer Transformation gegenstandslos wird: der Query-Block, dem der Hint zugeordnet ist, wird im Rahmen der Transformation in einen anderen Query-Block gemerget. Um den Hint wirksam werden zu lassen, muss man ihn entweder in der View ergänzen oder als globalen Hint für einen Query-Block, der die Transformation übersteht (bzw. durch sie erzeugt wird).

    War gar nicht so umfangreich, wie mir jetzt auffällt - aber wie üblich hat der Herr Koehler seine Ausführung mustergültig mit Belegen und Detail-Erläuterungen versehen.

    Samstag, September 05, 2015

    Vereinfachter csv-Import mit SQL Loader Express in 12c

    Ich will nicht behaupten, dass es mich tief erschüttert hätte, dass Tom Kyte vor einigen Tagen eine Auszeit angekündigt und seine AskTom-Seite an Chris Saxon und Connor McDonald übergeben hat - aber völlig unberührt gelassen, hat es mich jedenfalls nicht. Meine Beschäftigung mit Oracle-Datenbanken hat im Jahr 2000 begonnen und AskTom war gerade in den ersten Jahren der Ort, an dem ich nach Erklärungen suchte für alle Verhaltensweisen des RDBMS, die ich nicht verstanden hatte - und das waren zunächst ziemlich viele. Die Bücher, die der Herr Kyte im Lauf der Jahre veröffentlicht hat, stehen immer in Griffweite im Regal und obwohl ich AskTom in den letzten Jahren nur noch selten besucht habe - unter anderem, weil die interessanten Threads irgendwann unendlich lang wurden - bedauere ich Toms Abschied; und freue mich andererseits darüber, dass er plant, weitere Bücher zu schreiben, und dass er so kompetente Nachfolger für die Weiterführung von AskTom gefunden hat.

    Was mich zum eigentlichen Thema bringt: in seinem eigenen Blog (den ich auch seit vielen Jahren verfolge) hat Connor McDonald (von dem hier auch noch irgendwo ein Buch steht) vor kurzem einen Artikel zu einem interessanten neuen Feature in 12c veröffentlicht: dem SQL Loader Express. Dieser leistet etwas ganz Simples: er erlaubt ein vereinfachtes Einlesen von csv-Dateien - ein Feature, das mir bei Oracle seit vielen Jahren gefehlt hat, weil dieses Einlesen in anderen RDBMS schon seit langer Zeit sehr viel einfacher durchzuführen war. Dazu ein minimales Beispiel (das noch deutlich minimaler ist als das des Herrn McDonald und die entsprechenden Erläuterungen des von ihm verlinkten White Paper):

    -- csv-Datei emp.txt mit folgenden Daten
    7902,FORD,ANALYST
    7844,TURNER,SALESMAN
    7788,SCOTT,ANALYST
    7654,MARTIN,SALESMAN
    7566,JONES,MANAGER
    7369,SMITH,CLERK
    7876,ADAMS,CLERK
    7900,JAMES,CLERK
    7698,BLAKE,MANAGER
    7782,CLARK,MANAGER
    7521,WARD,SALESMAN
    7934,MILLER,CLERK
    7499,ALLEN,SALESMAN
    7839,KING,PRESIDENT
    
    -- Anlage einer entsprechenden Tabelle emp im Schema test
     create table emp(
      empno    number(4,0),
      ename    varchar2(10),
      job      varchar2(9)
    );
    
    -- Aufruf des sqlldr im Verzeichnis, in dem sich die csv-Datei befindet
    sqlldr userid=test/test data=emp.txt table=emp
    
    SQL*Loader: Release 12.1.0.2.0 - Production on Fri Oct 3 00:57:18 2014
    
    Copyright (c) 1982, 2014, Oracle and/or its affiliates.  All rights reserved.
    
    Express Mode Load, Table: EMP
    Path used:      External Table, DEGREE_OF_PARALLELISM=AUTO
    
    Table EMP:
      14 Rows successfully loaded.
    
    Check the log files:
      emp.log
      emp_%p.log_xt
    for more information about the load.
    

    Das zugehörige Log zeigt sehr detailliert, was im Hintergrund geschieht:

    Express Mode Load, Table: EMP
    Data File:      emp.txt
      Bad File:     emp_%p.bad
      Discard File:  none specified
    
     (Allow all discards)
    
    Number to load: ALL
    Number to skip: 0
    Errors allowed: 50
    Continuation:    none specified
    Path used:      External Table
    
    Table EMP, loaded from every logical record.
    Insert option in effect for this table: APPEND
    
       Column Name                  Position   Len  Term Encl Datatype
    ------------------------------ ---------- ----- ---- ---- ---------------------
    EMPNO                               FIRST     *   ,       CHARACTER
    ENAME                                NEXT     *   ,       CHARACTER
    JOB                                  NEXT     *   ,       CHARACTER
    
    Generated control file for possible reuse:
    OPTIONS(EXTERNAL_TABLE=EXECUTE, TRIM=LRTRIM)
    LOAD DATA
    INFILE 'emp.txt'
    APPEND
    INTO TABLE EMP
    FIELDS TERMINATED BY ","
    (
      EMPNO,
      ENAME,
      JOB
    )
    End of generated control file for possible reuse.
    
    created temporary directory object SYS_SQLLDR_XT_TMPDIR_00000 for path /home/oracle
    
    enable parallel DML: ALTER SESSION ENABLE PARALLEL DML
    
    creating external table "SYS_SQLLDR_X_EXT_EMP"
    
    CREATE TABLE "SYS_SQLLDR_X_EXT_EMP"
    (
      "EMPNO" NUMBER(4),
      "ENAME" VARCHAR2(10),
      "JOB" VARCHAR2(9)
    )
    ORGANIZATION external
    (
      TYPE oracle_loader
      DEFAULT DIRECTORY SYS_SQLLDR_XT_TMPDIR_00000
      ACCESS PARAMETERS
      (
        RECORDS DELIMITED BY NEWLINE CHARACTERSET US7ASCII
        BADFILE 'SYS_SQLLDR_XT_TMPDIR_00000':'emp_%p.bad'
        LOGFILE 'emp_%p.log_xt'
        READSIZE 1048576
        FIELDS TERMINATED BY "," LRTRIM
        REJECT ROWS WITH ALL NULL FIELDS
        (
          "EMPNO" CHAR(255),
          "ENAME" CHAR(255),
          "JOB" CHAR(255)
        )
      )
      location
      (
        'emp.txt'
      )
    )REJECT LIMIT UNLIMITED
    
    executing INSERT statement to load database table EMP
    
    INSERT /*+ append parallel(auto) */ INTO EMP
    (
      EMPNO,
      ENAME,
      JOB
    )
    SELECT
      "EMPNO",
      "ENAME",
      "JOB"
    FROM "SYS_SQLLDR_X_EXT_EMP"
    
    dropping external table "SYS_SQLLDR_X_EXT_EMP"
    
    Table EMP:
      14 Rows successfully loaded.
    

    Demnach umfasst die Operation folgende Schritte:
    • Generierung eines wiederverwendbaren sqlldr control files.
    • Erzeugung eines temporären directories für das Verzeichnis, in dem die csv-Datei liegt und in dem der sqlldr-Aufruf erfolgte.
    • Aktivierung von parallel dml für die Session.
    • Anlage einer external table basierend auf der Struktur der csv-Datei.
    • parallelisiertes Insert Append zur Übertragung der Daten der External Table in die Zieltabelle.
    • Drop der External Table.
    Das Verfahren erspart eine Menge Definitionsaufwand und kann als External Table Generator verwendet werden (wie David Aldridge in seinem Kommentar bemerkt).

    Nachtrag 14.09.2015: offenbar auch wieder ein Thema, das aktuell ein gewisses Interessae hervorruft - jedenfalls hat auch Oren Nakdimon ein schönes entsprechendes Beispiel veröffentlicht.

    Donnerstag, September 03, 2015

    Fehlende Foreign Keys mit dem SQL Developer ermitteln

    Regelmäßig erinnert mich Jeff Smith mit seinen Artikeln daran, dass es doch eine gute Idee wäre, endlich mal dazu überzugehen, den SQL Developer zu meinem Standardwerkzeug zu machen. In einem aktuellen Beitrag weist er beispielsweise darauf hin, dass der SQL Developer Data Modeller anhand von Spaltennamen und Datentypen dazu in der Lage ist, über eine Option "Discover Foreign Keys" Vorschläge zur Erzeugung fehlender FK-Constraints zu generieren. Das erfordert natürlich eine konsistente Namensgebung, so dass das Verfahren in vielen Datenmodellen, die mir im Laufe der Jahre begegnet sind, an seine Grenzen gekommen wäre. Aber ein nettes Feature ist das allemal.

    Montag, August 31, 2015

    Truncate-Verhalten in Oracle

    Jonathan Lewis hat zuletzt zwei Artikel zum Thema Truncate veröffentlicht, in denen er ein paar grundsätzliche Erläuterungen zu Verhalten gibt:
    • Truncate: erzeugt kleine Mengen von undo und redo, da nur die Metadaten für Space-Management und Dictionary Informationen protokolliert werden müssen. Diese Aussage überprüft der Autor durch einen Test, bei dem er die Sequenz "switch logfile, truncate table, dump logfile" ablaufen lässt. Ein interessantes Detail in diesem Zusammenhang ist, dass die data_object_id eines zugehörigen Index nach dem Truncate niedriger ist als die data_object_id der Tabelle, was damit zu tun hat, dass der Index vor der Tabelle truncated wird und somit zuerst eine neue Id erhält. Vor allem aber liefert der Artikel eine Übersicht zur (vermuteten) Schrittabfolge beim Truncate mit den erforderlichen Space-Management-Operationen (für Index und Tabelle) und den Änderungen der Dictionary Objekte. Auffällig ist, dass die Zahl der commits im Rahmen des Tests höher ausfällt als erwartet, aber es sieht so aus, dass die eigentliche Truncate-Operation in einer einzigen Transaktion zusammengefasst ist, während die übrigen Transaktionen weitere Aufräumarbeiten beim Space Management enthalten.
    • Truncate – 2: zeigt, dass man das implizite commit eines Truncate mit Hilfe einer autonomen Transaktion daran hindern kann, vorangehende DML-Operationen festzuschreiben (was nicht weiter überrascht).

    Dienstag, August 18, 2015

    Indizes löschen

    Jonathan Lewis hat - wieder einmal angeregt durch entsprechende Fragen in den OTN-Foren - einen Artikel zur Frage geschrieben: welche Informationen muss man analysieren, um bestimmen zu können, ob ein Index gefahrlos gelöscht werden kann? Bisher ist es zwar nur ein Artikel, aber da ein zweiter angekündigt ist, mache ich gleich eine Aufzählung daraus:

    Erklärt zunächst das grundsätzliche Probleme extremer Indizierung: zwar wird - potentiell - die Treffsicherheit der Zugriffe erhöht, aber man bezahlt dafür mit einer massiven Vergrößerung von undo/redo für die Index-Maintenance. Insofern erscheint es naheliegend, überflüssige Indizes zu löschen - also solche, die in den Ausführungsplänen des Optimizers nicht erscheinen -, aber dabei ergeben sich oft Schwierigkeiten:
    • die Tatsache, dass ein Index benutzt wird, bedeutet nicht unbedingt, dass er auch benutzt werden sollte. Möglicherweise übersieht der Optimizer einen geeigneteren Index.
    • das bedeutet im Gegenzug natürlich auch, dass ein unbenutzter Index unter Umständen benutzt werden sollte.
    • die Statistiken eines Index (insbesondere die Anzahl distinkter Einträge) sind eine Information, die der Optimizer berücksichtigen kann, auch wenn ein Index nicht in Query-Plänen erscheint. Theoretisch kann man diese Information auch über extended statistics erfassen, aber mehr als 20 column groups kann man damit pro Tabelle nicht erfassen.
    • unbenutzte Indizes können als Lösung des berüchtigten foreign key locking-Problems eine Rolle spielen.
    Zur Bestimmung der tatsächlichen Verwendung gibt es diverse Hilfsmittel, die allerdings jeweils ihre spezifischen Beschränkungen haben:
    • v$object_usage: hat eine beschränkte Aussagekraft, da die View nur eine simple ja/nein-Logik zur Verwendung abbildet und nichts über die Häufigkeit der Verwendung aussagt.
    • v$sql_plan: es ist teuer, diese View zu scannen - und ihre korrespondierenden Gegenstücke in AWR/Statspack basieren auf Sampling, so dass darin Queries fehlen können.
    • Analyse der Definitionen: durch Betrachtung der Index-Definitionen kann es bereits möglich sein, Redundanzen zu bestimmen und zu ermitteln, welche Indizes verzichtbar sein sollten. Natürlich kann dieses Vorgehen recht zeitaufwändig sein.
    • v$segstat, v$segment_statistics: liefern Informationen zu den Lese- und Schreiboperationen auf den Segmenten - wobei v$segment_statistics nur eine um Namen ergänzte Varainte von v$segstat darstellt. Weitere Details finden sich in der zugrunde liegenden Struktur x$ksolsstat. Die entscheidende Frage ist nun, wie man die darin enthaltenen Angaben sinnvoll interpretieren kann und zwischen der Arbeit, die im Rahmen der Index-Maintenance anfällt, und den eigentlichen Zugriffsoperationen unterscheidet - aber das ist eine Geschichte, die ein andermal erzählt werden soll; nämlich im angekündigten zweiten Artikel. 
    Diesen zweiten Artikel plane ich zu ergänzen, aber das mag - urlaubsbedingt - einige Zeit dauern.

    Samstag, August 15, 2015

    UNUSABLE indexes und TRUNCATE TABLE

    Ein wichtiger Hinweis von Mauro Pagano, der zwei Informationen zusammenbringt, die mir beide bekannt waren, ohne dass ich mich dafür verbürgen würde, dass ich in die angesprochene Falle nicht schon gelegentlich hineingetappt bin:
    • seit 11.2 sorgt ein ALTER INDEX UNUSABLE dafür, dass das zugehörige Index Segment (oder Index-Partition-Segment) entfernt wird.
    • ein TRUNCATE TABLE versetzt zugehörige Indizes wieder in den Status USABLE und erzeugt das Index-Segment wieder.
    Somit sollte die Reihenfolge vor der Durchführung von Load-Operationen immer so gewählt sein, dass das ALTER INDEX UNUSABLE erst nach einem TRUNCATE TABLE erfolgt.

    Sonntag, August 09, 2015

    "Fixed Subqueries" und Partitionierte Tabellen

    Jonathan Lewis weist in seinem aktuellen Scratchpad-Artikel darauf hin, dass neue Features des Optimizers nicht immer in allen relevanten Zusammenhängen folgerichtig integriert werden. Das Beispiel, an dem diese Schwierigkeit aufgezeigt wird, ist das der "fixed subqueries" - also Queries der Form "select 42 from dual" -, bei denen der Optimizer (seit 12c) dazu in der Lage ist, zu erkennen, dass der Wert 42 invariant ist, und daher bereits bei der Optimierung berücksichtigt werden kann. Im Artikel wird gezeigt, dass der Optimizer erwartungsgemäß dazu in der Lage ist, einschränkende Prädikate solcher "fixed subqueries" bei den Cardinality-Schätzungen korrekt zu berücksichtigen - und dass das nicht funktioniert, wenn der statische Wert durch eine Funktion verschleiert wird (also wenn satt 42 eine Funktion f(42) erscheint, die 42 zurückliefert). Wenn man aber statt einer einfachen eine partitionierte Tabelle verwendet, ergibt sich die erwähnte Uneinheitlichkeit: für den Fall der Funktionsverwendung ist das Verhalten folgerichtig, aber beim Einsatz des unveränderten Literalwertes wird dieser zwar bei der Cardinality-Schätzung berücksichtigt, nicht aber bei der Bestimmung der Pstart und Pstop values, die mit den Angaben KEY - KEY erscheinen - also zum Compile-Zeitpunkt anscheinend als unbekannt betrachtet werden. Offenbar ist das Verhalten also noch nicht in allen Zusammenhängen konsistent, was vermutlich in folgenden Releases korrigiert werden wird.

    Donnerstag, August 06, 2015

    Verwaiste SQL-Server-Dateien

    Ein schöner Hinweis von Andrej Kuklin, mit dem ich lange zusammen gearbeitet habe (und noch länger Fußball spiele): da der SQL Server bei der Löschung einer zuvor OFFLINE gesetzten Datenbank die zugehörigen Daten- und Log-Dateien nicht ebenfalls löscht, kann man relativ leicht verwaiste Dateien erzeugen, die von keiner DB mehr benötigt werden. Für den SQL Server 2012 liefert Andrej ein Skript, mit dessen Hilfe diese Überreste ermittelt werden können. Für ältere Releases gibt es andere Varianten, die im Artikel verlinkt sind.

    Montag, August 03, 2015

    Library Cache Verschmutzung

    Da ich den Fall hier eher wiederfinde als im OTN-Forum erzähle ich an dieser Stelle kurz einen Thread nach, den ich dort vor einigen Tagen begonnen habe. Meine Frage im Forum lautete: wieso sehe ich in einer RAC-Instanz (11.2.0.3, zwei Knoten, Standard Edition) diverse Queries, zu denen zahlreiche child cursor existieren, obwohl die Queries keine Bindewerte enthalten, so dass die Pläne eigentlich problemlos wiederverwendbar sein sollten? Die Antwort auf die Frage haben mir John Spencer, Randolf Geist und Mohamed Houri geliefert, die die fehlenden Einträge in meinem Lückentext ergänzten. Insbesondere wies mich Randolf darauf hin, dass in der Standard Edition eigentlich keine Parallelisierung im Spiel ist, aber Zugriffe auf gv$-Objekte intern trotzdem parallel operieren - was dazu passte, dass die fraglichen Queries alle auf dynamische Performance-Views zugriffen. Außerdem merkte er an, dass die Spalte REASON in gv$sql_shared_cursor nicht zum Spass eingeführt wurde, sondern wichtige Begründungsdetails dafür liefert, warum ein Plan nicht wiederverwendet werden kann. Möglicherweise hätte ich sofort genauer auf diese Angaben geschaut, wenn ich Tanel Poders nonshared Skript verwendet hätte, auf das Mohamed verwies, und das ein Pivoting der View-Informationen durchführt und die Reason exponiert darstellt. Der Angabe war dann - mehr oder minder - deutlich zu entnehmen, dass hier eine Abweichung zwischen dem parallel_query_default_dop und dem kxfr_Default_DOP vorliegt, was genau zum von John erwähnten "Bug 14711917 - High version count in RAC due to PX_MISMATCH (Doc ID 14711917.8)" passt. Die Ursache für die Abweichung liegt im gegebenen Fall wohl darin, dass die beiden RAC-Knoten tatsächlich einen unterschiedlichen CPU_COUNT besitzen. Den könnte man theoretisch korrigieren, aber sinnvoller erscheint die Verwendung des zugehörigen Patches oder das Upgrade auf 11.2.0.4. Einmal mehr zeigt sich, dass die OTN-Foren eine sehr große Hilfe bei der Problemlösung sein können. Im Thread habe ich meine Beobachtungen recht umfassend dokumentiert - und das spare ich mir deshalb hier.

    P.S.: der aufmerksame Leser mag einen Zusammenhang mit meiner letzten Notiz hier im Blog vermuten - und da läge er nicht daneben.

    Donnerstag, Juli 30, 2015

    Bizarre Plandarstellung mit dbms_xplan.display_cursor

    Vor einigen Tagen ist mir aufgefallen, dass in einer Datenbank (11.2.0.3), an deren Wartung ich seit kurzem beteiligt bin, für eine harmlose Nagios-Check-Query mit dbms_xplan.display_cursor höchst merkwürdige Pläne generiert wurden, die die einzelnen Schritte des Ausführungsplans in schier endloser Folge wiederholten. Ein Blick in v$sql_plan zeigte, dass hier tatsächlich extrem viele Plan-Duplikate vorlagen, aus v$sql_shared_cursor war zu ersehen, dass die Begründung der neuen Cursor in der Regel mit OPTIMIZER_MISMATCH oder PX_MISMATCH angegeben war, und aus v$sql ging hervor, dass die meisten dieser Cursor mit dem Flag IS_OBSOLETE=Y gekennzeichnet waren, das die Dokumentation folgendermaßen erläutert: "Indicates whether the cursor has become obsolete (Y) or not (N). This can happen if the number of child cursors is too large."

    Ein Blick auf den Ausführungsplan der Query zeigte, dass hier tatsächlich Parallelisierung beim Zugriff auf Dictionary-Objekte im Spiel war, aber das erklärt aus meiner Sicht noch nicht die Häufigkeit der Wiederholungen. Aber immerhin war schnell festzustellen, dass Timur Akhmadeev das Problem der bizarren Plandarstellung bereits 2012 beschrieben hat:
    • V$SQL.IS_OBSOLETE: erwähnt die fehlerhafte Plandarstellung und weist darauf hin, dass in solchen Fällen immer höchstens ein "aktiver" Repräsentant pro child_number vorliegt (mit IS_OBSOLETE = 'N') und zusätzlich viele inaktive (IS_OBSOLETE = 'Y').
    • Obsolete cursors: reproduziert das Verhalten mit einem einfachen Testfall: "After the point of 100 child cursors per parent, Oracle builds a new parent (V$SQL.ADDRESS holds its address) cursor, marking old parent and child cursors as obsolete. V$SQLAREA handles the situation well, but V$SQL doesn’t and that’s clearly a bug. The threshold point of when to build a new parent cursor is 100 by default and is controlled with a new hidden parameter _cursor_obsolete_threshold."
    Damit habe ich die Erklärung für das Anzeigeproblem. Jetzt fehlt mir nur noch eine Erklärung für die Erzeugung so vieler Cursor durch einen regelmäßig durchgeführten Nagios-Check mit einer relativ einfachen statischen SQL-Query.

    P.S.: mein nächster Schritt wird die Überarbeitung der Check-Query, die mir ganz grundsätzlich noch nicht so recht gefällt.

    P.P.S.: Stefan Koehler hat mich daran erinnert, dass Christian Antognini auch einen Artikel zum Thema (und insbesondere zum Parameter _cursor_obsolete_threshold) geschrieben hat, was mir vermutlich auch schon mal bekannt gewesen ist - zumindest habe ich den Artikel hier verlinkt.

    Freitag, Juli 24, 2015

    Indizierung für LIKE-Operationen in postgres

    Ein interessanter Hinweis von Daniel Westermann im DBI Services Blog: durch die Verwendung der pg_trgm extension ist es in postgres möglich, (GIN oder GiST) Indizes zur Unterstüzung von Like-Einschränkungen mit führendem (oder auch im Vergleichsstring enthaltenen) Platzhalter(n) zu erstellen. Laut Dokumentation gilt:
    The pg_trgm module provides GiST and GIN index operator classes that allow you to create an index over a text column for the purpose of very fast similarity searches. These index types support the above-described similarity operators, and additionally support trigram-based index searches for LIKE, ILIKE, ~ and ~* queries. (These indexes do not support equality nor simple comparison operators, so you may need a regular B-tree index too.)
    Und zur Erinnerung noch mal die - an gleicher Stelle aufgeführte - Klassifikation für GIN und GiST Indizes: "As a rule of thumb, a GIN index is faster to search than a GiST index, but slower to build or update; so GIN is better suited for static data and GiST for often-updated data." Ich muss mal anfangen, diese Features häufiger einzusetzen, damit ich sie endlich meinem aktiven Wissensbestand hinzufügen kann - anstatt beim Lesen immer nur zu denken: stimmt, das gibt es ja auch noch...

    Dienstag, Juli 21, 2015

    Schlechtere Performance durch semi_to_inner-Transformation in 12c

    Jonathan Lewis zeigt in seinem aktuellen Artikel 12c Downgrade einen Fall, in dem der Optimizer in 12c einen deutlich weniger effizienten Plan auswählt als in 11.2.0.4, weil er die Query besser versteht und erkennt, dass darin eine Transformation eines Semi-Joins zu einem Inner-Join möglich ist, was grundsätzlich eine sinnvolle Strategie sein sollte. Allerdings profitiert der semi-join nested loop von der gleichen Optimierung, die auch für das Caching der Ergebnisse skalarer Subqueries verwendet wird - aber nicht für Inner-Joins. Daher wird der Zugriff in 12c ineffektiver: insbesondere, wenn die aus der Driving-Table gelesenen Daten ungeordnet sind, da sich dann das Caching besonders positiv auswirkt.

    Donnerstag, Juli 16, 2015

    Performance-Probleme beim Zugriff auf DBA_FREE_SPACE

    Das Phänomen ist offenbar relativ bekannt, war mir aber bisher nicht begegnet (oder in Erinnerung geblieben): ein Icinga-Test zur Bestimmung des Füllgrads eines Tablespaces erreichte zuletzt Laufzeiten von über einer Minute, was zu Timeouts und Icinga-Fehlern führte. Schnell zu bestimmen war, dass das eigentliche Problem im Zugriff auf DBA_FREE_SPACE lag. Aber warum reagierte diese Dictionary-View so träge? Zur Prüfung habe ich zunächst einen Blick in die Definition in DBA_VIEWS geworfen, aber den hätte ich mir sparen können, denn ein Plan mit rowsource statistics zeigte die gleichen Informationen - und lieferte darüber hinaus die entscheidenden Details. Hier ein entsprechendes Beispiel, das ich mit 12.1.0.2 erstellt habe, und das sich in diesem Release noch einmal interessanter verhält als in 11.2, wo mir das Phänomen ursprünglich begegnet war:

    select /*+ gather_plan_statistics */
           tablespace_name
         , count(*)
      from dba_free_space
     group by tablespace_name
    
    TABLESPACE_NAME                  COUNT(*)
    ------------------------------ ----------
    SYSAUX                                  4
    USERS                                 915
    SYSTEM                                  1
    
    Abgelaufen: 00:00:16.49
    
    Plan hash value: 2867613348
    
    -----------------------------------------------------------------------------------------------------------------------------------------------
    | Id  | Operation                    | Name             | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
    -----------------------------------------------------------------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT             |                  |      1 |        |      3 |00:00:17.80 |     828K|    373K|       |       |          |
    |   1 |  HASH GROUP BY               |                  |      1 |      2 |      3 |00:00:17.80 |     828K|    373K|  1452K|  1452K|  760K (0)|
    |   2 |   VIEW                       | DBA_FREE_SPACE   |      1 |     68 |    920 |00:00:00.03 |     828K|    373K|       |       |          |
    |   3 |    UNION-ALL                 |                  |      1 |        |    920 |00:00:00.03 |     828K|    373K|       |       |          |
    |   4 |     NESTED LOOPS             |                  |      1 |      1 |      0 |00:00:00.01 |       6 |      2 |       |       |          |
    |   5 |      NESTED LOOPS            |                  |      1 |      1 |      0 |00:00:00.01 |       6 |      2 |       |       |          |
    |   6 |       INDEX FULL SCAN        | I_FILE2          |      1 |      3 |      3 |00:00:00.01 |       1 |      1 |       |       |          |
    |*  7 |       TABLE ACCESS CLUSTER   | FET$             |      3 |      1 |      0 |00:00:00.01 |       5 |      1 |       |       |          |
    |*  8 |        INDEX UNIQUE SCAN     | I_TS#            |      3 |      1 |      3 |00:00:00.01 |       2 |      0 |       |       |          |
    |*  9 |      TABLE ACCESS CLUSTER    | TS$              |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
    |* 10 |       INDEX UNIQUE SCAN      | I_TS#            |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
    |  11 |     NESTED LOOPS             |                  |      1 |      3 |     16 |00:00:00.01 |      19 |      6 |       |       |          |
    |  12 |      NESTED LOOPS            |                  |      1 |      3 |     16 |00:00:00.01 |      15 |      6 |       |       |          |
    |* 13 |       TABLE ACCESS FULL      | TS$              |      1 |      2 |      3 |00:00:00.01 |       6 |      2 |       |       |          |
    |* 14 |       FIXED TABLE FIXED INDEX| X$KTFBFE (ind:1) |      3 |      2 |     16 |00:00:00.01 |       9 |      4 |       |       |          |
    |* 15 |      INDEX UNIQUE SCAN       | I_FILE2          |     16 |      1 |     16 |00:00:00.01 |       4 |      0 |       |       |          |
    |  16 |     NESTED LOOPS             |                  |      1 |     63 |    904 |00:00:00.17 |     828K|    373K|       |       |          |
    |  17 |      NESTED LOOPS            |                  |      1 |    188 |    904 |00:00:00.17 |     828K|    373K|       |       |          |
    |* 18 |       HASH JOIN              |                  |      1 |     20 |    102 |00:00:00.01 |      10 |      1 |  1483K|  1483K|  695K (0)|
    |* 19 |        TABLE ACCESS FULL     | TS$              |      1 |      2 |      3 |00:00:00.01 |       6 |      0 |       |       |          |
    |* 20 |        TABLE ACCESS FULL     | RECYCLEBIN$      |      1 |     24 |    102 |00:00:00.01 |       4 |      1 |       |       |          |
    |* 21 |       FIXED TABLE FULL       | X$KTFBUE         |    102 |      9 |    904 |00:00:17.54 |     828K|    373K|       |       |          |
    |* 22 |      INDEX UNIQUE SCAN       | I_FILE2          |    904 |      1 |    904 |00:00:00.01 |       4 |      1 |       |       |          |
    |  23 |     NESTED LOOPS             |                  |      1 |      1 |      0 |00:00:00.01 |     302 |      2 |       |       |          |
    |  24 |      NESTED LOOPS            |                  |      1 |      1 |      0 |00:00:00.01 |     302 |      2 |       |       |          |
    |  25 |       MERGE JOIN CARTESIAN   |                  |      1 |     73 |    306 |00:00:00.01 |       5 |      0 |       |       |          |
    |  26 |        INDEX FULL SCAN       | I_FILE2          |      1 |      3 |      3 |00:00:00.01 |       1 |      0 |       |       |          |
    |  27 |        BUFFER SORT           |                  |      3 |     24 |    306 |00:00:00.01 |       4 |      0 |  6144 |  6144 | 6144  (0)|
    |* 28 |         TABLE ACCESS FULL    | RECYCLEBIN$      |      1 |     24 |    102 |00:00:00.01 |       4 |      0 |       |       |          |
    |  29 |       TABLE ACCESS CLUSTER   | UET$             |    306 |      1 |      0 |00:00:00.01 |     297 |      2 |       |       |          |
    |* 30 |        INDEX UNIQUE SCAN     | I_FILE#_BLOCK#   |    306 |      1 |     91 |00:00:00.01 |     206 |      2 |       |       |          |
    |* 31 |      TABLE ACCESS CLUSTER    | TS$              |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
    |* 32 |       INDEX UNIQUE SCAN      | I_TS#            |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
    -----------------------------------------------------------------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
    PLAN_TABLE_OUTPUT
    -----------------------------------------------------------------------------------------------------------------------------------------------
    
       7 - filter("F"."FILE#"="FI"."RELFILE#")
       8 - access("F"."TS#"="FI"."TS#")
       9 - filter("TS"."BITMAPPED"=0)
      10 - access("TS"."TS#"="F"."TS#")
      13 - filter(("TS"."CONTENTS$"=0 AND "TS"."BITMAPPED"<>0 AND INTERNAL_FUNCTION("TS"."ONLINE$")))
      14 - filter(("TS"."TS#"="KTFBFETSN" AND INTERNAL_FUNCTION("CON_ID")))
      15 - access("KTFBFETSN"="FI"."TS#" AND "KTFBFEFNO"="FI"."RELFILE#")
      18 - access("TS"."TS#"="RB"."TS#")
      19 - filter(("TS"."CONTENTS$"=0 AND "TS"."BITMAPPED"<>0 AND INTERNAL_FUNCTION("TS"."ONLINE$")))
      20 - filter(("RB"."TS#" IS NOT NULL AND "RB"."FILE#" IS NOT NULL AND "RB"."BLOCK#" IS NOT NULL))
      21 - filter((INTERNAL_FUNCTION("CON_ID") AND "KTFBUESEGBNO"="RB"."BLOCK#" AND "KTFBUESEGFNO"="RB"."FILE#" AND
                  "KTFBUESEGTSN"="RB"."TS#"))
      22 - access("RB"."TS#"="FI"."TS#" AND "KTFBUEFNO"="FI"."RELFILE#")
      28 - filter(("RB"."TS#" IS NOT NULL AND "RB"."FILE#" IS NOT NULL AND "RB"."BLOCK#" IS NOT NULL))
      30 - access("U"."TS#"="RB"."TS#" AND "U"."SEGFILE#"="RB"."FILE#" AND "U"."SEGBLOCK#"="RB"."BLOCK#")
           filter(("U"."TS#"="FI"."TS#" AND "U"."SEGFILE#"="FI"."RELFILE#"))
      31 - filter("TS"."BITMAPPED"=0)
      32 - access("TS"."TS#"="U"."TS#")
    
    Note
    -----
       - this is an adaptive plan
    

    Auffällig sind mehrere Punkte:
    • 16 Sekunden sind ziemlich viel für die Ermittlung dieses Ergebnisses.
    • die Laufzeit entfällt fast komplett auf eine der vier über UNION ALL verknüpften Teilabfragen.
    • in der langsamen Teilabfrage erfolgt ein Zugriff auf RECYCLEBIN$.
    • es handelt sich um einen adaptiven Plan.
    Im 11.2er System habe ich durch einen Blick in den Recyclebin festgestellt, dass dort tatsächlich mehr als 1500 Varianten einer Hilfstabelle lagen, die regelmäßig gelöscht wurde, ohne dass ein Purge des Objekts erfolgte. Nach Durchführung einer Purge-Operation für die Objekt-Inkarnationen (die leider nicht komplett automatisiert erfolgen kann, weil ein PURGE TABLE [object_name] nur die älteste Inkarnation löscht) läuft die Query wieder in weniger als einer Sekunde und das Problem ist gelöst (zumal die Test-Logik angepasst wurde, die für das Löschen der Tabelle verantwortlich war).

    In 12.1.0.2 ist die Lage allerdings eine andere: hier hat der Optimizer nach wenigen Ausführungen genügend Material gesammelt, um die geeignetere Version des adaptiven Plans zu verwenden:

    Plan hash value: 2991530507
    
    -----------------------------------------------------------------------------------------------------------------------------------------------
    | Id  | Operation                    | Name             | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
    -----------------------------------------------------------------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT             |                  |      1 |        |      3 |00:00:00.19 |    8462 |   3669 |       |       |          |
    |   1 |  HASH GROUP BY               |                  |      1 |      2 |      3 |00:00:00.19 |    8462 |   3669 |  1452K|  1452K|  740K (0)|
    |   2 |   VIEW                       | DBA_FREE_SPACE   |      1 |    922 |    920 |00:00:00.04 |    8462 |   3669 |       |       |          |
    |   3 |    UNION-ALL                 |                  |      1 |        |    920 |00:00:00.04 |    8462 |   3669 |       |       |          |
    |   4 |     NESTED LOOPS             |                  |      1 |      1 |      0 |00:00:00.01 |       6 |      1 |       |       |          |
    |   5 |      NESTED LOOPS            |                  |      1 |      1 |      0 |00:00:00.01 |       6 |      1 |       |       |          |
    |   6 |       INDEX FULL SCAN        | I_FILE2          |      1 |      3 |      3 |00:00:00.01 |       1 |      0 |       |       |          |
    |*  7 |       TABLE ACCESS CLUSTER   | FET$             |      3 |      1 |      0 |00:00:00.01 |       5 |      1 |       |       |          |
    |*  8 |        INDEX UNIQUE SCAN     | I_TS#            |      3 |      1 |      3 |00:00:00.01 |       2 |      0 |       |       |          |
    |*  9 |      TABLE ACCESS CLUSTER    | TS$              |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
    |* 10 |       INDEX UNIQUE SCAN      | I_TS#            |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
    |  11 |     NESTED LOOPS             |                  |      1 |     16 |     16 |00:00:00.01 |      19 |      7 |       |       |          |
    |  12 |      NESTED LOOPS            |                  |      1 |     16 |     16 |00:00:00.01 |      15 |      7 |       |       |          |
    |* 13 |       TABLE ACCESS FULL      | TS$              |      1 |      2 |      3 |00:00:00.01 |       6 |      1 |       |       |          |
    |* 14 |       FIXED TABLE FIXED INDEX| X$KTFBFE (ind:1) |      3 |     10 |     16 |00:00:00.01 |       9 |      6 |       |       |          |
    |* 15 |      INDEX UNIQUE SCAN       | I_FILE2          |     16 |      1 |     16 |00:00:00.01 |       4 |      0 |       |       |          |
    |  16 |     NESTED LOOPS             |                  |      1 |    904 |    904 |00:00:08.75 |    8135 |   3658 |       |       |          |
    |* 17 |      HASH JOIN               |                  |      1 |    904 |    904 |00:00:08.74 |    8131 |   3656 |  1087K|  1087K| 1288K (0)|
    |* 18 |       HASH JOIN              |                  |      1 |    102 |    102 |00:00:00.01 |      10 |      0 |  1483K|  1483K|  913K (0)|
    |* 19 |        TABLE ACCESS FULL     | TS$              |      1 |      2 |      3 |00:00:00.01 |       6 |      0 |       |       |          |
    |* 20 |        TABLE ACCESS FULL     | RECYCLEBIN$      |      1 |    102 |    102 |00:00:00.01 |       4 |      0 |       |       |          |
    |* 21 |       FIXED TABLE FULL       | X$KTFBUE         |      1 |   1000 |   7548 |00:00:00.13 |    8121 |   3656 |       |       |          |
    |* 22 |      INDEX UNIQUE SCAN       | I_FILE2          |    904 |      1 |    904 |00:00:00.01 |       4 |      2 |       |       |          |
    |  23 |     NESTED LOOPS             |                  |      1 |      1 |      0 |00:00:00.01 |     302 |      3 |       |       |          |
    |  24 |      NESTED LOOPS            |                  |      1 |      1 |      0 |00:00:00.01 |     302 |      3 |       |       |          |
    |  25 |       MERGE JOIN CARTESIAN   |                  |      1 |    306 |    306 |00:00:00.01 |       5 |      1 |       |       |          |
    |  26 |        INDEX FULL SCAN       | I_FILE2          |      1 |      3 |      3 |00:00:00.01 |       1 |      0 |       |       |          |
    |  27 |        BUFFER SORT           |                  |      3 |    102 |    306 |00:00:00.01 |       4 |      1 |  6144 |  6144 | 6144  (0)|
    |* 28 |         TABLE ACCESS FULL    | RECYCLEBIN$      |      1 |    102 |    102 |00:00:00.01 |       4 |      1 |       |       |          |
    |  29 |       TABLE ACCESS CLUSTER   | UET$             |    306 |      1 |      0 |00:00:00.01 |     297 |      2 |       |       |          |
    |* 30 |        INDEX UNIQUE SCAN     | I_FILE#_BLOCK#   |    306 |      1 |     91 |00:00:00.01 |     206 |      2 |       |       |          |
    |* 31 |      TABLE ACCESS CLUSTER    | TS$              |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
    |* 32 |       INDEX UNIQUE SCAN      | I_TS#            |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
    -----------------------------------------------------------------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
    PLAN_TABLE_OUTPUT
    --------------------------------------------------------------------------------------------------------------------------------------------------
    
       7 - filter("F"."FILE#"="FI"."RELFILE#")
       8 - access("F"."TS#"="FI"."TS#")
       9 - filter("TS"."BITMAPPED"=0)
      10 - access("TS"."TS#"="F"."TS#")
      13 - filter(("TS"."CONTENTS$"=0 AND "TS"."BITMAPPED"<>0 AND INTERNAL_FUNCTION("TS"."ONLINE$")))
      14 - filter(("TS"."TS#"="KTFBFETSN" AND INTERNAL_FUNCTION("CON_ID")))
      15 - access("KTFBFETSN"="FI"."TS#" AND "KTFBFEFNO"="FI"."RELFILE#")
      17 - access("KTFBUESEGTSN"="RB"."TS#" AND "KTFBUESEGFNO"="RB"."FILE#" AND "KTFBUESEGBNO"="RB"."BLOCK#")
      18 - access("TS"."TS#"="RB"."TS#")
      19 - filter(("TS"."CONTENTS$"=0 AND "TS"."BITMAPPED"<>0 AND INTERNAL_FUNCTION("TS"."ONLINE$")))
      20 - filter(("RB"."TS#" IS NOT NULL AND "RB"."FILE#" IS NOT NULL AND "RB"."BLOCK#" IS NOT NULL))
      21 - filter(("CON_ID"=0 OR "CON_ID"=3))
      22 - access("RB"."TS#"="FI"."TS#" AND "KTFBUEFNO"="FI"."RELFILE#")
      28 - filter(("RB"."TS#" IS NOT NULL AND "RB"."FILE#" IS NOT NULL AND "RB"."BLOCK#" IS NOT NULL))
      30 - access("U"."TS#"="RB"."TS#" AND "U"."SEGFILE#"="RB"."FILE#" AND "U"."SEGBLOCK#"="RB"."BLOCK#")
           filter(("U"."TS#"="FI"."TS#" AND "U"."SEGFILE#"="FI"."RELFILE#"))
      31 - filter("TS"."BITMAPPED"=0)
      32 - access("TS"."TS#"="U"."TS#")
    
    Note
    -----
       - statistics feedback used for this statement
       - this is an adaptive plan
    

    In step 17 wird der NESTED LOOPS Join durch einen HASH JOIN ersetzt, so dass der kostspielige Zugriff auf X$KTFBUE nur einmalig erfolgt (außerdem sieht man die durch das statistics feedback hervorgerufenen Cardinality-Korrekturen). Mit dem adaptiven Plan ist das Problem also nach kurzer Zeit keines mehr. Für ältere Releases bleibt aber festzuhalten, dass ein mit vielen Objekten gefüllter Recyclebin negative Auswirkungen auf die Query-Performance für Abfragen gegen DBA_FREE_SPACE hat. Das Problem ist, wie schon erwähnt, nicht unbekannt - nachdem ich mein Ergebnis hatte, habe ich bei Martin Widlake einen Artikel gleichen Inhalts gefunden (und es gibt weitere Artikel ähnlichen Inhalts). Weniger bekannt dürfte aber sein, dass die Schwierigkeit in 12c für alle praktischen Fragen als behoben betrachtet werden kann.

    Montag, Juli 13, 2015

    OS-Analyse für Logical I/O-Operationen

    Fritz Hoogland, dessen Blog eine der besten Quellen zu I/O-Fragen in Oracle ist, beschäftigt sich in seinem aktuellen Artikel mit der Performance von Logical-I/O-Zugriffen. Ausgangspunkt ist dabei, dass der Kernel aktueller Redhat-Versionen die Möglichkeit bietet, via systemtap die Performance einzelner OS-Routinen zu überprüfen. Aufgrund entsprechender Aussagen des Oracle Supports und eigenen Beobachtungen kann der Autor Consistent Reads der C-Funktion kcbgtcr() zuordnen, während Current reads wohl durch kcbgcur() abgebildet sind. Aufbauend auf diesen Voraussetzungen werden im Artikel diverse OS-Skripte und ihre Ergebnisse vorgestellt, aus denen hervorgeht, dass die Ausführung von kcbgtcr in der Regel sehr wenig Zeit benötigt (1-2 Mikrosekunden), aber in einzelnen Fällen dramatisch langsamer ist (um den Faktor 100K). Mit Hilfe eines (nicht unbedingt minimal invasiven) systemtab-Skripts kann der Herr Hoogland die Ursache der Abweichungen bestimmen: in den langsamen Fällen ruft ein logischer Blockzugriff einen physikalischen Lesezugriff hervor, weil der Block erst in den Cache gelesen werden muss. Vom Ergebnis her ist das nicht unbedingt eine große Überraschung, aber das - hier nur schemenhaft wiedergegebene - Analysevorgehen ist außerordentlich eindrucksvoll. Ich gehe davon aus, dass sich mit ähnlichen Verfahren sehr umfassende Einblicke in die interne Arbeitsweise des Oracle Servers erreichen lassen.

    Donnerstag, Juli 09, 2015

    Neue SQL Plan Operationen und Hints in 12c

    Sayan Malakshinov hat sich die Mühe gemacht, eine kommentierte Link-Liste zu den in 12c eingeführten SQL PLAN OPERATIONS und Hints zusammenzustellen. Das Ergebnis sieht ausgesprochen vielversprechend aus.

    Dienstag, Juli 07, 2015

    Insert-Sortierung und Index-Größe

    Mal wieder ein Titel, der den Eindruck erweckt, dass vielleicht eine größere Präzisierung möglich gewesen wäre - aber da Richard Foote dem Artikel, den ich hier wiedergebe, den Titel Why A Brand New Index Might Benefit From An Immediate Coalesce (One Slip) gegeben hat, fühle ich mich nicht dazu verpflichtet, länger darüber nachzudenken. Worum es geht, ist Folgendes: der Herr Foote zeigt in seinem Blog, dass ein Index auf einer via Insert as Select befüllten Tabelle deutlich größer ist als erforderlich. Die Reihenfolge im Testfall ist dabei:
    • Anlage einer Testtabelle mit Index (non-unique) und 1M rows.
      • der Index umfasst 2226 leaf blocks.
    • Sicherung von 30% der Daten (also 300K rows) in einer Hilfstabelle und Durchführung eines truncate auf der Quelltabelle.
      • der Index enthält jetzt keine Einträge.
    • Insert der in der Hilfstabelle gesicherten Datensätze in die Ursprungstabelle.
      • der Index umfasst 1112 leaf blocks. Also recht genau 50% der initialen Größe.
    Die Frage ist somit: warum ist der Index nicht auf 30% der Ursprungsgröße geschrumpft? Ein Blick auf die Index-Struktur zeigt, dass sehr viele Blocks jetzt nur zu 50% gefüllt sind und nicht mehr zu 90%. Ursache sind 50-50-block splits in der Index-Struktur, die sich ergeben, weil die Daten in der Hilfstabelle nicht sortiert vorlagen, sondern (mehr oder minder) willkürlich in freien Blocks untergebracht wurden: der Transfer dieser Sätze in die Ursprungstabelle fügt also nicht nur am Ende der Indexstruktur neue Einträge ein (was zu 90-10-block splits führen würde), sondern ergänzt Einträge mitten in den bereits angelegten Strukturen, was zur 50-50-Teilung der Index-Blocks führt. Um eine kompakte Index-Struktur zu erhalten, wäre eine Sortierung beim Insert as Select notwendig - oder die Verwendung eines Append-Hints, der eine Vorsortierung der eizufügenden Sätze durchführt. Dabei gilt natürlich, dass ein extrem kompakter Index nur unter bestimmten Umständen sinnvoll ist: entweder, wenn die Tabelle statisch ist (also überhaupt keine DML-Operationen erfolgen), oder wenn nur am Ende des Index eingefügt wird und somit in der Folge nur Splits vom Typ 90-10 erfolgen.

    Samstag, Juli 04, 2015

    I/O-Performance in Datenbanken und auf OS-Seite

    Auf Oracle-L hat Stefan Koehler heute einen interessanten Link zum Thema "Kommunikation zwischen DBAs und Storage-Administratoren" untergebracht: im referenzierten Artikel erläutert Bart Sjerps, dass beide Gruppen oft von unterschiedlichen Dingen sprechen, wenn sie über I/O-Performance diskutieren: während die Storage-Admins die reine Service time vor Augen haben - also die Zeit, die die Storage selbst benötigt, um einen einzelnen I/O request zu beantworten (= die von iostat ausgegebene Spalte svctm) -, interessieren sich die DBAs eher für die Average Wait time - also die Wartezeit in der Host I/O queue, die sich als Produkt der iostat-Angaben avgqu-sz (= Average queue size) und await (= Average wait in der queue) ergibt; denn das ist dann auch die Zahl, die in den internen Datenbankstatistiken erscheint (sofern das RDBMS eine entsprechende Instrumentierung besitzt). Dabei sind die Werte der Storage-Admins dann natürlich immer niedriger als die der DBAs und diese Abweichungen können dann Ursache für Missverständnisse sein.

    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.