Montag, Dezember 31, 2012

Platzangaben im Block

Jonathan Lewis hat dieser Tage eine Quiz-Frage formuliert, bei der es darum ging, wie es dazu kommen kann, dass der verfügbare Speicherplatz im (Daten-)Block einer Heap-Tabelle (angegeben als avsp – available space bzw. tosp – total space; der Unterschied der beiden Angaben wird in einer Fußnote erklärt - man findet eine ähnliche Erklärung aber auch schon in einer Erläuterung von Steve Adams vom Dezember 2000; ungefähr zu dieser Zeit habe ich zum ersten Mal eine Oracle-Datenbank gesehen) deutlich kleiner ist als der freie Bereich in der Blockmitte, der durch die Angaben fsbo (free space, beginning of: das Ende des block headers) und fseo (free space, end of: der Beginn der row Einträge am Ende des Blocks) bestimmt ist. Die Antwort darauf lautet: Oracle benötigt für jeden Satz mindestens 11 byte (2 im row directory und 9 im row heap). Wenn ein Datensatz diese Größe unterschreitet, dann wird die Differenz für den Fall späterer row migration reserviert. Interessant ist auch Randolf Geists Hinweis auf die Rolle von direct path/conventional path beim Insert.

Donnerstag, Dezember 27, 2012

Rowsource Statistics für SQL Operationen

Alexander Anokhin hat dieser Tage zwei sehr interessante Artikel zum Thema der Rowsource Statistics veröffentlicht, also jener detaillierten Informationen zur Laufzeit und Ressourcennutzung von Ausführungsschritten im Execution Plan, deren Erfassung über den Hint GATHER_PLAN_STATISTICS, den Parameter STATISTICS_LEVEL oder SQL-Trace aktiviert werden kann:

Teil 1: Timing: query execution statistics (rowsource statistics). Part 1: How it works
  • Aktivierung der Erfassung von Rowsource Statistics
  • Darstellung der Ergebnisse mit Hilfe von DBMS_XPLAN.DISPLAY_CURSOR, das allerdings nur die Optionen ALL und LAST anbietet, also Durchschnittswerte und die Ergebnisse der letzten Ausführung.
  • Darstellung der internen Funktionsaufrufe mit Anokhins DIGGER-Tool, das zeigt, dass alle Aufrufe in einer Snapshot-Routine gewrapped sind (qerstFetch mit: qerstSnapStats und qerstUpdateStats)
  • die Funktionen werden für jede einzelne Ergebniszeile aufgerufen, was dann schon zum zweiten Artikel überleitet, der Aussagen zur Genauigkeit der Ergebnisse und zum Performance-Overhead liefert.
  • die Häufigkeit der Timestamp-Aufrufe wird über der Parameter _rowsource_statistics_sampfreq (0 = keine Statistik, 1 = Timing für jeden Aufruf von qerstSnapStats()/qerstUpdateStats, N = jeder N-te Aufruf wird protokolliert) gesteuert. Wenn der Parameter nur ein Sample der Aufrufe protokolliert, können die Ergebnisse unpräzise werden, da dann ein (hoffentlich) repräsentativer Wert für N Ausführungen eingesetzt wird.
  • Andererseits führt das Timing für jeden Aufruf zum höchsten Overhead.
  • mehrere Tabellen zeigen, dass der Overhead der Rowsource Statistics mit niedrigerer _rowsource_statistics_sampfreq signifikant wächst. 
  • die Größe des Overheads hängt ab von:
    • Anzahl der Time-Calls, die wiederum von der Set-Größe und dem Sampling abhängt
    • Implementierung der Funktion zur Ermittlung der Timestamps, die je nach OS unterschiedlich performant ist
    • genaue Ausführung innerhalb eines OS (denn auch dort gibt's unterschiedliche Varianten, was anhand von Linux vorgeführt wird)
  • interessant ist auch noch die ausführliche Antwort auf einen Kommentar von Nikolay Savvinov
Nachtrag 08.01.2012: hier noch ein Link auf Randolf Geists Beobachtung, dass row source statistics sampling die Verwendung von Vector bzw. Batched I/O deaktiviert (was in der Nested Loops Optimierung in 11g eine Rolle spielt).

Sonntag, Dezember 23, 2012

String Aggregation in Oracle

Philipp Salvisberg von Trivadis hat eine schöne Zusammenstellung diverser Optionen zur Zusammenfassung von String-Werten in einer konkatenierten Liste veröffentlicht - also jener Anforderung, für die Tom Kyte vor vielen Jahren die STRAGG-Funktion lieferte: der Verknüpfung der String-Werte einer Gruppe (etwa der Mitarbeiter eines Departments in der EMP-Tabelle) in einer Komma-separierten Liste. In dieser Zusammenstellung erscheinen verschiedene PL/SQL-Versionen, user-defined aggregate functions (des ODCIAggregate interface), XML-Varianten und schließlich die LISTAGG-Aggregat-Funktion aus 11.2, jeweils mit einer Angabe ihrer Verfügbarkeit in den Oracle-Releases und einem Performance-Vergleich (bei dem die XML-Lösungen schlecht und LISTAGG am besten abschneidet).

Eine ähnliche Zusammenstellungen solcher String-Aggregationsfunktionen findet man auch bei Tim Hall, der außerdem noch die (undokumentierte) WM_CONCAT Funktion erwähnt und darüber hinaus auf eine von William Robertson vorgeschlagene Variante mit hierarchischen Queries und auf die Collect-Funktion, die Adrian Billington gelegentlich genauer erläutert hat, verweist.

Freitag, Dezember 21, 2012

TEMP usage Angabe in ASH

Eine kurze Notiz, da ich in letzter Zeit häufiger nach historischen Informationen zur Ressourcennutzung gesucht habe, die es (noch) nicht in ASH bzw. AWR gab: Marc Fielding weist darauf hin, dass DBA_HIST_ACTIVE_SESS_HISTORY ab 11.2 eine Spalte TEMP_SPACE_ALLOCATED enthält ("Amount of TEMP memory (in bytes) consumed by this session at the time this sample was taken"). Das sollte die nachträgliche Analyse entsprechender Probleme massiv erleichtern.

Costing für ROWNUM-Queries

Mit dem folgenden Eintrag bin ich eher weniger glücklich, weil es mir nicht gelungen ist, das beobachtete Verhalten in einem Test nachzustellen. Leider habe ich zur Zeit auch nur den Execution Plan, aber keinen Zugriff auf das System, in dem ich das Verhalten beobachtet habe. Aber vielleicht sollte ich erst mal erklären, wovon ich überhaupt rede: dieser Tage wurde mir eine Query mit einer Einschränkung WHERE rownum < 10 gezeigt, bei der der CBO in 11.1.0.7 ganz offenbar zu einer recht abwegigen Ausführungsstrategie gelangt war:

select *
  from view_xyz
 where rownum < 10;
-- dazu der Plan (anonymisiert und ohne Time-Angabe)
-----------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name    | Rows | Bytes | Cost (%CPU)| Pstart| Pstop |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |         |    9 |  1440 |    36   (3)|       |       |
|   1 |  PARTITION LIST SINGLE                |         |    2 |    52 |  9534   (1)|     1 |     1 |
|*  2 |   TABLE ACCESS FULL                   | DIM1    |    2 |    52 |  9534   (1)|     1 |     1 |
|*  3 |  COUNT STOPKEY                        |         |      |       |            |       |       |
|   4 |   NESTED LOOPS OUTER                  |         |   10 |  1600 |    36   (3)|       |       |
|   5 |    NESTED LOOPS OUTER                 |         |    5 |   765 |    32   (4)|       |       |
|   6 |     NESTED LOOPS OUTER                |         |    5 |   610 |    17   (6)|       |       |
|*  7 |      HASH JOIN OUTER                  |         |    5 |   555 |     7  (15)|       |       |
|   8 |       PARTITION LIST ALL              |         |    5 |   505 |     2   (0)|     1 |  1523 |
|   9 |        TABLE ACCESS FULL              | FAKT1   |    5 |   505 |     2   (0)|     1 |  1523 |
|  10 |       TABLE ACCESS FULL               | DIM2    |  171 |  1710 |     4   (0)|       |       |
|  11 |      TABLE ACCESS BY INDEX ROWID      | DIM3    |    1 |    11 |     2   (0)|       |       |
|* 12 |       INDEX RANGE SCAN                | DIM3_IX |    1 |       |     1   (0)|       |       |
|  13 |     PARTITION LIST SINGLE             |         |    1 |    31 |     3   (0)|     1 |     1 |
|  14 |      TABLE ACCESS BY LOCAL INDEX ROWID| DIM4    |    1 |    31 |     3   (0)|     1 |     1 |
|* 15 |       INDEX RANGE SCAN                | DIM4_IX |    1 |       |     2   (0)|     1 |     1 |
|  16 |    VIEW                               |         |    2 |    14 |     1   (0)|       |       |
|  17 |     CONCATENATION                     |         |      |       |            |       |       |
|  18 |      TABLE ACCESS BY INDEX ROWID      | DIM5    |    1 |    19 |     2   (0)|       |       |
|* 19 |       INDEX RANGE SCAN                | DIM5_IX |    1 |       |     1   (0)|       |       |
|  20 |      TABLE ACCESS BY INDEX ROWID      | DIM5    |    1 |    19 |     2   (0)|       |       |
|* 21 |       INDEX UNIQUE SCAN               | DIM5_IX |    1 |       |     1   (0)|       |       |
-----------------------------------------------------------------------------------------------------

Das entscheidende Problem dabei ist der Zugriff auf die Faktentabelle FAKT1 in step 8 und 9: die Tabelle wird ohne jede Einschränkung als BUILD table für den HASH JOIN OUTER verwendet, muss also komplett in den Speicher gelesen werden, ehe der Zugriff auf die Dimensionstabelle DIM2 erfolgt. Da die Faktentabelle 1523 Partitionen umfasst und mehr als 140M rows enthält, kann man auf die gewünschten 9 Ergebniszeilen lange warten. Dabei ergibt sich die cardinality 5 offenbar aus der halbierten Rownum-Einschränkung (9 geteilt durch die 2 aus step 16). Anscheinend vergisst der CBO in diesem Fall an irgendeiner Stelle, dass die rownum-Einschränkung im Fall eines HASH JOINs, bei dem die komplette Faktentabelle (PARTITION LIST ALL) in den Speicher gelesen werden muss, ehe der folgende Operation beginnen kann, nicht wirklich viel bringt.

Ein interessanter Fall, aber leider einer, den ich nicht reproduzieren konnte. Möglicherweise kann ich gelegentlich noch mal einen Blick auf das fragliche System werfen. Bis dahin bleibt's erst mal eine Geschichte mit offenem Schluss.

Sonntag, Dezember 16, 2012

SSAS: Test und Konfiguration

Christian Bracchi hat dieser Tage zwei interessante Artikel zum Thema SSAS veröffentlicht:
  • Analysis Services Testing Best Practices: mit einem plausiblen Vorschlag zur Durchführung von Performance-Tests für SSAS-Installationen (welcher concurrency Grad? Welches Test-Query-Tool? Welche Perfmon-Counter? Und abschließend der wichtige Hinweis: "BACKUP your test result!").
  • SQL 2012 Configuration Tuning: mit Informationen zur Wahl sinnvoller Konfigurationseinstellungen für relativ große Server mit NUMA. Interessant sind auch die Links auf weiterführende Artikel.

Freitag, Dezember 14, 2012

Cost für Group By

Alexandr Antonov weist in seinem Blog auf eine Änderung des Costings für GROUP BY Operationen in 11g hin und nennt dafür folgende Formel:

GROUP BY CARD = JOIN CARD * SEL(t1.fil1) / SEL(t2.fil2) 

Ich habe daraufhin noch mal darüber nachgedacht, wie die Formel für das Costing von GROUP BY vorher ausgesehen haben könnte und, nachdem mir dazu nicht viel eingefallen war, noch mal in Jonathan Lewis' Cost Based Oracle (S. 388) nachgeschlagen und dort folgende Erklärung gefunden:
In general, the optimizer estimates the number of distinct combinations of N columns by multiplying the individual num_distinct values, and then dividing by the square root of 2 (N-1) times.
Dazu ein kleiner Test mit 11.1.0.7:

drop table t3;
create table t3
as
select mod(rownum, 10) col1
     , mod(rownum, 20) col2
  from dual
connect by level <= 10000;

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

ALTER SESSION SET EVENTS '10053 trace name context forever, level 1';

select col1, col2, count(*)
  from t3
 group by col1, col2;

ALTER SESSION SET EVENTS '10053 trace name context OFF';

Im erzeugten CBO-Trace sieht man dann unter anderem Folgendes:

Access path analysis for T3
***************************************
SINGLE TABLE ACCESS PATH 
  Single Table Cardinality Estimation for T3[T3] 
  Table: T3  Alias: T3
    Card: Original: 10000.000000  Rounded: 10000  Computed: 10000.00  Non Adjusted: 10000.00
  Access Path: TableScan
    Cost:  7.11  Resp: 7.11  Degree: 0
      Cost_io: 7.00  Cost_cpu: 1842429
      Resp_io: 7.00  Resp_cpu: 1842429
  Best:: AccessPath: TableScan
         Cost: 7.11  Degree: 1  Resp: 7.11  Card: 10000.00  Bytes: 0

Grouping column cardinality [      COL1]    10
Grouping column cardinality [      COL2]    20
***************************************

OPTIMIZER STATISTICS AND COMPUTATIONS
***************************************
GENERAL PLANS
***************************************
Considering cardinality-based initial join order.
Permutations for Starting Table :0
Join order[1]:  T3[T3]#0
GROUP BY sort

GROUP BY adjustment factor: 0.707107
GROUP BY cardinality:  142.000000, TABLE cardinality:  10000.000000

Diese Angaben deuten an, dass die Aussagen aus Cost-Based Oracle grundsätzlich noch zutreffen: die GROUP BY cardinality ergibt sich als: (10 * 20)/1,4142 = 141,42. Was einerseits zur 142 im Trace passt und andererseits zeigt, dass mein Beispiel etwas zu symmetrisch ausgefallen ist (weil 2 durch die square root von 2 natürlich wieder square root von 2 ergibt). Der GROUP BY adjustment factor ist dabei anscheinend einfach 1,4142/2 = 0,7071 - was wiederum zeigt, dass meine Beispielwerte eher unglücklich gewählt sind, denn der Wert hängt nicht von den distinkten Werten ab, sondern von der Anzahl der GROUP BY Spalten:
  • 2 Spalten: GROUP BY adjustment factor: 0,7071 = 1,4142/2
  • 3 Spalten: GROUP BY adjustment factor: 0,5 = 1,4142/1,4142/2
  • 4 Spalten: GROUP BY adjustment factor: 0,3535 = 1,4142/1,4142/1,4142/2
Das ist nicht uninteressant, hat aber erst einmal noch nicht allzu viel mit den Ausführungen des Herrn Antonov zu tun. Daher noch ein Blick in die CBO-Traces für das Antonov'sche Beispiel. Dort findet man für den Fall der Verwendung des neuen Verfahrens eine andere GROUP BY cardinality als für den Fall der Verwendung des alten Verfahrens (_optimizer_improve_selectivity => false):

-- default Verhalten
GROUP BY adjustment factor: 1.000000
GROUP BY cardinality:  125.000000, TABLE cardinality:  500.000000
-- _optimizer_improve_selectivity => false
GROUP BY adjustment factor: 1.000000
GROUP BY cardinality:  500.000000, TABLE cardinality:  500.000000

Da nur eine Spalte im GROUP BY erscheint, überrascht der GROUP BY adjustment factor 1 nicht. Die unterschiedlichen GROUP BY cardinality-Angaben sind, so weit ich sehe, schon die einzigen (relevanten) Unterschiede der CBO-Traces für beide Versionen. Eine Erklärung für die innere Logik, die der durch _optimizer_improve_selectivity repräsentierten Berechnung zugrunde liegt, findet ich dort nicht (was allerdings auch nicht überrascht, da die Inhalte des CBO-Traces in aller Regel nicht unbedingt verbose erläutert sind). Dazu ein weiterer Test, der das Beispiel auf Alexandr Antonovs Blog behutsam erweitert:

drop table t1;
drop table t2;

CREATE TABLE t1 AS 
  SELECT LEVEL AS id1, 
         MOD(LEVEL, 10) fil1, 
         MOD(LEVEL, 5) fil3, 
         rpad('x', 1000) padding 
    FROM dual 
  CONNECT BY LEVEL < 10000 
;

CREATE TABLE t2 AS 
  SELECT LEVEL AS id2, 
         MOD(LEVEL, 20) fil2, 
         MOD(LEVEL, 15) fil4, 
         rpad('x', 1000) padding 
    FROM dual 
  CONNECT BY LEVEL < 10000 
;

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

-- Fall 1
explain plan for
SELECT t1.fil1
     , t2.fil2
  FROM t1,
       t2
 WHERE t2.id2 = t1.id1
   and t1.fil3 = 1
   AND t2.fil4 = 1
 GROUP BY t1.fil1
        , t2.fil2;

-- Fall 2
explain plan for
SELECT /*+ OPT_PARAM('_optimizer_improve_selectivity' 'false') */
       t1.fil1
     , t2.fil2
  FROM t1,
       t2
 WHERE t2.id2 = t1.id1
   and t1.fil3 = 1
   AND t2.fil4 = 1
 GROUP BY t1.fil1
        , t2.fil2;

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |   142 |  2840 |   797   (1)| 00:00:10 |
|   1 |  HASH GROUP BY      |      |   142 |  2840 |   797   (1)| 00:00:10 |
|*  2 |   HASH JOIN         |      |   667 | 13340 |   796   (1)| 00:00:10 |
|*  3 |    TABLE ACCESS FULL| T2   |   667 |  6670 |   398   (1)| 00:00:05 |
|*  4 |    TABLE ACCESS FULL| T1   |  2000 | 20000 |   398   (1)| 00:00:05 |
----------------------------------------------------------------------------

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

   2 - access("T2"."ID2"="T1"."ID1")
   3 - filter("T2"."FIL4"=1)
   4 - filter("T1"."FIL3"=1)

-- Fall 3
explain plan for
SELECT t1.fil1
     , t2.fil2
  FROM t1,
       t2
 WHERE t2.id2 = t1.id1
   and t1.fil3 = 1
   -- AND t2.fil4 = 1
 GROUP BY t1.fil1
        , t2.fil2;

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |   142 |  2414 |   797   (1)| 00:00:10 |
|   1 |  HASH GROUP BY      |      |   142 |  2414 |   797   (1)| 00:00:10 |
|*  2 |   HASH JOIN         |      |  2000 | 34000 |   796   (1)| 00:00:10 |
|*  3 |    TABLE ACCESS FULL| T1   |  2000 | 20000 |   398   (1)| 00:00:05 |
|   4 |    TABLE ACCESS FULL| T2   |  9999 | 69993 |   398   (1)| 00:00:05 |
----------------------------------------------------------------------------

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

   2 - access("T2"."ID2"="T1"."ID1")
   3 - filter("T1"."FIL3"=1)

Mit und ohne OPT_PARAM-Hint ergibt sich für diesen Fall jeweils der gleiche Plan, was ich als Indiz dafür nehme, dass hier keine "improved selectivity" wirksam wird: die Join-Cardinality (667) ist dabei absolut akkurat, aber das GROUP BY liefert tatsächlich nur 4 Sätze. Dabei ergibt sich die 142 - wie Fall 3 zeigt - unabhängig von allen Filter-Bedingungen und folglich auch unabhängig von der Größe der Ergebnismenge des HASH JOINs. Hier greift demnach die von Jonathan Lewis beschriebene Logik auf der Basis der Tabellenstatistiken. Über die Plausibilität der Annahme, dass eine Reduzierung der Satzanzahl nicht unmittelbar mit der Anzahl distinkter Wertkombinationen zu tun hat, kann man sicher diskutieren. Ohne behaupten zu wollen, ein völlig klares Bild der Zusammenhänge bekommen zu haben, ist mein Eindruck, dass die Logik der Bestimmung von cardinialities für GROUP BY Operationen insgesamt eine recht fehleranfällige ist. Außerdem frage ich mich, ob das Schlüsselwort "improved" bei Oracle ähnlich wie die Angabe "fast" etwas ist, das grundsätzlich Anlass zur Vorsicht geben sollte...

Donnerstag, Dezember 13, 2012

V$SQLFN_METADATA

Carsten Czarski weist in seinem Blog auf zwei interessante dynamische Performance-Views hin:
  • V$SQLFN_METADATA: "contains metadata about operators and built-in functions. Note that this view does not contain information about arguments because the number of arguments will be different for various functions. Information about arguments is contained in V$SQLFN_ARG_METADATA, which can be joined with V$SQLFN_METADATA to get information about any function and its arguments."
  • V$SQLFN_ARG_METADATA: "contains metadata about function arguments. There is one row for each argument of every function found in V$SQLFN_METADATA. There are no rows for functions that do not have any arguments."
Aus V$SQLFN_METADATA geht unter anderem hervor, ob es sich bei einer Funktion um eine analytische oder eine Aggregatfunktion handelt und seit welchem Release die Funktion (bzw. der Operator) im Oracle Server verfügbar ist.

Mittwoch, Dezember 12, 2012

Fehlende Column Statistics

Die nachträgliche Ergänzung neuer Spalten in bestehenden Tabellen kann einige unerwünschte Effekte mit sich bringen - in erster Linie besteht die Gefahr, dass neu hinzu kommende Werte die Satzlänge vergrößern, was dann unter Umständen zu row migration führen kann. Ein Aspekt, über den ich bisher nicht intensiver nachgedacht hatte, ist das Fehlen von column statistics, das sich in diesem Fall ergibt, wenn man im Anschluss an die Ergänzung der Spalte keine Aktualisierung der Statistiken durchführt. Dazu ein Beispiel:

-- 11.2.0.1
create table test_new_col
as
select rownum id
  from dual
connect by level <= 10000;

exec dbms_stats.gather_table_stats(user, 'test_new_col');

alter table test_new_col add col1 number;

update test_new_col set col1 = 1 ;

commit;

Ein Blick in user_tab_cols zeigt, dass in diesem Fall (natürlich) keine Statistiken für col1 vorliegen.

select column_name
     , num_distinct
     , num_nulls
     , last_analyzed
  from user_tab_cols
 where table_name = upper('test_new_col');

COLUMN_NAME                    NUM_DISTINCT  NUM_NULLS LAST_ANALYZED
------------------------------ ------------ ---------- -------------------
ID                                    10000          0 12.12.2012 08:06:04
COL1

Was macht der CBO aus dieser Information - bzw. aus ihrem Fehlen:

explain plan for
select count(*)
  from test_new_col
 where col1 = 0;

select * from table(dbms_xplan.display);

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |     4 |    12   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |     4 |            |          |
|*  2 |   TABLE ACCESS FULL| TEST_NEW_COL |   100 |   400 |    12   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

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

   2 - filter("COL1"=0)

explain plan for
select count(*)
  from test_new_col
 where col1 = 1

select * from table(dbms_xplan.display);

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |     4 |    12   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |     4 |            |          |
|*  2 |   TABLE ACCESS FULL| TEST_NEW_COL |   100 |   400 |    12   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

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

   2 - filter("COL1"=1)

Offensichtlich verwendet der CBO hier eine standard selectivity von 1%, was im Beispiel weder für Fall col1 = 0 (=> 0 rows) noch für Fall col1 = 1 (=> 10000 rows) passend ist. Der passende dbms_stats-Aufruf sollte folglich nach der Ergänzung einer neuen Spalte obligatorisch sein - besonders dann, wenn man über diese Spalte Einschränkungen durchführen möchte.

Freitag, Dezember 07, 2012

CTEs und NL Cardinalities

Dass CTEs (also Oracles subquery factoring mit einer WITH-clause) zu seltsamen Costing Effekten führen können, ist keine neue Beobachtung. Randolf Geist hat z.B. mehrere Artikel zum Thema geschrieben, in denen er u.a. auf der Basis von CBO Traces aufzeigt, dass für Queries mit CTEs bestimmte Transformationen nicht durchgeführt werden können (im Trace findet sich dann der Hinweis "CBQT: copy not possible on query block [...] because linked to with clause"). Einige der angesprochenen Probleme wurden offenbar in 11.2.0.3 behoben. Aber auf diesem Release-Stand bin ich noch nicht angekommen.

In 11.1.0.7 ist mir gestern folgender Effekt begegnet:

drop table test_dim;
drop table test_fact;

create table test_dim
as
with
generator as (
select to_date('01.01.2012', 'dd.mm.yyyy') - 1 + rownum a_date
  from dual
connect by level <= 366
)
select to_char(trunc(a_date, 'mm'), 'mm') a_month
     , min(a_date) min_date
     , max(a_date) max_date
  from generator
 group by trunc(a_date, 'mm')
 order by trunc(a_date, 'mm')
;

create table test_fact
as
with 
generator as (
select to_date('01.10.2012', 'dd.mm.yyyy') - 1 + rownum a_date
  from dual
connect by level <= 92
)
,
facts as (
select 1000 val
  from dual
connect by level <= 10000
)
select a_date
     , 1000 col1
  from generator
     , facts;

exec dbms_stats.gather_table_stats(user, 'test_dim')
exec dbms_stats.gather_table_stats(user, 'test_fact')

Also eine Tabelle test_dim mit 12 Sätzen (je einer für jeden Monat im Jahr 2012), einem Monatsnamen und dem ersten ersten und letzten Tag des Monats und eine Tabelle test_fact mit jeweils 10000 Sätzen für jeden Tag im letzten Quartal des Jahres - insgesamt also 92 * 10000 = 920000 rows - und einer bedeutungslosen Kennzahl col1. Dazu gibt's dann eine Query, die einige Angaben aus test_dim in einer CTE abruft und mit den Fakten joint:

-- Fall 1: materialize
explain plan for
with
date_range as (
select /*+ materialize */
       a_month
     , min_date
     , max_date
  from test_dim
 where a_month >= 10
)
select a_month
     , count(*)
  from date_range
  join test_fact
    on (test_fact.a_date between date_range.min_date and date_range.max_date)
 group by a_month
;

select * from table(dbms_xplan.display);

-- Fall 2: inline
explain plan for
with
date_range as (
select /*+ inline */
       a_month
     , min_date
     , max_date
  from test_dim
 where a_month >= 10
)
select a_month
     , count(*)
  from date_range
  join test_fact
    on (test_fact.a_date between date_range.min_date and date_range.max_date)
 group by a_month
;

select * from table(dbms_xplan.display);

Der Unterschied der beiden Queries liegt in der Behandlung der CTE, die in Fall 1 im temporary tablespace materialisiert und in Fall 2 textuell an der vorgesehenen Stelle integriert wird. Die beiden Varianten liefern folgende Pläne:

-- Fall 1: Materialize
----------------------------------------------------------------------------------------------------------
| Id  | Operation                  | Name                        | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |                             |     1 |    29 |   580   (2)| 00:00:07 |
|   1 |  TEMP TABLE TRANSFORMATION |                             |       |       |            |          |
|   2 |   LOAD AS SELECT           | TEST_FACT                   |       |       |            |          |
|*  3 |    TABLE ACCESS FULL       | TEST_DIM                    |     1 |    19 |     3   (0)| 00:00:01 |
|   4 |   HASH GROUP BY            |                             |     1 |    29 |   577   (2)| 00:00:07 |
|   5 |    NESTED LOOPS            |                             |  2300 | 66700 |   576   (2)| 00:00:07 |
|   6 |     VIEW                   |                             |     1 |    21 |     2   (0)| 00:00:01 |
|   7 |      TABLE ACCESS FULL     | SYS_TEMP_0FD9D6610_271DA033 |     1 |    19 |     2   (0)| 00:00:01 |
|*  8 |     TABLE ACCESS FULL      | TEST_FACT                   |  2300 | 18400 |   574   (2)| 00:00:07 |
----------------------------------------------------------------------------------------------------------

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

   3 - filter(INTERNAL_FUNCTION("A_MONTH")>=TO_DATE(' 2012-10-01 00:00:00', 'syyyy-mm-dd
              hh24:mi:ss'))
   8 - filter("TEST_FACT"."A_DATE">="DATE_RANGE"."MIN_DATE" AND
              "TEST_FACT"."A_DATE"<="DATE_RANGE"."MAX_DATE")

-- Fall 2: inline
Plan hash value: 423103375

---------------------------------------------------------------------------------
| Id  | Operation           | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |           |     1 |    27 |   582   (3)| 00:00:07 |
|   1 |  HASH GROUP BY      |           |     1 |    27 |   582   (3)| 00:00:07 |
|   2 |   NESTED LOOPS      |           | 84235 |  2221K|   577   (2)| 00:00:07 |
|*  3 |    TABLE ACCESS FULL| TEST_DIM  |     1 |    19 |     3   (0)| 00:00:01 |
|*  4 |    TABLE ACCESS FULL| TEST_FACT |   140K|  1096K|   574   (2)| 00:00:07 |
---------------------------------------------------------------------------------

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

   3 - filter(INTERNAL_FUNCTION("A_MONTH")>=TO_DATE(' 2012-10-01
              00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
   4 - filter("TEST_FACT"."A_DATE">="MIN_DATE" AND
              "TEST_FACT"."A_DATE"<="MAX_DATE")

Erwähnenswert ist dabei vielleicht auch noch, dass der CBO ohne Hint den inline-Plan verwendet, obwohl dessen Kosten geringfügig höher sind als die der Variante mit der Materialisierung (582 zu 580). Davon abgesehen sind die Cardinality-Schätzungen für test_fact in beiden Fällen ziemlich weit von den Ausführungs-Realitäten entfernt:
  • tatsächlich werden alle 920.000 Sätze der Tabelle gelesen
  • für die Version mit dem Materialize-Hint errechnet sich die Cardinality offenbar wieder einmal auf Grundlage der Standard-Arithmetik für Bound-Ranges, also jeweils 5% als Faktor für die untere und die oberer Grenze: 920000 * 0,05 * 0,05 = 2300.
  • weniger klar ist mir, woher die 140K kommen.
Ein Blick ins CBO-Trace verrät zumindest, welche Werte in die Rechnung eingehen:

NL Join
  Outer table: Card: 3.19  Cost: 3.00  Resp: 3.00  Degree: 1  Bytes: 24
Access path analysis for TEST_FACT
  Inner table: TEST_FACT  Alias: TEST_FACT
  Access Path: TableScan
    NL Join:  Cost: 1720.93  Resp: 1720.93  Degree: 1
      Cost_io: 1693.00  Cost_cpu: 458426063
      Resp_io: 1693.00  Resp_cpu: 458426063

  Best NL cost: 1720.93
          resc: 1720.93  resc_io: 1693.00  resc_cpu: 458426063
          resp: 1720.93  resp_io: 1693.00  resc_cpu: 458426063
Join Card:  447155.913681 = = outer (3.185075) * inner (920000.000000) * sel (0.152599)

Die sel (0.152599) ist dabei offenbar der Faktor, der von der Satzanzahl zur cardinality von 140K führt: 920000 * 0,152599 = 140391,08. Dabei erscheint die cardinality-Angabe selbst nicht im Trace (weder als 140K noch als 140391). Aber was bedeutet diese selectivity? Bisher habe ich dafür noch keine Erklärung. Der Zusammenhang wird auch dadurch nicht klarer, dass die in der CTE verwendete Einschränkung auf a_month offenbar gar keine Auswirkung auf die selectivity-Bestimmung hat:
  • a_month >= 1: tatsächlich: 920K; rows(test_fact) = 140K; rows(NL): 84235
  • a_month >= 10: tatsächlich: 920K; rows(test_fact) = 140K; rows(NL): 84235
  • a_month >= 12: tatsächlich: 310K; rows(test_fact) = 140K; rows(NL): 84235
  • a_month >= 13: tatsächlich: 0; rows(test_fact) = 140K; rows(NL): 84235 !!
  • a_month >= 20: tatsächlich: 0; rows(test_fact) = 140K; rows(NL): 84235 !!
Und auch die Auswirkung einer Änderung der Join-Bedingung zwischen CTE und test_fact ist nicht unmittelbar plausibel (bestimmt jeweils mit a_month >= 10 in der CTE, was wie gerade angesprochen vermutlich in keinem Fall relevant ist):
  • (test_fact.a_date between date_range.min_date and date_range.max_date): rows(test_fact) = 140K
  • (test_fact.a_date >= date_range.min_date): rows(test_fact) = 835K
  • (test_fact.a_date > date_range.min_date): rows(test_fact) = 833K
  • (test_fact.a_date < date_range.min_date): rows(test_fact) = 76326
  • (test_fact.a_date <= date_range.min_date): rows(test_fact) = 78828
Insgesamt scheint die Behandlung von Range-Prädikaten an dieser Stelle etwas unübersichtlich zu sein - ein nachvollziehbares Muster sehe ich da zunächst nicht. Und auf Anhieb habe ich auch weder in Cost-Based Oracle noch im Netz eine Erklärung der Berechnung gefunden - wobei ich sicher bin, dass die Herren Lewis, Geist etc. diese Erklärung haben. 

Mittwoch, Dezember 05, 2012

Häufige Problemquellen für Zugriffspläne im SQL Server

Joe Sack liefert im SQLPerformance.com-Blog eine Liste mit zehn Gründen für suboptimale Ausführungspläne im SQL Server, die letztlich - wie bei Oracle - vor allem mit falschen cardinalities zusammenhängen. Die Liste enthält:
  • fehlende oder veraltete Statistiken
  • nicht repräsentative Samples bei der Statistikerfassung (data skew)
  • Korrelation von Spaltenwerten: dagegen helfen im SQL Server multi-column stats (im Fall von Oracle: extended stats)
  • intra-table column comparisions, die man durch vorberechnete Spalten ersetzen kann/sollte
  • Verwendung von table variables (deren cardinality immer 1 ist - klingt problematisch, aber um das genauer einordnen zu können, müsste ich eine klarere Vorstellung von der Rolle von table variables haben)
  • Funktionen (multi-statement table-valued and scalar)
  • Datentypen (also vor allem implizite Typ-Konvertierungen)
  • komplexe Prädikate
  • komplexe Queries
  • verteilte Queries (mit remote-Zugriff)
Ich will die Unterschiede nicht marginalisieren, aber grundsätzlich sähe eine entsprechende Aufstellung für Oracle durchaus ähnlich aus.

Intra-Block Row Chaining für row pieces

Ein paar - relativ ungeordnete - Beobachtungen zum Intra-Block Row Chaining. Zunächst: worum handelt es sich dabei überhaupt? Im Abschnitt Row Format and Size des Concept Guides findet sich der Hinweis: Oracle Database can only store 255 columns in a row piece. Thus, if you insert a row into a table that has 1000 columns, then the database creates 4 row pieces, typically chained over multiple blocks." Das Wort "typically" deutet dabei schon an, dass mehrere row pieces durchaus auch in einem einzigen Block gespeichert werden können. Dazu ein kleines (und gekürztes) Beispiel:

-- Anlage einer Test-Tabelle mit 1000 Spalten in einem MSSM-Tablespace
create table test_chaining (
  col_1 number
, col_2 number
, col_3 number
...
, col_998 number
, col_999 number
, col_1000 number
) tablespace test_ts;

-- Insert eines einzelnen Datensatzes
insert into test_chaining values (
  1
, 2
, 3
...
, 998
, 999
, 1000
);

Also eine Tabelle mit 1000 Spalten - mehr sind nicht möglich: "ORA-01792: Höchstzahl für Spalten in einer Tabelle oder einer View ist 1000" - und einem einzigen Datensatz. Zu diesem Satz ermittle ich nun den zugehörigen Block, den ich anschließend per Dump ausgeben lasse:

select dbms_rowid.rowid_relative_fno(rowid) file_nr
     , dbms_rowid.rowid_block_number(rowid) block_nr
  from test_chaining;

alter system dump datafile 7 block 1414;

Der erstellte Block-Dump enthält (unter anderem) folgende Informationen (der Beginn des Dumps und die col-Listen sind gekürzt):

Start dump data blocks tsn: 8 file#:7 minblk 1414 maxblk 1414
...
tsiz: 0x1f68
hsiz: 0x1a
pbl: 0x0c408294
     76543210
flag=--------
ntab=1
nrow=4
frre=-1
fsbo=0x1a
fseo=0x1017
avsp=0xffd
tosp=0xffd
0xe:pti[0] nrow=4 offs=0
0x12:pri[0] offs=0x1b6c
0x14:pri[1] offs=0x176a
0x16:pri[2] offs=0x1367
0x18:pri[3] offs=0x1017
block_row_dump:
tab 0, row 0, @0x1b6c
tl: 1020 fb: -----L-- lb: 0x1  cc: 255
col  0: [ 3]  c2 08 2f
col  1: [ 3]  c2 08 30
col  2: [ 3]  c2 08 31
col  3: [ 3]  c2 08 32
...
col 252: [ 3]  c2 0a 63
col 253: [ 3]  c2 0a 64
col 254: [ 2]  c2 0b
tab 0, row 1, @0x176a
tl: 1026 fb: -------- lb: 0x1  cc: 255
nrid:  0x01c00586.0
col  0: [ 3]  c2 05 5c
col  1: [ 3]  c2 05 5d
col  2: [ 3]  c2 05 5e
col  3: [ 3]  c2 05 5f
...
col 252: [ 3]  c2 08 2c
col 253: [ 3]  c2 08 2d
col 254: [ 3]  c2 08 2e
tab 0, row 2, @0x1367
tl: 1027 fb: -------- lb: 0x1  cc: 255
nrid:  0x01c00586.1
col  0: [ 3]  c2 03 25
col  1: [ 3]  c2 03 26
col  2: [ 3]  c2 03 27
col  3: [ 3]  c2 03 28
...
col 252: [ 3]  c2 05 59
col 253: [ 3]  c2 05 5a
col 254: [ 3]  c2 05 5b
tab 0, row 3, @0x1017
tl: 848 fb: --H-F--- lb: 0x1  cc: 235
nrid:  0x01c00586.2
col  0: [ 2]  c1 02
col  1: [ 2]  c1 03
col  2: [ 2]  c1 04
col  3: [ 2]  c1 05
...
col 232: [ 3]  c2 03 22
col 233: [ 3]  c2 03 23
col 234: [ 3]  c2 03 24
end_of_block_dump
End dump data blocks tsn: 8 file#: 7 minblk 1414 maxblk 1414

Offensichtlich enthält der Block also 4 row pieces, von denen die ersten drei jeweils 255 Spalten umfassen, während das vierte nur 235 Spalten enthält. Interessant ist dabei auch, dass dieses vierte Stück offenbar die ersten Spalten ab col_1 enthält (was man am Inhalt c1 02 => 1 zu erkennen ist). Hemant Chitale hat vor einigen Jahren zwei Artikel zum Thema in seinem Blog veröffentlicht und dort auch ein paar Beobachtungen zu den zugehörigen Angaben in v$sesstat (bzw. v$mystat) vermerkt. Außerdem findet sich dort ein Verweis auf einen Oracle-L thread, in dem die Herren Poder und Antognini wichtige Ergänzungen liefern. Und wenn ich schon dabei bin hier noch ein paar Links:
  • Jonathan Lewis: Analyze this! liefert Informationen zum CHAIN_CNT, der migrated und chained rows umfasst, aber intra-row-chaining nicht vermerkt; nach einem ANALYZE TABLE test_chaining COMPUTE STATISTICS; bleibt der CHAIN_CNT = 0, was insofern plausibel ist, da die Verkettung nicht block-übergreifend ist
  • Tanel Poder: Detect chained and migrated rows in Oracle – Part 1; einen Part 2 habe ich nicht gefunden ...; darin wird die Semantik der Statistiken table fetch by rowid ("how many times Oracle took a ROWID (for example from an index) and went to a table to lookup the actual row") und table fetch continued row ("when we didn’t find all that we wanted from the original row piece and had to follow a pointer to the new location of the migrated row (or next row piece of a chained row)") erläutert.
Ausgehend von den Ausführungen des Herrn Poder noch ein kleiner Versuch:

select col_1 from TEST_CHAINING;
select col_500 from TEST_CHAINING;
select col_1000 from TEST_CHAINING;

-- v$sesstat:
NAME                                   COL_1  COL_500  COL_1000 
-------------------------------------- -----  -------  --------
session logical reads                     10       12        13
consistent gets from cache                10       12        13
consistent gets                           10       12        13
consistent gets from cache (fastpath)      7        9        10
table scan blocks gotten                   5        5         4
no work - consistent read gets             5        7         8
table scan rows gotten                     4        4         4
buffer is not pinned count                 2        4         5
table fetch by rowid                       1        1         1
table scans (short tables)                 1        1         1

Daraus ziehe ich im Moment nur zwei Schlüsse:
  • Intra-Block Row Chaining wird nicht als table fetch continued row vermerkt (ist also auch in dieser Perspektive kein "echtes" Chaining)
  • die erforderliche Arbeit unterscheidet sich für den Zugriff auf die erste, eine mittlere bzw. die letzte Spalte der Tabelle deutlich - und sie erhöht sich für weiter hinten liegende Spalten
Ich gebe zu: mal wieder mangelt es meinen Ausführungen an Struktur. Vielleicht sollte ich doch mal dazu übergehen meine Gedanken zu ordnen, ehe ich etwas schreibe...

Samstag, Dezember 01, 2012

ASH-Analyse von TX Lock contention

Kyle Hailey erläutert in seinem Blog, wie man in ASH protokollierte Wait Events vom Type 'enq: TX – row lock contention' ihren Ursachen zuordnen kann. Entscheidend ist dabei ist der lock mode:
  • mode 6 (exclusive) deutet in der Regel auf ein klassisches row lock hin, bei dem zwei Sessions den gleichen Satz ändern wolle.
  • mode 4 (share) kann mehrere wahrscheinliche Ursachen haben:
    • insert eines unique key, der bereits in einer anderen Session angelegt, aber noch nicht per commit festgeschrieben wurde (andernfalls bekäme man ja nur einen UK-Verletzungsfehler)
    • insert eines child records zu einem (FK-)parent, der gerade neu hinzugefügt oder gelöscht, aber nicht commited wurde
    • contention bei einer Änderung in einem bitmap index
Zur Analyse der tatsächlichen Ursache liefert der Herr Hailey eine Query, die durch den Join von v$active_session_history und all_objects unterschiedliche Ergebnismuster liefert. ASH ist einfach ein großartiges Werkzeug zur nachträglichen Analyse von Systemzuständen.

Mittwoch, November 28, 2012

Statistiktransfer von PROD nach DEV

Maria Colgan liefert im Oracle Optimizer Blog ein sehr kompaktes Beispiel für den Transfer von Optimizer-Statistiken aus einem Produktiv-System in ein Entwicklungs-System. Die Schritte dabei sind:
  • Anlage einer Hilfstabelle in PROD mit dbms_stats.create_stat_table, in der die PROD-Statistiken gespeichert werden können
  • Übertragen der PROD-Statistiken in die Hilfstabelle mit dbms_stats.export_schema_stats
  • Anlage eines Directories in PROD (falls nicht schon eines vorhanden ist)
  • Export der Hilfstabelle via expdp
  • Transfer des Dumps nach DEV
  • Import der Hilfstabelle aus dem Dump in die DEV-DB
  • Kopieren der Statistiken der Hilfstabelle ins dictionary via dbms_stats.import_schema_stats
Dass das Übertragen möglich ist, war mir bekannt, aber dass es so einfach ist, hatte ich offenbar vergessen (oder nie gewusst).

Eine (geringfügig komplexere) Variante für den Transfer der für eine einzelne Query relevanten Statistiken hat übrigens gerade Yury Velikanov im Pythian Blog erläutert.

Freitag, November 23, 2012

Sonderfälle der Plan-Interpretation

Jonathan Lewis hat dieser Tage zwei Fälle geschildert, in denen die Standard-Regeln der Plan-Interpretation nicht gelten:
  • Plan timing: skalare Subqueries, die in einer SELECT-Liste verwendet werden, erscheinen im Plan oberhalb des Query Blocks, der sie aufruft. Eigentlich geht es im Artikel um das Verständnis der Zeitangaben in den erweiterten (rowsource execution) Plan-Statistiken, aber die klären sich, wenn die Verabeitungsreihenfolge deutlich wird. Der Artikel enthält auch noch einen Verweis auf das Phänomen des scalar subquery caching, das mir zuletzt häufiger begegnet ist, und das dafür sorgt, dass eine skalare Subquery nicht für jeden Satz, sondern nur für jeden distinkten Wert der Ergebnismenge aufgerufen wird.
  • Plan Order: der Index-Zugriff einer konstanten Subquery erscheint im Plan unterhalb eines HASH JOINs, aber die rowsource execution statistics zeigen deutlich, dass die Ausführung nach der ergebnislosen Ausführung der Subquery abbricht (Starts = 0 für alle folgenden Schritte). Mit den rowsource execution statistics und dem sqlmonitor lassen sich solche Effekte inzwischen relativ leicht bestimmen.

Status einer Materialized View

Ein kleiner Test zur Semantik der Status-Angaben für Materialized Views in den relevanten Dictionary-Tabellen. Dabei geht es zunächst nur um die einfachsten Fälle (kein Query Rewrite, kein Fast Refresh):

-- 11.1.0.7
-- Aufbau Datenbasis
drop table test_mpr;
drop materialized view test_mv_mpr;

create table test_mpr 
as 
select rownum id
     , mod(rownum, 10) col1 
  from dual 
connect by level <= 1000;

create materialized view test_mv_mpr 
as 
select col1
     , count(*) row_count
  from test_mpr
 group by col1;

-- Analyse-Queries 
select object_name
     , object_type
     , status
  from dba_objects
 where object_name = 'TEST_MV_MPR';
 
select mview_name
     , invalid
     , known_stale 
     , unusable
  from dba_mview_analysis 
 where mview_name = 'TEST_MV_MPR';
 
select mview_name
     , staleness
     , compile_state
  from dba_mviews
 where mview_name = 'TEST_MV_MPR'; 

Dazu liefern die befragten Dictionary-Tabellen zunächst folgende Angaben:

-- dba_objects
OBJECT_NAME     OBJECT_TYPE         STATUS
--------------- ------------------- -------
TEST_MV_MPR     MATERIALIZED VIEW   VALID
TEST_MV_MPR     TABLE               VALID

-- dba_mview_analysis 
MVIEW_NAME      INVALID    KNOWN_STALE UNUSABLE
--------------- ---------- ----------- ----------
TEST_MV_MPR     N          N           N

-- dba_mviews
MVIEW_NAME      STALENESS           COMPILE_STATE
--------------- ------------------- -------------
TEST_MV_MPR     FRESH               VALID

So weit keine Überraschungen: die MV ist frisch aufgebaut und alle Status-Angaben sind folglich im grünen Bereich. Was passiert, wenn ich die Basistabelle lösche:

drop table test_mpr;

-- dba_objects
OBJECT_NAME     OBJECT_TYPE         STATUS
--------------- ------------------- -------
TEST_MV_MPR     MATERIALIZED VIEW   INVALID
TEST_MV_MPR     TABLE               VALID

-- dba_mview_analysis 
MVIEW_NAME      INVALID    KNOWN_STALE UNUSABLE
--------------- ---------- ----------- ----------
TEST_MV_MPR     Y          N           N

-- dba_mviews
MVIEW_NAME      STALENESS           COMPILE_STATE
--------------- ------------------- -------------
TEST_MV_MPR     NEEDS_COMPILE       NEEDS_COMPILE

Auch diese Angaben erscheinen mir völlig nachvollziehbar: nach der Löschung der Basistabelle ist die MV tatsächlich in einer unglücklichen Situation, ein Refresh ist nicht mehr möglich, und der Status INVALID beschreibt das zutreffend. Nun ein weniger massiver Eingriff: ich füge in der Basis-Tabelle ein paar neue Datensätze ein, ändere an den Strukturen aber nichts:

insert into test_mpr
select rownum id
     , mod(rownum, 10) col1
  from dual
connect by level <= 1000;

commit;

-- dba_objects
OBJECT_NAME     OBJECT_TYPE         STATUS
--------------- ------------------- -------
TEST_MV_MPR     MATERIALIZED VIEW   INVALID
TEST_MV_MPR     TABLE               VALID

-- dba_mview_analysis
MVIEW_NAME      INVALID    KNOWN_STALE UNUSABLE
--------------- ---------- ----------- ----------
TEST_MV_MPR     Y          N           N

-- dba_mviews
MVIEW_NAME      STALENESS           COMPILE_STATE
--------------- ------------------- -------------
TEST_MV_MPR     NEEDS_COMPILE       NEEDS_COMPILE

Die Status-Angaben sind in diesem Fall die gleichen wie im Fall der Löschung der Basis-Tabelle - und das finde ich nicht völlig plausibel, denn eigentlich würde ich erwarten, dass hier eine Unterscheidung möglich sein sollte. Die Einführung von weiteren Zustandsangaben wäre aus meiner Sicht kein Luxus gewesen.

Aus Gründen der Vollständigkeit hier noch die zugehörigenDefinitionen der Dokumentation:
  • DBA_OBJECTS
    • STATUS: Status of the object VALID, INVALID, N/A
  • DBA_MVIEW_ANALYSIS
    • INVALID: "Indicates whether this materialized view is in an invalid state (inconsistent metadata)"
    • KNOWN_STALE: "Indicates whether the data contained in the materialized view is known to be inconsistent with the master table data because that has been updated since the last successful refresh"
    • UNUSABLE: "Indicates whether this materialized view is UNUSABLE (inconsistent data) [...]. A materialized view can be UNUSABLE if a system failure occurs during a full refresh"
  • DBA_MVIEWS:
    • STALENESS: "Relationship between the contents of the materialized view and the contents of the materialized view's masters" Es folgen 5 Zustandsangaben, unter denen NEEDS_COMPILE allerdings nicht aufgeführt ist.
    • COMPILE_STATE: "Validity of the materialized view with respect to the objects upon which it depends". Dazu gibt's 3 Zustände. Zu NEEDS_COMPILE heisst es: " Some object upon which the materialized view depends has changed. An ALTER MATERIALIZED VIEW...COMPILE statement is required to validate this materialized view"

Mittwoch, November 21, 2012

Parallelisierung (Randolf Geist) - Teil 1

In der Reihe der OTN-Artikel zum Thema Database Performance & Availability wurde zuletzt eine zweiteilige Serie Understanding Parallel Execution von Randolf Geist veröffentlicht, die einen sehr guten Überblick zu den Voraussetzungen und Leistungen paralleler Operationen liefert (wobei die Aussagen für Exadata, aber auch für "normale" Datenbanken gelten).

Mein Exzerpt erhebt dabei mal wieder keinen Anspruch auf Vollständigkeit, sondern soll mir in erster Linie als Erinnerungshilfe dienen. Grundsätzlich würde ich ohnehin jedem, der sich mit Parallelisierung beschäftigt, die komplette Lektüre der beiden OTN-Artikel empfehlen. Außerdem ist das Thema mal wieder eines, bei dem ich am Übersetzen der technischen Begriffe scheitere, so dass kein wirklich konsistenter Text daraus wird:

Im ersten Artikel erklärt der Autor die Voraussetzungen für einen sinnvollen Einsatz paralleler Operationen:
  • wenn der serielle Plan nichts taugt (falsche Join Reihenfolge, ungeeignete Zugriffsverfahren), wird auch der parallele Plan keine Wunder bewirken
  • wenn PL/SQL-Funktionen eingesetzt werden, die nicht explizit als parallelisierbar definiert wurden, kann es vorkommen, dass im Plan ein Schritt PX COORDINATOR FORCED SERIAL erscheint, der bedeutet, dass der Plan letztlich seriell ausgeführt wird, obwohl PX-Operationen darin erscheinen (es gibt offenbar neben den Funktionen noch andere Gründe für dieses Verhalten). Da aber das Costing die Parallelisierung berücksichtigt, kann dieser Effekt zu massiven Fehlkalkulationen führen.
  • durch das verwendete Consumer/Producer-Modell kommt es vor, dass beide Gruppen paralleler Slave-Prozesse beschäftigt sind, wenn im Plan eigentlich eine parallele Weiterverarbeitung vorgesehen ist. In solchen Fällen treten blocking operations auf, die im Plan als BUFFERED oder BUFFER SORT ausgewiesen sind (wobei BUFFER SORT in seriellen Plänen eine andere Semantik hat). Dieses Abwarten ist inhaltlich nicht immer nachvollziehbar (der Autor zeigt das Problem am Beispiel eines HASH JOINs), aber anscheinend unvermeidlich: "It looks like that the generic implementation always generates a Parallel execution plan under the assumption for the final step that there is potentially another Parallel Slave Set active that needs to consume the data via re-distribution. This is a pity as it quite often implies unnecessary blocking operations as shown above."
  • die Verabeitungsreihenfolge für parallele Operationen entspricht nicht unbedingt der Reihenfolge, die für serielle Operationen gilt (und wo üblicherweise zuerst der im Plan am weitesten oben aufgeführte  Step ausgeführt wird, zu dem keine untergeordneten Steps existieren: also der erste Leaf-Step), da sich auch hier die Begrenzung auf zwei aktive parallel slave Gruppen auswirkt.
  • die BUFFER-Operationen aufgrund von blocking operations können zur Auslagerung auf die Platte führen, was natürlich der Performance schadet; auch ohne Auslagerung kann der Memory-Bedarf hoch sein.
  • Parallel Distribution Methods: Für den HASH JOIN (das übliche Join-Verfahren bei Parallelisierung) gibt es drei Verarbeitungs-Varianten, die in der Spalte "PQ Distrib" im Plan erscheinen:
    • Hash Distribution: die beiden Quelldatenmengen (row sources) werden über den Join-Key Hash-verteilt, was zwei aktive Slave-Gruppen erfordert, und der eigentliche Join wird wiederum von einer Slave-Gruppe durchgeführt, so dass sich (in der Regel) eine Buffered Operation ergibt
    • Broadcast Distribution: der Join (bzw. sein Probe Phase) wird zusammen mit einer der row source Operationen durchgeführt. Da keine Verteilung der Daten auf den Join-Key erfolgte, müssen die Ergebnisse der zweiten row source an alle Slaves, die den Join durchführen, weitergereicht werden (Broadcast). Dies führt zu einer Vervielfachung der intern verarbeiteten Datenmengen. Effizient ist das Verfahren, wenn die erste row source relativ klein ist.
    • Partition Distribution: wenn beide row sources in gleicher Weise partitioniert sind, ist ein partition-wise-Join möglich, der keine hash distribution der Daten erfordert, und deshalb von einer einzigen Slave-Gruppe ausgeführt werden kann und keine blocking operation hervorruft. Der partition-wise-Join ist damit das effizienteste der erwähnten Verfahren. Auch ohne Parallelisierung ist der partion-wise-Join sehr nützlich, da er die Größe der Join-Operationen reduziert.
  • MERGE JOIN und NESTED LOOPS sind ebenfalls parallelisierbar, kommen aber sehr viel seltener vor.
  • Für den partition-wise-Join sollte der DOP höchstens der Anzahl der Partitionen entsprechen.
  • mit Hilfe des Hints PQ_DISTRIBUTE lässt sich das Verhalten beeinflussen. Dabei lassen sich die Syntax-Details aus den OUTLINE-Informationen von DBMS_XPLAN entnehmen.
  • Der Abschnitt "Distribution of load operations" beschäftigt sich mit der Beeinflussung interner Sortierungen (z.B. zum Zweck einer möglichst effizienten Komprimierung)
  • "Plans Including Multiple Data Flow Operations (DFOs)" erläutert Fragen des geeigneten DOP und der Effekte einer Verknüpfung mehrerer Operationen mit unterschiedlichem DOP.

IOTs, CTAS und Sortierungen

Connor McDonald (auf dessen Blog Jonathan Lewis vor kurzem hingewiesen hatte - und dessen PL/SQL-Buch immer noch an meinem Arbeitsplatz steht) hat dieser Tage in seinem Blog ein paar interessante Effekte aus dem Kontext der IOTs angesprochen:
  • um LOGGING beim Aufbau einer IOT zu vermeiden, muss man CTAS verwenden. Bei Verwendung von INSERT /*+ APPEND */ wird auch für eine als NOLOGGING definierte Tabelle massiv redo erzeugt.
  • Der Execution Plan beim IOT-Aufbau über CTAS taugt nicht viel. Im gegebenen Beispiel zeigt der Plan einen INDEX FULL SCAN ohne Sortierungen, aber tatsächlich erfolgen für den zugehörigen Indes-Aufbau massive Sortier-Operationen.
Nachtrag 28.11.2012: Jonathan Lewis hat inzwischen auch noch einen Artikel zum Thema geschrieben und zeigt darin, wie das Logging durch spooling der Quelldaten in eine Datei und Einfügen ins Ziel per SQL-Loader vermieden werden kann.

Freitag, November 16, 2012

SQL Performance Explained von Markus Winand

Dass ich gerne mal ein Buch über Indizes von Richard Foote hätte, habe ich wahrscheinlich gelegentlich schon mal erwähnt, aber leider scheint damit auch weiterhin nicht zu rechnen zu sein - zumal die Herr Foote niemals versprochen hat, etwas Derartiges zu veröffentlichen. Stattdessen habe ich dieser Tage den im Sommer 2012 erschienenen Band SQL Performance Explained von Markus Winand gelesen, auf dessen interessante Seite Use The Index, Luke! ich hier auch schon verwiesen habe. Im ersten Moment ist es etwas ungewohnt, über Indizes zu lesen, ohne regelmäßigen Referenzen auf das Werk David Bowies zu begegnen, aber daran gewöhnt man sich ziemlich schnell ... 

Um es vorweg zu nehmen: das Buch ist aus meiner Sicht eine ausgesprochen empfehlenswerte Lektüre und liefert einen sehr zugänglichen Einstieg ins Thema SQL-Performance-Optimierung. Dabei wendet sich der relativ schmale Band (196 S.) in erster Linie an die Entwickler, die der Autor als die Gruppe betrachtet, die aufgrund ihrer Kenntnis der Applikationen (und - hoffentlich auch - der Daten) am besten dazu in der Lage ist, eine sinnvolle Indizierung durchzuführen, während DBAs und externen Beratern dieses Wissen in der Regel fehlt. Ich will an dieser Stelle nicht massiv widersprechen, denke aber, dass man viele SQL-Zugriffsprobleme auch ohne Kenntnis der Applikationslogik bestimmen kann (jedenfalls in Oracle und im SQL Server, da für diese RDBMS gilt, dass das data dictionary und die dynamischen Performance-Views sehr viele relevante Informationen liefern). Dass die Entwickler ein gutes Verständnis der Arbeitsweise von Indizes haben sollten, stimmt aber in jedem Fall. 

Das Thema des Buches sind B*Tree-Indizes und ihre Rolle in OLTP-Systemen. Diese starke Fokussierung auf eine zentrale - und beschränkte - Fragestellung und eine klare Strukturierung der Erklärungen sorgen dafür, dass die Darstellung sich nicht in Details verliert. Diese Struktur leidet auch nicht darunter, dass die Erläuterungen nicht auf ein einziges RDBMS beschränkt sind - neben Oracle und SQL Server werden MySQL und PostgreSQL untersucht -, im Gegenteil: durch den Vergleich der Systeme wird deutlich, wie viele Übereinstimmungen es in den grundsätzlichen Verfahrensweisen der Datenbanken im Bereich der Indizierung und der SQL-Optimierung gibt. Das Buch gliedert sich in acht Kapitel:
  • Anatomy of an Index: erläutert die Struktur von B*Tree-Indizes.
  • The Where Clause: erklärt die Rolle unterschiedlicher Operatoren (Equality, Range), Funktionen (und FBIs), NULL-Werten, Datentypen, Statistiken, Bindewerten und liefert dabei zahlreiche Antworten auf die klassische Frage, warum ein Index nicht verwendet wird. Einer der wichtigsten Punkte ist aus meiner Sicht die prägnante Erklärung von access und filter Prädikaten. Nützlich sind auch die Hinweise auf das unterschiedliche Verhalten unterschiedlicher RDBMS (z.B. Oracles fragwürdige Behandlung von Leerstrings als NULL).
  • Performance und Scalability: zeigt den Einfluss von Datenvolumen und Contention auf die Performance.
  • The Join Operation: behandelt die drei Join-Verfahren (Nested Loops, Hash Join, Merge Join) und ihre Nutzung von Indizes. Dabei wird auch das Thema der Code-Generierung von ORM-Tools angesprochen und vorgeführt, wie man deren traurige Leistungen in bestimmten Fällen korrigieren kann.
  • Clustering Data: erklärt den clustering factor und die Leistungsfähigkeit von index-only scans (covering indexes; "the second power of indexing"); außerdem wird die Struktur von IOTs (bzw. clustered indexes) erläutert. 
  • Sorting and Grouping: erklärt die Möglichkeiten zur Vermeidung von Sortierungen bei ORDER BY und GROUP BY Operationen durch die Nutzung geeigneter Indizes ("the third power of indexing", wobei die Verarbeitung "pipelined" erfolgt: der nächste Verarbeitungsschritt muss also nicht das Ende der Sortierung abwarten). Außerdem werden die Sortierreihenfolge (ASC, DESC) und die Position von NULL-Werten (FIRST, LAST) beim Sortieren thematisiert.
  • Partial Results: zeigt effiziente Verfahren zur Ausgabe paginierter Ergebnisse und geht (knapp) auf analytische Funktionen ein.
  • Modifiying Data: erklärt die Wirkung von Indizes auf DML-Operationen.
  • Appendix A: mit Hinweisen zur Darstellung und Interpretation von Ausführungsplänen in den behandelten RDBMS.
Ohne jeden Zweifel kennt der Autor seine Materie sehr genau - und ist dazu in der Lage, sie zu vermitteln.  Dabei bleibt die Darstellung nicht bei Behauptungen, sondern führt die angesprochenen Effekte immer wieder an praktischen Beispielen vor (sehr häufig sind das Ausführungspläne). In einigen Fällen dienen übersichtliche Grafiken zur Visualisierung von Zusammenhängen (Struktur von Branch- und Leaf-Knoten). Ein häufiges Phänomen bei meiner Lektüre war der Gedanke: da fehlt aber noch der Hinweis auf Effekt xyz (z.B. bind peeking, adaptive cursor sharing), der dann mit schöner Regelmäßigkeit wenige Seiten später erschien: aus didaktischer Sicht ist das wahrscheinlich günstig: zuerst wird das grundlegende Phänomen dargestellt, die Spezialfälle kommen dann mit einem gewissen Abstand. Ein anderer Punkt, der mir gut gefällt, ist der Hinweis auf einige klassische Mythen der Indizierung, z.B. auf die "unbalanced trees", die man durch regelmäßigen Rebuild bei Laune halten muss (aus Gründen der Deutlichkeit: es gibt keine "unbalanced trees" in b(alanced)*Tree-Indizes; und ein Index-Rebuild ist nur in sehr wenigen - und klar bestimmbaren - Fällen nützlich, auch wenn auf gewissen Seiten, die bei der Google-Suche häufig ganz oben erscheinen, etwas anderes behauptet wird oder wurde). Zu den Qualitäten des Buchs gehört auch die sprachliche Klarheit und pointierte Darstellung (wichtige Punkte werden als Merksätze grafisch hervorgehoben), wobei ich die englische Version gelesen habe, aber keinen Grund habe anzunehmen, dass Gleiches nicht auch für die deutsche Version gilt.

Gut gefällt mir wohl auch, dass die Einschätzungen des Autors in nahezu allen wichtigen Punkten mit den meinen übereinstimmen. Ein Punkt, den ich vielleicht anders akzentuieren würde, ist die Rolle von Bindewerten: natürlich sind sie in OLTP-Systemen zur Vermeidung von contention extrem wichtig, aber andererseits nehmen sie dem Optimizer relevante Informationen. Da ich mich aber auch eher mit ETL-Fragen im DWH-Kontext beschäftige, lässt sich dieser Aspekt vermutlich ziemlich schnell abhaken (ich glaube, das ist ein Punkt in dem auch die Propheten Kyte und Lewis leicht abweichende Positionen einnehmen). Ein paar kleinere Details habe ich in den Ausführungen vermisst (z.B. den INDEX SKIP SCAN, obwohl, so richtig vermisse ich den eigentlich nicht; den FIRST_ROWS_n-Modus für den CBO; den rowid-guess in IOTs und deren Overflow-Segment), aber das Erstaunliche ist viel mehr, was hier alles auf weniger als 200 Seiten angesprochen wird. Eine Frage, die mich noch interessieren würde, wäre, wo um alles in der Welt man Indizes mit einer tree depth von 6 findet? (mehr als 4 habe ich auch auf relativ großen Tabellen mit mehreren Milliarden Sätzen noch nicht gesehen, aber vielleicht ist das jenseits der Oracle-Welt anders)

Ich denke, dass SQL Performance Explained ein ungeheuer nützliches Buch für jeden ist, der beginnt, sich ernsthaft mit Fragen der SQL-Optimierung auseinander zu setzen - und das sollte aus meiner Sicht eigentlich jeder Entwickler, der SQL-Code schreibt. Im Bereich der SQL-Zugriffe lassen sich Laufzeiten häufig um Größenordnungen reduzieren, wenn man den richtigen Index benutzt (bzw. im DWH-Kontext eher: nicht benutzt, denn dort sind es mir schöner Regelmäßigkeit die Index-getriebenen NL-Joins, die zu Problemen führen) - um solche Verbesserungen in anderen Teilen des Codes zu erreichen, muss man sich schon sehr viel einfallen lassen. Selbst, wenn man sich schon länger mit Fragen der SQL-Optimierung beschäftigt, wird man hier noch allerlei nützliche Hinweise finden: für mich waren das vor allem die Erläuterungen zum Verhalten anderer RDBMS, mit denen ich seltener zu tun habe (SQL Server), bzw. fast nie (MySQL, PostgreSQL). Auch habe ich mir noch nie ernsthaft darüber Gedanken gemacht, dass Indizes auf einer SQL Server-Tabelle mit clustered index notwendigerweise die gleichen Probleme haben wie sekundäre Indizes auf IOTs. Ich kenne kein anderes Buch, dass die Grundlagen der SQL-Performance-Optimierung ähnlich gut erläutern würde (vielleicht am ehesten Christian Antogninis Troubleshooting Oracle Performance, das allerdings ein größeres Vorwissen voraussetzt und auch Aspekte anspricht, die eher in den DBA-Bereich fallen). Würde ich in diesem Blog Kaufempfehlungen aussprechen, dann wäre SQL Performance Explained ein Kandidat für eine solche.

Freitag, November 09, 2012

Hekaton

Microsoft hat dieser Tage eine neue in-memory Technologie vorgestellt, die in der nächsten Version des SQL Servers verfügbar sein soll, und den schönen Namen Hekaton trägt:
Hekaton is from the Greek word ἑκατόν for “hundred”. Our design goal for the Hekaton original proof of concept prototype was to achieve 100x speedup for certain TP operations.
Das wird sicher eine interessante technische Neuerung, der Trend zur Verlagerung von I/O-Operationen in den Speicher setzt sich fort - aber irgendwie ruft die Benamung bei mir die falschen Assoziationen auf:
Als Hekatombe [...] bezeichnete man im antiken Griechenland ursprünglich ein Opfer von 100 Rindern. Der Begriff entwickelte sich aber schon frühzeitig zur allgemeinen Bezeichnung für jedes reichere, aus Tieren bestehende Opfer, das sich in Bezug auf die Zahl und Art derselben nach den Vermögensumständen der Opfernden richtete sowie nach der Gottheit, der man es weihte, und nach dem Ort oder Fest, an dem es dargebracht wurde. [...] Im übertragenen Sinn spricht man auch bei einer erschütternd großen Zahl von Menschen, die einem Unglück zum Opfer gefallen sind, von einer Hekatombe.
Nun ja, vielleicht ist meine Sensibilität an dieser Stelle übertrieben, aber ich habe im Jahr 2004 für eine Firma gearbeitet, die damals ihr (internes) Projekt Tsunami umbenennen musste...

Notizen zur Performance von DELETE-Operationen

Gute Erklärungen für die folgenden Beobachtungen könnte ich mit hoher Wahrscheinlichkeit aus der einschlägigen Literatur bekommen, aber bekanntlich ist solide Recherche der Tod der Kreativität. Daher notiere ich einfach mal ein paar Punkte, die mir dieser Tage aufgefallen sind.

Seit Jahren warne ich die Entwickler in meiner Umgebung regelmäßig vor DELETE- und UPDATE-Operationen auf großen Datenmengen, da man daran in aller Regel keine Freude hat. Sofern es möglich ist, wandele ich solche Operationen gerne in INSERTs in neue Tabellen um, die ich dann anschließend gegen die Ursprungsobjekte austausche. Aber erst in den letzten Tagen ist mir klar geworden, wie viel Arbeit DELETE tatsächlich hervorruft. Dazu ein kleines Beispiel (11.1.0.7, ASSM, 8K Blockgröße, Noarchivelog auf einem nicht ganz neuen Windows-Desktop-PC):

drop table test_delete;
create table test_delete
as
select rownum id
  from dual
connect by level <= 1000000;

--> Abgelaufen: 00:00:00.89

exec dbms_stats.gather_table_stats(user, 'TEST_DELETE')

alter system flush buffer_cache;

-- Statistikerfassung über Snapshots aus v$sesstat 
-- vor und nach dem delete (erzeugt in einer anderen Session)
delete from test_delete;
--> Abgelaufen: 00:00:12.51

Während die Anlage der einspaltigen Tabelle mit 1M rows also weniger als 1 sec. benötigt, läuft das DELETE-Kommando über 12 sec. Ein Blick auf die Statistiken (bzw. die Deltas) in v$sesstat zeigt dabei ein paar interessante Werte, zu denen ich im Folgenden behutsame Interpretationen anschließe und dabei die Erläuterungen der Dokumentation berücksichtige:

-- Zeitangaben
NAME                                               DIFF
-------------------------------------------- ----------
DB time                                            1250
CPU used by this session                            761
redo log space wait time                            383
change write time                                   165
user I/O wait time                                   61

Demnach ist der eigentliche I/O-Anteil an der Laufzeit sehr bescheiden. Hoch sind hingegen die CPU-Nutzung und die Wartezeiten auf Platz im Log-Buffer (in meiner Test-Datenbank sind die Größe von log buffer und online redo logs nicht optimiert). Dass hier Wartezeiten entstehen, liegt nicht zuletzt an den Datenmengen, die in diesem Zusammenhang bewegt werden:

NAME                                               DIFF
-------------------------------------------- ----------
redo size                                     237244384 -- ca. 226 MB
undo change vector size                       103994836 -- ca.  99 MB
physical read bytes                            12804096 -- ca.  12 MB

Dabei entsprechen die 12 MB physical read bytes nahezu exakt der tatsächlichen Größe des Segments, das laut USER_TABLES 1557 Blocks umfasst (1557 * 8192 = 12754944). Das Löschen einer Tabelle von 12 MB führt demnach zur Erzeugung von über 200 MB redo und fast 100 MB undo! Dabei ist das Verhältnis von redo zu undo erst einmal keine besondere Überraschung, da auch zu jedem undo record redo-Informationen erzeugt werden müssen (in einem früheren Test-Lauf hatte ich auch noch IMU(also in-memory-undo)-Angaben, die anzeigen, dass das DELETE zunächst den in-memory-undo-Mechanismus verwendete, aber beim Erreichen eines gewissen Volumens davon wieder Abstand nahm - so jedenfalls deute ich den Wert IMU undo allocation size: 63996; habe aber gerade Jonathan Lewis' Oracle Core nicht zur Hand, wo das Verhalten im Detail geschrieben wird. Ein paar grundlegende Aussagen zu diesem Thema hat Jonathan Lewis auch gelegentlich in einem OTN-Thread gemacht. Im aktuellen Test-Lauf spielte IMU dann aber keine Rolle). Aber das Verhältnis der Netto-Datenmenge in der Tabelle zu undo und redo ist doch deutlich extremer als ich es erwartet hätte. Interessant sind auch die Statistiken zu den Block-Zugriffen, hier kombiniert mit allen Angaben, deren Wert im Umkreis von 1M liegt:

NAME                                               DIFF
-------------------------------------------- ----------
db block changes                                2019636
session logical reads                           1039698
db block gets                                   1037897
db block gets from cache                        1037897
redo entries                                    1006269
HSC Heap Segment Block Changes                  1000000
table scan rows gotten                          1000000
buffer is pinned count                           998477
free buffer requested                             16246
consistent gets                                    1801
consistent gets from cache                         1801
consistent gets from cache (fastpath)              1701
no work - consistent read gets                     1658
physical reads                                     1563
physical reads cache                               1563
switch current to new buffer                       1523
table scan blocks gotten                           1523
physical reads cache prefetch                      1495
buffer is not pinned count                          271
db block gets from cache (fastpath)                 200

Keine Überraschungen sind die table scan rows gotten. Ansonsten sehe ich diverse Werte, die im Bereich der Anzahl der Tabellenblocks (1557) liegen und andere, die eher der Satzanzahl entsprechen. Eine Ausnahme sind die db block changes, die die Dokumentation folgendermaßen erklärt: "the total number of changes that were part of an update or delete operation that were made to all blocks in the SGA. Such changes generate redo log entries and hence become permanent changes to the database if the transaction is committed." Blocks in der SGA sind natürlich auch die undo Blöcke, so dass die Verdopplung des Werts gegenüber 1M verständlich wird. Festzustellen ist in jedem Fall, dass die Anzahl der db block gets ("Number of times a CURRENT block was requested") in der Nähe der Satzanzahl liegt: offenbar ist hier keine BULK-artige Zusammenfassung mehrerer Änderungen eines Blocks möglich - und tatsächlich ändern sich redo size und undo change vector size nicht, wenn man die Löschung der Daten satzweise durchführt:

-- basierend auf dem ersten Test
create index ix_test_delete on test_delete(id);

-- Fall 1:
delete from test_delete;
--> Abgelaufen: 00:00:28.60
-- die Laufzeit ist also gegenüber dem ursprünglichen Test
-- ohne Index mehr als verdoppelt

-- Fall 2:
begin
for i in 1..1000000 loop
delete from TEST_DELETE where id = i;
end loop;
end;
/
--> Abgelaufen: 00:01:23.49

In beiden Fällen kommt man auf > 400MB redo und > 200MB undo, was angesichts des zusätzlichen Index in der Relation plausibel erscheint. Davon abgesehen ist das DELETE über alle Sätze natürlich schneller als die Löschung über Einzelkommandos: unter anderem steigt die Anzahl der consistent gets (1954 -> 3002599), da für jedes einzelne DELETE noch einmal ein für den Start der Transaktion konsistenter Zustand des Blocks, der geändert werden soll, erzeugt werden muss.

Viel weiter komme ich an dieser Stelle heute nicht mehr, aber zumindest die Beobachtung, dass die Performance von DELETE-Operationen weniger vom Datenvolumen als von der Anzahl geänderter Sätze abhängt, ist etwas, das ich mir merken sollte.

Mittwoch, November 07, 2012

Partitioned Outer Join

Beim Blick auf die nützliche Zusammenfassung der SQL-Optionen für Analyse und Reporting in der Dokumentation ist mir dieser Tage der (in 10g eingeführte) Partitioned Outer Join wieder ins Bewusstsein gekommen, dem ich wohl in der Vergangenheit schon mal begegnet war, damals aber noch nicht so genau wusste, wozu man ihn denn gebrauchen sollte. Da mir das inzwischen klarer ist, hier ein Beispiel:

Gegeben sind eine Fakten-Tabelle mit den jährlichen Umsätzen von Abteilungen (DEPTNO) und eine Datums-Dimension - beide im Beispiel so schmal wie möglich:

drop table d_test;
drop table f_test;

create table d_test (
    year number
);    

insert into d_test(year) values (2010);
insert into d_test(year) values (2011);
insert into d_test(year) values (2012);
insert into d_test(year) values (2013);

create table f_test (
    deptno number
  , year number
  , turnover number
);

insert into f_test (deptno, year, turnover) values (10, 2010, 500);
insert into f_test (deptno, year, turnover) values (10, 2011, 600);
insert into f_test (deptno, year, turnover) values (10, 2012, 500);
insert into f_test (deptno, year, turnover) values (20, 2011, 500);
insert into f_test (deptno, year, turnover) values (20, 2012, 700);

select * from d_test;

      YEAR
----------
      2010
      2011
      2012
      2013

select * from f_test;

    DEPTNO       YEAR   TURNOVER
---------- ---------- ----------
        10       2010        500
        10       2011        600
        10       2012        500
        20       2011        500
        20       2012        700

Um aus diesen Daten einen Bericht zum machen, in dem für jede Abteilung und jedes Jahr ein Datensatz enthalten ist - also Sätze mit einem Turnover = 0 zu ergänzen für alle Abteilungen, zu denen in der Faktentabelle für ein Jahr kein Datensatz vorliegt -, gibt's verschiedene Möglichkeiten. Eine Variante wäre, zunächst eine Referenz als Kreuzprodukt aller Jahre und Abteilungen zu bilden und diese dann per outer join mit den Fakten zu verbinden. Das funktioniert, ist aber relativ sperrig. Nicht möglich ist in diesem Fall der einfache Outer Join:

select r.year
     , t.year
     , t.deptno
     , t.turnover
  from f_test t
 right outer join
       d_test r
    on (t.year = r.year);

      YEAR       YEAR     DEPTNO   TURNOVER
---------- ---------- ---------- ----------
      2010       2010         10        500
      2011       2011         10        600
      2012       2012         10        500
      2011       2011         20        500
      2012       2012         20        700
      2013

Damit wird zwar das in den Fakten fehlende Jahr ergänzt, aber nur einmal und ohne Bezug zu den Abteilungen. Um das gewünschte Ergebnis zu erhalten, muss das Jahr an jede einzelne Abteilung gejoint werden: und genau das leistet der Partitioned Outer Join, bei dem die (zusätzliche Partitions-) Join-Bedingung in einer PARTITION BY-Klausel eingesetzt wird - womit sich PARTITION BY in die Reihe jener Oracle-Begriffe stellt, die je nach Kontext sehr viele unterschiedliche Dinge bezeichnen können ...

select t.deptno
     , t.year
     , r.year
     , t.turnover
  from f_test t partition by (deptno)
 right outer join
       d_test r
    on (t.year = r.year);

    DEPTNO       YEAR       YEAR   TURNOVER
---------- ---------- ---------- ----------
        10       2010       2010        500
        10       2011       2011        600
        10       2012       2012        500
        10                  2013
        20                  2010
        20       2011       2011        500
        20       2012       2012        700
        20                  2013

-- mit etwas Glättung:
select t.deptno
     , r.year
     , coalesce(t.turnover, 0) turnover
  from f_test t partition by (deptno)
 right outer join
       d_test r
    on (t.year = r.year)
 order by t.deptno
        , r.year

    DEPTNO       YEAR   TURNOVER
---------- ---------- ----------
        10       2010        500
        10       2011        600
        10       2012        500
        10       2013          0
        20       2010          0
        20       2011        500
        20       2012        700
        20       2013          0

Hier bekomme ich also die gewünschten acht Ergebnissätze (2 Abteilungen * 4 Jahre). Im Plan sieht man für diesen Fall einen klaren Hinweis auf die durchgeführte Operation:

--------------------------------------------------------------------------------------
| Id  | Operation                   | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |        |     1 |    65 |     9  (34)| 00:00:01 |
|   1 |  VIEW                       |        |     1 |    65 |     8  (25)| 00:00:01 |
|   2 |   MERGE JOIN PARTITION OUTER|        |     1 |    52 |     8  (25)| 00:00:01 |
|   3 |    SORT JOIN                |        |     4 |    52 |     4  (25)| 00:00:01 |
|   4 |     TABLE ACCESS FULL       | D_TEST |     4 |    52 |     3   (0)| 00:00:01 |
|*  5 |    SORT PARTITION JOIN      |        |     5 |   195 |     4  (25)| 00:00:01 |
|   6 |     TABLE ACCESS FULL       | F_TEST |     5 |   195 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

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

   5 - access("T"."YEAR"="R"."YEAR")
       filter("T"."YEAR"="R"."YEAR")

-- zum Vergleich der Plan für eine Query mit einer 
-- per cartesian join erzeugten Referenz (bei der
-- ich auch noch eine zweite Dimension ergänze und
-- feststelle, dass meine Namensgebung im Test mal
-- wieder nichts taugt ...
create table d_dept (
    deptno number
);

insert into d_dept (deptno) values (10);
insert into d_dept (deptno) values (20);

with
reference as (
select d1.deptno
     , d2.year
  from d_dept d1
     , d_test d2
)
select r.deptno
     , r.year
     , coalesce(t.turnover, 0) turnover
  from f_test t
 right outer join
       reference r
    on (t.year = r.year and t.deptno = r.deptno)
 order by t.deptno
        , r.year;

----------------------------------------------------------------------------------
| Id  | Operation               | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |        |     8 |   520 |    12  (17)| 00:00:01 |
|   1 |  SORT ORDER BY          |        |     8 |   520 |    12  (17)| 00:00:01 |
|*  2 |   HASH JOIN OUTER       |        |     8 |   520 |    11  (10)| 00:00:01 |
|   3 |    VIEW                 |        |     8 |   208 |     7   (0)| 00:00:01 |
|   4 |     MERGE JOIN CARTESIAN|        |     8 |   208 |     7   (0)| 00:00:01 |
|   5 |      TABLE ACCESS FULL  | D_DEPT |     2 |    26 |     3   (0)| 00:00:01 |
|   6 |      BUFFER SORT        |        |     4 |    52 |     4   (0)| 00:00:01 |
|   7 |       TABLE ACCESS FULL | D_TEST |     4 |    52 |     2   (0)| 00:00:01 |
|   8 |    TABLE ACCESS FULL    | F_TEST |     5 |   195 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------------

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

   2 - access("T"."DEPTNO"(+)="R"."DEPTNO" AND "T"."YEAR"(+)="R"."YEAR")

Offensichtlich ist, dass für den Partitioned Outer Join spezielle Schritte erscheinen (MERGE JOIN PARTITION OUTER, SORT PARTITION JOIN) und dass die Cardinality nur im Fall der Query mit Referenz korrekt (= 8) bestimmt wird. Über Hints konnte ich für den Partitioned Outer Join den MERGE JOIN auch in einen NESTED LOOPS umwandeln, allerdings habe ich es auf Anhieb nicht geschafft, einen HASH JOIN daraus zu machen. Auf der Suche nach einer Erklärung dafür, bin ich bei einem recht interessanten Thread (von 2005) in oracle-l gelandet, in dem Christian Antognini die Frage stellt, ob der Partitioned Outer Join die Verwendung der Join-Bedingung als access-Prädikat ausschließt und Lex de Haan die ANSI-Definition der Funktionalität zitiert. Die dort aufgeworfenen Fragen zeigen in jedem Fall, dass das Performance-Verhalten des Partitioned Outer Join ein Thema ist, das sich nicht ohne umfassendere Untersuchung erläutern lässt.

Außerdem habe ich dann auf der Suche nach Aussagen zur Arbeitsweise des Partitioned Outer Join auch noch einen Artikel von Adrian Billington entdeckt, der so ziemlich alles enthält, was ich hier aufgeschrieben habe, und noch ein paar zusätzliche Punkte - anders hätte ich's mir beim Herrn Billington auch nicht vorstellen können.

Dienstag, November 06, 2012

IFFS und Filterprädikate

Diesmal habe ich den Titel nicht - wie üblich - aus mangelnder Sorgfalt, sondern mit Bedacht vage gewählt, um dem Eintrag nicht seine ohnehin schon bescheidene Pointe zu nehmen... Davon abgesehen ist mir der beschriebene Effekt bestimmt schon ziemlich häufig begegnet, ohne dass ich darüber nachgedacht hätte. Aber genug der Vorrede: heute habe ich in einem Execution Plan ungefähr Folgendes gesehen:

-- 11.2.0.1

explain plan for
select *
  from test
 where c = 1;

select * from table(dbms_xplan.display);

Plan hash value: 850129961

--------------------------------------------------------------------------------
| Id  | Operation            | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |         |  1000 | 38000 |    22   (0)| 00:00:01 |
|*  1 |  INDEX FAST FULL SCAN| PK_TEST |  1000 | 38000 |    22   (0)| 00:00:01 |
--------------------------------------------------------------------------------

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

   1 - filter("C"=1)

Daraufhin habe ich einen Blick auf die Index-Spalten geworfen:

select column_name
     , column_position
  from user_ind_columns
 where index_name = 'PK_TEST';

COLUMN_NAME                    COLUMN_POSITION
------------------------------ ---------------
A                                            1

Anschließend habe ich mich einen Moment lang gewundert: ein IFFS mit einem Filterkriterium, das gar nicht im Index enthalten ist? Ein Blick auf die Objekt-Definition hat die Überraschung dann beendet - und auch ich spare mir hier jetzt die dramatische Steigerung:

create table test (
    a
  , b
  , c
  , constraint pk_test primary key(a)
) organization index
as
select rownum a
     , 'xxxxxxxxxx' b
     , mod(rownum, 10) c
  from dual
connect by level <= 10000

Es handelt sich also um eine IOT und deren Auftreten wird im Execution Plan offenbar immer als IFFS des PK-Index dargestellt, was durchaus einleuchtet, da das Index-Segment im Fall meiner Test-Tabelle ohne Overflow-Segment ja das einzige physikalische Objekt ist, auf das man sich beziehen kann. Und somit ist dann auch die Filteroperation einleuchtend, da das Index-Segment natürlich alle Tabellen-Attributen enthält - nur die Spaltenangabe der user_ind_columns passt nicht ganz zu den anderen Informationen. Da aber auch nur die PK-Spalte A im Beispiel als access-Prädikat verwendet werden kann, bin ich auch mit dieser Klassifizierung zufrieden.

Freitag, November 02, 2012

Oracle System Architektur (James Morle)

James Morle hat eine Serie zum Thema System Architecture gestartet, die eine ganze Reihe von Aspekten beleuchten soll, die beim Design einer Oracle Datenbank relevant sind.Wie der Autor bin auch ich gespannt, ob er dieses Projekt abschließen wird: "Now I’m looking at the list it seems more like a multi-volume book than a handful of blog posts." Eine andere Frage ist, ob ich die Serie hier komplett verlinken (und kommentieren?) werde, aber ich will mich bemühen:
  • System Architecture Series: Introduction to the Series and Licensing: beschäftigt sich mit der Frage der erforderlichen Oracle-Lizenzen. Dabei weist der Autor auf die Rolle der CPU cores (und der gewichtenden core factors) hin. Außerdem erklärt er, dass von aktuelleren Lizenzbedingungen in aller Regel nur Oracle profitiert und man deshalb nach Möglichkeit bei den alten Lizenzen bleiben sollte. Auch zeigt sein Beispiel, wie schnell Oracle-Lizenzen furchtbar teuer werden können (im Beispiel: 16.000$ Hardware zu 1.224.000$ für die Lizenzen)

Bestimmung von Intervallüberschneidungen

Im OTN-Forum SQL und PL/SQL hat Frank Kulash eine sehr schöne Lösung für das Problem der Bestimmung der Schnittmenge mehrerer Zeitintervalle geliefert, die eine Kombination den analytischen Funktionen LEAD, COUNT und SUM verwendet. Entscheidend ist die Betrachtung der Intervallanfänge als Events, die gezählt und bewertet werden.

Ich hatte ich Thread eine sehr viel sperrigere Variante angeboten, die die Intervalle in ihre Einzelelemente splittete (in diesem Fall Sekunden) - für längere Intervalle könnte man damit ziemlich effektiv Last auf dem Server erzeugen ... (was mir schon klar war, als ich das SQL schrieb: ich wollte erst einmal sehen, ob mir überhaupt eine Lösung für das, wie ich fand, interessante Problem einfallen würde).

Data Guard

Data Guard ist auch eine Technik, mit der ich mich noch nie ernsthaft beschäftigt habe - aber in diesem Fall könnte ich das (anders als im Exadata-Fall) auch auf einem simplen Desktop-System. Wie man einen solchen Test durchführen kann, erklärt Howard Rogers in seinem Blog - und mir fällt niemand ein, der bessere Anleitungen schreibt als der Herr Rogers. Die Beantwortung der Frage, was Data Guard eigentlich ist, überlasse ich ihm dann auch gleich:
Data Guard is Oracle’s premium “high availability” database architecture. “Premium” in this context means “Enterprise Edition Only”, so it costs big dollars. Happily, you are allowed to set it up and test it out for free. “High Availability” means that, unlike RAC, it uses two (or more) databases to store identical copies of your data in different physical locations. Should the proverbial meteorite strike, one of them should still be in existence and thus access to your data remains assured. 
Data Guard is, therefore, a highly-interesting technology that is pricey but invaluable. Getting a taste of it on a desktop is therefore something budding DBAs should definitely be interested in. This article will accordingly show you how to go about setting up an 11g Release 2 physical Data Guard configuration and what’s involved in managing it to cope with disasters and other management needs.
Nachtrag 19.02.2015: wieder ein Link auf eine Webseite von Howard Rogers, der keine Dauer beschieden war...

Donnerstag, November 01, 2012

Exadata Storage Indizes

Da ich noch nicht mit Exadata gearbeitet habe - und nicht damit experimentieren kann - fällt es mir schwer, mich ohne Praxisbezug mit dem Thema zu beschäftigen. Trotzdem hier ein paar Links auf Richard Footes Erklärung der Exadata Storage Indizes:
Und noch eine Serie mit dem Vergleich von herkömmlichen und Storage-Indizes, deren Grundgedanke ist, dass Storage-Indizes nicht alles leisten können, was die traditionellen Indizes leisten: