Mittwoch, Juli 30, 2014

Visualisierung von Oracle-Performance Informationen

Als hoffnungsloser Laie in Sachen Visualisierung schaue ich immer wieder voller Erstaunen auf die Beispiele, die Kyle Hailey in seinem Blog unterbringt. Ich muss mal wieder neidvoll anerkennen: es gibt Fälle, in denen sqlplus nicht die optimale Repräsentation von Ergebnissen liefert.

Montag, Juli 28, 2014

Notizen zur In-Memory Option in Oracle 12c

Sie ist sicher das aufsehenerregendste Feature des gerade veröffentlichten Patch-Sets 12.1.0.2: die In-Memory Option. Wenn man das Marketing fragt, macht sie Zugriffe um den Faktor X schneller - wobei man für X so ziemlich jeden Wert einsetzen kann, der einem gerade einfällt: ein passendes Szenario dafür wird sich basteln lassen. Vor ein paar Tagen habe ich meinen Download des Patches durchgeführt, aber bis zur Installation bin ich noch nicht gekommen, daher bleiben meine Aussagen mal wieder auf der Ebene der Nacherzählung der Beobachtungen anderer Naturforscher:
  • Jonathan Lewis: Analogy - liefert die von Maria Colgan formulierte Beschreibung der Grundidee der In-Memory Option in einem Satz: "In-memory columnar storage gives you bitmap indexes on OLTP systems without the usual disastrous locking side effects." Diese Zusammenfassung gefällt mir aus zwei Gründen: erstens ist sie griffig und zweitens passt sie zu einer entsprechenden Notiz die ich hier gelegentlich untergebracht hatte. Der Herr Lewis erläutert weiter:
    for each column stored you use a compression technique to pack the values for a large number of rows into a very small space, and for each stored row you can derive the rowid by arithmetic.  In highly concurrent OLTP systems there’s still room for some contention as the session journals are applied to the globally stored compressed columns (but then, private redo introduces some effects of that sort anyway); and the “indexes” have to be created dynamically as tables are initially accessed (but that’s a startup cost, it’s timing can be controlled, and it’s basically limited to a tablescan).
  • Martin KlierOracle 12c InMemory – don’t stop thinking about performance - weist darauf hin, dass die In-Memory Option nicht automatisch der berühmte fast=true Parameter ist, sondern nur unter bestimmten Voraussetzungen eine bessere Performance mit sich bringt - und belegt diese Aussage mit einem Beispiel.
  • Tim HallIn-Memory Column Store in Oracle Database 12c Release 1 (12.1.0.2) - wenn es ein neues Oracle-Feature gibt, lässt Tim Halls einführendes Beispiel in der Regel nicht lange auf sich warten. Wenn ich mir die Option gelegentlich genauer anschaue, wird dieser Artikel wahrscheinlich der Ausgangspunkt sein. Unter anderem findet sich darin folgende Taxonomie:
    The documentation claims the IM column store is good for the following.
    • Large scans that apply "=", "<", ">" and "IN" filters.
    • Queries that return a small number of columns from a table with a large number of columns.
    • Queries that join small tables to large tables.
    • Queries that aggregate data.
    • It also states it is not designed for the following.
    • Queries with complex predicates.
    • Queries that return a large number of columns.
    • Queries that return large numbers of rows.
    • Queries with multiple large table joins.
  • Maria ColganOracle Database In-Memory & the Optimizer - die (ehemalige?) "Optimizer Lady" (und aktuelle "In-Memory Lady"?) erläutert, was der Optimizer von der In-Memory Option hält und wie er damit umgeht. Der erste wichtige Hinweis dabei ist, dass es sich immer noch um den alten kostenbasierten Optimizer handelt, der nur ein paar Ergänzungen für den Umgang mit den Herausforderungen der Optimierung mit In-Memory Strategien bekommen hat. Es gibt in diesem Zusammenhang ein paar neue Hints (angefangen mit: INMEMORY) und die Analyse der Entscheidungen des Optimizers erfolgt immer noch mit dem Event 10053. Die Einführung der Option erfordert keine Erfassung zusätzlicher Statistiken und keine Veränderungen an bestehenden Applikationen - in manchen Fällen kann es sinnvoll sein, bestimmte Indizes zu entfernen, die speziell zu Reporting-Zwecken erzeugt wurden, aber generell sind bestehende Indizes weiterhin wichtig; insbesondere, wenn sie zur Sicherstellung der referentiellen Integrität oder zum punktuellen OLTP-Zugriff benötigt werden. Wenn das alles stimmt, was Frau Colgan da schreibt - und ich habe grundsätzlich keinen Grund an ihren Worten zu zweifeln -, dann hat Oracle diese Integration ausgesprochen schmerzlos bewerkstelligen können. Im Oracle Database In-Memory Blog finden sich auch noch ein paar andere Artikel, die aber eher einführenden Charakter hatten.
  • James Morle: Oracle’s In-Memory Database: The True Cost Of Licensing - erläutert einige technische Details der Memory-Nutzung und ihre Auswirkungen auf die Lizenzkosten (schlechtes Design führt zur Verschiebung von Operationen ins Memory, aber die sichtbare Konsequenz ist eine höhere CPU-Nutzung - statt bisher I/O und CPU - und wenn man zur Lösung der Probleme weitere CPUs ergänzt, steigen die Lizenzkosten; ohne dass die Probleme dadurch gelöst werden, denn eigentlich handelt es sich nicht um ein CPU-Problem).
