Performance Tuning mit Analytic Functions

  • Erstellt von Christian Schwitalla
  • Development, Datenbank

Analytic Functions gelten dank besonderer Eigenschaften als Geheimwaffe für so manches Performance-Problem. Noch dazu gibt es kaum Gründe, die gegen ihren Einsatz sprechen, bringen sie doch viel Leistung (fast) zum Nulltarif. Dennoch sind sie häufig unbekannt. Was verbirgt sich hinter diesem oft unterschätzten SQL-Feature?

Analytic Functions bilden eine Familie von SQL-Funktionen, die bereits seit Oracle-Datenbank 8i verfügbar sind. Sie sind auch als Fenster- oder Windowing-Funktionen bekannt, weil sie eine „Frame-Sicht“ auf die Daten bieten. Analytic Functions arbeiten auf der Ergebnismenge einer Abfrage.

Die Oracle-Dokumentation zu Datenbank 12.1 listet insgesamt 46 Funktionen auf, darunter AVG, LAG, MAX und RANK, und definiert Analytic Functions folgendermaßen:

Analytic functions compute an aggregate value based on a group of rows. They differ from aggregate functions in that they return multiple rows for each group. The group of rows is called a window and is defined by the analytic_clause. For each row, a sliding window of rows is defined. The window determines the range of rows used to perform the calculations for the current row. Window sizes can be based on either a physical number of rows or a logical interval such as time.

Darüber hinaus verfügen Analytic Functions über besondere Eigenschaften:

  • Sie bieten Zugriff auf Werte aus anderen Zeilen der Ergebnismenge.
  • Das mehrfache Lesen der Daten ist nicht notwendig.

 

Ein Beispiel mit LAG()

LAG is an analytic function. It provides access to more than one row of a table at the same time without a self join. Given a series of rows returned from a query and a position of the cursor, LAG provides access to a row at a given physical offset prior to that position.
(Auszug aus der Oracle-Dokumentation)

SELECT hire_date, last_name, salary,
       LAG(salary, 1, 0) OVER (ORDER BY hire_date) AS prev_sal
FROM employees
WHERE job_id = 'PU_CLERK'
ORDER BY hire_date;

HIRE_Date    LAST_Name    SALARY    PREV_SAL   
18-MAY-03Khoo31000
24-JUL-05Tobias28003100
24-DEC-05Baida29002800
15-NOV-06Himuro26002900
10-AUG-07Colmenares25002600

 

 

 

Tuning von SQL-Statements im Projekt

In einem Projekt sollte eine Reihe von SQL-Statements getunt werden. Es handelte sich um „Langläufer“, die das nächtliche Batch-Fenster immer häufiger gesprengt haben. Die Aufgabe der Statements bestand darin, Daten aus Quell-Tabellen in „Materialized Views“ (Snapshots) zu laden. Die Statements waren als Views gekapselt und nach einem gemeinsamen Schema aufgebaut. Ihre Aufgabe bestand darin, die korrekte Version eines Objektes aus der Quell-Tabelle herauszufinden.

Um das Prinzip zu veranschaulichen, betrachten wir folgendes, fiktives Beispiel:

  • Lesen von Adressen
  • Pro Kunde nur eine Adresse
  • Auswahl über TYP der Adresse:
    • bevorzugt Adresse mit TYP = FIRMA
    • wenn nicht vorhanden, dann Adresse mit TYP = POSTFACH
    • wenn nicht vorhanden, dann Adresse mit TYP = PRIVAT
  • Tabelle ADRESSEN
    • ID (PK)
    • KUNDEN_ID (UK)
    • TYP [ FIRMA | POSTFACH | PRIVAT ] (UK)
    • STRASSE
    • HAUSNUMMER
    • POSTFACH
    • PLZ
    • ORT

Beispieldaten:

 

KUNDEN_IDTYP
90810846361PRIVAT
90810846361
POSTFACH
217537839264
PRIVAT
481491166609       PRIVAT
481491166609POSTFACH     
481491166609FIRMA

 

Das bisherige SQL-Statement lautete:

SELECT A.KUNDEN_ID, A.TYP,
       A.STRASSE, A.HAUSNUMMER, A.POSTFACH, A.PLZ, A.ORT
