Sonntag, September 29, 2013

MySQL-Review von Thomas Kejser

Thomas Kejser - hab ich schon mal erwähnt, dass ich seine Artikel sehr schätze? - schreibt über MySQL aus der Sicht eines SQL Server Experten und liefert dabei eine schöne Liste zum Reifegrad der Implementierung von Features. Dabei kommt er zum (nicht besonders überraschenden) Ergebnis, dass MySQL keine Data Warehouse Engine ist, da es unter anderem keine Parallelisierung von Queries und keine Hash Joins anbietet. Auch die Instrumentierung kann ihn nicht überzeugen, während er die Partitionierungs-Features des Systems lobend erwähnt. Sein Fazit lautet:
Even after some exposure to MySQL I am still split in my opinion about the product. It it obvious that the features of MySQL are targeted towards coders who want to “whip up a quick database” and that the design philosophy is one inspired deeply by OLTP systems. This lack of warehouse database features sets some limits on what it can be used for. On the other hand, I am happy to see that MySQL doesn’t try to be everything to everyone. [...] We live in a world where the Web 2.0 developers have forsworn the beauty of relational algebra (mainly, I suspect, because they don’t actually grok single box parallelism) and where the big vendors have their heads so high up in the cloud that they have forgotten their customer base. It is encouraging to see MySQL keeping the relational candle lit, so that the lost NoSQL souls have somewhere to turn when the ACID burns leave them in the dark.
Sehr hübsch - die zugehörige Illustration sei Hieronymus Bosch überlassen...

Freitag, September 27, 2013

Shared-Pool-Verschmutzung durch Autotrace

Dass Autotrace nicht immer mit zuverlässigen Aussagen zur Ausführung von Queries dienen kann, ist weithin bekannt, und auch, dass die Ursache dieses Verhaltens darin liegt, dass Autotrace kein Bind Peeking durchführt. Weniger bekannt ist, dass die durch Autotrace erzeugten Pläne von folgenden Ausführungen wiederverwendet werden können, worauf Jonathan Lewis in seinem Blog hinweist. Randolf Geist hat das Thema vor einiger Zeit auch schon mal beleuchtet und festgestellt, dass die Gefahr, die von diesen wiederverwendeten Plänen ausgeht, nicht ganz so groß ist, da es sich dabei um spezielle EXPLAIN PLAN cursor handelt.

Mittwoch, September 25, 2013

SQL_ID aus SQL_TEXT erzeugen

Dass die SQL_ID als Hash für einen gegebenen SQL_TEXT erzeugt wird, konnte man vor längerer Zeit bei Tanel Poder erfahren. Jetzt hat Carlos Sierra eine handliche PL/SQL-Funktion veröffentlicht, mit der man die SQL_ID zu einer Query vor ihrer Ausführung berechnen kann (und im Artikel auch die Links auf den Artikel des Herrn Poder und eine entsprechende Python Funktion von Slavik Markovich aufgenommen).

Sonntag, September 22, 2013

Cardinality ohne Spaltenstatistiken

Ein Exkurs im Rahmen einer Anfrage im OTN Forum hat mir mal wieder gezeigt, wie viele Lücken mein Wissen über die Kalkulationsregeln des CBO hat. Die eigentliche Ausgangsfragestellung lasse ich dabei ausgeklammert, aber im Rahmen der Analyse wurde der Vorschlag gemacht, die column statistics einer Index-Spalte zu löschen. Die Frage ist jetzt, wie der CBO die Cardinality des Zugriffs bestimmt. Dazu das Beispiel, das ich auch im OTN-Thread beigesteuert habe, und in dem ich die cardinalities für IN-list-Zugriffe mit einem, zwei und zwanzig Elementen jeweils mit und ohne column statistics untersuche:

-- 11.1.0.7
drop table t;

create table t
as
select 'testtesttesttesttest' || rownum id
     , mod(rownum, 5) col2
     , lpad('*', 50, '*') padding
  from dual
connect by level <= 100000;

alter table t add constraint t_pk primary key (id, col2);

exec dbms_stats.gather_table_stats(user, 'T')

-- with column statistics
explain plan for
select *
  from t
where id in ('testtesttesttesttest1'
              );

select * from table(dbms_xplan.display);

------------------------------------------------------------------------------------
| Id  | Operation                   | Name | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |      |     1 |    80 |     4   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| T    |     1 |    80 |     4   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | T_PK |     1 |       |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("ID"='testtesttesttesttest1')

