- die SE ist nicht dazu in der Lage mehrere Cache-Einträge zusammenzufassen (also z.B. aus dem Cache aus den Werten aller Jahres-Elemente das ALL-Element abzuleiten), weil die Einträge getrennt voneinander abgelegt werden
- die SE orientiert sich nicht an den attribute relationships, um gecachte Daten zu aggregieren. Dazu sind echte Aggregationen erforderlich oder die Definition einer natural user hierarchy.
Donnerstag, Juli 26, 2012
SSAS Storage Engine Cache Aggregation
Chris Webb erläutert in seinem Blog die Arbeitsweise des Caches der Storage Engine und weist dabei auf einige erinnerungswürdige Details hin:
Big Table Joins im SQL Server
Thomas Kejser erläutert in seinem Blog die verschiedenen Möglichkeiten, große Tabellen zu joinen - wobei seine Ausführungen zu einem großen Teil auch für Oracle-Datenbanken (und andere RDBMS) zutreffen.
Der gesamte Artikel ist sehr interessant, aber besonders wichtig scheint mir der Hinweis auf die Abhängigkeit der HASH JOIN Performance vom verfügbaren Arbeitsspeicher zu sein. Dazu ein kleines Oracle-Beispiel (11.1.0.7):
Auf diese Tabellen greift eine recht harmlose aggrregierende Query zu:
Bei manuellem Speichermanagement und sukzessiver Verkleinerung der HASH_AREA_SIZE ergeben sich in V$SQL_WORKAREA folgende Angaben:
Auffällig ist dabei:
Der gesamte Artikel ist sehr interessant, aber besonders wichtig scheint mir der Hinweis auf die Abhängigkeit der HASH JOIN Performance vom verfügbaren Arbeitsspeicher zu sein. Dazu ein kleines Oracle-Beispiel (11.1.0.7):
-- Anlage zweier (relativ) großer Tabellen -- mit jeweils 128 MB create table test_big1 as select rownum id , mod(rownum, 5) col1 , lpad('*', 100, '*') padding from dual connect by level <= 1000000; create table test_big2 as select rownum id , mod(rownum, 5) col1 , lpad('*', 100, '*') padding from dual connect by level <= 1000000; -- Statistikerfassung exec dbms_stats.gather_table_stats(user, 'test_big1') exec dbms_stats.gather_table_stats(user, 'test_big2')
Auf diese Tabellen greift eine recht harmlose aggrregierende Query zu:
select /*+ use_hash(t1 t2) */ t1.col1, count(*) from test_big1 t1 , test_big2 t2 where t1.id = t2.id group by t1.col1;
Bei manuellem Speichermanagement und sukzessiver Verkleinerung der HASH_AREA_SIZE ergeben sich in V$SQL_WORKAREA folgende Angaben:
SQL_ID LAST_MEMORY_USED LAST_EXECUTION ACTIVE_TIME LAST_TEMPSEG_SIZE ------------- ---------------- ---------------- ----------- ----------------- c4mkumm56b2xs 31068160 OPTIMAL 2394526 <-- H_A_S: 300 MB 6nfcggd6pzpjq 11768832 1 PASS 3961418 22020096 <-- H_A_S: 100 MB a2ndxvbhd3fzn 1498112 1 PASS 14735056 27262976 <-- H_A_S: 10 MB f10jfqu1tu0bw 176128 123 PASSES 142458630 28311552 <-- H_A_S: 1 MB
Auffällig ist dabei:
- die massive Laufzeitverlängerung von 2,3 sec. für die optimale Verarbeitung (ohne Auslagerung von Zwischenergebnissen in den temporary tablespace) auf 142,4 sec. für die multi pass Operation
- der deutliche Unterschied der Laufzeiten der beiden 1 pass Ausführungen (3,9 sec. zu 14,7 sec.)
Das Ergebnis ist in keiner Weise überraschend: ausreichender Arbeitsspeicher ist für die Performance von HASH JOIN Operationen ganz entscheidend.
Datapump Schema Export
Morton Braten zeigt in seinem Blog, wie man mit Hilfe seines auf DBMS_DATAPUMP basierenden DATAPUMP_UTIL_PCK mit geringem Aufwand Schema-Backups durchführen kann, die dann mit DBMS_JOBS oder über DBMS_SCHEDULER regelmäßig aufgerufen werden können. Für Entwicklungszwecke kann das ein sehr brauchbares Sicherungsverfahren sein. Dabei gilt: "Don't forget to regularly test a restore (import) of your backup files. The only thing worse than not having a backup is having a backup that can't be restored... !"
Mittwoch, Juli 25, 2012
Table Compression und Wiederholungen
Es ist schon eine Weile her, dass ich intensiver über die technischen Details von table compression nachgedacht habe, so dass die folgenden Tests und Überlegungen wahrscheinlich manches wiederholen, was ich schon früher aufgeschrieben habe, aber das nehme ich billigend in Kauf. Ausgangspunkt meiner Überlegungen ist diesmal die Frage, welche Wirkung die Wiederholung der gleichen Werte in mehreren Spalten eines Datensatzes auf die Komprimierung hat. Dazu noch mal eine kurze Beschreibung der Grundlagen des Verfahrens: Oracle legt im komprimierten Block eine Symboltabelle für wiederholte Einträge an, deren Angaben in den einzelnen rows referenziert werden. Demnach ist zu erwarten, dass sich eine breite Struktur mit mehreren Spalten und identischen Attributwerten (mindestens) genauso gut komprimieren lässt wie eine schmale Struktur mit einer einzigen Spalte mit entsprechenden Wiederholungen. Dazu ein Test (11.1.0.7, db_block_size: 8K, ASSM), bei dem die Testtabellen zunächst ohne compression angelegt werden:
Dazu ein paar Statistikwerte:
Dabei entspricht die AVG_ROW_LEN den Erwartungen, aber die Differenz bei den BLOCKS bedarf einer Erklärung:
(Die Abweichung zwischen der über dbms_stats ermittelten BLOCKS-Angabe und der über dbms_rowid bestimmten Block-Anzahl schiebe ich dabei ohne weitere Prüfung auf dbms_stats)
Demnach packt Oracle also in der Regel 129 rows mit einer Breite von 51 Byte in einen Block der Tabelle TEST_COL1_MPR (129 * 51 = 6579), und 14 rows mit einer Breite von 510 Byte in die Blocks von TEST_COL10_MPR ( 14 * 510 = 7140). Angesichts des (default) PCT_FREE-Werts von 10, leuchtet mir die Füllung von TEST_COL10_MPR, aber TEST_COL1_MPR scheint deutlich weniger Sätze in jeden Block unterzubringen als möglich wäre - eine Beobachtung, die ich hier auch schon mal erwähnt und auf den Speicherplatzbedarf des row directories geschoben hatte. Bei Jonathan Lewis gibt's noch ein paar präzisere Erläuterungen zu diesem Aspekt. Demnach benötigt ein Satz insgesamt 5 Byte im row directory:
Als Ergebnis halte ich folgendes fest:
-- Anlage einer ersten Testtabelle -- mit 1M rows und einer Spalte (Breite: 50 Byte) -- Die Spalte wird mit 50 Sternen (*) gefüllt drop table test_col1_mpr; create table test_col1_mpr as select lpad('*', 50, '*') pad1 from dual connect by level <= 1000000; exec dbms_stats.gather_table_stats(user, 'TEST_COL1_MPR') -- Anlage einer ersten Testtabelle -- mit 100K rows und 10 Spalten (Breite: jeweils 50 Byte; insgesamt also 500 Bytes) -- Jede Spalte wird mit 50 Sternen (*) gefüllt drop table test_col10_mpr; create table test_col10_mpr as select lpad('*', 50, '*') pad1 , lpad('*', 50, '*') pad2 , lpad('*', 50, '*') pad3 , lpad('*', 50, '*') pad4 , lpad('*', 50, '*') pad5 , lpad('*', 50, '*') pad6 , lpad('*', 50, '*') pad7 , lpad('*', 50, '*') pad8 , lpad('*', 50, '*') pad9 , lpad('*', 50, '*') pad10 from dual connect by level <= 100000; exec dbms_stats.gather_table_stats(user, 'TEST_COL10_MPR')
Dazu ein paar Statistikwerte:
select table_name , pct_free , num_rows , blocks , avg_row_len from user_tables where table_name like 'TEST_COL%'; TABLE_NAME PCT_FREE NUM_ROWS BLOCKS AVG_ROW_LEN ------------------------------ ---------- ---------- ---------- ----------- TEST_COL10_MPR 10 100000 7265 510 TEST_COL1_MPR 10 1000000 7884 51
Dabei entspricht die AVG_ROW_LEN den Erwartungen, aber die Differenz bei den BLOCKS bedarf einer Erklärung:
with basedata as ( select blockid , count(*) row_count from (select dbms_rowid.rowid_block_number(rowid) blockid from TEST_COL1_MPR) group by blockid ) select row_count , count(*) cnt from basedata group by row_count order by row_count; ROW_COUNT CNT --------- ---------- 121 1 129 7751 with basedata as ( select blockid , count(*) row_count from (select dbms_rowid.rowid_block_number(rowid) blockid from TEST_COL10_MPR) group by blockid ) select row_count , count(*) cnt from basedata group by row_count order by row_count ROW_COUNT CNT --------- ---------- 12 1 14 7142
(Die Abweichung zwischen der über dbms_stats ermittelten BLOCKS-Angabe und der über dbms_rowid bestimmten Block-Anzahl schiebe ich dabei ohne weitere Prüfung auf dbms_stats)
Demnach packt Oracle also in der Regel 129 rows mit einer Breite von 51 Byte in einen Block der Tabelle TEST_COL1_MPR (129 * 51 = 6579), und 14 rows mit einer Breite von 510 Byte in die Blocks von TEST_COL10_MPR ( 14 * 510 = 7140). Angesichts des (default) PCT_FREE-Werts von 10, leuchtet mir die Füllung von TEST_COL10_MPR, aber TEST_COL1_MPR scheint deutlich weniger Sätze in jeden Block unterzubringen als möglich wäre - eine Beobachtung, die ich hier auch schon mal erwähnt und auf den Speicherplatzbedarf des row directories geschoben hatte. Bei Jonathan Lewis gibt's noch ein paar präzisere Erläuterungen zu diesem Aspekt. Demnach benötigt ein Satz insgesamt 5 Byte im row directory:
- size of each pointer in the row directory: 2 bytes
- minimum size of the “row”: 2 bytes (“flag” byte + “lock” byte)
- one extra byte for the column count – which will be zero for a completely null row
Wenn man diesen Wert in die Berechnung einbaut, nähern sich die Ergebnisse für die Testtabellen deutlich aneinender an:
- 129 * (51 + 5) = 7224
- 14 * (510 + 5) = 7210
Um die Untersuchung etwas stärker abzurunden, könnte man jetzt noch einen Test mit PCT_FREE = 0 und ohne ASSM durchführen, aber dazu fehlt mir gerade der Ehrgeiz - grundsätzlich kommt man jedenfalls recht genau auf die 8K, die im Block verfügbar sind (zu berücksichtigen wäre auch noch der Block-Header mit 57 Bytes). Stattdessen zurück zum Thema: wie gut lassen sich die beiden Test-Tabellen mit ihren überaus einheitlichen (und langweiligen) Inhalten komprimieren:
-- compression aktivieren alter table TEST_COL1_MPR compress; alter table TEST_COL10_MPR compress; -- Tabellen reorganisieren (erst dann wird die compression wirksam) alter table TEST_COL1_MPR move; alter table TEST_COL10_MPR move; -- Statistikerfassung exec dbms_stats.gather_table_stats(user, 'TEST_COL1_MPR') exec dbms_stats.gather_table_stats(user, 'TEST_COL10_MPR') select table_name , num_rows , blocks , avg_row_len from user_tables where table_name like 'TEST_COL%'; TABLE_NAME NUM_ROWS BLOCKS AVG_ROW_LEN ------------------------------ ---------- ---------- ----------- TEST_COL10_MPR 100000 151 510 TEST_COL1_MPR 1000000 1414 51
Nach den bisherigen Beobachtungen ist das jetzt ein einleuchtendes Ergebnis: die breitere Tabelle lässt sich deutlich stärker komprimieren, da sie die geringere Satzanzahl enthält und daher ein geringer Overhead für das (nicht komprimierbare) row directory entsteht. Pro Block lassen sich jetzt für beide Tabellen nahezu gleich viele komprimierte Sätze unterbringen:
-- TEST_COL1_MPR ROW_COUNT CNT --------- ---------- 171 1 709 1 724 1380 -- TEST_COL10_MPR ROW_COUNT CNT --------- ---------- 503 1 720 1 721 137
Als Ergebnis halte ich folgendes fest:
- wiederholte Werte lassen sich Attribut-übergreifend komprimieren (wie nicht anders zu erwarten war)
- breite Tabellen mit vielen Attributen und einer geringeren Satzanzahl lassen sich besser komprimieren als schmale Tabellen mit einer höheren Satzanzahl und wenigen Attributen, sofern es viele wiederholte Werte gibt. Verantwortlich dafür ist das row directory, dessen Platzbedarf von der Komprimierung nicht beeinflusst wird.
Freitag, Juli 20, 2012
Virtual Columns und CURSOR_SHARING=FORCED
Randolf Geist untersucht in seinem Blog das Verhalten von virtuellen Spalten (und extended statistics), wenn CURSOR_SHARING=FORCE eine Ersetzung von Literalen durch generierte Bindewerte hervorruft. Dabei kommt er zu zwei wichtigen Ergebnissen:
- EXPLAIN PLAN kümmert sich nicht um das CURSOR_SHARING=FORCE. Das ist demnach einer jener Fälle, in denen EXPLAIN PLAN lügt (und Autotrace ebenfalls).
- im tatsächlichen Ausführungsplan (dbms_xplan.display_cursor) führt die Einführung des Bindewerts dazu, dass die extended statistics (virtual column) nicht mehr wirksam sind.
Ein weiterer Grund also dafür, CURSOR_SHARING=FORCE mit Bedacht einzusetzen.
SQL_TRACE Overhead
Neil Johnson hat im ORAganism Blog ein paar Testergebnisse zur Frage des Overheads, den die Aktivierung von SQL Trace mit sich bringt, veröffentlicht. Die von ihm beobachtete deutliche Erhöhung der Laufzeit hatte ich nicht unbedingt erwartet. Vor allem für kurze CPU-intensive Queries ist der Overhead offenbar immens (> 200%), aber auch für lang laufende Queries ergeben die Messungen ca. 30% Overhead. Ganz glücklich ist der Herr Johnson mit seinen Tests übrigens nicht - wahrscheinlich wär's angebracht da gelegentlich einen eigenen Test durchzuführen.
Workarea-Nutzung im Merge
Alexander Anokhin weist in seinem Blog auf einen interessanten (und potentiell problematischen) Aspekt beim MERGE-Statement hin: die in der USING clause aufgeführten Spalten werden komplett in die workarea eingeführt, obwohl sie im Fall eines MERGEs ohne INSERT-Zweig unter Umständen nicht relevant sind - dann nämlich, wenn sie weder im ON, noch in der UPDATE-Spaltenliste erscheinen. Bei Verwendung von SELECT * kann dieses Verhalten zu einer extremen Vergrößerung des erforderlichen Speicherplatzes für die HASH Operation führen. Sichtbar wird das Problem, wenn man sich die column projection Angaben ansieht; und vermeidbar ist es durch eine Beschränkung der im USING aufgeführten Spalten auf die tatsächlich relevanten Informationen.
Hybrid Columnar Compression in Exadata
Jonathan Lewis erklärt in seinem Blog die Grundlagen der Hybrid Columnar Compression, die neben dem Offloading (Smart Scan) und den Storage Indexes zu den drei zentralen Features von Exadata zählen. Im Zentrum des compression Verfahrens steht die Compression Unit, die eine große Menge von Datensätzen in eine einzige row zusammenfasst, bei der eine bitmap-Struktur die Repräsentation der ursprünglichen Datensätze übernimmt, während in den Spalten die konkatenierten (und dabei komprimierten) Inhalte der Ursprungsspalten erscheinen. Dieser komprimierte Satz wird als chained row mit einer einzigen Spalte abgelegt, so dass ein Splitting an beliebiger Stelle möglich ist.
Ressourcennutzung bei Split Partition
Dieser Tage habe ich mir (bzw. wurde mir) die Frage gestellt, wie groß die Arbeit ist, die Oracle ausführen muss, um eine gefüllte Default-Partition so zu splitten, dass eine leere Default-Partition und eine gefüllte Lauf-Partition entstehen. Meine Vermutung und Antwort war, dass das eine reine Metadaten-Operation sein sollte. Aber warum vermuten, wenn man es ausprobieren kann? Dazu habe ich folgenden Test in 11.1.0.7 durchgeführt, bei dem ich zunächst zwei identische Testtabellen anlege und die Default-Partition in:
Hier die (um Statistikwerte, die für beide Operationen = 0 sind, gekürzte) Runstat-Ausgabe:
Demnach fallen in Fall 1 keine Write-Operationen an, während Fall 2 recht massive physikalische Schreiboperationen hervorruft. Also scheint Fall 1 tatsächlich eine reine Metadaten-Operation zu sein, während Fall 2 eine echte Reorganisation darstellt.
Im Anschluss noch eine zweite Frage: wie clever ist Oracle bei der Durchführung eines Splits einer Partition in zwei mit unterschiedlichen Datenmengen gefüllte Teile? Anders ausgedrückt: kann sich der Transfer darauf beschränken, die kleinere Datenmenge in eine neue Partition zu verschieben, oder spielt in diesem Fall die Definition des Splittings eine größere Rolle? Dazu ergänze ich in den Tabellen des ersten Tests eine dritte Lauf_id, zu der ebenfalls 5M rows gehören. Anschließend splitte ich die Tabelle so, dass in:
Für diesen Fall ergibt sich folgendes Resultat:
Beide Operationen rufen demnach die gleiche Ressourcennutzung hervor: offenbar kann Oracle in diesem Fall also sehr genau bestimmen, wie das Splitting am effizientesten durchgeführt werden kann.
- Fall 1: eine Lauf-Partition mit 10M rows und eine Default-Partition mit 0 rows aufteile
- Fall 2: eine Lauf-Partition mit 5M rows und eine Default-Partition mit 5M rows aufteile
Die Analyse erfolgt dabei mit Adrian Billingtons Version des runstats Utilities von Tom Kyte.
-- test_split_partition.sql -- Test zur Ressourcennutzung von Split Partition -- 20.07.2012 -- Aufbau einer Testtabelle mit 10M Sätzen in einer Default-Partition -- (5M rows für Lauf_Id 1; 5M rows für Lauf_Id 2; 5M rows für Lauf_Id 3) drop table test_partition_split1; create table test_partition_split1 ( lauf_id number , padding varchar2(100) ) partition by list( lauf_id ) ( partition p_max values (default) ); insert into test_partition_split1 select 1 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; insert into test_partition_split1 select 2 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; commit; exec dbms_stats.gather_table_stats(user, 'test_partition_split1') -- Aufbau einer identischen zweiten Testtabelle drop table test_partition_split2; create table test_partition_split2 ( lauf_id number , padding varchar2(100) ) partition by list( lauf_id ) ( partition p_max values (default) ); insert into test_partition_split2 select 1 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; insert into test_partition_split2 select 2 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; commit; exec dbms_stats.gather_table_stats(user, 'test_partition_split2') @ D:\db_scripts\reporting\sqlpath\billington\runstats.sql start -- Test 1 -- Split in p_lauf mit 10M rows und p_max ohne Daten alter table test_partition_split1 split partition p_max values (1, 2) into (partition p_lauf, partition p_max); @ D:\db_scripts\reporting\sqlpath\billington\runstats.sql middle -- Test 2 -- Split in p_lauf mit 5M rows und p_max mit 5M rows alter table test_partition_split2 split partition p_max values (1) into (partition p_lauf, partition p_max); @ D:\db_scripts\reporting\sqlpath\billington\runstats.sql stop l=write
Hier die (um Statistikwerte, die für beide Operationen = 0 sind, gekürzte) Runstat-Ausgabe:
=============================================================================================== RunStats report : 20-JUL-2012 09:54:36 =============================================================================================== ----------------------------------------------------------------------------------------------- 1. Summary timings ----------------------------------------------------------------------------------------------- Type Name Run1 Run2 Diff ----- -------------------------------------------------- ------------ ------------ ------------ TIMER elapsed time (hsecs) 285 951 666 TIMER cpu time (hsecs) 53 181 128 Comments: 1) Run1 was 70% quicker than Run2 2) Run1 used 70% less CPU time than Run2 ----------------------------------------------------------------------------------------------- 2. Statistics report ----------------------------------------------------------------------------------------------- Type Name Run1 Run2 Diff ----- -------------------------------------------------- ------------ ------------ ------------ ... STAT change write time 0 1 1 STAT physical write total multi block requests 0 950 950 STAT physical write IO requests 0 968 968 STAT physical write total IO requests 0 968 968 STAT physical writes 0 30,304 30,304 STAT physical writes direct 0 30,304 30,304 STAT physical writes non checkpoint 0 30,304 30,304 STAT redo size for direct writes 0 50,380 50,380 STAT physical write bytes 0 248,250,368 248,250,368 STAT physical write total bytes 0 248,250,368 248,250,368 ----------------------------------------------------------------------------------------------- 3. Latching report ----------------------------------------------------------------------------------------------- Type Name Run1 Run2 Diff ----- -------------------------------------------------- ------------ ------------ ------------ LATCH total latches used 198,222 397,045 198,823 Comments: 1) Run1 used 50,1% fewer latches than Run2 ----------------------------------------------------------------------------------------------- 4. Time model report ----------------------------------------------------------------------------------------------- Type Name Run1 Run2 Diff ----- -------------------------------------------------- ------------ ------------ ------------ TIME hard parse (sharing criteria) elapsed time 0 502 502 TIME hard parse elapsed time 2,538 3,128 590 TIME PL/SQL execution elapsed time 13,356 14,077 721 TIME parse time elapsed 3,514 9,784 6,270 TIME DB CPU 531,250 1,859,375 1,328,125 TIME sql execute elapsed time 2,614,057 9,367,270 6,753,213 TIME DB time 2,688,528 9,481,900 6,793,372 ----------------------------------------------------------------------------------------------- 5. About ----------------------------------------------------------------------------------------------- - RunStats v2.01 by Adrian Billington (http://www.oracle-developer.net) - Based on the original RUNSTATS utility by Tom Kyte =============================================================================================== End of report ===============================================================================================
Demnach fallen in Fall 1 keine Write-Operationen an, während Fall 2 recht massive physikalische Schreiboperationen hervorruft. Also scheint Fall 1 tatsächlich eine reine Metadaten-Operation zu sein, während Fall 2 eine echte Reorganisation darstellt.
Im Anschluss noch eine zweite Frage: wie clever ist Oracle bei der Durchführung eines Splits einer Partition in zwei mit unterschiedlichen Datenmengen gefüllte Teile? Anders ausgedrückt: kann sich der Transfer darauf beschränken, die kleinere Datenmenge in eine neue Partition zu verschieben, oder spielt in diesem Fall die Definition des Splittings eine größere Rolle? Dazu ergänze ich in den Tabellen des ersten Tests eine dritte Lauf_id, zu der ebenfalls 5M rows gehören. Anschließend splitte ich die Tabelle so, dass in:
- Fall 1: Lauf-Partition mit 10M rows, Default-Partition mit 5M rows
- Fall 2: Lauf-Partition mit 5M rows, Default-Partition mit 10M rows
-- test_split_partition2.sql -- Zweiter Test zur Ressourcennutzung von Split Partition -- 20.07.2012 -- Aufbau einer Testtabelle mit 15M Sätzen in einer Default-Partition -- (5M rows für Lauf_Id 1; 5M rows für Lauf_Id 2; 5M rows für Lauf_Id 3) drop table test_partition_split1; create table test_partition_split1 ( lauf_id number , padding varchar2(100) ) partition by list( lauf_id ) ( partition p_max values (default) ); insert into test_partition_split1 select 1 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; insert into test_partition_split1 select 2 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; insert into test_partition_split1 select 3 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; commit; exec dbms_stats.gather_table_stats(user, 'test_partition_split1') -- Aufbau einer identischen zweiten Testtabelle drop table test_partition_split2; create table test_partition_split2 ( lauf_id number , padding varchar2(100) ) partition by list( lauf_id ) ( partition p_max values (default) ); insert into test_partition_split2 select 1 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; insert into test_partition_split2 select 2 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; insert into test_partition_split2 select 3 lauf_id , lpad('*', 100, '*') padding from dual connect by level <= 1000000; commit; exec dbms_stats.gather_table_stats(user, 'test_partition_split2') @ D:\db_scripts\reporting\sqlpath\billington\runstats.sql start -- Test 1 -- Split in p_lauf mit 10M rows und p_max ohne Daten alter table test_partition_split1 split partition p_max values (1, 2) into (partition p_lauf, partition p_max); @ D:\db_scripts\reporting\sqlpath\billington\runstats.sql middle -- Test 2 -- Split in p_lauf mit 5M rows und p_max mit 5M rows alter table test_partition_split2 split partition p_max values (1) into (partition p_lauf, partition p_max); @ D:\db_scripts\reporting\sqlpath\billington\runstats.sql stop l=write
Für diesen Fall ergibt sich folgendes Resultat:
=============================================================================================== RunStats report : 20-JUL-2012 10:23:33 =============================================================================================== ----------------------------------------------------------------------------------------------- 1. Summary timings ----------------------------------------------------------------------------------------------- Type Name Run1 Run2 Diff ----- -------------------------------------------------- ------------ ------------ ------------ TIMER elapsed time (hsecs) 1,405 1,340 -65 TIMER cpu time (hsecs) 252 217 -35 Comments: 1) Run2 was 4,6% quicker than Run1 2) Run2 used 4,6% less CPU time than Run1 ----------------------------------------------------------------------------------------------- 2. Statistics report ----------------------------------------------------------------------------------------------- Type Name Run1 Run2 Diff ----- -------------------------------------------------- ------------ ------------ ------------ STAT physical write bytes 372,375,552 372,375,552 0 STAT physical write total bytes 372,375,552 372,375,552 0 STAT physical write total multi block requests 1,425 1,425 0 STAT physical writes 45,456 45,456 0 STAT physical writes direct 45,456 45,456 0 STAT physical writes non checkpoint 45,456 45,456 0 STAT physical write IO requests 1,446 1,448 2 STAT physical write total IO requests 1,446 1,448 2 STAT redo size for direct writes 75,324 75,384 60 ----------------------------------------------------------------------------------------------- 3. Latching report ----------------------------------------------------------------------------------------------- Type Name Run1 Run2 Diff ----- -------------------------------------------------- ------------ ------------ ------------ LATCH total latches used 569,083 487,430 -81,653 Comments: 1) Run2 used 14,3% fewer latches than Run1 ----------------------------------------------------------------------------------------------- 4. Time model report ----------------------------------------------------------------------------------------------- Type Name Run1 Run2 Diff ----- -------------------------------------------------- ------------ ------------ ------------ TIME repeated bind elapsed time 5 15 10 ----------------------------------------------------------------------------------------------- 5. About ----------------------------------------------------------------------------------------------- - RunStats v2.01 by Adrian Billington (http://www.oracle-developer.net) - Based on the original RUNSTATS utility by Tom Kyte =============================================================================================== End of report ===============================================================================================
Beide Operationen rufen demnach die gleiche Ressourcennutzung hervor: offenbar kann Oracle in diesem Fall also sehr genau bestimmen, wie das Splitting am effizientesten durchgeführt werden kann.
Montag, Juli 16, 2012
Costing für partitionierte Indizes
Dieser Tage ist mir aufgefallen, dass ich vom Costing für partitionierte Indizes keine klare Vorstellung habe. Da ein lokaler Index meinem Verständnis nach eine Meta-Struktur darstellt, die mehrere physikalisch getrennte Einzel-Indizes logisch zusammenfasst, war meine Annahme, dass der Zugriff auf alle Partitionen eines lokalen Index einer partitionierten Tabelle teurer sein müsste als der Zugriff über einen nicht partitionierten Index auf eine nicht partitionierte Tabelle. Aber wie sich das Verhältnis der Kosten dieser Fälle zueinander genau darstellt, war mir unklar. Daher dazu ein Test (mit 11.1.0.7):
Zusätzlich zur Auswertung mit Hilfe von dbms_xplan habe ich außerdem noch ein CBO-Trace (Event 10053) erzeugt. Die Angaben im CBO-Trace sind dabei für beide Fälle sehr ähnlich. Geringfügige Abweichungen gibt es zunächst bei den Basisstatistiken für Tabelle und Index:
Die Level-Angabe LVLS ist identisch, ebenso die Anzahl distinkter Keys (#DK) und der Clustering Factor (CLUF), was nicht überrascht, weil die Tabellen mit dem gleichen Verfahren gefüllt wurden und der CF ein Maß für die Sortierung der Tabelle in Hinblick auf den Index darstellt. Die Abweichungen der "per Key"-Werte (LB/K, DB/K) ergeben sich meiner Interpretation nach aus der Verwendung der "composite stats" für den partitionierten Index: sie entsprechen den Angaben der Einzelpartitionen. Wenn man die Index-Statistiken explizit global erhebt, erhält man für die "per Key"-Angaben Werte, die denen des nicht-partitionierten Falls entsprechen.
Wenn man die LVLS- und die #LB-Angabe des nicht-partitionierten Index in die Standard-Formel für das Costing von Indizes
Die resc_io-Angabe entspricht also exakt den Erwartungen. Hier auch noch der Plan und die Ausführungsstatistiken zur Query (jeweils um die Time-Angaben gekürzt):
Demnach sind die 202 also auch eine sehr gute Schätzung der tatsächlich anfallenden Arbeit von 203 LIOs.
Nun aber zur partitionierten Tabelle und zum partitionierten Index. Das Einsetzen der LVLS und #LB aus den globalen Index-Statistiken bringt für diesen Fall das gleiche Ergebnis wie für den nicht partitionierten Index:
Woher kommt diese Differenz? Meine Vermutung ist, dass diese zehn Kosten-Punkte für den Zugriff auf die fünf zusätzlichen Teil-Indizes entstehen, weil statt einer einzelnen Index-Struktur jetzt insgesamt sechs Indizes betrachtet werden müssen: also 5 zusätzliche Root-Block-Zugriffe und 5 zusätzliche Branch-Block-Zugriffe. Tatsächlich sehe ich die gleiche Differenz von 10 auch für den folgenden Fall (und nehme an, dass sie bei allen entsprechenden Szenarien auftritt):
Ich will jetzt nicht behaupten, dass das ein ernsthafter Beweis für meine Annahme ist: dazu ist der Test doch etwas zu anekdotisch. Aber da es mir gerade an Ausdauer mangelt, die Untersuchung zu plausibilisieren, lasse ich es erst einmal so stehen, um die Untersuchung möglicherweise bei Gelegenheit fortzusetzen.
Nachtrag 18.07.2012: Um die Beweisführung ein wenig stichhaltiger zu machen, ein weiteres Beispiel mit einer Einschränkung auf eine Teilmenge der Partitionen, denn die bisherigen Ergebnisse hätten sich auch als Aggregierung der Kosten der Zugriffe auf die Partitions-Indizes erklären lassen, obwohl das CBO-Trace keinen Hinweis auf ein solches Vorgehen zu enthalten schien und nur die globalen Statistiken lieferte:
Für die nicht partitionierte Tabelle hat die zusätzliche Einschränkung auf salesdate erwartungsgemäß keinen Einfluss auf die Kosten des Index-Zugriffs, die weiterhin mit 202 angegeben werden:
Wenn man die gleichen Einschränkungen für die partitionierte Tabelle angibt, erhält man folgende Ergebnisse:
Hier entfällt der Tabellenzugriff, weil die Datumseinschränkung bereits durch die Partition Elimination sichergestellt ist. Für meine Theorie zum Costing ist aber vor allem der Wert 206 interessant, den ich als Basiskosten + Zugriff auf drei Partitionen statt auf einen einzelnen Index (also 2 zusätzliche Zugriffe jeweils auf Root- und Branch-Block) interpretiere: also 202 + (2 * 2) = 206. Offenbar handelt es sich aber nicht um die aggregierten Kosten des Zugriffs auf die drei relevanten lokalen Indizes, denn die dürften nur etwa halb so hoch sein (106).
Bei einer Beschränkung auf eine Partition ändert sich für den nicht partitionierten Fall (natürlich) nichts. Für den partitionierten Index hingegen sinken die Kosten auf 36:
In diesem Fall werden dann offenbar statt der globalen die Partitionsstatistiken herangezogen, also blevel + (ix_sel * leaf_blocks) = 2 + ceil(0,01 * 3368) = 36.
Bei Hotsos kann man als registrierter Nutzer ein Paper von Gary Propeck (Januar 2012) downloaden, das sich mit den Kosten des Zugriffs auf partitionierte Tabellen und lokale Indizes beschäftigt. Der hier von mir entwickelten Theorie zur Kostenberechnung für den Zugriff über lokale Indizes bin ich dort allerdings nicht begegnet (was natürlich auch ein Hinweis darauf sein könnte, dass sie nicht zutrifft ...).
-- Fall 1 -- Löschung und Neuanlage einer partitionierten Tabelle -- mit einem lokalen Index drop table test_part_index_cost; create table test_part_index_cost ( id number , salesdate date , col1 number , padding varchar2(100) ) partition by range (salesdate) ( partition p201207 values less than ( to_date('01.08.2012','dd.mm.yyyy')) , partition p201208 values less than ( to_date('01.09.2012','dd.mm.yyyy')) , partition p201209 values less than ( to_date('01.10.2012','dd.mm.yyyy')) , partition p201210 values less than ( to_date('01.11.2012','dd.mm.yyyy')) , partition p201211 values less than ( to_date('01.12.2012','dd.mm.yyyy')) , partition p201212 values less than ( to_date('01.01.2013','dd.mm.yyyy')) ); -- Füllung mit langweiligen Testdaten: -- Id: eindeutig -- col1: 100 Werte ohne Clusterung -- padding: ein Haufen Sterne, die die Sätze breiter machen -- für jeden Tag des Halbjahrs werden 5000 Sätze erzeugt insert into test_part_index_cost with basedata as ( select rownum id , to_date('01.07.2012', 'dd.mm.yyyy') + trunc((rownum - 1)/5000) salesdate , mod(rownum, 100) col1 , lpad('*', 50, '*') padding from dual connect by level <= (184 * 5000) ) select * from basedata where salesdate <= to_date('31.12.2012', 'dd.mm.yyyy') ; exec dbms_stats. gather_table_stats(user, 'test_part_index_cost') -- Index-Anlage mit PCTFREE 90 (um einen etwas größeren Index zu erhalten) create index ix_test_part_index_cost on test_part_index_cost(col1) local pctfree 90; -- Fall 2 -- Anlage einer Tabelle ohne Partitionierung drop table test_index_cost; create table test_index_cost ( id number , salesdate date , col1 number , padding varchar2(100) ); -- Füllung mit den gleichen Daten, mit denen auch -- die partitionierte Tabelle gefüllt wurde insert into test_index_cost with basedata as ( select rownum id , to_date('01.07.2012', 'dd.mm.yyyy') + trunc((rownum - 1)/5000) salesdate , mod(rownum, 100) col1 , lpad('*', 50, '*') padding from dual connect by level <= (184 * 5000) ) select * from basedata where salesdate <= to_date('31.12.2012', 'dd.mm.yyyy') ; exec dbms_stats. gather_table_stats(user, 'test_index_cost') create index ix_test_index_cost on test_index_cost(col1) pctfree 90; -- Test-Zugriffe mit Erhebung von Plan-Statistiken select /*+ gather_plan_statistics */ count(*) from test_part_index_cost where col1 = 1; select /*+ gather_plan_statistics */ count(*) from test_index_cost where col1 = 1;
Zusätzlich zur Auswertung mit Hilfe von dbms_xplan habe ich außerdem noch ein CBO-Trace (Event 10053) erzeugt. Die Angaben im CBO-Trace sind dabei für beide Fälle sehr ähnlich. Geringfügige Abweichungen gibt es zunächst bei den Basisstatistiken für Tabelle und Index:
-- partitionierte Tabelle Table Stats:: Table: TEST_PART_INDEX_COST Alias: TEST_PART_INDEX_COST (Using composite stats) #Rows: 920000 #Blks: 9528 AvgRowLen: 66.00 Index Stats:: Index: IX_TEST_PART_INDEX_COST Col#: 3 USING COMPOSITE STATS LVLS: 2 #LB: 19990 #DK: 100 LB/K: 32.00 DB/K: 1533.00 CLUF: 919803.00 -- nicht partitionierte Tabelle Table Stats:: Table: TEST_INDEX_COST Alias: TEST_INDEX_COST #Rows: 920000 #Blks: 10097 AvgRowLen: 66.00 Index Stats:: Index: IX_TEST_INDEX_COST Col#: 3 LVLS: 2 #LB: 19988 #DK: 100 LB/K: 199.00 DB/K: 9198.00 CLUF: 919803.00
Die Level-Angabe LVLS ist identisch, ebenso die Anzahl distinkter Keys (#DK) und der Clustering Factor (CLUF), was nicht überrascht, weil die Tabellen mit dem gleichen Verfahren gefüllt wurden und der CF ein Maß für die Sortierung der Tabelle in Hinblick auf den Index darstellt. Die Abweichungen der "per Key"-Werte (LB/K, DB/K) ergeben sich meiner Interpretation nach aus der Verwendung der "composite stats" für den partitionierten Index: sie entsprechen den Angaben der Einzelpartitionen. Wenn man die Index-Statistiken explizit global erhebt, erhält man für die "per Key"-Angaben Werte, die denen des nicht-partitionierten Falls entsprechen.
Wenn man die LVLS- und die #LB-Angabe des nicht-partitionierten Index in die Standard-Formel für das Costing von Indizes
also: blevel + (ix_sel * leaf_blocks) + (ix_sel_with_filters * clustering_factor)einsetzt erhält man, wenn man die ix_sel als 1% einsetzt und den letzten Teil der Formel ignoriert, weil der Tabellen-Zugriff in diesem Fall entfällt:
2 + ceil(0.01 * 19988) = 202Im CBO-Trace findet man dazu folgende Angabe:
Access Path: index (AllEqRange) Index: IX_TEST_INDEX_COST resc_io: 202.00 resc_cpu: 3278531 ix_sel: 0.010000 ix_sel_with_filters: 0.010000 Cost: 202.20 Resp: 202.20 Degree: 1
Die resc_io-Angabe entspricht also exakt den Erwartungen. Hier auch noch der Plan und die Ausführungsstatistiken zur Query (jeweils um die Time-Angaben gekürzt):
----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 202 (100)| | 1 | SORT AGGREGATE | | 1 | 3 | | |* 2 | INDEX RANGE SCAN| IX_TEST_INDEX_COST | 9200 | 27600 | 202 (0)| ----------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("COL1"=1) ------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | Buffers | ------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 | 203 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 | 203 | |* 2 | INDEX RANGE SCAN| IX_TEST_INDEX_COST | 1 | 9200 | 9200 | 203 | -------------------------------------------------------------------------------------
Demnach sind die 202 also auch eine sehr gute Schätzung der tatsächlich anfallenden Arbeit von 203 LIOs.
Nun aber zur partitionierten Tabelle und zum partitionierten Index. Das Einsetzen der LVLS und #LB aus den globalen Index-Statistiken bringt für diesen Fall das gleiche Ergebnis wie für den nicht partitionierten Index:
2 + ceil(0.01 * 19990) = 202Tatsächlich liegen die Kosten aber bei 212, also um genau 10 höher:
Access Path: index (AllEqRange) Index: IX_TEST_PART_INDEX_COST resc_io: 212.00 resc_cpu: 3349745 ix_sel: 0.010000 ix_sel_with_filters: 0.010000 Cost: 212.20 Resp: 212.20 Degree: 1 ----------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Pstart| Pstop | ----------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 212 (100)| | | | 1 | SORT AGGREGATE | | 1 | 3 | | | | | 2 | PARTITION RANGE ALL| | 9200 | 27600 | 212 (0)| 1 | 6 | |* 3 | INDEX RANGE SCAN | IX_TEST_PART_INDEX_COST | 9200 | 27600 | 212 (0)| 1 | 6 | ----------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("COL1"=1) --------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | Buffers | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 | 220 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 | 220 | | 2 | PARTITION RANGE ALL| | 1 | 9200 | 9200 | 220 | |* 3 | INDEX RANGE SCAN | IX_TEST_PART_INDEX_COST | 6 | 9200 | 9200 | 220 | ---------------------------------------------------------------------------------------------
Woher kommt diese Differenz? Meine Vermutung ist, dass diese zehn Kosten-Punkte für den Zugriff auf die fünf zusätzlichen Teil-Indizes entstehen, weil statt einer einzelnen Index-Struktur jetzt insgesamt sechs Indizes betrachtet werden müssen: also 5 zusätzliche Root-Block-Zugriffe und 5 zusätzliche Branch-Block-Zugriffe. Tatsächlich sehe ich die gleiche Differenz von 10 auch für den folgenden Fall (und nehme an, dass sie bei allen entsprechenden Szenarien auftritt):
select count(*) from test_index_cost where col1 < 3; ----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 3 | 609 (1)| | 1 | SORT AGGREGATE | | 1 | 3 | | |* 2 | INDEX RANGE SCAN| IX_TEST_INDEX_COST | 27879 | 83637 | 609 (1)| ----------------------------------------------------------------------------- select count(*) from test_part_index_cost where col1 < 3; ----------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Pstart| Pstop | ----------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 3 | 619 (1)| | | | 1 | SORT AGGREGATE | | 1 | 3 | | | | | 2 | PARTITION RANGE ALL| | 27879 | 83637 | 619 (1)| 1 | 6 | |* 3 | INDEX RANGE SCAN | IX_TEST_PART_INDEX_COST | 27879 | 83637 | 619 (1)| 1 | 6 | -----------------------------------------------------------------------------------------------------
Ich will jetzt nicht behaupten, dass das ein ernsthafter Beweis für meine Annahme ist: dazu ist der Test doch etwas zu anekdotisch. Aber da es mir gerade an Ausdauer mangelt, die Untersuchung zu plausibilisieren, lasse ich es erst einmal so stehen, um die Untersuchung möglicherweise bei Gelegenheit fortzusetzen.
Nachtrag 18.07.2012: Um die Beweisführung ein wenig stichhaltiger zu machen, ein weiteres Beispiel mit einer Einschränkung auf eine Teilmenge der Partitionen, denn die bisherigen Ergebnisse hätten sich auch als Aggregierung der Kosten der Zugriffe auf die Partitions-Indizes erklären lassen, obwohl das CBO-Trace keinen Hinweis auf ein solches Vorgehen zu enthalten schien und nur die globalen Statistiken lieferte:
select /*+ index(t) */ count(*) from test_index_cost t where col1 = 1 and salesdate >= '01.10.2012';
Für die nicht partitionierte Tabelle hat die zusätzliche Einschränkung auf salesdate erwartungsgemäß keinen Einfluss auf die Kosten des Index-Zugriffs, die weiterhin mit 202 angegeben werden:
-------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 9405 (1)| | 1 | SORT AGGREGATE | | 1 | | |* 2 | TABLE ACCESS BY INDEX ROWID| TEST_INDEX_COST | 4625 | 9405 (1)| |* 3 | INDEX RANGE SCAN | IX_TEST_INDEX_COST | 9200 | 202 (0)| -------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("SALESDATE">=TO_DATE(' 2012-10-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss')) 3 - access("COL1"=1)
Wenn man die gleichen Einschränkungen für die partitionierte Tabelle angibt, erhält man folgende Ergebnisse:
select /*+ index(t) */ count(*) from test_part_index_cost t where col1 = 1 and salesdate >= '01.10.2012'; -------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Pstart| Pstop | -------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 9410 (1)| | | | 1 | SORT AGGREGATE | | 1 | | | | | 2 | PARTITION RANGE ITERATOR| | 4625 | 206 (0)| 4 | 6 | |* 3 | INDEX RANGE SCAN | IX_TEST_PART_INDEX_COST | 4625 | 206 (0)| 4 | 6 | -------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("COL1"=1)
Hier entfällt der Tabellenzugriff, weil die Datumseinschränkung bereits durch die Partition Elimination sichergestellt ist. Für meine Theorie zum Costing ist aber vor allem der Wert 206 interessant, den ich als Basiskosten + Zugriff auf drei Partitionen statt auf einen einzelnen Index (also 2 zusätzliche Zugriffe jeweils auf Root- und Branch-Block) interpretiere: also 202 + (2 * 2) = 206. Offenbar handelt es sich aber nicht um die aggregierten Kosten des Zugriffs auf die drei relevanten lokalen Indizes, denn die dürften nur etwa halb so hoch sein (106).
Bei einer Beschränkung auf eine Partition ändert sich für den nicht partitionierten Fall (natürlich) nichts. Für den partitionierten Index hingegen sinken die Kosten auf 36:
------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Cost (%CPU)| Pstart| Pstop | ------------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | 1586 (0)| | | | 1 | SORT AGGREGATE | | 1 | | | | | 2 | PARTITION RANGE SINGLE | | 50 | 1586 (0)| 4 | 4 | |* 3 | TABLE ACCESS BY LOCAL INDEX ROWID| TEST_PART_INDEX_COST | 50 | 1586 (0)| 4 | 4 | |* 4 | INDEX RANGE SCAN | IX_TEST_PART_INDEX_COST | 1550 | 36 (0)| 4 | 4 | ------------------------------------------------------------------------------------------------------------
In diesem Fall werden dann offenbar statt der globalen die Partitionsstatistiken herangezogen, also blevel + (ix_sel * leaf_blocks) = 2 + ceil(0,01 * 3368) = 36.
Bei Hotsos kann man als registrierter Nutzer ein Paper von Gary Propeck (Januar 2012) downloaden, das sich mit den Kosten des Zugriffs auf partitionierte Tabellen und lokale Indizes beschäftigt. Der hier von mir entwickelten Theorie zur Kostenberechnung für den Zugriff über lokale Indizes bin ich dort allerdings nicht begegnet (was natürlich auch ein Hinweis darauf sein könnte, dass sie nicht zutrifft ...).
Donnerstag, Juli 12, 2012
PK Validierung
Jonathan Lewis hat in seinem Blog gerade einen äußerst interessanten Artikel über den - ineffizienten - SQL-Code geschrieben, den Oracle bei der Validierung eines PKs auf der Basis eines als non unique definierten Index verwendet: die Operation führt mehrere INDEX FAST FULL SCANs im Rahmen von HASH JOINs aus - obwohl eigentlich damit zu rechnen wäre, dass die Menge der Duplikate eher klein sein sollte, was einen NESTED LOOPS-Zugriff geeigneter machen sollte (wobei die äußere Schleife über einen INDEX FULL SCAN nach Duplikaten - also wiederholten Index-Werten - suchen könnte, und dazu keine Sortierung benötigte, sondern ein SORT GROUP BY NOSORT verwenden könnte). Nach der Analyse des Problems sucht der Herr Lewis dann nach einer Lösung und will eine Baseline einsetzen, um den geeigneten Plan festzulegen - aber das Problem erweist sich als sehr sperrig und widersetzt sich allen Bemühungen (bis hin zum Einsatz von DBMS_ADVANCED_REWRITE). Eine Lösung findet er letztlich nicht, präsentiert aber einen spannenden Fall.
Oracle Text
Zu Oracle Text kann ich nahezu nichts sagen, mir ist aber bekannt, dass Carsten Czarski viel darüber weiß. Der verlinkte Artikel enthält ein relativ einfaches Beispiel zum Thema und außerdem Verweise auf den Blog ORACLE TEXT: TIPPS, TRICKS, BEST PRACTICE, in dem Ulrike Schwinn und Carsten Czarski regelmäßig über zugehörige Fragen schreiben.
Mittwoch, Juli 11, 2012
Dunkle Geheimnisse des CBO
Jonathan Lewis weist in seinem Blog auf den internen Parameter _optimizer_random_plan hin, der eine Menge Dinge erklärt, die bislang unerklärlich schienen...
Samstag, Juli 07, 2012
Generiertes SQL, CTEs und Transformationen
Randolf Geist hat vor einigen Wochen über einen Fall geschrieben, in dem der CBO nicht den Plan mit den niedrigsten Kosten wählt: im Spiel sind dabei generierte Queries eines Report Generators (ein Quell steter Freude ...) und CTEs (aka with clause; aka subquery factoring). Dass generierte Queries ziemlich viel Ärger machen können, liegt auf der Hand, aber schade ist, dass das subquery factoring so viele unerfreuliche Begleiterscheinungen hervorruft, denn ich halte es für ein sehr nützliches Werkzeug zur Strukturierung komplexer Abfragen. Erinnerungswürdig ist daher auch die abschließende Warnung "Watch out if you upgrade to 11.2.0.3. Potentially a lot of execution plans using the WITH clause might change, and as you know, not always for the better."
Interval Partitioning und parallelisierte Zugriffe
Dass Interval Partitioning eine sehr nette Idee ist, deren praktische Realisierung allerdings offenbar eine Menge praktischer Probleme hervorgerufen hat, habe ich gelegentlich angesprochen. Im Pythian-Blog zeigt Christo Kutrovsky nun, dass GROUP BY Operationen über den partition key im Fall von Interval Partitioning nicht auf Partitionsebene ausgeführt werden, was die Operation massiv verteuern kann. Außerdem liefert er auch einen Workaround für entsprechende Fälle:
We need to convert the range-interval table to a pure range table. Fortunately you can do this on the fly, but it requires a quick lock on the entire table. This also moves the “anchor” partition to the highest in the table, which allows you to drop the lowest partitions before the anchor partition.Oder wie der Dichter sagt: es geht auch anders, aber so geht es auch ...
Donnerstag, Juli 05, 2012
SQL Baseline Plan aus AWR übernehmen
Ich wollte schon seit einiger Zeit ausprobieren, wie man einen Plan aus dem AWR in eine SQL Baseline übertragen kann. Jetzt hat Anand Prakash einen entsprechenden Artikel geschrieben, dessen Vorgehen ich bei nächster Gelegenheit nachzuvollziehen will.
Mittwoch, Juli 04, 2012
Arbeitsweise von Column Stores
Thomas Kejser, den ich hier inzwischen recht regelmäßig verlinke, hat eine kompakte (RDBMS-unabhängige) Erklärung der Arbeitsweise von column stores veröffentlicht. Deren entscheidender Trick ist es offenbar, die Anzahl der aufeinanderfolgenden Wiederholungen pro Schlüssel zu erfassen, und dadurch eine massive Komprimierung zu erreichen (was mich an die compression in bitmap indizes erinnert):
Because the typical case for large tables is that they DO have a lot of repetitions, column stores can often achieve significant compression benefits. The quoted compression rates from different vendors (as compared with the raw format) is typically in the range of 5-50x – depending on what data you store of course.
Das Entpacken einer solchen Struktur ist dabei dann relativ einfach:
because of the way data is stored (with duplicates added in the right places), this “join” can proceed in a linear, O ( n ), highly parallelised manner, no matter which column we select. This data structure also has the property of making it easy to sequentially access memory.
Weitere Zitate spare ich mir, weil ich den Artikel dann gleich komplett kopieren könnte.
Montag, Juli 02, 2012
Rebuild eines partitionierten Index mit Komprimierung
Richard Foote bezeichnet die Index Compression als "perhaps one of the most under used and neglected index options available." Und wenn der Herr Foote so etwas schreibt, dann entspricht das vermutlich den Tatsachen. In seinem Blog findet man eine kleine Serie (aus dem Jahr 2008) zum Thema, die die folgenden vier Beiträge umfasst:
- Index Compression Part I (Low) mit den Grundlagen des Verfahrens und dem oben zitierten Satz
- Index Compression Part II (Down Is The New Up) mit einer Erklärung der Fälle, in denen index compression nichts bringt: "when the leading column is very selective or the compressed columns are very selective and have very few or possibly no repeating values in an index, then we have a problem."
- Index Compression Part III (2+2=5) über die (sinnlose) Komprimierung eines single column unique index.
- Index Compression Part IV (Packt Like Sardines In a Crushd Tin Box) mit weiteren technischen Details
Grundsätzlich dient index compression dem gleichen Zweck wie die table compression - nämlich dazu, die Größe eines Segments in der Datenbank zu reduzieren, um damit - auf Kosten eines relativ harmlosen CPU-Overheads - Storage und I/O-Operationen beim Zugriff einzusparen, was fast in jedem Fall ein sinnvoller Handel ist. Die technische Implementierung ist dabei allerdings eine andere, denn die index compression ersetzt immer nur führende wiederholte Werte im Index, was ihre initiale Definition etwas komplizierter macht als die Tabellen-Komprimierung. Umgekehrt bringt die Index-Komprimierung dafür nicht den Nachteil, dass nachfolgende UPDATEs die Segment-Struktur durcheinander bringen und die Größe des Objekts ungünstig beeinflussen. Ein Nachteil der index compression ist allerdings, dass sie auf Partitionsebene nicht nachträglich definiert und beim rebuild berücksichtigt werden kann, sondern einheitlich für das komplette Objekt bei der Anlage angegeben sein muss, was den Umbau bestehender Indizes etwas komplizierter macht (will heißen: eine Löschung und einen Neuaufbau erfordert). Einen entsprechenden Hinweis mit dem Verweis auf das MOS-Dokument 312843.1 "Rebuild a partitioned index specifying compression raises Ora-28659" findet man hier.
Abonnieren
Posts (Atom)