Montag, Februar 18, 2013

MERGE im SQL Server

Zu den Dingen, die ich in der Vergangenheit in Hinblick auf den SQL Server häufiger gelobt habe, gehört das extrem flexible und mächtige MERGE-Statement, mit dem man z.B. die Ergänzung einer Slowly Changing Dimension Typ 2 in einer einzigen Operation zusammenfassen kann (was in Oracle mehrere SQL-Schritte erfordert, da es dort unter anderem nicht die Unterscheidung zwischen NOT MATCHED BY SOURCE und NOT MATCHED BY TARGET gibt). Leider scheint diese Komplexität aber auf Kosten der Stabilität zu gehen, wie Aaron Bertrand dieser Tage festgestellt hat: Im vorgestellten Fall führen MERGEs auf indexed views (also jenen Objekten, die Oracle und andere als Materialized Views bezeichnen) unter bestimmten – und alles andere als exotischen – Bedingungen zu einem „wrong result“ bug (einem Fehlerfall, der in relationalen Datenbanken deutlich häufiger vorkommt, als man annehmen sollte). Und offenbar ist das nur einer von relativ vielen Bugs, die beim SQL Server im Zusammenhang mit MERGE auftreten (und die zumindest zum Teil mit den Problemen zu tun haben, die der SQL Server immer noch mit der Sicherstellung der Lesekonsistenz hat) und deshalb kommt der Herr Bertrand zu einem recht unerfreulichen Fazit:
Personally, I think the syntax is great (albeit daunting to learn), but every time an issue comes up, it erodes my confidence in the practicality of replacing existing DML with the new construct.
With that in mind, not to be Chicken Little, but I would not feel comfortable recommending anyone to use MERGE unless they implement extremely comprehensive testing. Some of these issues are also present with standard UPSERT methodologies, but the problems are more obvious there. MERGE, merely through its single-statement nature, makes you want to believe in magic. Maybe someday it will deliver, but right now I know it’s not going to be able to saw a person in half without some serious help.
Offen gesagt halte ich “extremely comprehensive testing“ für ziemlich viel verlangt, wenn man mit Grundfunktionalitäten der Datenbank arbeitet … - aber ich spare mir den Kommentar, das so was (bzw. Ähnliches) mit Oracle nicht passieren könnte (obwohl mir dort eine grundsätzliche Instabilität des MERGE-Befehls nicht bekannt ist).

SPM und adaptive cursor sharing

Beinahe hätte ich einen Artikel im Oracle Optimizer blog unerwähnt gelassen ... Diesmal schreibt Allison Lee (nehme ich jedenfalls an: im Artikel erscheint nur der Vorname) über das Zusammenspiel von adaptive cursor sharing (ACS) und SQL plan management (SPM). Grundsätzlich gilt dabei:
If a child cursor is bind-aware, the decision to share or not is made irrespective of whether the query is controlled by SPM. But once the query and its current bind values are sent to the optimizer for optimization, SPM constrains the optimizer's choice of plans, without regard to whether this query is being optimized due to ACS.
Zur Erläuterung der Aussage dient ein Beispiel mit einer Tabelle mit relativ starken skew Effekten (also einer deutlichen ungleich-Verteilung der Werte), auf die mit drei Queries mit unterschiedlichen Bindewerten zugegriffen wird, wobei der Hint BIND_AWARE dafür sorgt, dass ACS bei der ersten Ausführung wirksam  wird. Im Beispiel werden nur zwei der drei Pläne über die DBMS_SPM-Prozedur LOAD_PLANS_FROM_CURSOR_CACHE geladen. Daraus ergibt sich: " Now if we run the query with the three different bind values, SPM will constrain the optimizer to pick from the two accepted plans in the SQL plan baseline." Mein Eindruck ist, dass die beiden Features nicht unbedingt vollständig miteinander harmonieren: für eine Query, für die ACS wirksam ist, würde ich SPM nicht unbedingt in Betracht ziehen, sofern die Anzahl potentiell relevanter Pläne nicht sehr überschaubar ist.

Donnerstag, Februar 14, 2013

SCD-Typen

Zu meinen schlechten Angewohnheiten gehört es, Datenbankobjekte mit Nummern zu versehen, was man mit viel gutem Willen als Versionierungsversuch deuten mag - mit etwas Glück habe ich mir dann auch noch irgendwo notiert, wo die Unterschiede der Versionen liegen und warum ich sie nicht gleich gelöscht habe. Insofern darf ich mich wahrscheinlich nicht über den Herrn Kimball und seine Nummerierung von SCD-Typen beschweren. Falls ich dabei wieder mal den Überblick verlieren sollte, kann ich jedenfalls die Details im Design Tip #152 Slowly Changing Dimension Types 0, 4, 5, 6 and 7 von Margy Ross auf der Webseite der Kimball Group nachlesen. Dabei verwenden die Typen oberhalb von 3 eine Minidimension für häufig geänderte Werte und/oder kombinieren einfachere SCD-Typen.

Mittwoch, Februar 13, 2013

SQL Plan Management und Upgrades

Jonathan Lewis zeigt in seinem Blog ein Beispiel dafür, dass die Übernahme von outline data - also den Hints, die Oracle beim Speichern einer SQL Plan Baseline sichert - bei einem Versions-Upgrade nicht in jedem Fall das gewünschte Ergebnis bringt, da die Hints über Versionsgrenzen hinweg nicht unbedingt ausreichend präzise sind, um einen gewünschten Plan zu definieren. Im vorgestellten Beispiel führt das Upgrade von 10.2.0.3 auf 11.2.0.3 zu Problemen, da die Weiterentwicklung der NL Optimierungen (nlj_prefetch bzw. nlj_batching) dem CBO neue Entscheidungsmöglichkeiten gibt. Dieser Effekt ist vermutlich nicht auf das gegebene Beispiel beschränkt.

Samstag, Februar 09, 2013

Sichtbarkeit von dynamic sampling

Dom Brooks weist darauf hin, dass man der OTHER_XML-Spalte in V$SQL_PLAN entnehmen kann, ob für eine Query dynamic sampling verwendet wurde, wobei man in 11.1 nur eine ja/nein-Angabe und in 11.2 das sampling level findet. Außerdem liefert er Queries zum Auslesen der XML-Werte. Die OTHER_XML-Spalten diverser dictionary views mit Plan-Bezug enthalten eine ganze Menge wichtiger Informationen (Bindewerte, Outline-Hints etc.), aber mir persönlich wäre es lieber, wenn man dafür eine andere Repräsentation gewählt hätte.

Freitag, Februar 08, 2013

ANSI Join Syntax

Dieser Tage hat Charles Hooper einen Artikel zum Thema ANSI Join Syntax geschrieben, in dem er die in seinem Beispiel auftretenden Transformations-Entscheidungen des CBO genauer untersucht. Ich dachte kurz daran, einen Kommentar zum Thema zu schreiben, sah dann aber, dass so ziemlich alles, was ich anmerken wollte, bereits in einem älteren Artikel beim Herrn Hooper zu finden war, zu dem Tim Hall und William Robertson ein paar Kommentare zur Verteidigung der ANSI-Syntax beigesteuert hatten. Relevanter als die Frage der Lesbarkeit ist aber die lange Bug-Liste, die dort zur ANSI-Join-Syntax aufgeführt ist. Hier noch die Links zu den beiden Artikeln:

Montag, Februar 04, 2013

Bestimmung der compression von Tabellen

Heute wurde im OTN Forum die Frage gestellt: woran kann man erkennen, ob die Inhalte einer Partition komprimiert sind? Ich denke, das ist eine ziemlich interessante Frage, und eine, auf die Jonathan Lewis in einem Kommentar zu einem seiner Artikel zum Thema compression auf der AllThingsOracle-Seite vor kurzem eine kompakte Antwort gegeben hat:
Q: Is there a way to tell if the data is actually compressed?
A: In effect, no. 
You might be able to make a reasonable guess - check the average rows per block (num_rows / blocks), then cross-check with the avg_row_len (which reports the uncompressed row length, even when the rows are compressed). You could also use dbms_rowid to convert rowids to file and block numbers and check the number of rows per block individually - because you might have a table where half the data was compressed and half wasn't. 
In principle there is a method, mentioned originally in the 9.2 manuals I think. Column spare1 of table sys.seg$ has two bits set to show whether or not a segment is compressed and whether it holds any compressed data, so you could query seg$ (joined to various other data dictionary tables) to check. The tests are: bitand(spare1, 2048) = 2048 to check if the segment is defined with compression, and bitand(spare1, 4096) = 4096 to check if any of the data is compressed - unfortunately when I did a quick check of bit 4096 in a recent version of Oracle I found that it got set as soon as bit 2048 got set. Even it it worked, of course, all you could learn from a single bit was that some of the data in the table was compressed, you'd still have no idea about how much was compressed.
Auf Anhieb könnte man meinen, dass der Fall viel einfacher läge, denn schließlich enthält dba_tab_partitions eine Spalte COMPRESSION (und eine weiter Spalte COMPRESS_FOR). Aber darin steht nur der für folgende Operationen gültige Status: zum aktuellen Zustand gibt es keine Aussage. Dazu ein Beispiel:

drop table test_part_compress;

create table test_part_compress ( 
    startdate date
  , col1 number
  , col2 varchar2(100) 
)
partition by range (startdate) (
    partition p1 values less than (to_date('01.01.2013','dd.mm.yyyy'))
  , partition p2 values less than (to_date('01.02.2013','dd.mm.yyyy'))
) tablespace test_ts;

insert into test_part_compress
select to_date('31.12.2012', 'dd.mm.yyyy') + mod(rownum, 2) startdate
     , mod(rownum, 10) col1
     , lpad('*', 100, '*') col2
  from dual
connect by level <= 10000;

exec dbms_stats.gather_table_stats(user, 'test_part_compress')

select partition_name
     , num_rows
     , blocks
     , compression
     , compress_for
  from dba_tab_partitions
 where table_name = 'TEST_PART_COMPRESS';

PARTITION_NAME                   NUM_ROWS     BLOCKS COMPRESS COMPRESS_FOR
------------------------------ ---------- ---------- -------- ------------
P1                                   5000         84 DISABLED
P2                                   5000         84 DISABLED

alter table test_part_compress modify partition p1 compress;

exec dbms_stats.gather_table_stats(user, 'test_part_compress')

select partition_name
     , num_rows
     , blocks
     , compression
     , compress_for
  from dba_tab_partitions
 where table_name = 'TEST_PART_COMPRESS';

PARTITION_NAME                   NUM_ROWS     BLOCKS COMPRESS COMPRESS_FOR
------------------------------ ---------- ---------- -------- ------------
P1                                   5000         84 ENABLED  BASIC
P2                                   5000         84 DISABLED

alter table test_part_compress move partition p1;

exec dbms_stats.gather_table_stats(user, 'test_part_compress')

select partition_name
     , num_rows
     , blocks
     , compression
     , compress_for
  from dba_tab_partitions
 where table_name = 'TEST_PART_COMPRESS';

PARTITION_NAME                   NUM_ROWS     BLOCKS COMPRESS COMPRESS_FOR
------------------------------ ---------- ---------- -------- ------------
P1                                   5000          8 ENABLED  BASIC
P2                                   5000         84 DISABLED

alter table test_part_compress modify partition p1 nocompress;

select partition_name
     , num_rows
     , blocks
     , compression
     , compress_for
  from dba_tab_partitions
 where table_name = 'TEST_PART_COMPRESS';

PARTITION_NAME                   NUM_ROWS     BLOCKS COMPRESS COMPRESS_FOR
------------------------------ ---------- ---------- -------- ------------
P1                                   5000          8 DISABLED
P2                                   5000         84 DISABLED

Die Ergebnisse zeigen, dass die Angabe der COMPRESS oder NOCOMPRESS-Eigenschaft zunächst keine direkte Wirkung auf das vorliegende Segment hat: erst ein folgendes MOVE führt zur physikalischen Reorganisation. Das bedeutet dann auch, dass man im Fall der Partition P1 am Ende des Tests nicht ohne Weiteres bestimmen kann, ob ihr Inhalt komprimiert ist. Zur Bestimmung bleiben also die Vorschläge des Herrn Lewis. Dabei lasse ich die Prüfungen zur Größe außen vor, denn im Test ist der Unterschied zwischen 8 Blocks und 84 Blocks ausreichend deutlich:

select partition_name
     , header_file
     , header_block
  from dba_segments
 where segment_name = 'TEST_PART_COMPRESS';

PARTITION_NAME                 HEADER_FILE HEADER_BLOCK
------------------------------ ----------- ------------
P1                                       5       147272
P2                                       5       147104

select block#
     , blocks
     , bitand(spare1, 2048) compression_defined
     , bitand(spare1, 4096) compressed
  from sys.seg$ t
 where file# = 5
   and BLOCK# in (147104, 147272)