FROM ADRESSEN A
WHERE
( ( A.TYP = 'FIRMA' ) OR
  ( A.TYP = 'POSTFACH'
    AND NOT EXISTS ( SELECT 1 FROM ADRESSEN B
                     WHERE B.KUNDEN_ID = A.KUNDEN_ID
                       AND B.TYP = 'FIRMA' ) ) OR
  ( A.TYP = 'PRIVAT'
    AND NOT EXISTS ( SELECT 1 FROM ADRESSEN C
                     WHERE C.KUNDEN_ID = A.KUNDEN_ID
                       AND C.TYP IN ( 'FIRMA' , 'POSTFACH' ) ) ) )

Der Execution-Plan des bisherigen SQL-Statements sah folgendermaßen aus:

 

VorgangOptionenObjekt
SELECT STATEMENT   
   FILTER
      TABLE ACCESSSTORAGE FULL    ADRESSEN
      INDEXUNIQUE SCAN
ADRESSEN_UK   
      INDEXRANGE SCAN
ADRESSEN_UK

 

Hier die Laufzeiten des bisherigen SQL-Statements in Sekunden (Performancetests mit Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit unter VirtualBox auf einem Office Laptop):

 

100 Rows:0,02
10 000 Rows:0,21
1 000 000 Rows:12,58
100 000 000 Rows:1190,00

 

Das Problem: Jeder Satz in der Tabelle ADRESSEN musste dahingehend überprüft werden, ob er in der richtigen Version vorliegt, oder ob gegebenenfalls zu dem gleichen Kunden eine andere Adresse vorliegt, die Vorrang vor dem aktuellen Satz hat (daher die beiden „NOT EXISTS“-Klauseln). Durch die Index-Scans skaliert die Antwortzeit zwar recht gut, jedoch waren die Antwortzeiten immer länger geworden, da die Anzahl der zu betrachtenden Sätze stetig gewachsen ist.

Gelöst hat das Problem der Einsatz der Analytic Function RANK():

SELECT KUNDEN_ID, TYP, STRASSE, HAUSNUMMER, POSTFACH, PLZ, ORT
FROM
(
  SELECT KUNDEN_ID, TYP,
         STRASSE, HAUSNUMMER, POSTFACH, PLZ, ORT,
         RANK() OVER (PARTITION BY KUNDEN_ID
                      ORDER BY CASE TYP WHEN 'FIRMA' THEN 1
                                        WHEN 'POSTFACH' THEN 2
                                        WHEN 'PRIVAT‘ THEN 3 END )
         AS RANK
  FROM ADRESSEN
)
WHERE RANK = 1

Der Execution-Plan des neuen SQL-Statements:

 

VorgangOptionenObjekt
SELECT STATEMENT   
   VIEW
      WINDOW
SORT PUSHED RANK   
         TABLE ACCESS    STORAGE FULLADRESSEN   

 

Jetzt werden die Daten lediglich in einem „Full Table Scan“ gelesen. Es ist kein weiterer Zugriff auf die Daten der Tabelle ADRESSEN ist notwendig. Der Eintrag WINDOW SORT PUSHED RANK zeigt, dass die Analytic Function ausgeführt wird. Dies geschieht in Memory, abhängig von der Datenmenge gegebenenfalls auch in TEMP-Tablespace. Das spiegelt sich auch in den Ausführungszeiten wider (in Sekunden):

 

bisherneuDifferenz
100 Rows0,020,020%
10 000 Rows0,210,09-200%
1 000 000 Rows12,586,23-200%
1 000 000 000 Rows    1190,00    630,00    -200%

 

Einige Statistiken dazu:

 

bisherneu
consistent gets:9.366193
logical read bytes from cache:    76.726.272    1.581.056   
CPU used by this session:12

 

Die Ausführungszeit sank signifikant um 200 Prozent. Im Projekt konnten sogar Verbesserungen bis zu 800 Prozent beobachtet werden. Damit konnte das Performance-Problem der zu lange laufenden Batch-Jobs erfolgreich gelöst werden. Die Eingriffe in das bestehende System waren auf die SQL-Statements begrenzt.

Die Tests der neuen Statements waren recht einfach:

altes Select
MINUS
neues Select

und

altes Select
MINUS
neues Select

In beiden Fällen ist die leere Menge als Ergebnis zu erwarten.

Das neue SQL-Statement bietet darüber hinaus noch weitere Vorteile:

  • Es ist einfacher aufgebaut, dadurch verständlicher und wartbarer – zwei Subselects wurden eliminiert.
  • Es ist in der Ausführung weniger ressourcenintensiv – nur ein „Full Table Scan“ findet jetzt statt.
  • Es wird mit wachsender Datenmenge besser zurechtkommen.