explain plan for
select *
  from t
where id in ('testtesttesttesttest1'
            , 'testtesttesttesttest2'
              );

select * from table(dbms_xplan.display);

-------------------------------------------------------------------------------------
| Id  | Operation                    | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |      |     2 |   160 |     5   (0)| 00:00:01 |
|   1 |  INLIST ITERATOR             |      |       |       |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| T    |     2 |   160 |     5   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN          | T_PK |     2 |       |     4   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("ID"='testtesttesttesttest1' OR "ID"='testtesttesttesttest2')


explain plan for
select *
  from t
where id in ('testtesttesttesttest1'
            , 'testtesttesttesttest2'
            , 'testtesttesttesttest3'
            , 'testtesttesttesttest4'
            , 'testtesttesttesttest5'
            , 'testtesttesttesttest6'
            , 'testtesttesttesttest7'
            , 'testtesttesttesttest8'
            , 'testtesttesttesttest9'
            , 'testtesttesttesttest10'
            , 'testtesttesttesttest11'
            , 'testtesttesttesttest12'
            , 'testtesttesttesttest13'
            , 'testtesttesttesttest14'
            , 'testtesttesttesttest15'
            , 'testtesttesttesttest16'
            , 'testtesttesttesttest17'
            , 'testtesttesttesttest18'
            , 'testtesttesttesttest19'
            , 'testtesttesttesttest20'
              );

select * from table(dbms_xplan.display);

-------------------------------------------------------------------------------------
| Id  | Operation                    | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |      |    20 |  1600 |    26   (0)| 00:00:01 |
|   1 |  INLIST ITERATOR             |      |       |       |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| T    |    20 |  1600 |    26   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN          | T_PK |    20 |       |    21   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("ID"='testtesttesttesttest1' OR "ID"='testtesttesttesttest10'
              OR "ID"='testtesttesttesttest11' OR "ID"='testtesttesttesttest12' OR
              "ID"='testtesttesttesttest13' OR "ID"='testtesttesttesttest14' OR
              "ID"='testtesttesttesttest15' OR "ID"='testtesttesttesttest16' OR
              "ID"='testtesttesttesttest17' OR "ID"='testtesttesttesttest18' OR
              "ID"='testtesttesttesttest19' OR "ID"='testtesttesttesttest2' OR
              "ID"='testtesttesttesttest20' OR "ID"='testtesttesttesttest3' OR
              "ID"='testtesttesttesttest4' OR "ID"='testtesttesttesttest5' OR
              "ID"='testtesttesttesttest6' OR "ID"='testtesttesttesttest7' OR
              "ID"='testtesttesttesttest8' OR "ID"='testtesttesttesttest9')

-- without column statistics
exec dbms_stats.delete_column_stats(user, 'T', 'ID')

explain plan for
select *
  from t
where id in ('testtesttesttesttest1'
              );

select * from table(dbms_xplan.display);

------------------------------------------------------------------------------------
| Id  | Operation                   | Name | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |      |  1000 | 80000 |    86   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| T    |  1000 | 80000 |    86   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | T_PK |   400 |       |     5   (0)| 00:00:01 |
------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("ID"='testtesttesttesttest1')

explain plan for
select *
  from t
where id in ('testtesttesttesttest1'
            , 'testtesttesttesttest2'
              );

select * from table(dbms_xplan.display);

-------------------------------------------------------------------------------------
| Id  | Operation                    | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |      |  1000 | 80000 |    87   (0)| 00:00:01 |
|   1 |  INLIST ITERATOR             |      |       |       |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| T    |  1000 | 80000 |    87   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN          | T_PK |   400 |       |     6   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("ID"='testtesttesttesttest1' OR "ID"='testtesttesttesttest2')

explain plan for
select *
  from t
where id in ('testtesttesttesttest1'
            , 'testtesttesttesttest2'
            , 'testtesttesttesttest3'
            , 'testtesttesttesttest4'
            , 'testtesttesttesttest5'
            , 'testtesttesttesttest6'
            , 'testtesttesttesttest7'
            , 'testtesttesttesttest8'
            , 'testtesttesttesttest9'
            , 'testtesttesttesttest10'
            , 'testtesttesttesttest11'
            , 'testtesttesttesttest12'
            , 'testtesttesttesttest13'
            , 'testtesttesttesttest14'
            , 'testtesttesttesttest15'
            , 'testtesttesttesttest16'
            , 'testtesttesttesttest17'
            , 'testtesttesttesttest18'
            , 'testtesttesttesttest19'
            , 'testtesttesttesttest20'
              );

