Donnerstag, März 28, 2013

CPU-Kosten von Compression im SQL Server

Thomas Kejser hat vor ein paar Tagen ein paar Bemerkungen zu den Wirkungen von Page-Compression auf die Performance von Selects und Updates aufgeschrieben. Hier noch mal eine kurze Definition der Page-Compression, basierend auf den Aussagen der Dokumentation - demnach besteht Page Compression aus drei Teilen:
  • row compression: verwendet den kleinsten geeigneten Datentyp zur Speicherung der Daten
  • prefix compression: wiederholte prefix Werte (einzelner Spalten) werden in einer compress information (CI) Struktur direkt hinter dem page header eingefügt und an den passenden Stellen referenziert
  • dictionary compression: wiederholte Werte der page werden in die CI area eingefügt und an den passenden Stellen referenziert (wobei sich diese Ersetzung Spalten-übergreifend erfolgt)
Das Verfahren weist demnach recht massive Ähnlichkeiten zu Oracles compression auf und stellt ebenfalls eine Deduplication durch Ersetzung wiederholter Token dar (wobei Oracle im Fall von Tabellen keine prefix compression durchführt).

Beim Herrn Kejser wird eine recht große Tabelle ("the TPC-H LINEITEM table at scale factor 10. That is about 6.5GB of data.") gelesen bzw. per update geändert und im Ergebnis sind die Operationen auf dem komprimierten Objekt deutlich langsamer als die auf einer entsprechenden Tabelle ohne Komprimierung. Das Fazit lautet:
Quoting a few of my tests from my presentation, I have shown you that PAGE compression carries a very heavy CPU cost for each page access. Of course, not every workload is dominated by accessing data in pages – some are more compute heavy on the returned data. However, in these days when I/O bottlenecks can easily be removed, it is worth considering if the extra CPU cycles to save space are worth it.
It turns out that it is possible to also show another expected results: that locks are held longer when updating compressed pages (Thereby limiting scalability if the workload contains heavily contended pages).
Ein ähnlicher Versuch mit einer Oracle 11.1.0.7-Test-Datenbank liefert mir ein verwandtes Ergebnis:

drop table t1;

create table t1
as
select round(rownum/1000) col1
     , mod(rownum, 1000) col2
     , round(rownum/100) col3
     , mod(rownum, 100) col4
     , round(rownum/10) col5
     , mod(rownum, 10) col6
  from dual
connect by level <= 1000000;

insert into t1
select * from t1;

insert into t1
select * from t1;

insert into t1
select * from t1;

insert into t1
select * from t1;

drop table t2;

create table t2 compress
as
select * from t1;

exec dbms_stats.gather_table_stats(user, 'T1')
exec dbms_stats.gather_table_stats(user, 'T2')

select segment_name
     , blocks
     , round(bytes/1024/1024) mbyte 
  from dba_segments
 where segment_name in ('T1', 'T2');

SEGMENT_NAME             BLOCKS      MBYTE
-------------------- ---------- ----------
T1                        30720        480
T2                        17920        280

In diesem Fall ist der Größenunterschied zwischen der komprimierten und der nicht-komprimierten Version nicht gewaltig (Reduzierung auf 58,33% der Basisgröße), aber beim lesenden Zugriff ergibt sich eine (stabil) messbare Erhöhung von CPU_TIME und Gesamtlaufzeit für den Fall der komprimierten Tabelle T2:

select /* test t1 */
       sum(col1)
     , sum(col2)
     , sum(col3)
     , sum(col4)
     , sum(col5)
     , sum(col6)
  from t1;

select /* test t2 */
       sum(col1)
     , sum(col2)
     , sum(col3)
     , sum(col4)
     , sum(col5)
     , sum(col6)
  from t2;

select substr(sql_text, 1, 20) sql_text
     , disk_reads
     , buffer_gets
     , elapsed_time
     , cpu_time
     , user_io_wait_time
  from v$sql
 where sql_id in ('1whhzus1w5j57', '123rusnujf8dp');

