Mittwoch, März 21, 2012

Extended Statistics und column usage

Dieser Tage beschäftige ich mich etwas intensiver mit extended statistics (zu denen ich gelegentlich schon ein paar Links aufgeführt habe). Und weil sie dabei nicht unbedingt genau so reagieren, wie ich mir das vorstelle, notiere ich hier mal ein paar Beobachtungen.

Zunächst lege ich eine Tabelle mit korrelierten Spaltenwerten an:

create table test_extended_stats
as
select rownum id
     , mod(rownum, 100) col1
     , mod(rownum, 100) col2
  from dual
connect by level <= 100000;

exec dbms_stats.gather_table_stats(user, 'test_extended_stats')

Die Werte für col1 und col2 sind vollständig korreliert, was der cbo natürlich nicht wissen kann. Deshalb kann die cardinality für die Einzelwerte akkurat geschätzt werden - aber nicht für die Kombination der Werte:

set autot trace
select count(*) from test_extended_stats where col1 = 1;

  COUNT(*)
----------
      1000

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

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

   2 - filter("COL1"=1)

select count(*) from test_extended_stats where col2 = 1;

  COUNT(*)
----------
      1000

------------------------------------------------------------------------------------------
| Id  | Operation          | Name                | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                     |     1 |     3 |    66   (4)| 00:00:01 |
|   1 |  SORT AGGREGATE    |                     |     1 |     3 |            |          |
|*  2 |   TABLE ACCESS FULL| TEST_EXTENDED_STATS |  1000 |  3000 |    66   (4)| 00:00:01 |
------------------------------------------------------------------------------------------

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

   2 - filter("COL2"=1)

select count(*) from test_extended_stats where col1 = 1 and col2 = 1;

  COUNT(*)
----------
      1000

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

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

   2 - filter("COL1"=1 AND "COL2"=1)

Wie erwartet erkennt der cbo, dass jeder Wert für col1 und col2 jeweils 1000 mal erscheint: die Selektivität ist also 1000/100000 = 0,01 und die cardinality folglich 100000 * 0,01 = 1000. Für die Kombination der Spalten werden die Wahrscheinlichkeiten dann als unabhängig betrachtet und es ergibt sich für die cardinality: 100000 * 0,01 * 0,01 = 10. Und das ist dann natürlich falsch. Das ist ein idealtypischer Anwendungsfall für extended statistics, die solche Korrelationseffekte über die interne Anlage einer virtuellen Spalte abbilden:

select dbms_stats.create_extended_stats(null, 'TEST_EXTENDED_STATS', '(col1, col2)') from dual;

DBMS_STATS.CREATE_EXTENDED_STATS(NULL,'TEST_EXTENDED_STATS','(COL1,COL2)')
--------------------------------------------------------------------------
SYS_STUFLHATC5RBD6JHJZWT$X2AAH

select count(*) from test_extended_stats where col1 = 1 and col2 = 1;

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

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

   2 - filter("COL1"=1 AND "COL2"=1)

Das funktioniert also zunächst noch nicht, was erst einmal daran liegt, dass für die neue virtuelle Spalte noch keine Statistiken existieren:

select column_name
     , sample_size
     , num_distinct
     , last_analyzed
     , histogram 
  from dba_tab_cols
 where table_name = 'TEST_EXTENDED_STATS'
 order by column_name;

COLUMN_NAME                              SAMPLE_SIZE NUM_DISTINCT LAST_ANALY HISTOGRAM
---------------------------------------- ----------- ------------ ---------- ---------
COL1                                          100000          100 21.03.2012 NONE
COL2                                          100000          100 21.03.2012 NONE
ID                                            100000       100000 21.03.2012 NONE
SYS_STUFLHATC5RBD6JHJZWT$X2AAH                                               NONE

Dann eben ein weiterer dbms_stats-Aufruf:

exec dbms_stats.gather_table_stats(user, 'test_extended_stats')