An dieser Stelle beende ich meine Aufzählung, obwohl es noch zahlreiche weitere (und sicher ebenfalls lesenswerte) Artikel gibt - aber für den Moment genügt mir das Material. Der nächste Schritt ist die eigenständige Erkundung - ich frage mal beim Herrn Franklin nach, ob ich mir Terror und Erebus ausleihen kann...

Freitag, Juli 25, 2014

Patchset 12.1.0.2 veröffentlicht

Seit einigen Tagen ist das Patchset 12.1.0.2 für erste OS-Versionen verfügbar und einige der interessantesten Ergänzungen wurden bereits in diversen Artikeln vorgestellt - insbesondere natürlich die In-Memory-Option (für die verschiedentlich das Acronym IMDB verwendet wurde, was möglicherweise noch für Verwirrung sorgen wird). Auf den ersten Blick sieht es so aus, als wären insbesondere die Ergänzungen, die sich potentiell auf die Datenbank-Performance auswirken, sehr umfangreich - und man kann darüber spekulieren, warum derart wichtige Änderungen in einem Patchset untergebracht wurden. Es folgt eine kleine (und ungeordnete) Link-Liste auf einige interessante Artikel zum Thema:
  • Franck Pachot - Beyond In-Memory, what's new in 12.1.0.2? Weist auf die Möglichkeit der Partitionierung für Hash Cluster, das Attribute Clustering und die JSON-Unterstützung hin.
  • Richard Foote - 12.1.0.2 Released With Cool Indexing Features (Short Memory): mit Hinweis auf Advanced Index Compression (automatische Auswahl geeigneter Kompression pro Leaf-Block), sowie auf Attribute Clustering und (natürlich) In-Memory Option hin.
  • Marco Gralike - ORACLE DATABASE 12.1.0.2.0 – NATIVE JSON SUPPORT (1): Einführung zum JSON-Support. Ein recht umfassendes Beispiel zur JSON-Unterstützung liefert auch Carsten Czarski.
  • Kevin Closson: Oracle Database 12c Release 12.1.0.2 – My First Observations. Licensed Features Usage Concerns – Part I: mit der Zusammenfassung "This release is hugely important." Außerdem wird darauf hingewiesen, dass die übliche Monitoring Skripte zur Feature-Nutzung die In-Memory Column Store Verwendung nicht korrekt anzeigen. Aus seinen Bemerkungen zur Lizenzierung hat sich offenbar eine recht heftige Debatte entwickelt, deren Verlauf ich hier allerdings nicht nacherzählen möchte.
  • Julian Dontcheff - Oracle Database 12.1.0.2 New Features: Kurzes Beispiel zur In-Memory Option und Aufzählung weiterer neuer Features.
Möglicherweise erweitere ich diese Liste gelegentlich (auch angesichts der ganzen "Part 1" Hinweise), aber vielleicht ist es zweckdienlicher die neuen Features einzeln zu betrachten.

Donnerstag, Juli 24, 2014

Korrektur der Join Cardinality

Laut Randolf Geist ist es "one of the most obvious weak spots of the optimizer: A filter on a table that is joined to another table that has a skewed value distribution in the FK column(s)." Der Satz ist die Antwort auf eine Frage im OTN-Forum, die ein recht kompaktes Beispiel enthielt, bei dem der Optimizer sich hinsichtlich der Cardinality eines Joins massiv verschätzt. Hier eine vereinfachte Version des einfachen Beispiels:

-- 11.2.0.1
drop table t1;
drop table t2;

create table t1
as
select rownum id
     , 'val_' || rownum col1
  from dual
connect by level <= 20;

create table t2
as
select case when rownum > 1000 then 20
            else mod(rownum, 19) + 1
            end id
  from dual
connect by level <= 10000;  

exec dbms_stats.gather_table_stats(user, 't1')
exec dbms_stats.gather_table_stats(user, 't2')

set autot on explain
 
select count(*)
  from t1
     , t2
 where t1.id = t2.id
   and t1.col1 = 'val_19'; 
   
select count(*)
  from t1
     , t2
 where t1.id = t2.id
   and t1.col1 = 'val_20';    
   
set autot off  

Das Skript liefert folgende Ergebnisse:

SQL> select count(*)
  2    from t1
  3       , t2
  4   where t1.id = t2.id
  5     and t1.col1 = 'val_19';

  COUNT(*)
----------
        52

Ausführungsplan
----------------------------------------------------------
Plan hash value: 906334482

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |     1 |    13 |     5  (20)| 00:00:01 |
|   1 |  SORT AGGREGATE     |      |     1 |    13 |            |          |
|*  2 |   HASH JOIN         |      |   500 |  6500 |     5  (20)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| T1   |     1 |    10 |     2   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| T2   | 10000 | 30000 |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------

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

   2 - access("T1"."ID"="T2"."ID")
   3 - filter("T1"."COL1"='val_19')

SQL> select count(*)
  2    from t1
  3       , t2
  4   where t1.id = t2.id
  5     and t1.col1 = 'val_20';

  COUNT(*)
----------
      9000

Ausführungsplan
----------------------------------------------------------
Plan hash value: 906334482

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |     1 |    13 |     5  (20)| 00:00:01 |
|   1 |  SORT AGGREGATE     |      |     1 |    13 |            |          |
|*  2 |   HASH JOIN         |      |   500 |  6500 |     5  (20)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| T1   |     1 |    10 |     2   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| T2   | 10000 | 30000 |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------

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

   2 - access("T1"."ID"="T2"."ID")
   3 - filter("T1"."COL1"='val_20')

Obwohl sich die Anzahl der Ergebnissätze massiv unterscheidet (52 zu 9000) schätzt der Optimizer die Join-Cardinality in beiden Fällen auf 500. Dieser Wert ergibt sich aus der Verwendung der Standard-Formeln zur Berechung von Join-Selectivity und -Cardinality:

Join Selectivity = 1 / greater(num_distinct(t1.id), num_distinct(t2.id))
Join Cardinality = Join Selectivity * cardinality t1 * cardinality t2

Im Beispiel ergibt sich:

Join Selectivity = 1 / greater(20, 20) = 0,05
Join Cardinality = 0,05 * 1 * 10000 = 500

Im gegebenen Fall kann der menschliche Betrachter zwar unmittelbar erkennen, dass ein Wert für col1 einen entsprechenden Wert für id bedingt, aber der Optimizer ist in einer weniger günstigen Situation: er besitzt jeweils nur die Statistiken einer Tabelle: er weiß, dass der Zugriff auf t1 nur einen Satz liefern wird, aber zum Zeitpunkt der Optimierung kann er nicht bestimmen, welchem Satz der zweiten Tabelle diese Angabe entsprechen wird: dazu müsste er den Join bereits durchführen. Auch Histogramme helfen in diesem Fall nicht. Allerdings nennt Randolf Geist zwei Möglichkeiten mit deren Hilfe der Optimizer zu korrekten Cardinalities für den Join kommt - wobei beide Varianten wiederum recht massive Nachteile mit sich bringen. In 11.2 kann man den Hint precompute_subquery einsetzen, der zwar nicht offiziell dokumentiert ist, aber von Tanel Poder recht detailliert beschreiben wurde. Im Plan ergibt sich durch die Verwendung des Hints ein recht merkwürdiges Bild:

select count(*)
  from t2
 where t2.id in (select /*+ precompute_subquery */ t1.id
                   from t1
                  where t1.col1 = 'val_20');

  COUNT(*)
----------
      9000

Ausführungsplan
----------------------------------------------------------
Plan hash value: 3321871023

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     3 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |     3 |            |          |
|*  2 |   TABLE ACCESS FULL| T2   |   500 |  1500 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------

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

   2 - filter("T2"."ID"=20)

exec dbms_stats.gather_table_stats(user, 't2', method_opt => 'for all columns size 254')

--> erneute Ausführung

Ausführungsplan
----------------------------------------------------------
Plan hash value: 3321871023

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     3 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |     3 |            |          |
|*  2 |   TABLE ACCESS FULL| T2   |  9000 | 27000 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------

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

   2 - filter("T2"."ID"=20)

In diesem Fall wird die Subquery vor der Optimierung der Haupt-Query ausgewertet und das Ergebnis des ersten Schritts bei der Planung des zweiten Schritts zugrunde gelegt. Für die id-Spalte in t2 müssen allerdings Histogramme vorliegen, damit die ungleiche Verteilung der Werte berücksichtigt werden kann. Allerdings stellt Randolf Geist fest: "However the PRECOMPUTE_SUBQUERY is undocumented (and comes with some odd behaviour for more complex queries), hence no good solution." In Oracle 12 gibt es als Alternative dazu noch das dynamic_sampling auf Level 11, das die Join-Cardinalities durch explizite Prüfung der Datengrundlage korrrigiert:

-- 12.1.0.1
select /*+ dynamic_sampling (11) */count(*)
  from t1
     , t2
 where t1.id = t2.id
   and t1.col1 = 'val_20';

Ausführungsplan
----------------------------------------------------------
Plan hash value: 906334482

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |     1 |    13 |    10   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE     |      |     1 |    13 |            |          |
|*  2 |   HASH JOIN         |      |  9000 |   114K|    10   (0)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| T1   |     1 |    10 |     3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| T2   | 10000 | 30000 |     7   (0)| 00:00:01 |
----------------------------------------------------------------------------

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

   2 - access("T1"."ID"="T2"."ID")
   3 - filter("T1"."COL1"='val_20')

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

Aber auch hier gilt:
However, this feature, too, comes at a price. At present I think one of the two row sources joined will be processed in its entirety during the sampling, so I don't think it's a viable option for large tables being joined, but I haven't done any exhaustive tests with this new feature. Since Oracle themselves don't enable it by default it looks like a bit experimental anyway.
Zu dieser Aussage passt die Beobachtung, dass ein zugehöriges SQL Trace (Event 10046) unter anderem Folgendes enthält:

SELECT /* DS_SVC */ /*+ cursor_sharing_exact dynamic_sampling(0) no_sql_tune 
  no_monitoring optimizer_features_enable(default) result_cache */ SUM(C1) 
FROM
 (SELECT /*+ qb_name("innerQuery") NO_INDEX_FFS( "T2#0")  */ 1 AS C1 FROM 
  "T2" "T2#0", "T1" "T1#1" WHERE ("T1#1"."COL1"='val_20') AND ("T1#1"."ID"=
  "T2#0"."ID")) innerQuery


call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        1      0.00       0.00          0         22          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.00       0.00          0         22          0           1

Misses in library cache during parse: 0
Optimizer mode: ALL_ROWS
Parsing user id: 110     (recursive depth: 1)
Number of plan statistics captured: 1

Rows (1st) Rows (avg) Rows (max)  Row Source Operation
---------- ---------- ----------  ---------------------------------------------------
         1          1          1  RESULT CACHE  7cy3yc33w1z8m0wa5xpc39wqcb (cr=22 pr=0 pw=0 time=4854 us)
         1          1          1   SORT AGGREGATE (cr=22 pr=0 pw=0 time=4824 us)
      9000       9000       9000    HASH JOIN  (cr=22 pr=0 pw=0 time=5399 us cost=10 size=6500 card=500)
         1          1          1     TABLE ACCESS FULL T1 (cr=3 pr=0 pw=0 time=34 us cost=3 size=10 card=1)
     10000      10000      10000     TABLE ACCESS FULL T2 (cr=19 pr=0 pw=0 time=1922 us cost=7 size=30000 card=10000)

Unter den Row Source Operation-Angaben findet sich also noch die card=500 des ursprünglichen Plans - und das Ergebnis dieser Query wird dann zur Bestimmung der korrekten Cardinality verwendet: das scheint noch nicht unbedingt eine besonders effiziente Lösung zu sein.

Da der Artikel etwas länger geworden ist, spendiere ich ausnahmsweise ein Fazit: der Optimizer hat seine Probleme mit der Bestimmung von Join Kardinalitäten, wenn ein Filter auf einer Tabelle eingesetzt wird, die mit einer zweiten Tabelle gejoint wird, bei der für die Spalten des Foreign Key eine Ungleichverteilung vorliegt. Es gibt zwei mögliche Workarounds zur Korrektur dieser Fehleinschätzungen, aber diese Workarounds schaffen möglicherweise mehr Probleme als sie lösen. Ein expliziter Dank noch mal an Randolf Geist, der an dieser Stelle mal wieder erstaunliche Details geliefert hat.

Nachtrag 28.08.2014: Sayan Malakshinov weist darauf hin, dass der PRECOMPUTE_SUBQUERY-Hint für query blocks angegeben und somit im Zusammenspiel mit SQL Profiles, Baselines und Patches eingesetzt werden kann.

Dienstag, Juli 22, 2014

Einfügen von Zeilen aus Excel im SQL Developer

Dass der SQL Developer ein großartiges Tool ist, dem ich womöglich verfallen wäre, wenn ich meine Seele nicht an sqlplus verkauft hätte, habe ich vielleicht gelegentlich schon mal erwähnt. Ein in entsprechender Situation sehr nützliches Detail zeigt Jeff Smith in einem aktuellen Blog-Eintrag: die Möglichkeit, Daten aus Excel per Copy&Paste im SQL Developer in eine Tabelle einzufügen. Das zumindest kann ich mit sqlplus nicht durchführen...

Sonntag, Juli 20, 2014

Implizite Typ-Konvertierung und Index-Zugriff

Das Thema wurde sicherlich schon in diversen Blog-Einträgen erläutert - ich war aber zu faul, um danach zu suchen. Außerdem wollte ich hier mal wieder ein kleines Code-Beispiel unterbringen.