SQL_TEXT             DISK_READS BUFFER_GETS ELAPSED_TIME   CPU_TIME USER_IO_WAIT_TIME
-------------------- ---------- ----------- ------------ ---------- -----------------
select /* test t1 */      30293       30302      6180032    6176061            568945
select /* test t2 */      17400       17405      7694043    7668834            328810

Zwar sinkt die USER_IO_WAIT_TIME, aber gegenüber der CPU_TIME fällt sie kaum ins Gewicht.  Anscheinend also wieder mal ein Fall, der für SQL Server und Oracle ein ähnliches Verhalten zeigt - wobei ich (ohne länger darüber nachzudenken) annehme, dass die Locking Probleme des SQL Servers bei Oracle keine Rolle spielen.

Montag, März 25, 2013

Umbenennung und Löschung von Virtual Columns

Charles Hooper hat dieser Tage einen Artikel veröffentlicht, der einige seltsame Effekte im Zusammenhang der Umbenennung von Virtual Columns und der Verwenung des Hints _OPTIMIZER_IGNORE_HINTS anspricht. Interessant finde ich dabei vor allem die seltsamen Effekte, die die Umbenennung hervorrufen kann. Dazu ein kleines Beispiel (mit 11.2.0.1.0), das auf dem Test des Herrn Hooper basiert und diesen behutsam erweitert:

drop table t1;

create table t1(
    a number
  , b number    
);

create index t1_idx on t1(a + 1);

select column_name
     , data_default
  from user_tab_cols
 where table_name = 'T1';

COLUMN_NAME                    DATA_DEFAULT
------------------------------ ------------
A
B
SYS_NC00003$                   "A"+1

Der Test legt eine Tabelle mit zwei Spalten (A, B) an und ergänzt dann einen FBI mit Bezug auf die Spalte A, was implizit zur Erzeugung einer virtuellen Spalte (SYS_NC00003$) führt. Wenn man den FBI wieder löscht, dann verschwindet auch die virtuelle Spalte:

drop index t1_idx;

select column_name
     , data_default
  from user_tab_cols
 where table_name = 'T1';

COLUMN_NAME                    DATA_DEFAULT
------------------------------ ------------
A
B

Das Verhalten ändert sich, wenn man die virtuelle Spalte explizit umbenennt. In diesem Fall bleibt die Spalte auch nach der Löschung des Index verfügbar:

create index t1_idx on t1(a + 1);

alter table t1 rename column SYS_NC00003$ to C;

drop index t1_idx;

select column_name
     , data_default
  from user_tab_cols
 where table_name = 'T1';

COLUMN_NAME                    DATA_DEFAULT
------------------------------ -------------
A
B
C                              "A"+1

Offenbar behandelt Oracle diese virtuelle Spalte jetzt als benutzerdefiniert und verzichtet deshalb auf die automatische Bereinigung. Merkwürdig wird der Fall, wenn man jetzt die Basisspalte zur virtuelle Spalte löscht:

alter table t1 drop column A;

select column_name
     , data_default
  from user_tab_cols
 where table_name = 'T1';

COLUMN_NAME                    DATA_DEFAULT
------------------------------ ------------
B
SYS_NC00002$                   "A"+1

Die Löschung der Spalte A führt demnach dazu, dass die virtuelle Spalte wieder einen synthetischen Namen erhält (allerdings nicht mehr SYS_NC00003$, sondern SYS_NC00002$) - aber nicht zur Löschung der Spalte, die jetzt ziemlich in der Luft hängt:

select b
     , SYS_NC00002$
  from t1;

select b
*
FEHLER in Zeile 1:
ORA-00904: "A": ungültiger Bezeichner

Dieser Verhalten ist auf direktem Weg nicht zu erreichen, denn der Versuch die Basisspalte einer explizit erzeugten Virtual Column zu löschen, ruft einen Fehler hervor:

create table t2(
    a number
  , b number
);

alter table t2 add c generated always as (a + 1);

alter table t2 drop column a;
alter table t2 drop column a
                           *
FEHLER in Zeile 1:
ORA-54031: Spalte, die gelöscht werden muss, wird in einem virtuellen Spaltenausdruck benutzt

Mein Eindruck ist, dass das Verhalten an dieser Stelle nicht vollständig konsistent ist: offenbar werden in diesem Fall bei der Löschung nicht alle Abhängigkeiten überprüft.

Samstag, März 23, 2013

OBIEE und Performance

Sollte ich in näherer Zukunft intensiver mit OBIEE arbeiten, dann wäre Robin Moffatts siebenteilige Serie Performance and OBIEE im Rittman/Mead-Blog eine Pflichtlektüre.

Donnerstag, März 21, 2013

Optimizer Index Caching und IOTs

Timur Akhmadeev zeigt in seinem Blog, dass das Costing für secondary indexes bei Verwendung des (spätestens seit der Ankunft von system statistics ohnehin fragwürdigen) Parameters optimizer_index_caching (bzw. eines entsprechenden Hints) zu absurd niedrigen Ergebnissen führt: anscheinend wird der Zugriff auf den PK der IOT in solchen Fällen als kostenlos betrachtet. Schuld ist möglicherweise ein Bug-Fix, der in 10.2.0.4 eingeführt wurde, aber dem Herrn Akhmadeev fehlt (genau wie mir) ein ausreichend altes Release, um diese Annahme zu überprüfen.

Dienstag, März 19, 2013

Data Pump für Partitionen

Gwen Lazenby erläutert im Blog der Oracle University, dass es seit Release 11.1 möglich ist, eine oder mehrere Partitionen einer partitionierten Tabelle mit Hilfe der Datafile Copy Option des data pump Exports in eine andere Datenbank zu transferieren. Dabei stellt Datafile Copy eine Variante zum Transportable Tablespace Feature dar, bei der (im read only Modus) ebenfalls Metadaten über expdp verschoben werden und datafiles auf OS-Ebene kopiert werden - aber im Fall von Datafile Copy ist es möglich, diesen Kopiervorgang auf einzelne datafiles - bzw. die in ihnen enthaltenen Objekte - zu beschränken. Auf syntaktischer Ebene wird die Operation durch folgende Angaben ausgewiesen:
  • table=name_der_tabelle:name_der_partition
  • transportable=always
Der Metadaten-Export enthält dann die Information, welche datafiles auf OS-Ebene kopiert werden müssen. Nach dem File-Transfer können dann die Metadaten mit impdp in die Zieldatenbank eingespielt werden, wobei die Option partition_options=departition angegeben wird, um die Partition in eine selbständige Tabelle umzuwandeln. Wenn eine Tabelle (bzw. Partition) über sehr viele datafiles verteilt ist, kann das Verfahren vermutlich extrem unscharf sein - und möglicherweise auch ein gewisses Sicherheitsrisiko mit sich bringen, sofern die Daten der Objekte, deren Metadaten nicht übertragen werden, im Rahmen des Imports nicht explizit überschrieben werden (was ich - nicht zuletzt aus Performance-Gründen - für sehr unwahrscheinlich halte), da der Hex-Editor hie möglicherweise noch interessante Strings entdecken könnte.

Mittwoch, März 13, 2013

XML DB Einführung

Eine schöne Einführung zum Thema XML DB gibt Ulrike Schwinn im aktuellen DBA Community Tipp Oracle XML DB für DBAs. Besonders interessant fand ich darin die Erläuterungen zu den Native Oracle XML DB Web Services, mit deren Hilfe man seit 11.1 mit minimalem Konfigurationsaufwand eine web service Schnittstelle verfügbar machen kann, über die z.B. PL/SQL-Prozeduren via HTTP aufgerufen werden können und ein XML-Ergebnis liefern (authentication vorausgesetzt). Eine noch detailliertere Anleitung mit allen erforderlichen Schritten findet man einmal mehr bei Tim Hall.

