Posts mit dem Label Histograms werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Histograms werden angezeigt. Alle Posts anzeigen

Dienstag, November 06, 2018

Join Cardinality und Histogramme

Jonathan Lewis hat zuletzt eine Artikelserie veröffentlicht, die sich damit beschäftigen, wie sich die Existenz unterschiedlicher Histogramm-Typen auf die Bestimmung der Join-Cardinality auswirkt. Seine Überlegungen basieren dabei auf einem umfangreicheren Artikel von Chinar Aliyev aus dem Jahr 2016. Da der Herr Lewis da natürlich sehr viele Details beleuchtet, verzichte ich weitgehend auf die Zusammenfassung und beschränke mich auf die Erfassung der Links, um die Hoffnung zu erhalten, sie bei Bedarf wiederfinden zu können:
Wahrscheinlich wird die Artikelserie noch umfangreicher - und vielleicht erweitere ich dann auch die vorliegende Liste.

Freitag, November 03, 2017

Verhalten der auto_sample_size in 12c

Nigel Byliss erläutert im Oracle Optimizer Blog, die erfreulichen Änderungen, die für die auto_sample_size in 12c eingeführt wurden. Dabei ist die auto_sample_size der default-Wert für den Parameter estimate_percent der dbms_stats.gather_*_stats Prozeduren. Obwohl sie für viele Statistiken tatsächlich bereits seit ihrer Einführung sehr gute Ergebnisse lieferte, gab es einen Bereich, in dem ihre Ergebnisse recht erbärmlich ausfallen konnten, nämlich die Erstellung von Histogrammen, denn dafür wurde stets ein mikroskopisches Sample von gerade einmal 5500 Datensätzen verwendet.

In 12c wird nun folgendes Verfahren verwendet:
  • es erfolgt ein full table scan (also 100% Sample).
  • die Ermittlung von NDV-Werten (sprich: number of distinct values) erfolgt ohne Sortierung, sondern verwendet einen "approximate NDV algorithm", der mit Hash Werten arbeitet. Die Genauigkeit dieses Algorithmus ist dicht an 100%.
  • frequency und top frequency Histogramme werden mit den Daten des full table scans aufgebaut - also nicht mehr auf Basis einer minimalen Stichprobe. Zur Erinnerung: ein top frequency Histogramm kommt in Frage wenn die top 254 Werte mehr als 99% aller not null Werte ausmachen.
  • hybrid histograms verwenden weiterhin ein kleineres Sample: dieser Schritt ist also von der Basiserfassung getrennt.
  • Index-Statistiken werden mit einer automatisch ermittelten Stichprobengröße erzeugt.
Da mir die 5500 (non null) Zeilen in der Vergangenheit regelmäßig Ärger bereitet haben, halte ich diese Veränderung für ausgesprochen vorteilhaft.

Freitag, Februar 24, 2017

Online Statistics Gathering in 12c

Maria Colgan hat in den letzten Wochen zwei Artikel zum Thema der Erfassung von Optimizer Statistiken bei der Objektanlage veröffentlicht:
  • Online Statistics Gathering: seit Oracle 9 werden die Statistiken für Indizes im Rahmen der Anlage eines Index automatisch erfasst: da in diesem Zusammenhang ohnehin ein Full Scan der Daten und Sortierungen erforderlich sind, kann man die zusätzliche Erfassung der Statistiken problemlos in die Operation integrieren. Mit Oracle 12c wird diese Technik jetzt auch für Tabellen verwendet, wenn sie über direct path Operationen wie CTAS und INSERT append (dort allerdings nur, wenn vorher noch keine Daten in der Tabelle existierten) befüllt werden. Über den Parameter _optimizer_gather_stats_on_load kann man den Mechanismus deaktivieren.
  • Histogram sample size and Online Statistics Gathering: darin weist die Autorin darauf hin, dass im Fall der automatischen Statistikerstellung im Rahmen der Ladeoperationen in den Spaltenstatistiken unter Notes ein Eintrag STATS_ON_LOAD erscheint. Führt man anschließend eine Statistikerfassung mit der Option GATHER AUTO durch, dann werden nur Histogramme erzeugt (NOTES = HISTOGRAM_ONLY) und diese mit dem üblichen traurig kleinen Sample von ca. 5500 not null Werten.
Sollte Frau Colgan hier noch weitere Artikel liefern, werde ich sie an dieser Stelle ergänzen.

Montag, Dezember 05, 2016

Änderung der endpoint value Berechnung für Histogramme seit Oracle 11.2.0.4

Jonathan Lewis und vor ihm bereits Franck Pachot haben zuletzt darauf hingewiesen, dass in 11.2.0.4 eine Änderung der Berechnung der endpoint values für Histogramme von char und nchar (nicht aber varchar2 und nvarchar2) Spalten eingeführt wurde, die eine Neuerzeugung entsprechender Histogramme nach einem Upgrade von einer früheren Version erforderlich macht, da sich sonst recht bizarre costing-Effekte ergeben.

In Francks Test existiert eine Spalte mit den beiden Werten 'Y' und 'N', die massive ungleichmäßig verteilt sind (100K zu 1K). Zu erwarten wäre, dass diese Verteilung durch ein Frequency-Histogramm exakt abgebildet wird, was in 11.2.0.3 auch der Fall ist. Nach dem Upgrade auf 11.2.0.4 (ohne Neuerzugung des Histogramms) wird die Cardinality für beide Werte aber mit der Formel für nicht im frequency Histogramm vorliegende Werte berechnet: also "Häufigkeit des seltensten Werts im Histogramm" geteilt durch zwei (im Beispiel also 500). Demnach scheinen nach dem Upgrade also alle Werte unbekannt zu sein. Verantwortlich dafür ist die interne Ablage der endpoint values im Histogramm: bis 11.2.0.3 erfolgte das Padding der Werte mit spaces (ASCII 0x20), in 11.2.0.4 erfolgt es mit Nullen (ASCII 0x00) - was dem Verhalten entspricht das scon ihn älteren Releases für varchar2 verwendet wurde. Insofern ist die Neuerzeugung der Histogramme unvermeidbar.