Um was es geht, ist Folgendes: Oracle sieht großzügig über fehlerhafte Typ-Angaben bei den Einschränkungen von Abfragen hinweg und korrigiert sie stillschweigend via implizite Typ-Umwandlung. Grundsätzlich ist das eine nette Geste, aber natürlich bringt das Verhalten gelegentlich auch Probleme mit sich. Unter anderem gibt es Situationen, in denen eine solche implizite Konvertierung einen Index-Zugriff ausschalten kann. Dazu ein kleines Beispiel, das nur vier sehr einfache Fälle berücksichtigt: die Verwendung einer numerischen bzw. einer String-Bedingung für eine numerische bzw. eine String-Spalte:

-- 11.2.0.1
drop table t;

create table t
as 
select rownum num_col
     , to_char(rownum) char_col
  from dual 
connect by level <= 100000;

exec dbms_stats.gather_table_stats(user, 'T')

create unique index t_num_col_idx on t(num_col);

create unique index t_char_col_idx on t(char_col);

set autot trace

-- number column and number condition
select count(*) 
  from t 
 where num_col = 1;

------------------------------------------------------------------------------------
| Id  | Operation          | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |               |     1 |     5 |     1   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |               |     1 |     5 |            |          |
|*  2 |   INDEX UNIQUE SCAN| T_NUM_COL_IDX |     1 |     5 |     1   (0)| 00:00:01 |
------------------------------------------------------------------------------------

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

   2 - access("NUM_COL"=1)

-- number column and string condition 
select count(*) 
  from t 
 where num_col = '1';

------------------------------------------------------------------------------------
| Id  | Operation          | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |               |     1 |     5 |     1   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |               |     1 |     5 |            |          |
|*  2 |   INDEX UNIQUE SCAN| T_NUM_COL_IDX |     1 |     5 |     1   (0)| 00:00:01 |
------------------------------------------------------------------------------------

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

   2 - access("NUM_COL"=1)

-- string column and number condition 
select count(*) 
  from t 
 where char_col = 1;

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

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

   2 - filter(TO_NUMBER("CHAR_COL")=1)

-- string column and string condition 
select count(*) 
  from t 
 where char_col = '1';

-------------------------------------------------------------------------------------
| Id  | Operation          | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                |     1 |     6 |     1   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |                |     1 |     6 |            |          |
|*  2 |   INDEX UNIQUE SCAN| T_CHAR_COL_IDX |     1 |     6 |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

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

   2 - access("CHAR_COL"='1')

Das Verhalten ist demnach in den gegebenen Fällen recht übersichtlich:
  • number column and number condition: keine Typumwandlung.
  • number column and string condition: der String in der Bedingung wird in einen numerischen Wert umgewandelt, wobei diese Umwandlung im Plan nicht sichtbar ist. In diesem Fall ist die Umwandlung ungefährlich, weil der Vergleich nur für numerische Werte sinnvoll ist: würde als Vergleichswert ein nicht-numerischer Wert erscheinen, so ergäbe sich ein Parse-Fehler (ORA-01722: Ungültige Zahl).
  • string column and number condition: dies ist der problematische Fall: der Zugriff erfolgt über Full Table Scan, weil aufgrund des Vergleichs ein Filter-Prädikat (TO_NUMBER("CHAR_COL")=1) eingeführt werden muss.
  • string column and string condition: keine Typumwandlung.
Hier angekommen fällt mir auf, dass ich keine Lust habe, die komplette Matrix möglicher Konvertierungen auf ihre Behandlung der impliziten Konvertierung hin zu untersuchen, aber das hat Maxym Kharchenko dankenswerterweise gelegentlich bereits erledigt. Beim Blick auf diese Matrix fällt es mir schwer, eine allgemeingültige Regel zu formulieren, die das Verhalten allgemein beschreibt - aber für meine Zwecke genügt das Fazit, dass numerische Spalten, die als String-Typen abgelegt werden, Probleme hervorrufen können, was wieder ein Argument dafür wäre, Attribute in geeigneten Typen abzubilden - wenn man dafür noch Argumente bräuchte.

