Mittwoch, Mai 07, 2014

Descending Index Quiz

Jonathan Lewis hat heute mal wieder eine Quizfrage gestellt und ausnahmsweise bin ich ziemlich schnell auf die Lösung gekommen. Die Frage lautet: wieso liefert ein descending Index einen duplicate Key Fehler, während ein entsprechender aufsteigender Index fehlerlos angelegt werden kann:

SQL> create unique index t1_i1 on t1(v1 desc);
create unique index t1_i1 on t1(v1 desc)
                                *
ERROR at line 1:
ORA-01452: cannot CREATE UNIQUE INDEX; duplicate keys found
 
 
SQL> create unique index t1_i1 on t1(v1);
 
Index created.

Die zugehörige Tabellendefinition könnte ungefähr folgendermaßen aussehen:

create table t1(v1 number);

insert into t1(v1) values (NULL);

insert into t1(v1) values (NULL);

Die Erklärung dafür lautet: ein descending index ist ein FBI (function based index) auf einer zusätzlichen generierten Spalte, bei der ein 0xFF an das Einerkomplement jedes Werts der Ursprungsspalte angehängt wird, und diese Ergänzung macht aus der NULL dann einen Wert, der als Duplikat betrachtet wird. Den normalen Index ohne die desc-Angabe interessieren die NULLS nicht, was die fehlerlose Anlage erklärt. Und den entscheidenden Hinweis zum Thema habe ich in Richard Footes hier verlinktem Artikel gefunden (und dort in einem Kommentar des Herrn Lewis).

Montag, Mai 05, 2014

Komprimierungseffektivität bestimmen mit dbms_compression.get_compression_ratio

Heute wurde im OTN Forum die Frage diskutiert, ob dbms_compression.get_compression_ratio für bereits komprimierte Tabellen eine Aussage zur Effektivität der Komprimierung liefert: also ob sich durch Updates so viele Entpackungen von Datensätzen ergeben haben, dass eine erneute Komprimierung sinnvoll wäre. Ich habe zur Beantwortung mein Standardverfahren eingesetzt:
  1. Nachdenken
  2. Zu keinem eindeutigen Ergebnis kommen
  3. Test erstellen und ausprobieren
Der Test in 11.2.0.1 dazu sah etwa folgendermaßen aus - und bediente sich aus ähnlichen Beispielen von Christian Antognini und Neil Johnson (die, hätte ich sie sorgfältig gelesen, die im Raum stehende Frage bereits beantwortet hätten):

-- Test mit einem User mit DBA-Rechten

-- Anlage eines Tablespace für temporäre Testobjekte
create tablespace scratch datafile 'E:\ORACLE_DATA\TEST\SCRATCH.DBF' size 50M;

-- Anlage einer Test-Tabelle
drop table t;

create table t 
as
select rownum id
     , lpad('*', 50, '*') compressable_col
  from dual
connect by level <= 1000000;

exec dbms_stats.gather_table_stats(user, 'T')  

-- Aufruf der Prozedur zur Bestimmung der Komprimierungseffekte
DECLARE
  l_blkcnt_cmp       BINARY_INTEGER;
  l_blkcnt_uncmp     BINARY_INTEGER;
  l_row_cmp          BINARY_INTEGER;
  l_row_uncmp        BINARY_INTEGER;
  l_cmp_ratio        NUMBER;
  l_comptype_str     VARCHAR2(100);