Jonathan Lewis erwähnt in seinem Artikel auch noch mal die wichtige Tatsache, dass die Erzeugung vieler Histogrammtypen in 12c mit einem "approximate NDV" Verfahren erfolgt: die 5500 row samples, die in der Vergangenheit häufig zu Problemen geführt hatten, sind demnach in vielen Fällen nicht mehr im Spiel.

Montag, September 26, 2016

Histogramme für Spalten mit PK/UK

Jonathan Lewis hat dieser Tage in einer Diskussion der Oracle-L Mailing-Liste darauf hingewiesen, dass auch eine Spalte mit einem PK oder UK von einem Histogramm profitieren kann - und diese Aussage jetzt in seinem Blog erläutert und mit einem Beispiel versehen. Interessant ist ein solches Histogramm dann, wenn die Werteverteilung zwischen Minimum und Maximum sehr uneinheitlich ist, so dass sich große Bereiche ergeben, in denen fast keine Daten existieren, während in anderen Bereichen gleichen Umfangs sehr viele Ergebnisse zu finden sind. In seinem Beispiel erfolgt eine Abfrage auf einen solchen sparse-besetzten Bereich und Oracle erkennt anschließend bei der Statistikerfassung (mit Standardeinstellungen), dass hier ein Histogramm nützlich ist. Das die Anlage von Histogrammen begründende Phänomen "data skew" (sprich: Ungleichverteilung) betrifft also nicht nur das Auftreten ungleichmäßig vieler Datensätze für einen gegebene Wert, sondern auch die ungleiche Verteilung in bestimmten Wertebereichen für eindeutige Werte.

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.

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.

Sonntag, Oktober 26, 2014

Histogramme, Cardinality und der Optimizer Mode first_rows_100

Eine interessante Antwort auf die Frage nach unplausiblen Cardinalities, die sich trotz frequency histogram ergeben, hat Jonathan Lewis in einem OTN-Forums-Thread gegeben. Der vom Fragesteller gelieferte handliche Testfall zeigte sehr deutlich, dass in seinem System mit 11.2.0.4 für einfache Queries sehr merkwürde Cardinalities ermittelt wurden, obwohl das vorhandene Histogramm die tatsächliche Verteilung exakt abbildete. Diverse Beiträger konnten nur feststellen, dass das Verhalten in ihren Systemen (zwischen 11.2.0.1 und 12.1.0.2) nicht reproduzierbar war, aber der Herr Lewis konnte das fehlende Puzzle-Teil benennen: wenn man den Optimizer-Mode auf first_rows_100 umstellt, werden aus den plausiblen Schätzungen augenblicklich recht nutzlose Zahlen. Ich pflege die Wirkung der first_rows_n-Einstellungen regelmäßig zu unterschätzen und sollte mir endlich merken, wie invasiv dieser Modus tatsächlich ist.

Samstag, Mai 03, 2014

Histogramme löschen

Jonathan Lewis zeigt, wie man in 10g Histogramme löschen kann - ab 11g gibt es zu diesem Zweck den DBMS_STATS.DELETE_COLUMN_STATS-Parameter col_stat_type, mit dem die Löschung explizit durchgeführt werden kann. Neben dem Verfahren erklärt der Autor auch noch einmal seine grundsätzliche Strategie der expliziten Anlage der tatsächlich benötigten Histogramme:
my preferred method of collecting statistics is to use method_opt => ‘for all columns size 1' (i.e. no histograms) and then run scripts to create the histograms I want. This means that after any stats collection I need to run code that checks to see which tables have new stats, and then re-run any histogram code that I’ve written for that table.
Mit den neuen Histogrammtypen in 12c sieht der Herr Lewis aber seltener die Notwendigkeit der manuellen Erzeugung von Histogrammen, die er an diversen Stellen erläutert 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).

Montag, August 05, 2013

Histogramme in 12c

In einer kleinen Artikelserie beschäftigt sich Jonathan Lewis mit den Änderungen im Bereich der Histogramme im neuen Oracle-Release 12c:
  • 12c histograms: liefert eine Zusammenfassung zu den Veränderungen/Verbesserungen, die in 12c eingeführt wurden. Meine Zusammenfassung dieser Zusammenfassung wäre:
    • Erweiterung des bisher nur für basic column stats verfügbaren approximate NDV Mechanismus auf frequency histograms.
    • Bereitstellung eines neuen Typs von fequency histograms: "Top-N histogram" (die in vielen Fällen height-balanced histograms ersetzen).
    • Erhöhung der maximalen Bucket-Anzahl auf 2000 (der default bleibt aber 254).
    • Bereitstellung eines neuen Typs von height-balanced histograms: "hybrid histogram".
  • 12c Histograms pt.2: erklärt den neuen Mechanismus zur Erzeugung von frequency histograms und die Implementierung der Top-N frequency histograms.
    • der für die Erzeugung von frequency histograms in 12c verwendete approximate NDV Mechanismus geht folgendermaßen vor: zu jedem erzeugten hash value wird im Rahmen des Table Scans die erste rowid und die Gesamtanzahl der Vorkommen festgehalten. Wenn die Anzahl der unterschiedlichen hash values am Ende des Scans unterhalb der Anzahl vorgesehener Buckets liegt (also 254 oder 2000), dann können die zu den hash values gehörigen Werte per rowid lookup ermittelt werden.
    • wenn die Anzahl der erforderlichen Buckets (d.h. die Anzahl der ermittelten hash values) größer ist als die Anzahl der möglichen Buckets kann Oracle in 12c an Stelle eines height-balanced histograms ein Top-N frequency histogram verwenden, sofern die meisten Daten einer überschaubaren Anzahl von Werten zugeordnet sind. In diesem Fall wird jeweils ein Bucket für den minimalen und den maximalen Wert reserviert und die übrigen N-2 buckets für die am häufigsten vorkommenden Werte verwendet. Das Verfahren sollte in geeigneten Fällen deutlich bessere Dienste leisten als ein height-balanced histogram.
  • 12 Histogram fixes: mit Links auf ältere Artikel zum Verhalten in Extremfällen (mit langen Strings), das sich in 12c verändert hat, weil die Definition von ENDPOINT_ACTUAL_VALUE geändert wurde - was aber nicht alle Probleme löst.
