Lösungen

Lösung: Index Rebuild: Magic or Voodoo?

Meine Leser haben das Problem natürlich richtig erkannt. Ich danke Jure Bratina, Andrew Sayer und Martin Berger für Ihre Beiträge.
Im folgenden sehen Sie jetzt den ganzen Testcase in kommentieer Form. Das meiste ist, denke ich, selbsterklärend.
Am Anfang wird das Szenario aufgebaut. Das Schema ist, wie gesagt, das SH Beispielschema.

alter table sales add  (sparse varchar2(300)); 
update sales set sparse = rpad('sometext',300, '*');
commit;
create index sparse_idx on sales (sparse);
select blocks from user_segments where segment_name ='SPARSE_IDX';

Lassen Sie uns jetzt die Grösse des Indexsegements prüfen:

select blocks from user_segments where segment_name ='SPARSE_IDX';

   BLOCKS
---------
    44032

select leaf_blocks from user_indexes where index_name ='SPARSE_IDX';

LEAF_BLOCKS
-----------
      41766

Jetzt kommt ein Update, den ich in ähnlicher Form auch in der Original Datenbank gefunden habe:

update sales set sparse=NULL;

918843 Zeilen aktualisiert.

exec dbms_stats.gather_table_stats(user,'SALES');

Und wie wirkt sich das auf die Statistiken und die Segemente aus?

select leaf_blocks from user_indexes where index_name ='SPARSE_IDX';

LEAF_BLOCKS
-----------
          0
select blocks from user_segments where segment_name ='SPARSE_IDX';

    BLOCKS
----------
     44032

Da der Optimizer lediglich die Index Statistiken prüft, nicht aber die Segementstatistiken, hält der Optimizer den Index für winzig klein.
Das also alle nicht null Werte der Spalte sparse im Index sein müssen, ist es aus Sicht des Optimizers am besten, den angeblich kleinen Index zu scanen.
Das Indexsegment hat aber immer noch die volle Grösse. Erst ein Index rebuld schafft hier Abhilfe.

Lösung: Eine unerwartete Bedingung in der Where Clausel

Die merkwürdige Bedingung wird von der Datenbank automatisch generiert.
Die Ursache ist die DDL Optimization, die es in der Form seit Version 11G gibt.
Wenn man bei einer Tabelle eine zusätzliche Spalte einfügt, muss diese Spalte nicht zwingend physisch erzeugt werden.
Es kann auch eine “DDL optimized” Spalte erzeugt werden und wenn man einen Default angibt, kann diese Spalte auch not null sein.
Damit erspart die Datenbank sich den Aufwand, jeden Datensatz um eine Spalte zu erweitern.
Satt dessen wird nur ein Eintrag ins Dictionary gemacht, was natürlich viel schneller geht.
Jede Datenzeile kann einen Wert für die “DDL optimized” Spalte enthalten, wenn der Wert über einen insert eingefügt wurde.
Wenn kein Wert eingefügt wird, wird der Default Wert verwendet.
Da es möglich ist, dass kein erfasster Wert existiert, muss die Datenbank den spaltennamen durch die Formel ersetzen.
Hier eine einfaches Beispiel :

create table x (y number);
insert into x select rownum from dual connect by rownum < 1000000;
commit;
alter table x add ( z number default 1 not null);
select 1 from x where z=1;

Wenn wir uns den Execution Plan der Abfrage ansehen, bemerken wir, dass der Spaltenname Z durch die Formel


(NVL("Z",1)=1)

ersetzt wurde.

Hier noch der Link zu Carlos Blog: Interesting case where a full table scan is chosen instead of an index

Lösung: Die Collection in der Collection

Die entscheidenden Hinweise zur Lösung der Aufgabe habe ich bei Adrian Billington gefunden. Es ist schon ein älterer Eintrag, aber immer noch gültig: introduction to bulk pl/sql enhancements in 9i.
Der Code unten sollte mit den Anmerkungen selbst erklärend sein.
Um zu zeigen, wie die Zugriffe auf die Daten funktionieren, habe ich noch eine Ausgabe über DBMS_output eingefügt.