BLOCK#     BLOCKS COMPRESSION_DEFINED COMPRESSED
------ ---------- ------------------- ----------
147104         88                   0          0
147272         16                   0       4096

Das scheint also exakt die gewünschte Information zu liefern. Eine andere Variante zur Bestimmung nennt Randolf Geist in seinem Beitrag zum OTN-Thread: die Verwendung der Funktion dbms_compression.get_compression_type:

select subobject_name
     , data_object_id
  from dba_objects
 where object_name = 'TEST_PART_COMPRESS'
   and subobject_name is not null;

SUBOBJECT_NAME                 DATA_OBJECT_ID
------------------------------ --------------
P1                                     101978
P2                                     101977

select dbms_rowid.rowid_relative_fno(rowid) file_nr
     , dbms_compression.get_compression_type(user, 'TEST_PART_COMPRESS', rowid) compression_type
     , count(*) cnt
  from test_part_compress
 group by dbms_rowid.rowid_relative_fno(rowid)
        , dbms_compression.get_compression_type(user, 'TEST_PART_COMPRESS', rowid);

OBJECT COMPRESSION_TYPE        CNT
------ ---------------- ----------
101978                2       5000
101977                1       5000

Zumindest für den aktuell erreichten Zustand einer komprimierten Partition, deren Status aktuell NOCOMPRESS ist, sind die Tests demnach aussagekräftig: Für P1 wird der COMPRESSION_TYPE = 2 angegeben, der anscheinend für Basic und OLTP compression steht (sagt jedenfalls Kerry Osborne). Der Vollständigkeit halber hier noch ein paar weitere Schritte in die eingeschlagene Richtung:

-- move nach NOCOMPRESS-Definition
alter table test_part_compress move partition p1;
exec dbms_stats.gather_table_stats(user, 'test_part_compress')

PARTITION_NAME                   NUM_ROWS     BLOCKS COMPRESS COMPRESS_FOR
------------------------------ ---------- ---------- -------- ------------
P1                                   5000         74 DISABLED
P2                                   5000         84 DISABLED

-- beide Partitionen liefern jetzt COMPRESSION_TYPE = 1 (keine compression)
OBJECT COMPRESSION_TYPE        CNT
------ ---------------- ----------
101979                1       5000
101977                1       5000

BLOCK#     BLOCKS COMPRESSION_DEFINED COMPRESSED
------ ---------- ------------------- ----------
147096         80                   0          0
147104         88                   0          0

-- erneute COMPRESS-Definition
alter table test_part_compress modify partition p1 compress;
exec dbms_stats.gather_table_stats(user, 'test_part_compress')

PARTITION_NAME                   NUM_ROWS     BLOCKS COMPRESS COMPRESS_FOR
------------------------------ ---------- ---------- -------- ------------
P1                                   5000         74 ENABLED  BASIC
P2                                   5000         84 DISABLED

-- der COMPRESSION_TYPE bleibt 1, was plausibel ist, da die Daten ja tatsächlich nicht komprimiert sind
OBJECT COMPRESSION_TYPE        CNT
------ ---------------- ----------
101979                1       5000
101977                1       5000

-- aber seg$ behauptet, dass die Partition jetzt als COMPRESSed definiert ist und ihre Sätze komprimiert sind,
-- was nicht den Tatsachen entspricht
BLOCK#     BLOCKS COMPRESSION_DEFINED COMPRESSED
------ ---------- ------------------- ----------
147096         80                2048       4096
147104         88                   0          0

Auf Anhieb scheint dbms_compression.get_compression_type im gegebenen Fall also die brauchbarere Information zu liefern - und immerhin gibt es überhaupt Möglichkeiten, die unzureichende Aussage aus dba_tab_partitions zu ergänzen.

Samstag, Februar 02, 2013

Evolution der Execution Plans für skalare Subqueries

Jonathan Lewis hat dieser Tage ein Beispiel für die Weiterentwicklung der Darstellung von Ausführungsplänen für skalare Subqueries in den Version 8.1.7.4 bis 11.2.0.3. Deutlich wird dabei, dass die Plandarstellung immer besser und vollständiger wird, dass es aber auch in aktueller Releases Fälle gibt, in denen der Plan, die tatsächliche Verarbeitung nicht exakt repräsentiert. Wenn ich drüber nachdenke, ist das wahrscheinlich keine große Überraschung.