Dienstag, März 12, 2013

Data Warehousing mit Oracle

Dieser Tage ist es mir endlich gelungen, das Buch Data Warehousing mit Oracle von Claus Jordan, Dani Schnider, Joachim Wehner und Peter Welker fertig zu lesen. Dass die Lektüre relativ lange dauerte, ist insofern erstaunlich, als der Band mit 227 Seiten recht überschaubar ist - und es sich um ein sehr gutes Buch handelt; die Dauer der Leseoperation erklärt sich aus Wait Events, die mit dem Buch nichts zu tun hatten ...

Aber der Reihe nach: Data Warehousing mit Oracle ist 2011 im ehrwürdigen Hanser Verlag erschienen und bereits auf dem Umschlag geben die Autoren ihre Zugehörigkeit zum renommierten Beratungshaus trivadis zu erkennen und werden im Vorwort als DWH-Berater mit langjähriger Projekterfahrung ausgewiesen - und zumindest im Fall des Herrn Schnider kann ich mich an diverse interessante Blog-Artikel erinnern, die ich gelegentlich auch schon mal hier erwähnt habe. Der Band ist - wie erwähnt - recht schmal, dabei aber sehr klar gegliedert: einem Einleitungskapitel folgen Erläuterungen zu folgenden Themen:
  • Data Warehouse Grundlagen: liefert zahlreiche Definitionen und erklärt die theoretischen Grundlagen des Themas (mit kurzem Verweis auf die Positionen von Inmon und Kimball). Neben eher allgemeinen  Definitionen der relevanten Terminologie erscheinen vor allem präzise Vorschläge zu Konventionen und Vorgehensweisen - angefangen bei den Bezeichnungen der Architektur-Ebenen (Stage, Cleansing, Core - darüber dann die Data Marts), über Vorschläge zur Historisierung bis hin zu Namenskonventionen für instrumentierende Metadaten-Spalten.
  • Datenintegration: liefert Vorschläge zur Implementierung von Quality Checks und Data Profiling, erklärt unterschiedliche Vorgehensweisen zur Delta-Extraktion und beschäftigt sich eingehend mit der Implementierung und Optimierung von ETL-Prozessen.
  • Aufbau und Betrieb eines Data Warehouse: geht genauer auf die Rolle der Schichten der DWH-Architektur ein und liefert präzise Aussagen zu Funktion und Abgrenzung der Elemente. An vielen Stellen werden auf Erfahrungswerten basierende Praxishinweise gegeben: etwa der, dass ein Operational Data Store (ODS) zur Bereitstellung sehr aktueller Daten in vielen Fällen die erforderliche Mühe nicht lohnt. Daneben gibt es eine ganze Reihe präziser Vorschläge zur Datenmodellierung und zur Implementierung von Transformationen (etwa zur Versionierung von Dimensionen). Nach einer kurzen Einführung zum Thema MOLAP folgen praktische Hinweise zur Verwendung von Oracle-Features: Aussagen zu empfohlenen Blockgrößen, PGA- und SGA-Dimensionierung, Parallelisierung, Statistikerhebung, Partitionierung, Indizierung, Backup + Recovery etc. Darüber hinaus liefert das Kapitel Vorschläge zum Entwicklungsprozess, zum Einsatz von Versionsverwaltungs-Repositories und zum Deployment von Änderungen und geht recht intensiv auf das Performance-Monitoring im DWH ein.
  • Business Intelligence-Plattformen: ein recht kurzes Kapitel mit einer Vorstellung von BI-Plattformen im Allgemeinen und der OBIEE im Besonderen. Recht instruktiv, aber mit 10 Seiten doch sehr knapp.
Im Zusammenhang der Implementierungsvorschläge erscheinen neben SQL-Code auch Hinweise auf die entsprechenden Optionen der ETL-Tools von Oracle (OWB, ODI), wobei Oberflächenscreenshots die Ausnahme bleiben (was ich erwähne, weil ich in der Vergangenheit ziemlich viele Bücher aus dem Microsoft-Umfeld gelesen/durchgeblättert habe, die nicht viel mehr als kommentierte Screenshot-Sammlungen darstellten).

Zu den Pluspunkten des Buches gehören aus meiner Sicht die klare Struktur und die hohe (inhaltliche und sprachliche) Präzision der Erläuterungen. Die Autoren kommen nie ins Plaudern, sondern immer sehr schnell auf den Punkt - den Inhalt hätte man ohne große Mühe auf die doppelte Länge bringen können: mein Dank an die Verfasser, dass sie darauf verzichtet haben. Gut gefallen mir die Konventionsvorschläge, bei denen ich mich gelegentlich auch schon bedient habe, und die klaren Stellungnahmen zu zahlreichen praktischen Problemen. Bei den recht umfangreichen Ausführungen zum Thema Performance habe ich fast ausschließlich Aussagen gefunden, die sich mit meinen Ansichten decken, und nichts, was meinen deutlichen Widerspruch hervorrufen würde.

Schwieriger ist die Suche nach Kritikpunkten. An einigen Stellen wird die Darstellung doch etwas zu knapp, etwa im letzten Kapitel, bei dem ich nach Lektüre des letzten Satzes ("Der Aufruf existierender bzw. das Definieren neuer Reports erfolgt direkt von der OBIEE-Homepage") kurz darüber nachdachte, ob da vielleicht Seiten fehlten...

Insgesamt halte ich Data Warehousing mit Oracle für ein sehr lesenswertes Buch: sowohl Einsteiger als auch erfahrene DWH-Entwickler/-DBAs/-Architekten werden hier jede Menge interessanter Informationen finden, die in der praktischen Arbeit mit Oracle-DWHs sehr nützlich sein können.

Erwähnenswert ist vielleicht auch noch die "patentierte Bindung", die dafür sorgt, dass das Buch offen liegen bleibt - meistens jedenfalls ...

Montag, März 11, 2013

Analytics mit row-Angabe in der window clause

Kim Berg Hansen, der Steven Feuerstein regelmäßig bei der PL/SQL Challenge unterstützt, erläutert das unterschiedliche Verhalten von row- und range-Angaben (letztere sind der default) in der window clause von Analytics, wobei sein Beispiel eine analytische Summenfunktion zur Ermittlung einer rolling sum ist (also einer Summe bis zum gegebenen Satz): mit dem default (range) erscheint für gleiche Werte in der ORDER BY-Sortierspalte auch die gleiche aggregierte Summe, was nicht unbedingt intuitiv ist. Möchte man eine rolling sum haben, die für jeden Satz die bis dorthin aggregierten Summen ausweist, so muss man eine window clause mit row-Angabe verwenden - also z.B.:

sum(sal) over (partition by deptno
                   order by sal
                    rows between unbounded preceding and current row)

Neben dem plausibleren Ergebnis hat die Verwendung der row-Angabe auch den Vorteil, dass sie im Fall der Verschachtelung mehrerer analytischer Funktionen einen WINDOW NOSORT-Schritt an einer Stelle möglich macht, an der sonst eine zusätzliche Sortierung erfolgen müsste. Daher liefert der Herr Hansen folgende Merksätze:
  • If I am doing analytic on the entire partition (or entire resultset), then I skip the window clause (that is: I do not write ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING).
  • But once I have an ORDER BY, I generally do not rely on default window clause, but rather explicitly writes either ROWS or RANGE BETWEEN.
Meine Zusammenfassung ist in diesem Fall übrigens (wieder einmal) deutlich unverständlicher als die Quelle, die den Fall anhand kompakter Beispiele nachvollziehbar erläutert.

Mittwoch, März 06, 2013

SQL_ID ohne SQL_TEXT

Julian Dontcheff liefert eine Erklärung für das Phänomen, dass in V$SQL SQL_ID-Angaben erscheinen, zu denen V$SQLTEXT keinen Text liefern kann:
These are Oracle internal or so called pseudo cursors which are just structures providing direct access database objects by avoiding the overhead of writing an explicit SELECT statement. These structures access directly most of the time the data dictionary tables, CLOBs and BLOBs. Sometimes even NCHAR and NVARCHAR2.
Zu diesen SQL_IDs gibt es also tatsächlich keine Texte, weil es sich nicht um SQL-Statements handelt. Im Artikel wird (mit Verweis auf ein MOS-Dokument) auch noch eine View-Definition geliefert, mit deren Hilfe ein paar Informationen zu den Zugriffen zusammengestellt werden können.

Dienstag, März 05, 2013

Index-Duplikate

Bei Jonathan Lewis findet sich eine interessante Liste von Möglichkeiten, wie man in USER_IND_COLUMNS eine (anscheinend) gleiche Spaltenkombination in gleicher Reihenfolge für zwei verschiedene Indizes (anscheinend) einer Tabelle erhalten kann, ohne den Fehler ORA-01408: such column list already indexed hervorzurufen. Zu den Möglichkeiten gehören:
  • Verwendung eines nosegment Index
  • Anlage von Cluster und im Cluster integrierter Tabelle jeweils mit Index
  • Verwendung unterschiedlicher Schemata
  • Verwendung von blank oder unsichtbarem Sonderzeichen in Spaltennamen
Leider wurde ich für meinen Beitrag (Änderung der Definition von USER_IND_COLUMNS) disqualifiziert: "I think I’m going to disqualify you for tampering with intent to discredit data dictionary views." Womit er natürlich wieder mal Recht hat, der Herr Lewis.

Data Explorer

Dieser Tage wurde ein Public Preview von Microsofts Data Explorer zum Download bereit gestellt und von sachkundiger Seite kommentiert. Chris Webb liefert in diesem Zusammenhang die Definition:
In a nutshell, Data Explorer is self-service ETL for the Excel power user – it is to SSIS what PowerPivot is to SSAS. In my opinion it is just as important as PowerPivot for Microsoft’s self-service BI strategy.
Im gleichen Artikel gibt's neben grundsätzlichen Informationen noch diverse Links auf offizielle Aussagen von Microsoft. Zusätzlich hat der Herr Webb noch eine detaillierte Anleitung zum Thema Importing Data From Multiple Log Files Using Data Explorer veröffentlicht, das den Einsatz von Data Explorer am Beispiel des Imports der Statistiken seines Blogs vorführt. Wenn man an den Statistiken des Herrn Webb nicht so sehr interessiert ist, kann man auch der (möglicherweise noch detaillierteren) Anleitung folgen, mit der Dan English  die Installation des Tools und das Einlesen von Daten aus der Internet Movie Database (imdb) vorstellt. Und sollte das noch nicht genügen, dann gäbe es noch einen Artikel von Jason Thomas mit einem weiteren Tutorial und Links auf diverse weitere Blogs, auf deren Erwähnung ich an dieser Stelle verzichte.

Chris Webb geht dann noch einen Schritt weiter und zeigt in zwei Artikeln, wie man mit Hilfe des Data Explorer geographische Daten über Web Service nach Excel importieren kann. Das alles sieht recht harmlos aus und dürfte anhand der Anleitungen sehr leicht nachvollziehbar sein.

Samstag, März 02, 2013

Stack Profiler für Oracle

Tanel Poder ist fürchterlich produktiv dieser Tage, so dass ich kaum nachkomme, seine Artikel zu lesen (bzw. Podcasts anzuschauen) - oder gar darüber zu schreiben. Im Artikel Troubleshooting high CPU usage with poor-man’s stack profiler – in a one-liner! zeigt er, wie man (in Abwesenheit von SQL Monitoring, also in jedem Fall unterhalb von 11g) mit Hilfe seines (Solaris) Scripts os_explain (das seiner Meinung nach auch für andere OS anzupassen ist) bestimmen kann, welcher Aufruf den größten Anteil an der Laufzeit einer Query hat. Im Beispiel zeigt er das Ergebnis am Beispiel eines Zugriffs auf DBA_LOCK_INTERNAL. Natürlich gäbe es da auch noch ein paar andere Trace-Möglichkeiten, aber zur Untersuchung einer aktuell laufenden Operation ist das allemal ein nettes zusätzliches Werkzeug.

Kostenlose Joins

Randolf Geist präsentiert in seinem Blog ein interessantes Beispiel, in dem ein über Nested Loops verarbeiteter Outer Join mit einer Join-Bedingung, die auf der Seite der driving table eine Spalte anspricht, die immer NULL ist, vom CBO als nahezu kostenlos betrachtet wird: die Gesamtkosten der Query entsprechen denen des Zugriffs auf die driving table und der CBO addiert nur die Kosten eines einzigen Lookups, da er - aus guten Gründen - annimmt, dass die Lookups keine Treffer liefern können. Allerdings weist Randolf darauf hin, dass die Loops offenbar trotzdem ausgeführt werden, was die Entscheidung, die Kosten weitgehend zu ignorieren, doch ein wenig fragwürdig erscheinen lässt. Hier die entsprechenden rowsource Statistiken meiner Windows8-Spieldatenbank (11.2.0.1):

-------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |        |      1 |        |   1000K|00:00:01.54 |   82063 |
|   1 |  NESTED LOOPS OUTER          |        |      1 |   1000K|   1000K|00:00:01.54 |   82063 |
|   2 |   TABLE ACCESS FULL          | T1     |      1 |   1000K|   1000K|00:00:00.73 |   82063 |
|   3 |   TABLE ACCESS BY INDEX ROWID| T2     |   1000K|      1 |      0 |00:00:00.55 |       0 |
|*  4 |    INDEX RANGE SCAN          | T2_IDX |   1000K|      1 |      0 |00:00:00.26 |       0 |
-------------------------------------------------------------------------------------------------

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

   4 - access("T1"."NULL_FK"="T2"."ID")

Demnach fällt für step 3 und 4 Arbeit an (wie die Starts und A-Time anzeigen), allerdings offenbar nicht im Bereich der Lesezugriffe (Buffers = 0). Aber immerhin erkennt der CBO, dass es sich hier um einen Sonderfall handelt, der eine korrigierte Kalkulation verdient.

In einem zweiten Artikel "Cost-free" joins - 2 untersucht Randolf Geist das Verhalten von MERGE Joins, die der CBO bekanntlich relativ selten auswählt, da sie in der Regel das Sortieren beider Datenquellen erfordern (sofern diese nicht schon durch einen vorangehenden Schritt - etwa einen INDEX RANGE SCAN - sortiert sind). Interessant ist der Hinweis, dass die Reihenfolge der Datenquellen (anders als beim HASH JOIN) keine größere Rolle spielt und dass es nicht möglich ist, diese Reihenfolge zu ändern (ebenfalls anders als beim HASH JOIN). Der Fall, in dem ein kostenloser Join auftritt, ist hier ein MERGE Outer Join einer sehr großem Tabelle, die mit einer Lookup-Tabelle verknüpft wird, auf die ein INDEX UNIQUE SCAN erfolgt - wobei der Effekt problematisch wird, sobald Parallelisierung ins Spiel kommt. Aber da der Fall etwas unübersichtlich ist, kopiere ich an dieser Stelle einfach das Fazit des Artikels:
For MERGE JOINs there are some special cases where the current costing model doesn't properly reflect the actual work - together with some strange behaviour of the MERGE JOIN code when using Parallel Execution this can lead to questionable execution plans preferred by the optimizer.
Carefully check the resulting execution plans when using Parallel Execution and MERGE JOINs get preferred by the optimizer.