Die Zusammenfassung weiterer Artikel werde ich bei Erscheinen - und Gelegenheit - ergänzen.

    Dienstag, April 09, 2013

    Details zur AUTO_SAMPLE_SIZE in 11g

    Hong Su erläutert im Blog der CBO Entwickler das Vorgehen bei der Statistikerfassung mit AUTO_SAMPLE_SIZE in 11g. Insbesondere erwähnt er folgende Punkte:
    • in 11g führt AUTO_SAMPLE_SIZE automatisch zu einem Full Table Scan, der die Anzahl, den Minimal- und den Maximalwert zu jedem Attribut ermittelt. Vor 11g wurden im gleichen Fall Queries mit Sampling durchgeführt, wobei das Sample sukzessive vergrößert wurde, sofern das Ergebnis bestimmten (auf internen Metriken basierenden) Anforderungen entsprach.
    • im Rahmen des FTS wird ein Hash-basierter Algorithmus verwendet, um die NDV-Angabe zu bestimmen. Vor 11g wurde dazu ein entsprechendes COUNT(DISTINCT ...) in der sample-basierten Ermittlungs-Query verwendet. Das hash-basierte Verfahren liefert eine Genauigkeit von nahezu 100%.
    • Auch weitere statistische Werte werden im Rahmen des FTS mit dem gleichen Verfahren und hoher Genauigkeit bestimmt (number of NULLs, AVG column length).
    • Die Erfassung von Histogrammen und Index-Statistiken erfolgt weiterhin über Sampling, allerdings wird die Anzahl der Sample-Versuche gegenüber dem alten Verfahren deutlich reduziert. Für die Index-Statistiken werden dabei die NDV-Werte der entsprechenden Columns (im Fall einspaltiger Indizes) bzw. ColumnGroups (i.e. extended statistics; für mehrspaltige Indizes) übernommen.
    Im Fall der Histogramme sind allerdings die Samples bekanntlich erschütternd klein (5500 Sätze), was im Zusammenspiel mit der Annahme, dass ein im Histogramm unbekannter Wert die halbe Auftrittswahrscheinlichkeit hat wie der am seltensten im Histogramm erscheinende Wert, zu unschönen Ergebnissen führen kann. Da dieser Satz doch mal wieder etwas sperrig geworden ist, hier noch das zugehörige Beispiel (das ich in ähnlicher Form sicher schon mal hier irgendwo eingebaut hatte):

    -- 11.2.0.1
    -- eine Tabelle mit 1M rows, davon genau 1 Satz mit col1 = 0
    -- für alle anderen Sätze ist col1 = 1
    create table t
    as
    select case when mod(rownum, 1000000) = 500000 then 0 else 1 end col1
      from dual
    connect by level <= 1000000;
    
    -- Statistikerfassung mit AUTO_SAMPLE_SIZE
    begin
    dbms_stats.gather_table_stats(
        user
      , 'T'
      , estimate_percent => dbms_stats.auto_sample_size
      , method_opt => 'for all columns size auto'
    );
    end;
    /
    
    select column_name
         , num_distinct
         , num_buckets
         , sample_size
      from dba_tab_cols
     where table_name = 'T';
    
    COLUMN_NAME                    NUM_DISTINCT NUM_BUCKETS SAMPLE_SIZE
    ------------------------------ ------------ ----------- -----------
    COL1                                      2           1        5525
    
    select column_name
         , endpoint_number
         , endpoint_value
      from dba_histograms
     where table_name = 'T';
    
    COLUMN_NAME                    ENDPOINT_NUMBER ENDPOINT_VALUE
    ------------------------------ --------------- --------------
    COL1                                      5525              1
    
    explain plan for
    select *
      from t
     where col1 = 1;
    
    --------------------------------------------------------------------------
    | Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
    --------------------------------------------------------------------------
    |   0 | SELECT STATEMENT  |      |   999K|  2929K|   436   (3)| 00:00:06 |
    |*  1 |  TABLE ACCESS FULL| T    |   999K|  2929K|   436   (3)| 00:00:06 |
    --------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       1 - filter("COL1"=1)
    
    explain plan for
    select *
      from t
     where col1 = 0;
    
    --------------------------------------------------------------------------
    | Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
    --------------------------------------------------------------------------
    |   0 | SELECT STATEMENT  |      |   500K|  1464K|   436   (3)| 00:00:06 |
    |*  1 |  TABLE ACCESS FULL| T    |   500K|  1464K|   436   (3)| 00:00:06 |
    --------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       1 - filter("COL1"=0)
    
    explain plan for
    select *
      from t
     where col1 = 42;
    
    --------------------------------------------------------------------------
    | Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
    --------------------------------------------------------------------------
    |   0 | SELECT STATEMENT  |      |   500K|  1464K|   436   (3)| 00:00:06 |
    |*  1 |  TABLE ACCESS FULL| T    |   500K|  1464K|   436   (3)| 00:00:06 |
    --------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       1 - filter("COL1"=42)
    

    Hier ist der NUM_DISTINCT-Wert korrekt mit 2 angegeben, aber da der CBO im Histogramm nur einen Wert (nämlich 1) findet, nimmt er an, dass jeder unbekannte Wert (die seltene 0 genau wie die abwegige 42) halb so oft erscheint wie der seltenste (und in diesem Fall einzige) Wert - also: 999K/2 = 500K.

    In diesem extremen Fall liefert erst estimate_percent => 100 mit Sicherheit korrekte Angaben (da jedes Sample Gefahr läuft, den seltenen Wert zu verpassen):

    begin
    dbms_stats.gather_table_stats(
        user
      , 'T'
      , estimate_percent => 100
      , method_opt => 'for all columns size auto'
    );
    end;
    /
    
    select column_name
         , num_distinct
         , num_buckets
         , sample_size
      from dba_tab_cols
     where table_name = 'T';
    
    COLUMN_NAME                    NUM_DISTINCT NUM_BUCKETS SAMPLE_SIZE
    ------------------------------ ------------ ----------- -----------
    COL1                                      2           2     1000000
    
    select column_name
         , endpoint_number
         , endpoint_value
      from dba_histograms
     where table_name = 'T';
    
    COLUMN_NAME                    ENDPOINT_NUMBER ENDPOINT_VALUE
    ------------------------------ --------------- --------------
    COL1                                         1              0
    COL1                                   1000000              1
    
    explain plan for
    select *
      from t
     where col1 = 0;
    
    --------------------------------------------------------------------------
    | Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
    --------------------------------------------------------------------------
    |   0 | SELECT STATEMENT  |      |     1 |     3 |   436   (3)| 00:00:06 |
    |*  1 |  TABLE ACCESS FULL| T    |     1 |     3 |   436   (3)| 00:00:06 |
    --------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       1 - filter("COL1"=0)
    

    Dynamic Sampling wäre in diesem Fall natürlich auch eine Alternative.

    Mittwoch, August 22, 2012

    Optimierung durch Histogramm-Löschung

    Jonathan Lewis hat in seinem Blog einen interessanten Fall beschrieben, in dem die Löschung von Histogrammen einen positiven Effekt auf die Performance von Zugriffen brachte. Allerdings sind in seinem Beispiel die Histogramme nicht das grundlegende Problem (oder höchstens ein Teil davon), denn sie liefern eigentlich eine korrekte Information, die den CBO dann allerdings auf einen unglücklichen Weg bringt, bei dem massive Loops (in diesem Fall hervorgerufen durch FILTER-Operationen) die Laufzeit erhöhen. Die Ursache für dieses Verhalten ist anscheinend ein Bug beim Costing von (zumindest bestimmten Typen von) Subqueries. Interessant ist der Artikel auch als Beispiel für ein strukturiertes Vorgehen bei der Analyse von SQL-Zugriffsproblemen.

    Mittwoch, Juni 06, 2012

    Histogramme für Spalten mit extremer Ungleichverteilung

    Mal wieder ein mäßig präziser Titel. Gemeint ist Folgendes: wenn in einer Spalte ein bestimmter Wert für nahezu sämtliche Sätze vorliegt und andere Werte extrem selten erscheinen, dann ist es nicht unwahrscheinlich, dass die seltenen Werte in einem mit auto_sample_size erzeugten Histogramm übersehen werden, was dann zur Folge hat, dass ihre Cardinality massiv überschätzt wird. Dazu ein Beispiel mit 11.2.0.1:

    create table t_predominant
    as
    select rownum id
         , case when mod(rownum, 30000) = 10000 then 2
                when mod(rownum, 30000) = 20000 then 3
           else 1 end col_skew
         , lpad('*', 50, '*') padding
      from dual
    connect by level <= 1000000;
    
    select col_skew, count(*)
      from t_predominant
     group by col_skew;
    
    COL_SKEW   COUNT(*)
    -------- ----------
           1     999933
           2         34
           3         33
    
    begin
      dbms_stats.gather_table_stats(
          user
        , 't_predominant'
        , estimate_percent=>dbms_stats.auto_sample_size
        , method_opt=>'for columns col_skew size 254'
      );
    end;
    /
    

    Ich lege also eine Tabelle mit 1M rows an, von denen fast alle in der col_skew den Wert 1 enthalten. Nur in 33 bzw. 34 Fällen erscheinen die Werte 2 und 3. Anschließend erzeuge ich explizit Histogramme für die Spalte col_skew mit dem Standard-Wert für estimate_percent. Ein Blick auf die Cardinality-Schätzungen beim Zugriff, zeigt, dass sich der CBO für die seltenen Fälle massiv verkalkuliert:

    explain plan for
    select count(*) from T_PREDOMINANT where COL_SKEW = 1;
    
    ------------------------------------------------------------------------------------
    | Id  | Operation          | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
    ------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT   |               |     1 |     3 |  2453   (1)| 00:00:30 |
    |   1 |  SORT AGGREGATE    |               |     1 |     3 |            |          |
    |*  2 |   TABLE ACCESS FULL| T_PREDOMINANT |   999K|  2929K|  2453   (1)| 00:00:30 |
    ------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       2 - filter("COL_SKEW"=1)
    
    explain plan for
    select count(*) from T_PREDOMINANT where COL_SKEW = 2;
    
    ------------------------------------------------------------------------------------
    | Id  | Operation          | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
    ------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT   |               |     1 |     3 |  2453   (1)| 00:00:30 |
    |   1 |  SORT AGGREGATE    |               |     1 |     3 |            |          |
    |*  2 |   TABLE ACCESS FULL| T_PREDOMINANT |   500K|  1464K|  2453   (1)| 00:00:30 |
    ------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       2 - filter("COL_SKEW"=2)
    

    Für den Fall mit "col_skew = 1" ist das Ergebnis präzise, aber woher kommen die abwegigen 500K für den Fall "col_skew = 2". Die Antwort findet man bei Jonathan Lewis: "If the value you supply does not appear in the histogram, but is inside the low/high range of the histogram then the cardinality will be half the cardinality of the least frequently occurring value that is in the histogram."

    select column_name
         , num_distinct
         , low_value
         , high_value
         , histogram
         , num_buckets
         , sample_size
      from dba_tab_cols
     where table_name = 'T_PREDOMINANT'
       and column_name = 'COL_SKEW';
    
    COLUMN_NAME     NUM_DISTINCT LOW_VALUE  HIGH_VALUE HISTOGRAM       NUM_BUCKETS SAMPLE_SIZE
    --------------- ------------ ---------- ---------- --------------- ----------- -----------
    COL_SKEW                   3 C102       C104       FREQUENCY                 1        5568
    

    Der Wert 2 liegt zwischen LOW_VALUE und HIGH_VALUE, erscheint aber offensichtlich nicht im Histogramm und deshalb berechnet sich seine Cardinality als ("cardinality of the least frequently occurring value")/2: im gegebenen Fall ist die "cardinality of the least frequently occurring value" die des einzigen im Histogramm enthaltenen Wertes - also 1000K, so dass sich die 500K für die Werte 2 und 3 ergeben. Die sample_size von 5568 ist dabei ein besonderer Effekt der auto_sample_size, auf den Randolf Geist gelegentlich hingewiesen hat (der verlinkte Kommentar weist auch explizit auf den hier angesprochenen Fall der Nichtberücksichtigung seltener Werte im Histogramm hin).

    Allerdings ergibt sich in meinem Test-Fall die gleiche Cardinality von 500K auch für Werte jenseits der LOW_VALUE, HIGH_VALUE-Grenzen:

    explain plan for
    select count(*) from T_PREDOMINANT where COL_SKEW = 4;
    
    ------------------------------------------------------------------------------------
    | Id  | Operation          | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
    ------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT   |               |     1 |     3 |  2453   (1)| 00:00:30 |
    |   1 |  SORT AGGREGATE    |               |     1 |     3 |            |          |
    |*  2 |   TABLE ACCESS FULL| T_PREDOMINANT |   500K|  1464K|  2453   (1)| 00:00:30 |
    ------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       2 - filter("COL_SKEW"=4)
    
    explain plan for
    select count(*) from T_PREDOMINANT where COL_SKEW = 10000000000000;
    
    ------------------------------------------------------------------------------------
    | Id  | Operation          | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
    ------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT   |               |     1 |     3 |  2453   (1)| 00:00:30 |
    |   1 |  SORT AGGREGATE    |               |     1 |     3 |            |          |
    |*  2 |   TABLE ACCESS FULL| T_PREDOMINANT |   500K|  1464K|  2453   (1)| 00:00:30 |
    ------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       2 - filter("COL_SKEW"=10000000000000)
    

    Jonathan Lewis schreibt dazu: "If the value you supply is outside the low/high range of the histogram Oracle starts with the “half the least popular” value, then applies the normal (for 10g) linear decay estimate so that the cardinality drops the further outside the known range your requested value falls." Das scheint hier nicht einzutreten, aber möglicherweise liegt hier ein ähnlicher Fall vor, wie der von Randolf Geist im Zusammenhang mit column groups beschriebene: "If there is only a single distinct value in the statistics then the "out-of-range" detection of the optimizer is not working correctly."

    Um ein brauchbares Histogramm zu erhalten, ändere ich in meinem Testfall die estimate_percent auf 100:

    begin
      dbms_stats.gather_table_stats(
          user
        , 't_predominant'
        , estimate_percent=>100
        , method_opt=>'for columns col_skew size 254'
      );
    end;
    /
    
    explain plan for
    select count(*) from T_PREDOMINANT where COL_SKEW = 2;
    
    ------------------------------------------------------------------------------------
    | Id  | Operation          | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
    ------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT   |               |     1 |     3 |  2453   (1)| 00:00:30 |
    |   1 |  SORT AGGREGATE    |               |     1 |     3 |            |          |
    |*  2 |   TABLE ACCESS FULL| T_PREDOMINANT |    34 |   102 |  2453   (1)| 00:00:30 |
    ------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       2 - filter("COL_SKEW"=2)
    

    Ein paar alternative Lösungen zum Problem findet man bei Nikolay Savvinov, der sich dabei wiederum auf Vorschläge von Jonathan Lewis bezieht, nämlich die Anlage eines FBI (und eine Anpassung der zugehörigen Queries) bzw. die manuelle Erzeugung geeigneter Histogramme.

    Beim Herrn Savvinov habe ich dann noch einen Link auf einen weiteren Artikel von Randolf Geist entdeckt, der so ziemlich alles, was ich hier gerade aufgeschrieben habe, ausführlich ausführt und erläutert - sogar das Beispiel ist nahezu identisch mit dem, das ich mir gebastelt habe - und noch allerlei zusätzliche Informationen liefert. Vielleicht hätte ich dort erst mal suchen sollen ...

    Freitag, September 02, 2011

    Frequency-Histogramme und die 254 Bucket-Grenze

    Und gleich die nächste interessante Quizfrage im Blog des Herrn Foote. Bei der letzten Frage war mir die Antwort schon vor der Auflösung klar, aber diesmal hatte ich absolut keine Erklärung für das beschriebene Verhalten: für eine Spalte existierte ein Frequency-Histogramm so lange darin 254 unterschiedliche - und gleichverteilte - Werte vorlagen. Das Hinzufügen eines einzelnen Satzes mit einem neuen und weit außerhalb des bisherigen Ranges liegenden Wert (also Nr. 255) führte dann dazu, dass bei erneuter Statsitikerzeugung mit der METHOD_OPT SIZE AUTO Option kein Histogram mehr erzeugt wurde - obwohl ein solches mit der neuen Datenverteilung sehr viel sinnvoller gewesen wäre als für den Fall vor der Ergänzung (für den ohnehin eine Gleichverteilung galt, die der cbo in Abwesenheit von Histogrammen von vornherein angenommen hätte). Die Erklärung des Verhaltens lautet:
    When using METHOD_OPT SIZE AUTO, every column with 254 or less distinct values that has been referenced within a predicate, will have a Frequency-based histogram. Each and every one of them, regardless of whether the data is actually skewed or not.
    [...]
    If a column has more than 254 distinct values, whether it then has a Height-Based histogram depends on how the data is skewed.
    Für die Spalte mit dem neuen und von den bisherigen Werten massiv abweichenden Wert gilt:
    Having inserted the outlier value, it now has 255 distinct values and so no longer qualifies for an automatic frequency based histogram. However, if all its values are evenly distributed, then it won’t qualify for a height based histogram either and Column 3 only has just the one outlier value, all other values are evenly distributed values. Unfortunately, Oracle doesn’t pick up on rare outlier values (even if you collect 100% statistics and it’s one of the low/high points of the column) and so will not generate a height-based histogram.
    Der Artikel führt auch noch vor, dass das Fehlen des Histogramms dann recht extreme Fehleinschätzungen des cbo nach sich zieht, da dieser nun wieder eine Gleichverteilung annimmt. Vorgestern Abend bin ich an dieser Quizfrage fast 1,5 h hängengeblieben - es ist einfach großartig, was man bei den kompetentesten Oracle-Bloggern alles lernen kann.

    Nachtrag 03.09.2011: Randolf Geist hat in einem Kommentar zu Richard Footes Artikel noch einige wichtige Details zur Kostenberechnung des CBO mit Frequency Histograms ausgeführt.

    Montag, Juni 27, 2011

    Histogramme und implizite Typumwandlung

    In Kyle Haileys Blog findet sich ein interessantes Beispiel für die unerfreulichen Folgen von impliziter Typumwandlung auf die Cardinalities und Costs einer Query im Zusammenhang mit Histogrammen.

    Freitag, Januar 14, 2011

    Histogramme - 4

    Bei meinen Histogramm-Tests bin ich noch an einem Detail hängengeblieben, nämlich an den ENDPOINT_NUMBER-Angaben in USER_HISTOGRAMS (oder der anscheinend identischen USER_TAB_HISTOGRAMS). Während man den ENDPOINT_VALUE für frequency histograms relativ leicht als einen distinkten Wert der jeweiligen Spalte wiedererkennen kann, sind die ENDPOINT_NUMBER-Angaben interpretationsbedürftiger:

    create table test
    as
    select case when rownum <= 10000 then mod(rownum, 10) else 100 end rn
         , lpad('*', 100, '*') pad
      from dual
    connect by level <= 1000000;
    
    select rn
         , count(*)
      from test
     group by rn
     order by rn;
    
      RN   COUNT(*)
    ---- ----------
       0       1000
       1       1000
       2       1000
       3       1000
       4       1000
       5       1000
       6       1000
       7       1000
       8       1000
       9       1000
     100     990000
    

    Also 10 Werte mit jeweils 1.000 Vorkommen und einer, der 990.000 mal erscheint. Wenn man dazu USER_HISTOGRAMS befragt, sieht man Werte, die dazu in irgendeiner Relation zu stehen scheinen, aber die Art dieser Relation erschliesst sich nicht unmittelbar - zumindest nicht für mich:

    select column_name
         , endpoint_value
         , endpoint_number
      from user_histograms
     where table_name = 'TEST'
       and column_name = 'RN';
    
    COLUMN_NAME                              ENDPOINT_VALUE ENDPOINT_NUMBER
    ---------------------------------------- -------------- ---------------
    RN                                                    0              65
    RN                                                    1             126
    RN                                                    2             171
    RN                                                    3             235
    RN                                                    4             292
    RN                                                    5             348
    RN                                                    6             400
    RN                                                    7             462
    RN                                                    8             532
    RN                                                    9             586
    RN                                                  100            5482 
     

    Die Antwort auf die Frage nach der Bedeutung dieser Werte kennt einmal mehr Jonathan Lewis: die ENDPOINT_NUMBER ist eine kumulierte Anzahl der Sätze pro Wert. Dankenswerterweise liefert er auf gleich eine Query mit deren Hilfe man die kumulierten Zahlen wieder in simple Gruppierungszählungen zurückverwandeln kann (über Analytics):

    select
       endpoint_value                          column_value,
       endpoint_number - nvl(prev_endpoint,0)  frequency
    from       (
       select
               endpoint_number,
               lag(endpoint_number,1) over(
                       order by endpoint_number
               )                               prev_endpoint,
               endpoint_value
       from
               user_tab_histograms
       where
               table_name  = 'TEST'
       and     column_name = 'RN'
       )
    order by
       endpoint_number;
    
    COLUMN_VALUE  FREQUENCY
    ------------ ----------
               0         65
               1         61
               2         45
               3         64
               4         57
               5         56
               6         52
               7         62
               8         70
               9         54
             100       4896 
     

    Vergleicht man diese Ergebnisse mit den Werten der gruppierenden Query oben, dann ergibt sich, dass die Frequency jeweils ca. 0,5% der absoluten Werte beträgt. Das wiederum lässt vermuten, dass hier ein Sampling im Spiel ist, und um das zu prüfen, habe ich ein kleines Script test.sql geschrieben, das die Statistiken mit unterschiedlicher estimate_percent-Angabe erhebt und anschließend die Query von Jonathan Lewis und eine Abfrage der SAMPLE_SIZE aus USER_TABELS durchführt:

    -- test.sql 
    exec dbms_stats.gather_table_stats (ownname=>user, tabname=>'test', method_opt=>'FOR ALL COLUMNS SIZE 11', estimate_percent=> &sample_pct)
    
    select
       endpoint_value                          column_value,
       endpoint_number - nvl(prev_endpoint,0)  frequency
    from       (
       select
               endpoint_number,
               lag(endpoint_number,1) over(
                       order by endpoint_number
               )                               prev_endpoint,
               endpoint_value
       from
               user_tab_histograms
       where
               table_name  = 'TEST'
       and     column_name = 'RN'
       )
    order by
       endpoint_number;
    
    select table_name
         , sample_size
      from user_tab_columns
     where table_name = 'TEST'
       and column_name = 'RN';

    Hier die Ergebnisse für verschiedene Sampling-Angaben:

    SQL> @ test
    Geben Sie einen Wert für sample_pct ein: 100
    
    PL/SQL-Prozedur erfolgreich abgeschlossen.
    
    Abgelaufen: 00:00:03.56
    
    COLUMN_VALUE  FREQUENCY
    ------------ ----------
               0      10000
               1      10000
               2      10000
               3      10000
               4      10000
               5      10000
               6      10000
               7      10000
               8      10000
               9      10000
             100     900000
    
    11 Zeilen ausgewählt.
    
    Abgelaufen: 00:00:00.00
    
    TABLE_NAME                     SAMPLE_SIZE
    ------------------------------ -----------
    TEST                               1000000
    
    SQL> @ test
    Geben Sie einen Wert für sample_pct ein: 10
    
    PL/SQL-Prozedur erfolgreich abgeschlossen.
    
    Abgelaufen: 00:00:00.90
    
    COLUMN_VALUE  FREQUENCY
    ------------ ----------
               0        964
               1        985
               2       1054
               3        984
               4        995
               5        956
               6        978
               7        986
               8       1032
               9       1003
             100      89937
    
    11 Zeilen ausgewählt.
    
    Abgelaufen: 00:00:00.00
    
    TABLE_NAME                     SAMPLE_SIZE
    ------------------------------ -----------
    TEST                                 99874
    
    SQL> @ test
    Geben Sie einen Wert für sample_pct ein: 1
    
    PL/SQL-Prozedur erfolgreich abgeschlossen.
    
    Abgelaufen: 00:00:00.48
    
    COLUMN_VALUE  FREQUENCY
    ------------ ----------
               0         88
               1        112
               2         98
               3         91
               4         91
               5        104
               6         94
               7        120
               8        102
               9        106
             100       9044
    
    11 Zeilen ausgewählt.
    
    Abgelaufen: 00:00:00.00
    
    TABLE_NAME                     SAMPLE_SIZE
    ------------------------------ -----------
    TEST                                 10050
    
    SQL> @ test
    Geben Sie einen Wert für sample_pct ein: dbms_stats.auto_sample_size
    
    PL/SQL-Prozedur erfolgreich abgeschlossen.
    
    Abgelaufen: 00:00:00.73
    
    COLUMN_VALUE  FREQUENCY
    ------------ ----------
               0         58
               1         61
               2         43
               3         55
               4         48
               5         55
               6         38
               7         47
               8         66
               9         51
             100       4925
    
    11 Zeilen ausgewählt.
    
    Abgelaufen: 00:00:00.00
    
    TABLE_NAME                     SAMPLE_SIZE
    ------------------------------ -----------
    TEST                                  5447
     

    Mit einem 100%-Sample kommt Jonathan Lewis' Analysequery also auf die gleichen Ergebnisse, die auch das GROUP BY Statement für das Auftreten der distinkten Werte liefert. Die anderen %-Samples liefern jeweils die erwarteten Prozentsätze. Mit der Angabe dbms_stats.auto_sample_size erhält man die Werte, die ohne Angabe von estimate_percent erscheinen - und das überrascht nicht, da diese Angabe laut Doku den internen Default darstellt.

    Nachtrag 14.04.2012: die Sample Size von ca. 5.500 Sätzen ist der (nicht ganz unproblematische) Standard-Wert für die Histogrammerstellung mit auto_sample_size, wozu ich zuletzt hier ein paar Details aufgeführt habe.

    Montag, Januar 10, 2011

    Histogramme - 3

    Nachdem ich zuletzt ein ganz harmloses Testbeispiel zur Darstellung der Struktur von Frequency Histogrammen definiert hatte, um dann festzustellen, dass das Beispiel ganz so harmlos nicht war - und sich ganz anders verhielt, als ich vorher vermutet hatte - suche ich weiter nach einem Beleg dafür, dass sich Histogramme in weniger extremen Fällen tatsächlich so verhalten, wie ich behautptet hatte:

    Zunächst lege ich eine Tabelle mit einem sehr häufigen Wert (100 mit 990.000 Sätzen) und 100 relativ seltenen Werten an, die jeweils 100 mal vorkommen.

     drop table test; 
    create table test
    as
    select case when rownum <= 10000 then mod(rownum, 100) else 100 end rn
         , lpad('*', 100, '*') pad
      from dual
    connect by level <= 1000000;
    
    exec DBMS_STATS.GATHER_TABLE_STATS(user, 'TEST', METHOD_OPT => 'FOR ALL COLUMNS SIZE 101')
    
    select column_name
         , NUM_BUCKETS
         , NUM_DISTINCT
         , HISTOGRAM
      from user_tab_columns
     where table_name = 'TEST'
       and column_name = 'RN'
    
    COLUMN_NAME                    NUM_BUCKETS NUM_DISTINCT HISTOGRAM
    ------------------------------ ----------- ------------ ----------
    RN                                      39          101 FREQUENCY
    

    Nun ja, näher an den Erwartungen, aber noch nicht ganz dran ... - wieso sind es nur 39 buckets, statt der 101, die ich verlangt hatte.

    Vielleicht ein Effekt der Sortierung der Daten? Immerhin dürften die Werte <> 100 ja auf die ersten 10.000 Sätze beschränkt sein. Deshalb werfe ich meine Testmenge mit dbms_random etwas durcheinander:

    drop table test;
    create table test
    as
    select case when rownum <= 10000 then mod(rownum, 100) else 100 end rn
         , lpad('*', 100, '*') pad
      from dual
    connect by level <= 1000000
     order by dbms_random.value;
    exec DBMS_STATS.GATHER_TABLE_STATS(user, 'TEST', METHOD_OPT => 'FOR ALL COLUMNS SIZE 101')
    
    select column_name
         , NUM_BUCKETS
         , NUM_DISTINCT
         , HISTOGRAM
      from user_tab_columns
     where table_name = 'TEST'
       and column_name = 'RN';
    
    COLUMN_NAME                    NUM_BUCKETS NUM_DISTINCT HISTOGRAM
    ------------------------------ ----------- ------------ ---------
    RN                                      42          101 FREQUENCY
    

    Näher dran, aber noch immer nicht das, was ich erwartet hatte. Vielleicht ist der Wert 100 immer noch zu predominant - immerhin betrifft er immer noch 99% der Werte. Gehen wir auf 90% herunter.

    create table test
    as
    select case when rownum <= 100000 then mod(rownum, 100) else 100 end rn
         , lpad('*', 100, '*') pad
      from dual
    connect by level <= 1000000;
    
    exec DBMS_STATS.GATHER_TABLE_STATS(user, 'TEST', METHOD_OPT => 'FOR ALL COLUMNS SIZE 101')
    
    select column_name
         , NUM_BUCKETS
         , NUM_DISTINCT
         , HISTOGRAM
      from user_tab_columns
     where table_name = 'TEST'
       and column_name = 'RN';
    
    COLUMN_NAME                    NUM_BUCKETS NUM_DISTINCT HISTOGRAM
    ------------------------------ ----------- ------------ ---------
    RN                                     100          101 FREQUENCY
    
    select column_name
         , ENDPOINT_VALUE
      from user_histograms
     where table_name = 'TEST'
       and column_name = 'RN'
     order by column_name;
    
    COLUMN_NAME                    ENDPOINT_VALUE
    ------------------------------ --------------
    RN                                          0
    RN                                          1
    RN                                          2
    RN                                          3
    RN                                          4
    RN                                          5
    RN                                          6
    RN                                          7
    RN                                          9
    RN                                         10
    RN                                         11
    ...
    RN                                         98
    RN                                         99
    RN                                        100
    
    100 Zeilen ausgewählt.
    

    Das wäre dann also - endlich - das erwartete Resultat. Anscheinend ist der von Tom Kyte angesprochene predominant value bei 90% der Gesamtsätze nicht mehr so übermächtig, dass er die bucket-Anzahl reduzieren würde. Für heute reicht mir das.

    Samstag, Januar 08, 2011

    Histogramme - 2

    Das Schöne an der praktischen Überprüfung von Annahmen ist, dass man schnell erkennen kann, wenn man sich gründlich täuscht. Ich hatte angenommen, dass ein frequency histogram einen bucket für jeden distinkten Wert enthalten würde - aber für meinen Testfall ergaben sich ganz andere Ergebnisse:

    exec DBMS_STATS.GATHER_TABLE_STATS(user, 'TEST', METHOD_OPT => 'FOR ALL COLUMNS SIZE 100')
    
    select column_name
         , ENDPOINT_VALUE
      from user_histograms
     where table_name = 'TEST'
       and column_name = 'RN'
     order by column_name;
    
    COLUMN_NAME                    ENDPOINT_VALUE
    ------------------------------ --------------
    RN                                         36
    RN                                        100
    
    select column_name
         , NUM_BUCKETS
         , NUM_DISTINCT
         , HISTOGRAM
      from user_tab_columns
     where table_name = 'TEST'
       and column_name = 'RN'
    
    COLUMN_NAME                              NUM_BUCKETS NUM_DISTINCT HISTOGRAM
    ---------------------------------------- ----------- ------------ ---------
    RN                                                 2          100 FREQUENCY
    

    Also nur zwei Buckets trotz 100 distinkten Werten. Noch lustiger wird der Fall, wenn ich die Statistikerzeugung noch einmal durchführe:

    exec DBMS_STATS.GATHER_TABLE_STATS(user, 'TEST', METHOD_OPT => 'FOR ALL COLUMNS SIZE 100')
    
    select column_name
         , ENDPOINT_VALUE
      from user_histograms
     where table_name = 'TEST'
       and column_name = 'RN'
     order by column_name;
    
    COLUMN_NAME                              ENDPOINT_VALUE
    ---------------------------------------- --------------
    RN                                                  100
    
    select column_name
         , NUM_BUCKETS
         , NUM_DISTINCT
         , HISTOGRAM
      from user_tab_columns
     where table_name = 'TEST'
       and column_name = 'RN';
    
    COLUMN_NAME                              NUM_BUCKETS NUM_DISTINCT HISTOGRAM
    ---------------------------------------- ----------- ------------ ---------
    RN                                                 1          100 FREQUENCY
    

    Diesmal wurde also nur ein bucket erzeugt - das Verhalten ist also nicht deterministisch! Um den Fall noch obskurer zu machen, lieferte mir Autotrace anschließend nur noch absurde Ergebnisse, die darauf hindeuteten, dass die Entscheidung für Index-Zugriff oder FTS nun völlig willkürlich erfolgte. An dieser Stelle konnte dann SQL_TRACE zumindest belegen, dass AUTOTRACE Unfug erzählte (was ja gelegentlich vorkommt). Außerdem sahen die Zugriffspläne in SQL_TRACE jetzt so aus, als wären funktionstüchtige Histogramme im Spiel:

    select /* test */ count(pad) 
    from
     test where rn = 1
    
    
    call     count       cpu    elapsed       disk      query    current        rows
    ------- ------  -------- ---------- ---------- ---------- ----------  ----------
    Parse        1      0.00       0.02          0          0          0           0
    Execute      1      0.00       0.04          0          0          0           0
    Fetch        2      0.00       0.00          0          3          0           1
    ------- ------  -------- ---------- ---------- ---------- ----------  ----------
    total        4      0.00       0.06          0          3          0           1
    
    Misses in library cache during parse: 1
    Optimizer mode: ALL_ROWS
    Parsing user id: 174  
    
    Rows     Row Source Operation
    -------  ---------------------------------------------------
          1  SORT AGGREGATE (cr=3 pr=0 pw=0 time=0 us)
          1   TABLE ACCESS BY INDEX ROWID TEST (cr=3 pr=0 pw=0 time=0 us cost=2 size=104 card=1)
          1    INDEX RANGE SCAN TEST_IDX (cr=2 pr=0 pw=0 time=0 us cost=1 size=0 card=1)(object id 202123)
    
    select /* test */ count(pad) 
    from
     test where rn = 10
    
    
    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        2      0.00       0.00          0          3          0           1
    ------- ------  -------- ---------- ---------- ---------- ----------  ----------
    total        4      0.00       0.00          0          3          0           1
    
    Misses in library cache during parse: 1
    Optimizer mode: ALL_ROWS
    Parsing user id: 174  
    
    Rows     Row Source Operation
    -------  ---------------------------------------------------
          1  SORT AGGREGATE (cr=3 pr=0 pw=0 time=0 us)
          1   TABLE ACCESS BY INDEX ROWID TEST (cr=3 pr=0 pw=0 time=0 us cost=2 size=104 card=1)
          1    INDEX RANGE SCAN TEST_IDX (cr=2 pr=0 pw=0 time=0 us cost=1 size=0 card=1)(object id 202123)
    
    select /* test */ count(pad) 
    from
     test where rn = 100
    
    
    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        2      0.22       0.23         15       7474          0           1
    ------- ------  -------- ---------- ---------- ---------- ----------  ----------
    total        4      0.22       0.23         15       7474          0           1
    
    Misses in library cache during parse: 1
    Optimizer mode: ALL_ROWS
    Parsing user id: 174  
    
    Rows     Row Source Operation
    -------  ---------------------------------------------------
          1  SORT AGGREGATE (cr=7474 pr=15 pw=0 time=0 us)
     999901   TABLE ACCESS FULL TEST (cr=7474 pr=15 pw=0 time=0 us cost=2285 size=103971816 card=999729)
    

    card=1 für die Fälle 1 und 10 und card=999729 für den Wert 100 - also fast völlig zutreffende Annahmen. Aber warum sehen die Histogramme in user_tab_columns und user_histograms so ganz anders aus, als ich erwartet hatte? Die Erklärung lieferte dann mal wieder Tom Kyte:
    This happens when you have some values that utterly dominate the other values - as you do - that one really high value can be used to infer the other buckets.
    Das Vorliegen eines extrem häufigen Werts (hier die 100) bringt dbms_stats also zu einer völlig anderen Strategie: statt eines buckets pro Wert wird jetzt insgesamt nur ein bucket (oder zwei buckets) angelegt, aber offenbar genügt diese Information, um zu sinnvollen Zugriffen zu kommen. Wenn kein "predominant value" exisitiert, werden die Histogramme übrigens so erzeugt, wie ich es grundsätzlich erwartet hatte - aber das ist eine andere Geschichte, die ein andermal erzählt werden soll...