Ist die Migrationsdatenbank langsamer?

Für eine Mirgation wurde eine Datenbank in einer virtuellen Umgebung bereitgestellt.  Erste Tests zeigen, dass die Migrationsdatenbank ein Vielfaches langsamer ist als die Produktionsdatenbank. Die Tests konzentrieren sich auf eine bestimmte Abfrage.

Hier ein Ausschnitt aus einem AWR der Migrationsdatenbank. Zu sehen sind die relevanten Daten des Sql Befehls, den man untersuchen soll als Ausschnitt aus der Liste „SQL ordered by elapsed time“.

 

apsed Time (s)

Executions

Elapsed Time per Exec (s)

%Total

%CPU

%IO

199.24

1

199.24

98.16

3.93

96.72

Zum Vergleich die selben Daten aus der produktiven Datenbank:

Elapsed Time (s)

Executions

Elapsed Time per Exec (s)

%Total

%CPU

%IO

11.02

1

11.02

65.95

99.98

0.0

Was fällt Ihnen auf? Mit welcher Arbeitshypothese würden Sie die Untersuchung beginnen und was würden Sie prüfen? Hinweis: Der Befehl ist ein count welcher nur eine Tabelle liest. Der Execution plan ist in beiden Fällen identisch, es ist jeweils ein Full Table Scan.

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.

Warum ist die neue Hardware langsamer?

Man kauft neue Hardware um schneller zu werden. Das ist eine ganz normale Erwartung. Was aber wenn die neue Hardware langsamer ist als die alte? Die Spekulationen über die Ursache gingen wild hin und her. Da ich auf diesen Einsatz urlaubsbedingt lange warten musste, war die Spannung gross als die Untersuchung endlich beginnen konnte.

Eine schneller Überprüfung zeigte, dass die neue Hardware nicht langsamer war als die alte. Den entscheidenden Hinweis lieferte ein raw trace. Ich zeige hier nur ein entscheidenden Ausschnitt.

Auf der alten Hardware sah der trace so aus:


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

Cursor #25 ist ein grosses Select, das langsam läuft. Auf der neuen Hardware hingegen sah der trace wie folgt aus:


FETCH #601010888:c=31200,e=22483,p=0,cr=3706,cu=50,mis=0,r=1,dep=0,og=1,plh=3621104505,tim=39783214696
WAIT #601010888: nam='SQL*Net message from client' ela= 171 driver id=1413697536 #bytes=1 p3=0 obj#=-1 tim=39783217398
LOBGETLEN: c=0,e=3,p=0,cr=0,cu=0,tim=39783217416
WAIT #0: nam='SQL*Net message to client' ela= 0 driver id=1413697536 #bytes=1 p3=0 obj#=-1 tim=39783217423
WAIT #0: nam='SQL*Net message from client' ela= 117 driver id=1413697536 #bytes=1 p3=0 obj#=-1 tim=39783217546
WAIT #0: nam='SQL*Net message to client' ela= 0 driver id=1413697536 #bytes=1 p3=0 obj#=-1 tim=39783217560
LOBREAD: c=0,e=12,p=0,cr=1,cu=0,tim=39783217567

Cursor #601010888: entspricht Cursor #25 auf der alten Hardware. Die Datenbank auf der neuen Hardware ist Version 11, die Datenbank auf der alten Hardware ist Version 10.
Offensichtlich besteht ausser bei der Version mindestens ein weiterer Unterschied zwischen beiden Datenbanken. Was ist es? Wie wirkt sich dieser Unterschied aus?
Beide Datenbanken werden über das exakt selbe Programm angesprochen, welche mit MS Visual Studio realisiert ist.

 

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.

 

Warum ist kein Alias nötig?

Hier ist eine Frage von OTN. Ich bin gespannt, was Sie zu sagen haben. (Bitte nicht die Antwort auf OTN nachsehen.)


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 using (order_id )
where STATE_PROVINCE = 'FL'
group by order_id)
group by order_id;

Die Frage lautet: Warum brauchen wir kleinen Alias für order_id in der „Group order_id“ Klausel der Unterabfrage, obwohl die Spalte in beiden Tabellen angezeigt wird?
So, um es ein bisschen schwieriger zu machen, habe ich eine kleine zusätzliche Hürde errichtet. STATE_PROVINCE ist in der OE-Schema für Version 12 (für andere Versionen habe ich nicht getestet) Bestandteil einer Typvariable. Die Abfrage läuft nicht wie hier gezeigt. Wie sollen wir die Abfrage so ändern, dass es funktionieren wird?

 

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. 😉

 

 

Woher kommen die Downgrades?

In einem DWH kam es verstärkt zu downgrades der Parallelität. Es schien mit jedem Tag schlimmer zu werden. Tatsächlich zeigte sich, dass die frei verfügbaren parallel prozesse konstant abnahmen. Eine nähere Untersuchung der verwendeten PX zeigte bei vielen Toad als Quelle.
Wie war es möglich, dass die Anzahl dieser Toad verursachten parallelen sessions ständig wuchs?
Andererseits es waren immer noch ausreichend PX zur Verfügung, dennoch kam es zu downgrade. Welchen Parameter sollte man sich hier näher ansehen?

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;

Viele Hierarchien gleichzeitig navigieren

Nehmen wir an Sie hätten eine grosse Anzahl flacher Hierachien. Als Beispiel nehmen wir die emp table aus dem Scott Schema.
Daraus erzeugen wir eine Tabelle Tree_emp wie folgt:


create table tree_emp as select * from emp;

alter table tree_emp modify (empno number(18));
alter table tree_emp modify (mgr number(18));

INSERT INTO tree_emp
( EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO
)
;
SELECT EMPNO+(10000*step.n) empno,
ENAME,
JOB,
MGR+(10000*step.n) mgr,
HIREDATE,
TRUNC(DBMS_RANDOM.value(low => 8, high => 50))*100 SAL,
COMM,
DEPTNO
FROM emp,
(SELECT rownum n FROM dual CONNECT BY rownum <= 1000000
) step
;

Weisen Sie für alle Presidenten (mgr is NUll) die Summe der Gehälter sämtlicher Untergebener aus. Schreiben Sie dau eine pipeline table function, die mehrere Bäume gleichzeitig navigieren kann. Verwenden Sie Oracle’s connect by Syntax.für die Baumnavigation.
Sie können nach Belieben indexieren.

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