BEGIN
      dbms_compression.get_compression_ratio(
        -- input parameters
        scratchtbsname   => 'SCRATCH',                           -- scratch tablespace
        ownname          => user,                                -- owner of the table
        tabname          => 'T',                                 -- table name
        partname         => NULL,                                -- partition name
        comptype         => dbms_compression.comp_for_oltp,      -- compression algorithm
        subset_numrows   => 1000000,
        -- output parameters
        blkcnt_cmp       => l_blkcnt_cmp,    -- number of compressed blocks
        blkcnt_uncmp     => l_blkcnt_uncmp,  -- number of uncompressed blocks
        row_cmp          => l_row_cmp,       -- number of rows in a compressed block
        row_uncmp        => l_row_uncmp,     -- number of rows in an uncompressed block
        cmp_ratio        => l_cmp_ratio,     -- compression ratio
        comptype_str     => l_comptype_str   -- compression type
      );
      dbms_output.put_line('type: '|| l_comptype_str);
      dbms_output.put_line('blocks comp: '|| l_blkcnt_cmp);
      dbms_output.put_line('blocks uncomp: '|| l_blkcnt_uncmp);
      dbms_output.put_line('ratio: '|| to_char(l_cmp_ratio,'99.999'));
END;
/

type: "Compress For OLTP"
blocks comp: 203
blocks uncomp: 1024
ratio:   5.000

select blocks from dba_segments where segment_name = 'T';

    BLOCKS
----------
      9216

Die "blocks uncomp" haben also nichts mit der tatsächlichen Größe des Basisobjekts zu tun - und bereits die erforderliche Angabe des "scratchtbsname" konnte vermuten lassen, dass hier temporäre Objekte und Sampling im Spiel sind. Diese Annahme bestätigt dann auch das zugehörige SQL-Trace:

create table "TEST".DBMS_TABCOMP_TEMP_UNCMP tablespace "SCRATCH" nologging as
  select /*+ FULL("TEST"."T") */ *  from "TEST"."T"  sample block( 22) mytab
 
create table "TEST".DBMS_TABCOMP_TEMP_CMP organization heap  tablespace
  "SCRATCH" compress for oltp nologging as select /*+ FULL("TEST"."T") */ *
  from "TEST".DBMS_TABCOMP_TEMP_UNCMP mytab

Hier wird also via CTAS eine nicht komprimierte Tabelle aus einem Sample des Quellobjekts erzeugt und zu diesem Sample wird dann anschließend eine komprimierte Version angelegt. Ungefähr so hätte ich das wahrscheinlich auch gemacht, wenn ich einen entsprechenden Test manuell durchgeführt hätte. Zur Beantwortung der ursprünglichen Frage könnte die Procedure demnach nur indirekt dienen.

Samstag, Mai 03, 2014

Histogramme löschen

Jonathan Lewis zeigt, wie man in 10g Histogramme löschen kann - ab 11g gibt es zu diesem Zweck den DBMS_STATS.DELETE_COLUMN_STATS-Parameter col_stat_type, mit dem die Löschung explizit durchgeführt werden kann. Neben dem Verfahren erklärt der Autor auch noch einmal seine grundsätzliche Strategie der expliziten Anlage der tatsächlich benötigten Histogramme:
my preferred method of collecting statistics is to use method_opt => ‘for all columns size 1' (i.e. no histograms) and then run scripts to create the histograms I want. This means that after any stats collection I need to run code that checks to see which tables have new stats, and then re-run any histogram code that I’ve written for that table.
Mit den neuen Histogrammtypen in 12c sieht der Herr Lewis aber seltener die Notwendigkeit der manuellen Erzeugung von Histogrammen, die er an diversen Stellen erläutert hat.

Freitag, Mai 02, 2014

Komplexe Gruppenbildung mit Analytics, Model clause oder MATCH_RECOGNIZE

Stew Ashton (der zu den regelmäßigen Beiträgern im OTN Forum gehört, wenn es um komplexere SQL-Fragestellungen geht) liefert in seinem Blog einige Varianten zur Lösung des Problems "Group records that are within 6 hours of the first record in the group", zu dem Tom Kyte bei AskTom schrieb: "I don't see offhand a way to do that with analytics." Anstelle von Analytics kommen zwei andere Möglichkeiten in Frage: die Model clause und seit 12c auch die MATCH_RECOGNIZE clause, wobei erstere von der Möglichkeit der Verwendung prozeduraler Logik und letztere von der Möglichkeit der Mustererkennung profitiert. Ich spare mir eine genauere Beschreibung der Lösungen - habe aber den Eindruck, dass die Syntax von MATCH_RECOGNIZE vergleichsweise handhabbar sein sollte (im Vergleich zur Model clause, die mir noch immer wenig Freude bereitet).