select * from table(dbms_xplan.display);

-------------------------------------------------------------------------------------
| Id  | Operation                    | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |      |  1000 | 80000 |   102   (0)| 00:00:01 |
|   1 |  INLIST ITERATOR             |      |       |       |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| T    |  1000 | 80000 |   102   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN          | T_PK |   400 |       |    21   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("ID"='testtesttesttesttest1' OR "ID"='testtesttesttesttest10'
              OR "ID"='testtesttesttesttest11' OR "ID"='testtesttesttesttest12' OR
              "ID"='testtesttesttesttest13' OR "ID"='testtesttesttesttest14' OR
              "ID"='testtesttesttesttest15' OR "ID"='testtesttesttesttest16' OR
              "ID"='testtesttesttesttest17' OR "ID"='testtesttesttesttest18' OR
              "ID"='testtesttesttesttest19' OR "ID"='testtesttesttesttest2' OR
              "ID"='testtesttesttesttest20' OR "ID"='testtesttesttesttest3' OR
              "ID"='testtesttesttesttest4' OR "ID"='testtesttesttesttest5' OR
              "ID"='testtesttesttesttest6' OR "ID"='testtesttesttesttest7' OR
              "ID"='testtesttesttesttest8' OR "ID"='testtesttesttesttest9')

Meine vorläufige Interpretation dazu lautet:
  • mit (akkuraten) column statistics kann der CBO die cardinalities in allen Fällen (1, 2, 20) exakt bestimmen.
  • nach der Löschung der Statistiken der in der condition verwendeten Spalte wird in allen Fällen eine cardinality von 400 angenommen (wobei die cost Angaben sich sukzessive erhöhen, wenn sich die Länge der IN-list vergrößert).
  • die 400 bleiben übrigens auch erhalten, wenn man auch die Statistiken der zweiten PK-Spalte löscht und sogar, wenn man die zweite Spalte nicht in den PK aufnimmt (die zugehörigen Tests habe ich hier unterschlagen, da sie mit geringfügigem Aufwand aus dem bestehenden Test erzeugt werden können). In diesem Fall wird allerdings aus dem range scan ein unique scan und die cardinality ist für die Variante mit einem Element korrekt mit 1 bestimmt - offenbar erkennt der CBO in diesem Fall, dass ein unique scan auf einen Wert auch nur (höchstens) einen Satz liefern kann.
  • Anscheinend ist dieser Faktor 400/100000 = 0,004 ein fixierter Wert für diesen Fall.
  • wenn man den Index komplett entfernt, kommt der CBO im Test stabil auf eine cardinality von 1000 also 0,001.
Ich vermute, dass diese Annahmen des CBO irgendwo dokumentiert und bekannt sind, aber den einzigen Hinweis, den ich auf Anhieb finden konnte, liefert Richard Foote, der einen 0,4% bzw. 1% guess des CBO erwähnt, aber nicht näher erläutert. Möglicherweise trage ich hier die Details noch nach, sollte ich sie irgendwo finden.

Samstag, September 21, 2013

Skalare Subqueries in der WHERE clause

Vor neunundzwanzig Tagen hat Tanel Poder seinen Artikel Scalar Subqueries in Oracle SQL WHERE clauses (and a little bit of Exadata stuff too) veröfffentlicht und ehe der aus meinem reader verschwindet, sollte ich dazu ein paar Notizen machen.
  • eine scalar subquery in der WHERE clause hat die Form: WHERE ... = (select * from ...). Entscheidend ist dabei das Gleichheitszeichen, das diesen Fall von anderen Subqueries unterscheidet.
  • im Plan erscheint der Zugriff eingerückt unter dem Zugriff auf die Daten der main query, wenn in der Transformation eine push subquery operation durchgeführt wird (sichtbar als PUSH_SUBQ in den outline Hints): "So, as subqueries in a WHERE clause exist solely for producing data for filtering the parent query blocks rows, the PUSH_SUBQ means that we push the subquery evaulation deeper in the plan, deeper than the parent query block’s data access path, between the access path itself and data layer, which extracts the data from datablocks of tables and indexes. This should allow us to filter earlier, reducing the row counts in an earlier stage in the plan, thus not having to pass so many of them around in the plan tree, hopefully saving time and resources."
  • wenn man die Transformation mit einem NO_PUSH_SUBQ verbietet, ergibt sich ein Plan, in dem ein Filter step erscheint und darunter der Hauptabellenzugriff und der Zugriff für die Subquery auf einer Einrückungsebene (so wie Filter-Prädikate in der Regel auftreten - oder, vorsichtiger ausgedrückt, wie sie mir normalerweise begegnen), wobei in den Prädikaten die Filter-Query explizit aufgeführt ist.
  • mit PUSH_SUBQ erfolgt die Filterung beim Tabellenzugriff auf die Haupttabelle (die trotzdem komplett gelesen werden muss), während NO_PUSH_SUBQ die Filterung in den Filter step verschiebt. Für gewöhnliche Oracle-Datenbanken ist der Performance-Unterschied nicht allzu groß, aber in einem Exadata-System kann der Smart Scan die Filterung in die storage cells verschieben, was die Laufzeit der Test-Query dramatisch reduziert.
Klingt mal wieder, als wäre Exadata ein spannendes Thema...

Freitag, September 20, 2013

_fifteenth_spare_parameter

Security ist ein Thema, dem mit dem ich mich nicht unbedingt besonders gerne und besonders häufig beschäftige, aber auf einen Verweis auf den Artikel Fix for oradebug disable auditing available (11.2.0.3/11.2.0.4/12.1.0.1) von Alexander Kornbrust kann ich nicht verzichten, da dort der wunderbare verborgene Parameter _fifteenth_spare_parameter erscheint, den Oracle mit dem ebenbürtigen Kommentar "fifteenth spare parameter" versehen hat. Inhaltlich geht es im Artikel übrigens darum, dass man via ORADEBUG Auditing mit Hilfe von OS-Kommandos deaktivieren kann - und mit einer passenden Einstellung von _fifteenth_spare_parameter lässt sich die ORADEBUG-Funktion einschränken oder deaktivieren. Das hätte ich normalerweise vermutlich gleich wieder vergessen, aber eine bessere Erinnerungsstütze als _fifteenth_spare_parameter kann ich mir kaum vorstellen...

Samstag, September 14, 2013

MV Refresh über Partition Exchange

Jonathan Lewis beschreibt in seinem Blog ein Verfahren, das ich in ähnlicher Weise gelegentlich auch schon verwendet hatte - und mich dabei immer gewundert habe, dass es nicht häufiger eingesetzt bzw. in technischen Blogs beschrieben wird: die Verwendung von Partition Exchange zum performanten Austausch einer Materialized View gegen eine neu erzeugte prebuilt table. Die Idee dabei ist einfach, dass eine MV, die permanent für Abfragen verfügbar bleiben muss, nicht einfach über truncate und insert append neu befüllt werden kann (wie es die atomic_refresh Option erlaubt). Stattdessen kann man aber den Neuaufbau der MV in einer Hilfstabelle durchführen, die man dann gegen die bisher verwendete Tabelle per Partition Exchange austauscht, was natürlich voraussetzt, dass die MV als partitionierte Tabelle mit einer einzigen Partition angelegt wurde. Da in der MV nach dem Aufbau keine weiteren DML-Operationen durchgeführt werden, kann man die Segmente der Tabelle und zugehöriger Indizes so klein wie möglich machen und zur Beschleunigung des Aufbaus kann man diesen auch noch als nologging durchführen, um die Generierung von undo und redo zu verringern. Der Artikel beruhigt mich, denn ich hatte immer das vage (und unangenehme) Gefühl, bei meiner Implementierung - die allerdings keine echten MVs, sondern reguläre Dimensionstabellen erzeugte - irgendetwas Wichtiges übersehen zu haben, was aber anscheinend nicht der Fall ist.

Donnerstag, September 12, 2013

Fehlende Partitions-Statistiken

Doug Burns schrieb es dieser Tage in seinem Blog: "blogging is over!" Möglicherweise stimmt das sogar, aber solange ich keine Beispiele dafür finde, dass jemand eine umfassende technische Erläuterung auf 140 Zeichen unterbringt, bleibe ich Blog-Schreiber und -Leser. Möglicherweise neige ich inzwischen auch zu Sentimentalität - oder einfach zur Senilität; die Grenze dazwischen ist vermutlich nicht immer ganz scharf.