Freitag, Juli 18, 2014

Charles Hooper über Troubleshooting Oracle Performance

Zu meinen für das laufende Jahr ernsthaft ins Auge gefassten Vorsätzen gehört es, eine Besprechung zum zweiten Band der Poetik des Aristoteles ... - ich wollte sagen: zur zweiten Auflage von Christian Antogninis Standardwerk Troubleshooting Oracle Performance zu schreiben. Da ich dazu aber noch ein paar Vorarbeiten zu leisten habe - zu denen die Lektüre des Buches zählt... -, beschränke ich mich vorläufig auf einen Link auf die umfangreiche und detaillierte (ich könnte auch sagen: geradezu einschüchternde) Besprechung von Charles Hooper, der meiner Einschätzung nach ohnehin die besten Rezensionen zu Oracle-Büchern schreibt. Neben allerlei grundsätzlichen Erläuterungen und Einschätzungen hat der Herr Hooper sogar einen erweiterten Index zum Buch erstellt, da er die (für mich nachvollziehbare) Einschätzung formuliert: "the index at the back of most Apress books seems to be limited in value". "Rezension" ist insofern vielleicht gar nicht die richtige Gattungsbezeichung für den Artikel, aber eine bessere fällt mir gerade auch nicht ein.

Nachtrag 11.11.2014: inzwischen habe ich ein paar Sätze zum Thema bei Amazon untergebracht - mehr werde ich hier auch nicht mehr dazu sagen, denn der Herr Hooper hat ja schon alles gesagt.

Montag, Juli 14, 2014

Partitial Join Evaluation in 12c

Franck Pachot erläutert im dbi service Blog eine interessante Transformation, die in 12c eingeführt wurde: die Partial Join Evaluation (PJE). Diese Option erlaubt es dem Optimizer, einen Join, der eine distinkte Ergebnismenge liefern soll, intern in einen Semi-Join umzuwandeln und dadurch die Prüfung zu beenden, wenn ein erster Datensatz gefunden wird, der die Bedingung erfüllt. Seine Test-Query:
select distinct deptno,dname from dept join emp using(deptno)
wird in 11g über einen HASH JOIN mit folgendem HASH UNIQUE verarbeitet. In 12c ergibt sich ein HASH JOIN SEMI mit folgendem HASH UNIQUE und die rowsource Statistiken zeigen, dass der Zugriff auf die Probe-Menge tatsächlich beendet wird, wenn der erste Treffer erreicht wurde (was besonders nützlich ist, sofern dieser erste Treffer relativ früh gefunden wird, wofür die spezielle Versuchsanordnung Sorge trägt).

Montag, Juli 07, 2014

Partion Views in 11.2

Zu den ersten Ergebnissen, die Google bei der Suche nach "Partition View" liefert, gehört Oracle7 Tuning, release 7.3.3. Jenes Release 7.3 wurde 1996 veröffentlicht und seit mehr als zehn Jahren wird man bei Oracle nicht müde zu betonen, dass die "Partition Views" als Feature deprecated sind - aber offenbar funktionieren sie auch in Release 11.2 noch immer, wie ich heute beim Durchspielen eines im OTN-Forum vorgestellten Beispiels feststellen konnte. Ich spare mir hier die Wiederholung des Versuchsaufbaus und der Analyse, die man im Thread nachlesen kann. An dieser Stelle nur eine kurze grundsätzliche Erläuterung: Partition Views waren eine Art Vorläufer der "echten" Partitionierung und bestehen aus einer View mit über UNION ALL verknüpften Basistabellen, bei denen Check-Constraints die Zuordnung von Werten zu einer bestimmten Basistabelle sicherstellen. Natürlich ist "echte" Partitionierung ein deutlich mächtigeres Werkzeug, aber ich neige zur Einschätzung von David Aldrige: "Considering the enormous cost of an upgrade to Enterprise Edition and the Partitioning Option, I'd consider them [i.e. Partition Views] if I was using Standard Edition. The extra work is manageable."