Mittwoch, April 30, 2014

Foreign Key Indizes und Spaltenreihenfolgen

Richard Foote hat in seinem Blog schon vor einiger Zeit die Frage beantwortet, ob ein zur Vermeidung von Locking Problemen für einen Foreign Key definierter Index in Reihenfolge und Spaltenauswahl der FK-Definition vollständig entsprechen muss - und die Antwort lautet: nein. Ähnlich wie im Fall eines Index zur Unterstützung eines PK-Constraints genügt es, wenn die führenden Spalten des Index denen des Constraints entsprechen, wobei die Reihenfolge keine Rolle spielt. Dazu gibt's dann natürlich auch ein Beispiel, das auch noch einmal die grundsätzlichen Vorteile der FK-Indizierung darstellt (Vermeidung kostspieliger Locks beim Löschen von PK-Einträgen der Parent-Tabelle, die schnell in deadlocks übergehen können).

Nachtrag 09.05.2014: Außerdem hat Richard Foote auch noch gezeigt, dass auch invisible indexes zur Vermeidung von Locking Effekten verwendet werden können, was wieder ein Hinweis darauf ist, dass man beim Löschen anscheinend nutzloser Indizes vorsichtig sein muss.

Donnerstag, April 24, 2014

Geschichte des Nested Loops

Jonathan Lewis erläutert wie sich der NESTED LOOPS Join und seine Repräsentation im Execution Plan im Laufe der Releases seit 8.1.7.4 verändert haben. Ich verzichte auf eine Wiederholung der Details, da ich die meisten davon schon in älteren Beiträgen erwähnt habe.

Dienstag, April 22, 2014

Insert Performance mit Bitmap und B*Tree Index

Bitmap Indizes machen eine Menge Ärger, wenn es um DML geht. Diese Erkenntnis ist nicht neu und weitgehend richtig, und ich habe hier auch schon mal gelegentlich einen - eher anekdotischen - Fall vorgeführt, in dem ein Insert in eine Tabelle mit Bitmap Index deutlich langsamer von statten ging als ein entsprechendes Insert in eine Tabelle mit B*Tree Index (wobei ich bei erneuter Betrachtung des Beispiels darüber nachdenke, ob das Ergebnis nicht ziemlich massiv durch eine unglückliche Dimensionierung der redo log files beeinflusst war). Nun hat Jonathan Lewis einen Artikel zum Thema veröffentlicht, der erklärt, warum ein Insert im Bitmap-Fall auch schneller erfolgen kann als im B-Tree-Fall und inwiefern dieses Verhalten erklären könnte, wieso Bitmap Indizes nicht zur Vermeidung eines Table Locks beim Löschen von Datensätzen aus der zugehörigen Parent-Tabelle verwendet werden können. Das gleiche Thema hat übrigens dieser Tage auch Richard Foote in seinem Blog angesprochen, dabei aber eine andere Argumentationslinie verfolgt und den Artikel nach einem Kommentar des Herrn Lewis auch noch mal überarbeitet.