Immerhin hat der Herr Burns seine Bemerkung mit dem Vorsatz verbunden, wieder häufiger zu bloggen und das fände ich gerade in seinem Fall auch sehr erfreulich. Sein erster technischer Beitrag nach einer längeren Pause trägt den Titel 10053 Trace Files - Global Stats on Partitioned Tables und beschäftigt sich mit den Analyse-Möglichkeiten von Optimizer Trace Files zur Beantwortung von Fragen nach der Verwendung lokaler oder globaler Statistiken. Dabei ist klar, dass der CBO globale Statistiken verwendet, sobald auf mehr als eine Partition zugegriffen wird, und dass lokale Partitions-Statistiken verwendet werden, wenn der Zugriff sich auf die fragliche Partition beschränkt. Die Frage im Artikel lautet: was passiert, wenn beim Zugriff auf die einzelne Partition keine Statistiken für diese Partition vorliegen? In diesem Fall weicht der CBO auf die globalen Statistken aus (und nicht etwa auf dynamic sampling), was im CBO-Trace durch den Hinweis "(Using composite stats)" ausgewiesen ist. Eine große Überraschung ist das eher nicht, aber es ist schön, einen Test als expliziten Beleg dafür zu haben.

Mittwoch, September 11, 2013

Postgres Marginalien

Zwei Kleinigkeiten, denen ich bei meiner Arbeit mit postgres begegnet bin:

Mit dem Nummerngenerator generate_series lassen sich sehr komfortabel Testdaten erzeugen:

-- aufsteigend in einer Schritten
test_import=# select * from generate_series(1,10);
 generate_series
-----------------
               1
               2
               3
               4
               5
               6
               7
               8
               9
              10
(10 Zeilen)

-- absteigend in zweier Schritten
test_import=# select * from generate_series(10,0,-2);
 generate_series
-----------------
              10
               8
               6
               4
               2
               0
(6 Zeilen)

Das scheint mir doch etwas komfortabler zu sein, als meine übliche connect-by-dual-Methode für Oracle, obwohl ich auch mit der sehr zufrieden bin.

Auch nett sind temporary sequences  die ich dieser Tage an Stelle einer Variablen in einem Script verwendet habe. Für temporary sequences gilt:
If specified, the sequence object is created only for this session, and is automatically dropped on session exit. Existing permanent sequences with the same name are not visible (in this session) while the temporary sequence exists, unless they are referenced with schema-qualified names.
Normalerweise hätte ich das als eher seltsames Feature angesehen, aber vorgestern brauchte ich einfach ein paar Ids, die ich bei der initialen Füllung von Tabellen verwenden wollte - eine permanente Sequence schien mir etwas übertrieben, aber das temporäre Objekt passte in diesem Fall erstaunlich gut. Hier noch ein kleiner Test, der zeigt, dass die Sequenz angelegt und im Dictionary registriert wird, aber nach dem Ende der Session verschwindet.

test_import=# create temporary sequence test_seq;
CREATE SEQUENCE
test_import=# select * FROM pg_class c WHERE c.relkind = 'S';
 relname  | relnamespace | reltype | reloftype | relowner | relam |
----------+--------------+---------+-----------+----------+-------+
 test_seq |        33778 |   33781 |         0 |       10 |     0 |
(1 Zeile)

test_import=# \q
--> Neuanmeldung

postgres=# select * FROM pg_class c WHERE c.relkind = 'S';
 relname | relnamespace | reltype | reloftype | relowner | relam |
---------+--------------+---------+-----------+----------+-------+
(0 Zeilen)

Übrigens gefällt mir psql als Kommandozeilen-Tool ziemlich gut - man merkt, dass sich da jemand in Sachen Dokumentation und Bedienbarkeit einige Mühe gegeben hat.

Samstag, September 07, 2013

Histogramm Grundlagen