COLUMN_NAME                              SAMPLE_SIZE NUM_DISTINCT LAST_ANALY HISTOGRAM
---------------------------------------- ----------- ------------ ---------- ---------
COL1                                            5498          100 21.03.2012 FREQUENCY
COL2                                            5498          100 21.03.2012 FREQUENCY
ID                                            100000       100000 21.03.2012 NONE
SYS_STUFLHATC5RBD6JHJZWT$X2AAH                100000          100 21.03.2012 NONE

select count(*) from test_extended_stats where col1 = 1 and col2 = 1;

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

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

   2 - filter("COL1"=1 AND "COL2"=1)

Und schon ist die cardinality korrekt. Ein paar Aspekte sind für mich bemerkenswert. Zunächst die sample_size, die gewissen Schwankungen zu unterliegen scheint: ursprünglich wurden für col1 und col2 alle 100.000 Sätze betrachtet, nach der Anlage der extended statistics aber nur noch ca. 5% davon. Da keine Anpassungen durchgeführt wurden, dürfte hier für estimate_percent der default-Wert vorliegen und laut Dokumentation ist das auto_sample_size:

select dbms_stats.get_param('ESTIMATE_PERCENT') from dual;

DBMS_STATS.GET_PARAM('ESTIMATE_PERCENT')
---------------------------------------------------------------
DBMS_STATS.AUTO_SAMPLE_SIZE

Abgesehen von Greg Rahns Plädoyer für die Verwendung der auto_sample_size, die eine schnelle und exakte Ermittlungen der Statistiken gewährleiste, habe ich nicht allzu viele Aussagen zum Verhalten dieser Option gefunden. Zumindest scheint sie auf Basis der gleichen Daten nicht unbedingt die gleiche sample-Größe hervorzurufen - und ich habe zuletzt ein paar undurchsichtige Fälle beobachtet, in denen ein gather_table_stats mit dem default-sample keine geeignete Statistikbasis lieferte. Aber diese Beobachtung bleibt erst mal anekdotisch, da ich sie bislang nicht in einem Test nachstellen kann. Eine andere Frage ist, warum im gegebenen Beispiel nur für col1 und col2 ein Histogramm erzeugt wurde, nicht aber für die virtuelle Spalte SYS_STUFLHATC5RBD6JHJZWT$X2AAH. Grundlage für die Anlage von Histogrammen ist dabei die Verwendung der Spalten in WHERE-Bedingungen, die in sys.column_usage$ protokolliert wird (leider gibt es dazu offenbar keine externalisierte data dictionary View, wie man in Christian Antogninis Troubleshooting Oracle Performance, S. 145ff. nachlesen kann). Ein Blick in col_usage$ zeigt, dass dort tatsächlich nur col1 und col2 aufgeführt sind:

-- die Mischung der sys-Objekte mit dba_objects ist dabei meiner Faulheit geschuldet, 
-- sollte am Ergebnis aber nichts ändern ...
select c.name column_name
     , u.equality_preds
     , u.timestamp
  from sys.col_usage$ u
     , sys.col$ c
 where c.obj# = u.obj#
   and c.intcol# = u.intcol#
   and u.obj# in (select object_id 
                    from dba_objects
                   where object_name = 'TEST_EXTENDED_STATS');

COLUMN_NAME                    EQUALITY_PREDS TIMESTAMP
------------------------------ -------------- ----------
COL1                                        1 21.03.2012
COL2                                        1 21.03.2012

Anscheinend läuft die Aktualisierung von col_usage$ mit einer gewissen Verzögerung, denn es dauerte in meinem Test ein paar Minuten, ehe dort Einträge erschienen. Ein dbms_stats-Aufruf führt aber offenbar zur unmittelbaren Aktualisierung der col_usage$-Angaben. Aber warum wird in col_usage$ keine Verwendung für die virtuelle Spalte SYS_STUFLHATC5RBD6JHJZWT$X2AAH protokolliert? Dazu noch mal ein etwas größeres Test-Script:

REM test_col_usage.sql

-- Tabelle löschen
drop table test_extended_stats;

-- Tabelle neu anlegen
create table test_extended_stats
as
select rownum id
     , mod(rownum, 100) col1
     , mod(rownum, 100) col2
  from dual
connect by level <= 100000;

-- extended statistics anlegen
select dbms_stats.create_extended_stats(null, 'TEST_EXTENDED_STATS', '(col1, col2)') from dual;

-- Statistiken nach Anlage der virtuellen Spalte erfassen
-- in Lauf 1 auskommentiert, in Lauf 2 aktiv
-- exec dbms_stats.gather_table_stats(user, 'test_extended_stats')

-- Query mit Einschränkung auf col1 und col2 ausführen
select count(*) from test_extended_stats where col1 = 1 and col2 = 1;

-- dba_tab_cols vor dem dbms_stats-Aufruf
select column_name
     , sample_size
     , num_distinct
     , last_analyzed
     , histogram 
  from dba_tab_cols
 where table_name = 'TEST_EXTENDED_STATS'
 order by column_name;

-- Statistiken erzeugen 
exec dbms_stats.gather_table_stats(user, 'test_extended_stats')

-- column usage
select c.name column_name
     , u.equality_preds
     , u.timestamp
  from sys.col_usage$ u
     , sys.col$ c
 where c.obj# = u.obj#
   and c.intcol# = u.intcol#
   and u.obj# in (select object_id from dba_objects where object_name = 'TEST_EXTENDED_STATS');

-- dba_tab_cols nach dem dbms_stats-Aufruf
select column_name
     , sample_size
     , num_distinct
     , last_analyzed
     , histogram
  from dba_tab_cols
 where table_name = 'TEST_EXTENDED_STATS'
 order by column_name;

Lauf 1 bringt folgende Ausgabe:

SQL> @ C:\temp\test_col_usage.sql

Tabelle wurde gelöscht.

Tabelle wurde erstellt.

DBMS_STATS.CREATE_EXTENDED_STATS(NULL,'TEST_EXTENDED_STATS','(COL1,COL2)')
----------------------------------------------------------------------------------
SYS_STUFLHATC5RBD6JHJZWT$X2AAH

  COUNT(*)
----------
      1000

COLUMN_NAME                    SAMPLE_SIZE NUM_DISTINCT LAST_ANALY HISTOGRAM
------------------------------ ----------- ------------ ---------- ---------------
COL1                                                               NONE
COL2                                                               NONE
ID                                                                 NONE
SYS_STUFLHATC5RBD6JHJZWT$X2AAH                                     NONE

PL/SQL-Prozedur erfolgreich abgeschlossen.

COLUMN_NAME                    EQUALITY_PREDS TIMESTAMP
------------------------------ -------------- ----------
COL1                                        1 21.03.2012
COL2                                        1 21.03.2012

COLUMN_NAME                    SAMPLE_SIZE NUM_DISTINCT LAST_ANALY HISTOGRAM
------------------------------ ----------- ------------ ---------- ---------------
COL1                                  5620          100 21.03.2012 FREQUENCY
COL2                                  5620          100 21.03.2012 FREQUENCY
ID                                  100000       100000 21.03.2012 NONE
SYS_STUFLHATC5RBD6JHJZWT$X2AAH      100000          100 21.03.2012 NONE

Für Lauf 2 hingegen mit dem unmittelbar nach der Anlage der extended statistics durchgeführten dbms_stats-Aufruf erscheint auch die virtuelle Spalte SYS_STUFLHATC5RBD6JHJZWT$X2AAH in col_usage$ und folglich wird auch ein Histogramm erzeugt:

SQL> @ C:\temp\test_col_usage.sql

Tabelle wurde gelöscht.

Tabelle wurde erstellt.

DBMS_STATS.CREATE_EXTENDED_STATS(NULL,'TEST_EXTENDED_STATS','(COL1,COL2)')
----------------------------------------------------------------------------------
SYS_STUFLHATC5RBD6JHJZWT$X2AAH

PL/SQL-Prozedur erfolgreich abgeschlossen.

  COUNT(*)
----------
      1000

COLUMN_NAME                    SAMPLE_SIZE NUM_DISTINCT LAST_ANALY HISTOGRAM
------------------------------ ----------- ------------ ---------- ---------------
COL1                                100000          100 21.03.2012 NONE
COL2                                100000          100 21.03.2012 NONE
ID                                  100000       100000 21.03.2012 NONE
SYS_STUFLHATC5RBD6JHJZWT$X2AAH      100000          100 21.03.2012 NONE