Die Antwort auf die angesprochene Frage ist - wie das im Fall von Antworten, die man kennt, ja häufiger vorkommt - eigentlich ganz einfach: ein Blick auf die Session Statistiken eines entsprechenden Test-Falls zeigt, dass die Maintenance des B*Tree-Index sehr viel größere Ressourcen erfordert als die des Bitmap-Index, mehr LIOs, mehr Redo und Undo, mehr Memory in PGA/UGA etc. Diese Statistiken habe ich in einem Kommentar im Scratchpad aufgeführt, und Jonathan Lewis schreibt dazu:
As Martin has pointed out, there are a number of statistics that show large differences between the B-tree and bitmap approaches, but the one he didn’t mention was the key: sorts (rows).
Also knapp vorbei - schade eigentlich... Die Lösung lautet: Oracle verschiebt die Bitmap-Index-Maintenance ans Ende der Insert-Operation - in ähnlicher Weise wie das im Fall eines direct path Inserts für alle Index-Typen geschieht. Die vorsortierten Werte werden dann in den Index-Baum ge-merged, was I/O, Block-Splits etc. minimiert. Andererseits macht diese Verzögerung eine Verwendung im Rahmen der FK-Lock-Vermeidung unmöglich. Eine ähnliche Verzögerungsstrategie kann Oracle offenbar auch bei Delete- und Update-Operationen verwenden. Wie gesagt: ganz einfach, wenn man die Lösung kennt.

Cardinality für IN-Listen mit NULL-Werten in 12.1

Ein sperriger Titel für eine einfache Beobachtung. Vor etwas mehr drei Jahren hatte ich hier überprüft, wie sich die von Jonathan Lewis in Cost Based Oracle beschriebene Berechnung von Cardinalities für IN-Listen unterschiedlicher Struktur in neueren Releases verändert hatte. Zu den Fällen, die im Buch untersucht wurde, gehörten:
  • wiederholte Werte
  • Werte außerhalb des Korridors bekannter Werte (not between low_value and high_value)
  • NULL-Werte
Wobei es zu jedem dieser Fälle unterschiedliche Varianten gab. Bereits in 11.2.0.1 verhielten sich alle untersuchten Fälle bis auf einen ganz plausibel. Der verbliebene Fall war die Behandlung von NULL-Werten - und diesen Fall habe ich mir jetzt noch mal mit 12.1.0.1 angesehen:

-- 12.1.0.1
create table test_inlist
as
select rownum pk_col
     , mod(rownum, 12) mon_col
     , lpad('*', 100, '*') pad_col
  from dual
connect by level <= 120000;

exec dbms_stats.gather_table_stats (ownname=>user, tabname=>'TEST_INLIST', estimate_percent=>dbms_stats.auto_sample_size)

select column_name
     , num_distinct
     , num_nulls
     , density
     , num_buckets
  from user_tab_columns
 where table_name = 'TEST_INLIST'
   and column_name = 'MON_COL';

COLUMN_NAME                    NUM_DISTINCT  NUM_NULLS    DENSITY NUM_BUCKETS
------------------------------ ------------ ---------- ---------- -----------
MON_COL                                  12          0 ,083333333           1

select count(*)
  from test_inlist
 where mon_col in (3, 25, null);

Plan hash value: 3620027018

----------------------------------------------------------------------------------
| Id  | Operation          | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |             |     1 |     3 |   529   (1)| 00:00:01 |
|   1 |  SORT AGGREGATE    |             |     1 |     3 |            |          |
|*  2 |   TABLE ACCESS FULL| TEST_INLIST | 10000 | 30000 |   529   (1)| 00:00:01 |
----------------------------------------------------------------------------------

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

   2 - filter("MON_COL"=3 OR "MON_COL"=25)

Die einschränkende Bedingung enthält somit einen Wert innerhalb des bekannten Wertebereichs (3), einen Wert weit jenseits des bekannten Wertebereichs (25) und einen NULL-Wert. In 11.2.0.1 wurde dafür noch eine Cardinality von 20000 berechnet: 10000 für den Wert 3, 0 für den Wert 25 und wieder 10000 für den Wert NULL. In 12.1 ist das Verhalten korrigiert, denn mon_col in (NULL) liefert natürlich kein Ergebnis, so dass die angenommenen 10000 dem tatsächlichen Ergebnis entsprechen.