Jonathan Lewis hat bei AllThingsOracle eine Serie von Artikeln zum Thema Histogramme begonnen. Und wenn der Herr Lewis sich die Mühe macht, ein solches Thema in einer Artikelserie zu behandeln, dann kann man davon ausgehen, dass sich eine Verlinkung lohnt. Zumindest der erste Artikel erweckt dabei den Eindruck, dass es tatsächlich eher um Konzepte und Grundlagen geht als um Spezial- und Sonderfälle, aber ich höre dem Autor auch dann gerne zu, wenn ich das, was er erzählt, schon weiß - vielleicht höre ich dann sogar besonders gerne zu ...
  • Histograms Part 1 – Why?: beschäftigt sich mit den Grundlagen, der Gleichverteilungsannahme des CBO und den sich daraus ergebenden Problemen bei Ungleichverteilung (data skew). Als Lösung für solche Probleme kommen neben Histogrammen auf virtual columns in Frage. Vorgestellt wird das Konzept der frequency histograms und die klassischen Probleme der Histogramme werden erläutert (sie vertragen sich nicht mit Bindewerten; ihre exakte Ermittlung ist teuer; sampling führt oft zu schwachen Ergebnissen - Stichwort: auto_sample_size; man muss den richtigen Zeitpunkt für die Ermittlung treffen).
  • Histograms Part 2: mit einer umfassenden Diskussion von height-balanced histograms - und vor allem ihrer Schwächen. Der Artikel beginnt mit einer anschaulichen Erläuterung des Prinzips dieses Histogrammtyps, der dann verwendet wird, wenn die Anzahl unterschiedlicher Werte eine bestimmte Schwelle überschreitet (vor 12c waren das 254 mögliche Buckets), so dass nicht mehr jeder einzelne Wert ins Histogramm eingefügt werden kann, sondern die Endpunktwerte für gleichgroße Abschnitte der (sortierten) Gesamtmenge bestimmt werden müssen. Dabei werden Werte als populär gekennzeichnet, wenn sie mindestens zwei Buckets umspannen. Für extrem häufige Werte ist das kein Problem, aber Werte, die knapp weniger als zwei Buckets umfassen (und somit nahezu populär sind), werden in ihrer Kardinalität falsch eingeschätzt. Für die Schwächen der height-balanced histograms gibt es zwei Abhilfen: die manuelle Erzeugung geeigneter Histogramme und die in 12c ergänzten Histogrammtypen (top n histograms bzw. die Erhöhung der maximal möglichen Bucket-Anzahl).
  • 12c Histograms pt.3: ja, es ist kleinlich, aber was die einheitliche Benamung von Artikeln einer Serie angeht, gewinnt der Herr Lewis diesmal keine Punkte. Teil 3 jedenfalls erläutert die Implementierung von hybriden Histogrammen. Diese Strategie verschiebt die Endpunkte der Buckets bei einer Wertwiederholung und erfasst neben den Endpunkten der Buckets auch eine Information darüber, wie oft ein Endpunktwert wiederholt wurde, was die Bestimmung populärer Werte massiv verbessert. Wenn ich den vorangehenden Satz noch einmal lese, wird mir klar, dass er rein gar nichts erklärt - aber zu meiner Verteidigung muss ich sagen, dass mir auch die Erläuterung im Artikel erst nach wiederholter Lektüre und intensiverem Nachdenken klar wurde: die Erläuterung des Verfahrens ist also nicht ganz einfach...
Bei Erscheinen der folgenden Artikel werde ich diesen Eintrag wahrscheinlich ergänzen (was für Teil 2 und 3 nun erledigt wäre).

Dienstag, September 03, 2013

B*Tree und Bitmap Index-Mischung

Jonathan Lewis zeigt, wie man mit kleineren Tricks Index-Partitionen erzeugen kann, die einen "gemischten" Index simulieren, der für aktuelle Partitionen als B*Tree und für historische Partitionen als Bitmap Index erzeugt wird. Der Trick liegt darin, zwei Indizes zu erzeugen, von denen der eine nur den erforderlichen Wert indiziert und der andere zusätzlich einen NULL-Wert enthält (um ihn vom ersten Index zu unterscheiden). Initial werden beide Indizes als unusable definiert und dann baut man jeweils nur die gewünschten Partitionen auf. Durch das (vermutlich) seit 11.2 verfügbare Table Expansion Feature ist der CBO dann dazu in der Lage die Zugriffe auf die beiden Indizes über UNION ALL zu verknüpfen (sofern der skip_unusable_indexes Parameter auf true gesetzt ist, was aber seit 10g default ist). Das ganze wirkt ein wenig wacklig, könnte aber in entsprechenden Situationen sehr effektiv sein.