PL/SQL-Prozedur erfolgreich abgeschlossen.

COLUMN_NAME                    EQUALITY_PREDS TIMESTAMP
------------------------------ -------------- ----------
COL1                                        1 21.03.2012
COL2                                        1 21.03.2012
SYS_STUFLHATC5RBD6JHJZWT$X2AAH              1 21.03.2012

COLUMN_NAME                    SAMPLE_SIZE NUM_DISTINCT LAST_ANALY HISTOGRAM
------------------------------ ----------- ------------ ---------- ---------------
COL1                                  5591          100 21.03.2012 FREQUENCY
COL2                                  5591          100 21.03.2012 FREQUENCY
ID                                  100000       100000 21.03.2012 NONE
SYS_STUFLHATC5RBD6JHJZWT$X2AAH        5591          100 21.03.2012 FREQUENCY

Das Ergebnis lässt sich regelmäßig reproduzieren, aber eine schlüssige Erklärung dafür fehlt mir noch.

Kommentare:

  1. Hallo Martin,

    ein paar Kommentare:

    1. Du kannst die Extended Statistics direkt in DBMS_STATS.GATHER_TABLE_STATS einbauen im METHOD_OPT-Parameter, also in Deinem Falle:

    ... METHOD_OPT => 'FOR ALL COLUMNS SIZE AUTO FOR COLUMNS (COL1, COL2) SIZE AUTO'...

    Das spart den extra Aufruf für das Generieren der Extended Statistics und verhindert das Problem mit nicht existierenden Statistiken

    2. Die 5% sind ein Problem, auf das ich schon verschiedentlich hingewiesen habe und haben nichts mit den Extended Statistics zu tun, sondern mit der Tatsache, dass ein Histogramm erstellt wird.

    Bei der Verwendung von AUTO_SAMPLE_SIZE verwendet 11g ja den neuen NDV APPROXIMATE-Algorithmus, der grundsätzlich alle Daten liest (100%), aber nicht den Overhead der Aggregation mit sich bringt.

    Das gibt also sehr akkurate Statistiken in kurzer Zeit.

    Wenn aber ein Histogramm erstellt wird, verwendet AUTO_SAMPLE_SIZE für den zusätzlichen Pass zum Erstellen des Histogramms nur noch ca. 5500 Zeilen, also je nach Tabellengröße ein sehr kleines Sampling. Das führt dazu, dass die Histogramme doch recht ungenau sein können und wichtige Informationen "übersehen", und auch zu so merkwürdigen Effekten, dass in der Spalten-Statistiken z.B. 3 DISTINCT values stehen, das Histogramm aber nur einen Wert beinhaltet, also im Grunde inkonsistente Informationen. Oracle versucht hier m.E. zu sehr die Performance der Statistik-Erstellung in den Vordergrund zu stellen. Mir wären höherwertige Histogramme lieber.

    Leider führt der einzige Workaround für höherwertige Histogramme nur über ein ESTIMATE_PERCENT ungleich AUTO_SAMPLE_SIZE, und das schaltet dann den neuen NDX APPROXIMATE Algorithmus wieder ab...

    Insofern kann es Sinn machen, die Statistiken in zwei Schritten zu erstellen: Für alle Spalten ohne Histogramme per AUTO_SAMPLE_SIZE, und ein zweites Mal nur für die Spalten mit Histogrammen mit explizitem ESTIMATE_PERCENT.

    Das ist natürlich auch nicht optimal, da dann SIZE AUTO keinen Sinn mehr macht (man muss dann schon wissen, welche Spalten Histogramme haben sollen), und das Segment muss mindestens zweimal gelesen werden...

    3. Vielen Dank für den Hinweis mit dem fehlenden Spalten-Monitoring für die erweiterten Statistiken, das ist mir bisher nicht aufgefallen.

    Randolf

    AntwortenLöschen
  2. Hallo Randolf,

    vielen Dank für die Hinweise. Nr. 1 macht die Anlage deutlich übersichtlicher und Nr. 2 erklärt einiges.

    Martin

    AntwortenLöschen