Mittwoch, Dezember 05, 2012

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.