Nachtrag 09.08.2014: eine dazu passende Diskussion unter Teilnahme von Tim Gorman, Iggy Fernandez und Jonathan Lewis hat sich gerade auch in der oracle-l Liste ergeben.

SQL Plan Baselines und Parse Probleme

Jonathan Lewis erinnert in seinem Blog daran, dass Baselines keinen positiven Effekt auf Parse-Probleme haben: "The first thing that the optimizer does for a query with a stored sql plan baseline is to optimize it as if the baseline did not exist." In solchen Fällen sind SQL Profiles, SQL Patches oder einfache Query-Hints möglicherweise eine bessere Lösung.

Samstag, Juli 05, 2014

Default-Werte für Datumsintervallendpunkte

Stew Ashton, der sich in seinem Blog in der Regel mit komplexen SQL-Queries beschäftigt, hält dort ein Plädoyer für den Verzicht auf NULL-Werte bei der Angabe der minimalen und maximalen Werte für Datumsintervalle. Stattdessen spricht er sich für die Verwendung der klassischen Minimal- und Maximalwerte aus, die durch den Oracle Datentyp DATE nahegelegt werden, also den 01.01.4712 B.C. (ich nehme an, das ist Oracles Version von der Erschaffung der Welt) und den 31.12.9999 A.C. Über seine ursprünglich vorgebrachten Argumente für diese Ansicht wurde in den Kommentaren zum Artikel recht intensiv diskutiert - und einige wurden entkräftet, aber der entscheidendste Punkt scheint mir immer noch zutreffend zu sein: komplexe SQL-Queries, die sich auch noch mit den NULL-Fällen für Intervall-Grenzen herumschlagen müssen, werden äußerst hässlich. Deshalb halte ich ein definiertes Maximaldatum insgesamt doch für das kleinere Übel als einen inhaltlich womöglich besser begründbaren NULL-Wert. 

Donnerstag, Juli 03, 2014

Multi-Attribut-Vergleiche

Ein interessanter Hinweis von Jonathan Lewis, der auf eine Präsentation von Markus Winand bei den Trivadis CBO-Tagen zurückgeht (die ich mir auch gern angeschaut hätte, wenn ich mir dergleichen leisten könnte): in manchen RDBMS ist es möglich, mehrspaltige Vergleiche der folgenden Form durchzuführen:

where (col1, col2) < (const1, const2)

Die Logik dabei ist: das Prädikat liefert TRUE, wenn col1 < const1 ist, oder wenn gilt: col1 = const1, aber col2 < const2 (und NULL-Werte machen vermutlich den üblichen Ärger). Im Oracle-Fall gibt es eine solche Logik nur intern bei der Auswertung von multi column range partitioning, aber nicht in SQL Queries. Nach Lektüre des Artikels habe ich mir die Frage gestellt, was postgres von diesem Konstrukt hält:

with
basedata as (
select 1 col1, 2 col2, 1 col3, 3 col4
)
select * from basedata where (col1, col2) < (col3, col4);

 col1 | col2 | col3 | col4
------+------+------+------
    1 |    2 |    1 |    3
(1 Zeile)

with
basedata as (
select 1 col1, 2 col2, 1 col3, 3 col4
)
select * from basedata where (col1, col2) > (col3, col4);

 col1 | col2 | col3 | col4
------+------+------+------
(0 Zeilen)

Funktioniert also wie beschrieben. Obwohl ich den Eindruck habe, dass die RDBMS-Dialekte eine gewisse Tendenz zur Vereinheitlichung zeigen, gibt's doch immer wieder interessante Detail-Unterschiede.