— Zuerst muss man den Typ für die nested collection erzeugen

CREATE OR REPLACE type emp_t
AS
  object
  (
    EMPNO NUMBER(4),
    ENAME VARCHAR2(10),
    SAL  NUMBER(7,2),
    COMM NUMBER(7,2) 
  )
/
create or replace TYPE tbl_emp  AS TABLE OF emp_t
/

 

DECLARE
  CURSOR c1
  IS
    SELECT deptno,
      dname,
      CAST (MULTISET( SELECT empno,ename,sal, comm FROM emp e WHERE e.deptno= d.deptno
                     ) AS tbl_emp  
           ) as emps -- ich brauche einen alias um die eingebette Collection referenzieren zu können
  FROM dept d;

  TYPE tbl_dept IS TABLE OF c1%ROWTYPE;
  depts tbl_dept ;
BEGIN
   OPEN c1;
   FETCH c1 BULK COLLECT INTO depts;
   CLOSE c1;
   FOR i IN 1..depts.COUNT
   LOOP
      DBMS_OUTPUT.PUT_LINE(depts(i).deptno);
      FOR j IN  1.. depts(i).emps.COUNT
      LOOP
        DBMS_OUTPUT.PUT_LINE('***'||depts(i).emps(j).ename);
      END LOOP;
   END LOOP;
END;
/

 

Lösung: Ist die Migrationsdatenbank langsamer?

In OTN wurde einmal gefragt, ob der Optimizer beim Erstellen des Planes die Grösse des Buffercaches berücksichtigt. Unter den üblichen Vorbehalten (es könnte in die Kostenbewertung mit einfliessen) würde ich sagen: Nein. Wie sollte sich die Grösse des Caches auch auswirken? Wenn ich einen grösseren Buffer cache habe, ist wahrscheinlicher dass ein X Segement im Cache ist, daher mache ich einen Indexzugriff ? (Ich argumentiere hier analog zum Parameter OPTIMZER_INDEX_CACHING.) Das ist keine gute Logik.  Die Grösse es Buffer caches ist kein sicherer Indikator dafür, ob ein spezifisches Segment auch tatsächlich im Cache ist. Wie man später sehen wird, kann  die Grösse des Caches auch ein Argument für den Full Table Scan sein.

Für die Erstellung des Execution Plans ist die Selektivität der entscheidende Faktor.

Wie jedoch ein bestehender Execution Plan umgesetzt wird, steht auf einem anderen Blatt. Hier hat die  Execution Enigine unter Umständen noch ein Wort mit zu sprechen.

Zum Beispiel bei einem Full Table Scan (FTS). Oft wird dieser als „direct path read“ umgesetzt. Das heisst, dass zwingend ein physischer I/O gemacht werden muss. (Für die Datenbank ist es auch dann noch ein physischer I/O wenn das Resultat aus dem File System Cache kommt. )

Wenn die Execution Engine aber erkennt, dass eine Tabelle sich fast vollständing im Buffer Cache befindet, kann von einem „direct path read“ auf einen „scattered read“ umgeschalten werden, Der „scattered read“ kann im Gegensatz zum „direct path read“ Nutzen aus dem Buffer Cache ziehen.

Ein ältere, aber gute Beschreibung stammt aus der Feder von Tanel Poder: Optimizer statistics-driven direct path read decision for full table scans .

Kurz gesagt gibt es zwei Voraussetzungen ob ein Umschalten über die Execution Engine stattfinden kann

  1. Der Buffer Cache muss gross genug sein, dass das Segment, das Gegenstand des FTS ist im Buffer Cache Platz hat
  2. Das Segment muss auch tatsächlich fast vollständig im Buffer Cache sein

Der Erste Punkt lässt sich ja leicht klären.  Die Analyse ergab folgendes: Die Tabelle war in der Produktion und in der Migration ca. 25GB gross.
Der Buffercache betrug in der Produktion aktuell 55 GB. In der Migration waren es lediglich 5 GB.
Damit lässt sich mit Sicherheit sagen: In der Produktion kann die besagte Tabelle vollständig gecacht werden. Theoretisch kann die Runtime Engine einen Scan im Memory veranlassen. In der Miragtion ist dies sicher nicht möglich.
War jetzt aber in der Produktion eine ausreichend grosse Anzahl Blöcke gecacht um einen Scan im Memory zu machen?
Zur Klärung dieser Frage dient folgendes Statement, welches die Anzahl der gecachten Blöcke einer Tabelle ermittelt.


SELECT sum(num_buf)
FROM X$KCBOQH x, dba_objects o
WHERE x.obj#=o.object_id
AND object_name='my table'
;

Wenn die so ermittelte Anzahl fast identisch zur Anzahl der Blöcke der Tabelle ist (nach meinen Tests > 90%, aber ohne Gewähr) wird auf „scattered read“ umgestellt.
Die Analyse der produktiven Datenbank ergab, dass die besagte Tabelle wirklich zum grössten Teil im Buffer Cache war. Ich konnte auch zeigen, dast der schnelle FTS nicht garantiert war, sondern nur dann stattfindet wenn die Tabelle ausreichend gecache. Wir machten noch die Gegenprobe und erhöhten den Buffer Cache der Migrationsumgebung. Sodann luden wir die Tabelle in den Cache.
Danach war der FTS auch in der Migrationsumgebung schnell.

Einige Anmerkungen:

Ein neuere, noch tiefere Analyse des Themas findet sich bei Frits Hoogland:

Investigating the full table direct path / buffered decision

Aufgrund verschiedener Beobachtungen denke ich, dass der Storage Server einer Exadata ähnliche Algorithmen in Bezug auf den Smart Scan anwenden kann, und ein Smart Scan in einen Scan des Flash Cache umgewandelt werden kann. Auch diese Information muss ich aber ohne Gewähr geben.

Lösung: Warum ist die neue Hardware langsamer?

Wie Martin Preiss auf Twitter meldete hatte Tanel bereits dokumentiert LOBREAD SQL Trace entry in Oracle 11.2 dass Einträge wie LOBREAD tatasächlich auf LOBs hinweisen. Der Traceeintrag kam vermutlich mit Version 11.2.0.2.
So wussten wir, dass es eine Schemaänderung in der Datenbank auf der neuen Hardware stattgefunden hatte. Der nächste entscheidende Hinweis kam über diesem Auschnitt des raw trace (u.a. wieder einmal entdeckt von Martin Berger):


FETCH #25:c=1154407,e=1152124,p=0,cr=102603,cu=0,mis=0,r=101 ,dep=0,og=1,tim=650755949521

verglichen mit:

FETCH #601010888:c=31200,e=22483,p=0,cr=3706,cu=50,mis=0, r=1 ,dep=0,og=1,plh=3621104505,tim=39783214696

Nun, sieht so aus als ob wir ohne LOB 101 Datensätze auf einmal in einem Arry Fetch holen. Mit LOB ist es jeweils nur einer. Wie ist das möglich, wenn der Programmcode identisch ist?
Verhindert der LOB in irgendeiner Weise den Array Fetch?
Nun, in der Tat ist das der Fall, wie hier dokumentiert:Single Row Fetch from a LOB (Danke Hemant). Stefan Köhler hat darauf hin gewiesen, dass es auch vom Treiber abhängt single row fetch depends on client.
Nachdem wir die LOB Spalte durch eine Varchar2 Spalte ersetzt hatten, war die neue Hardware in diesem test schneller als die alte.

Lösung: Warum ist kein Alias nötig?

Nun, beantworten wir zunächst die Zusatzfrage. Es scheint, dass in sql die Typen nicht direkt genutzt werden können.
Wie Martin Berx im Kommentar zum Englischen Blog richtig bemerkt, muss man zuerst auf den Typ casten.
In diesem Fall:

TREAT(CUST_ADDRESS AS CUST_ADDRESS_TYP).STATE_PROVINCE = 'FL'

Dies it ein nützlicher kleiner Trick, den man immer wieder einmal brauchen kann.
Jetzt kann man das Statement ausführen. Mein Vermutung war, dass der natural Join dafür sorgt, dass man keinen Alias genötigt.
In der Tat resultiert

SELECT order_id,
SUM(quantity*unit_price)
FROM order_items
HAVING SUM(quantity *unit_price) > ALL
(SELECT SUM(quantity*unit_price)
FROM customers
JOIN orders o USING (customer_id)
JOIN order_items oi ON (oi.order_id=o.order_id )
WHERE TREAT(CUST_ADDRESS AS CUST_ADDRESS_TYP).STATE_PROVINCE = 'FL'
GROUP BY order_id
)
GROUP BY order_id

in:

FEHLER in Zeile 10:
ORA-00918: Spalte nicht eindeutig definiert

Erst wenn man einen Prefix für Order id im group by definiert geht es:


SELECT order_id,
SUM(quantity*unit_price)
FROM order_items
HAVING SUM(quantity *unit_price) > ALL
(SELECT SUM(quantity*unit_price)
FROM customers
JOIN orders USING (customer_id)
JOIN order_items ON (order_items.order_id=orders.order_id )
WHERE TREAT(CUST_ADDRESS AS CUST_ADDRESS_TYP).STATE_PROVINCE = 'FL'
GROUP BY o.order_id
)
GROUP BY order_id

Meiner Meinung nach sollte es eigentlich immer so funktionieren wie im Fall des natural join. Da hat man schon definiert, dass die Felder gleich sind, soll sich der Optimizer doch eines aussuchen.

 

Lösung: Woher kommen die Downgrades?

Wie Stefan schon korrekt im Englischen Blog bemerkte, sind die downgrades zu einem Teil die Folge von parallelen Sessions die Toad stehen lässt. Dabei kann der auslösende Clientprozess schon lange nicht mehr existieren, die Datenbank sessions bestehen weiter. Dadurch kamen wir immer mehr unter Druck, da die Anzahl dieser Sessions langsam anwuchs. Es konnten immer weniger neue px alloziert werden, da der pool nicht unendlich ist.

Als Massnahme dagegen haben wir ein Profile aktiviert, dass inaktive Session nach einer gewissen Zeit killt.

Der zweite Grund lag daran, dass der Parameter parallel_adaptive_multi_user  gesetzt war. Dieser sorgte dafür, dass etwa die Häfte des parallel session pools für parallel processing verwendet werden konnte. Der Parameter war offenbar in einem DWH sinnwidrig. Die lokalen DBA’s bestanden aber darauf die Default wert nicht zu verändern. Zum ersten Mal war ich mit der absurden Idee konfrontiert, dass ein Default wert so etwas wie ein Tabu darstellt. Stattdessen wurde der Parameter parallel_threads_per_cpu triumphierend auf den Default von 2 gesetzt. Jedoch hatte das OS bereits alle Threads als CPU gemeldet, sodass jetzt doppelt so viele CPUs gemeldet waren als wirklich vorhanden waren, ebenso doppelt so viele Threads.

Für die downgrades haben sich drei sinnlose Werte in den Parametern glücklich neutralisiert. Wozu allerdings der Resource Manager noch lief, ist mir ehrlich gesagt nicht klar. 😉

 

 

Lösung: Viele Hierarchien gleichzeitig navigieren

Zuerst Legen wir die nötigen Indexe an:


create index i1 on tree_emp( empno);
create index i2 on tree_emp( mgr);

Dannach definieren wir das pipe table package. Natürlich geht es auch effizienter, aber ich möchte hier das Prinzip zeigen.


create or replace package parallel_access
as

TYPE R_REC IS RECORD (empno tree_emp.empno%TYPE,
sum_salaries Number); — result record, change defintion according to your needs
TYPE refcur_t IS REF CURSOR RETURN R_REC;

TYPE result_Tab is TABLE OF R_REC;

FUNCTION passData (p_ref refcur_t) RETURN result_Tab
PIPELINED
PARALLEL_ENABLE(PARTITION p_ref BY ANY); — function will inherit parallelism from ref cursor

END parallel_access;
/

create or replace package body parallel_access
as
FUNCTION passData (p_ref refcur_t) RETURN result_Tab
PIPELINED PARALLEL_ENABLE(PARTITION p_ref BY ANY)
IS
out_rec r_rec;
BEGIN
execute immediate ‚alter session set „_old_connect_by_enabled“=true‘;
LOOP — for each parallel process
FETCH p_ref INTO out_rec;
EXIT WHEN p_ref%NOTFOUND;
SELECT sum(sal)
INTO out_rec.sum_salaries
FROM tree_emp
CONNECT BY PRIOR EMPNO = MGR
START WITH mgr = out_rec.empno;

PIPE ROW(out_rec);
END LOOP;
RETURN;
END passData;

END parallel_access;
/

Verwenden Können wir as Pacakge wie folgt. Parallel 4 ist nur ein Beispiel, man kann die Parallelität je nach Bedarf wählen.

SELECT b.* FROM
TABLE(parallel_access.passdata (CURSOR( select /*+ parallel (d 4) */ empno , null from tree_emp where mgr is null))) b;

Solution: Runtimestatistiken lesen

Runtime statistiken sind für mich das wichtigste Optimierungswerkzeug für nicht parallele Abfragen.
Runtime statistiken lassen sich schnell erzeugen und beinhalten in der Regel schon alle nötigen Informationen für eine Optimierung.
Ich wiederhole zunächst die Runtimestatistiken der Rätselfrage:

 

-----------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads     |
-----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |      |      1 |        |      1 |00:00:15.42 |    3540 |      3262 |
|   1 |  SORT AGGREGATE              |      |      1 |      1 |      1 |00:00:15.42 |    3540 |      3262 |
|*  2 |   TABLE ACCESS BY INDEX ROWID|   T1 |      1 |  52695 |     10 |00:00:15.42 |    3540 |      3262 |
|*  3 |    INDEX RANGE SCAN          |   I1 |      1 |     17M|   4577 |00:00:00.16 |      11 |         9 |
-----------------------------------------------------------------------------------------------------------

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

   2 - filter(("NVL("X1_STATUS",'A')<>'U' AND NVL("X2_STATUS",'A')<>'D' 
               AND NVL("X3_STATUS",'A')<>'D' AND NVL("X4_STATUS",'A')<>'D' 
               AND NVL("X5_STATUS",'A')<>'U'))
   3 - access("T1_ID"=:B2 AND "T2_ID"=:B4 AND "T3_ID"=:B1)

Bei dem Problem fällt zunächst einmal auf, dass die Schätzung des Optimizers beim Indexzugriff in Operation 3 weit daneben liegt. Hierzu werden die „estimated rows“  (E-rows) mit den „actual rows“ (A-Rows) verglichen. Da es sich um eine sehr einfach zusammengesetzte Bedingung mit lauter „=“ Vergleichen handelt liegt es nahe, hier eine funktionale Abhängigkeit zu vermuten.

Die entsprechende Gegenmassnahme wären extended statistics.
Jedoch wird die meiste Zeit in der Operation 2 verloren. Hier fallen die „actual rows“ von 4577 Datensätzen in Operation 3 auf 10 Datensäte ab. Verantwortlich dafür ist die Filterbedingung in Operation 2:

2 - filter(("NVL("X1_STATUS",'A')<>'U' AND NVL("X2_STATUS",'A')<>'D' 
               AND NVL("X3_STATUS",'A')<>'D' AND NVL("X4_STATUS",'A')<>'D' 
               AND NVL("X5_STATUS",'A')<>'U'))

Gemäss der Tuning Technik „Throw-away“ erfunden vom Oak Table Mitglied Martin Berg müssen wir zusätzliche Spalten in den Index hinzuzufügen um den „throw-away“ in operation 2 zu vermeiden.

Ich habe sehr kluge Empfehlungen in meinem Kommentaren gefunden. Allerdings muss man in diesem Falle nicht sehr kompliziert vorgehen. Wir haben die Status Felder getestet und herausgefunden welche die Selektivität des Indexes erhöhen. Diese haben wir einfach in den Index eingefügt. Fälschlicherweise war der Entwickler der Applikation der Meinung die Statusfelder würden Nichts nützen, weil bei einem ungleich Vergleich kein Index verwendet werden kann.

Das ist nicht korrekt. Die Baumstruktur kann nicht verwendet werden. Jedoch wird bei einem Index Range Scan über die Baumstruktur nur der erste Leaf Block gesucht. Ab dann wird serial in den Leaf Blöcken weiter gesucht.

Bei dieser seriellen Suche kann über beliebige Vergleichsoperatoren gefiltert werden. Wie wir anhand der unten stehenden Runtime Statistiken sehen, reicht das schon um eine gute Performance zu gewährleisten. Der Verbesserungsfaktor liegt bei run 700.

Nicht einmal die Verbesserung der Statistiken ist für diesen Fall letztlich erforderlich, da der Optimizer zwar falsch schätzt, aber die zusätzlichen Spalten dennoch als Vorteil wahrnimmt.

Natürlich wäre die extened statistics unabhängig davon trotzdem sinnvoll.

 

-----------------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name                | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                     |      1 |        |      1 |00:00:00.02 |   12 |         4 |
|   1 |  SORT AGGREGATE              |                     |      1 |      1 |      1 |00:00:00.02 |   12 |         4 |
|*  2 |   TABLE ACCESS BY INDEX ROWID| MM_SALES_DELIVERIES |      1 |  52695 |     10 |00:00:00.02 |   12 |         4 |
|*  3 |    INDEX RANGE SCAN          | PR_SALE_DEL_03      |      1 |  52695 |     10 |00:00:00.01 |    6 |         0 |
-----------------------------------------------------------------------------------------------------------------------

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

   2 - filter((NVL("MM_DISTRIBUTION_DEL_STATUS",'H')<>'D' AND NVL("MM_OUTBOUND_DEL_STATUS",'H')<>'D' AND
              NVL("MM_OUTBOUND_STATUS",'H')<>'U'))
   3 - access("MM_WAREHOUSE_ID"=:B4 AND "MM_FIRM_ID"=:B1 AND "MM_ITEM_ID"=:B2 AND "MM_FROM_LOCATION_ID"=:B3)
       filter((NVL("MM_DISTRIBUTION_STATUS",'H')<>'U' AND NVL("MM_DIRECT_DEL_STATUS",'H')<>'D'))

Lösung: Rätsel der verlorenen Session

Wie schon in Twitter spekuliert war der Befehl connect und der Vorgang war logon. Die Ursache konnte nach einigem Suchen gefunden werden: Der Reportserver versuchte sich mit ungültigen credentials an zu melden.

Nachdem man die credentials korrigiert hatte sind selbstverständlich auch die row locks verschwunden.

Den ersten guten Hinweis auf die Lösung bekam ich im Blog von Dominic Brooks: Failed Logins can cause row cache lock on dc_users. Auch Martin Preiss hatte diese Website gefunden. Stefan Koehler hat einen sehr ähnlichen Fall wunderbar ausführlich dokumentiert: Wait event „library cache lock“ by (security) design and exploring system state dumps (oradebug). Den wichtigen Hinweis auf das „connection management call elapsed time“ event im AWR hatte Franck Pachot auf Twitter gegeben. Natürlich war auch Spürnase Martin Berger wie immer auf der richtigen Fährte.