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:
  • 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.

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):

-- 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:

-- 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:
  • 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
Hier zunächst der erweiterte Test:

-- 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):

-- 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) = 202
Im 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) = 202
Tatsä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:
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.