Analytische Funktionen – erfolgreich eingesetzt

Werbung
Analytische Funktionen – erfolgreich eingesetzt
Dani Schnider
Trivadis AG
Glattbrugg, Schweiz
Schlüsselworte:
Analytische Funktionen, SQL, Performance Optimierung, Data Warehousing
Zusammenfassung
Analytische Funktionen wurden mit Oracle8i Release 2 eingeführt und in Oracle9i erweitert.
Obwohl ihre zahlreichen und mächtigen Möglichkeiten somit schon seit einigen Jahren zur
Verfügung stehen, werden analytische Funktionen heute leider immer noch selten eingesetzt.
Hier soll gezeigt werden, wie verschiedene Fragestellungen mit Hilfe dieser OracleFunktionalität elegant und effizient formuliert werden können und wo die Vorteile – aber
auch Grenzen – gegenüber herkömmlichen SQL-Abfragen liegen.
Einleitung
Ein einfaches Beispiel mit der guten alten EMP-Tabelle zu Beginn: Für jeden Mitarbeiter soll
der Name, sein Gehalt sowie die Gehaltsumme seiner Abteilung angezeigt werden:
DEPTNO
---------10
10
10
20
20
20
20
20
30
30
30
30
30
30
ENAME
SAL
SUM_SAL
---------- ---------- ---------CLARK
2450
8750
KING
5000
8750
MILLER
1300
8750
SMITH
800
10875
JONES
2975
10875
SCOTT
3000
10875
ADAMS
1100
10875
FORD
3000
10875
ALLEN
1600
9400
WARD
1250
9400
MARTIN
1250
9400
BLAKE
2850
9400
TURNER
1500
9400
JAMES
950
9400
Diese Abfrage lässt sich nicht mit einem einfachen SELECT-Befehl formulieren, da Daten
auf unterschiedlichen Verdichtungsstufen angezeigt werden. Während sich die Spalten
ENAME und SAL auf einen einzelnen Mitarbeiter beziehen, ist die Spalte SUM_SAL eine
Aggregation (= Verdichtung) pro Abteilung. Eine mögliche Lösung ist die Verwendung einer
Inline-View:
SELECT e1.deptno, e1.ename, e1.sal, e2.sum_sal
FROM emp e1, (SELECT deptno, SUM(sal) sum_sal
FROM emp
GROUP BY deptno) e2
WHERE e1.deptno = e2.deptno
ORDER BY e1.deptno
Solche Lösungen funktionieren bestens und werden auch oft angewendet. Der Nachteil
besteht hier darin, dass die Tabelle EMP zweimal gelesen werden muss. Klar, bei 14
Datensätzen ist dies kein Problem, aber bei grösseren Datenmengen können solche Abfragen
zu Performanceverlusten führen.
Das gleiche Ergebnis lässt sich einfach und elegant mit einer analytischen Funktion ermitteln.
Dabei muss die Tabelle nur einmal gelesen werden, da die Berechnung von analytischen
Funktionen im gleichen Durchlauf – sozusagen „nebenbei“ – erfolgt.
SELECT
,
,
,
FROM
deptno
ename
sal
SUM(sal) OVER (PARTITION BY deptno) sum_sal
emp
Grundprinzip der analytischen Funktionen
Jede analytische Funktion bezieht sich auf Teilmengen der Daten, auf sogenannte Partitionen
(die nichts mit partitionierten Tabellen zu tun haben). Anhand der Partitionen wird definiert,
wie die Resultate der analytischen Funktion gruppiert werden. Die Aufteilung der Partitionen
wird durch die Klausel PARTITION BY angegeben. Wird sie weggelassen, bezieht sich die
analytische Funktion auf die gesamte Resultatmenge.
Durch die Klausel ORDER BY kann festgelegt werden, wie die Daten innerhalb der Partition
sortiert werden. Für viele der analytischen Funktionen ist die Angabe der Reihenfolge
zwingend, da sie für die Funktionsberechnung wesentlich ist. Die Reihenfolge der Ausgabe
der Resultatmenge kann jedoch von der Reihenfolge der Berechnung abweichen und wird –
wie üblich in SQL – mit einem ORDER BY am Ende des SELECT-Befehls definiert.
Falls für eine analytische Funktion ein ORDER BY spezifiziert wurde, kann die Menge der
für die Berechnung relevanten Datensätze durch die Definition eines Windows zusätzlich
eingeschränkt werden. Normalerweise umfasst das Window die Datensätze vom Beginn der
Partition bis zum aktuellen Datensatz, es kann aber auch explizit durch ROWS (physische
Grenze) oder RANGE (logische Grenze) eingeschränkt werden.
Kompliziert? Anhand eines weiteren einfachen Beispiels soll das Grundprinzip der
analytischen Funktionen illustriert werden: Für jeden Mitarbeiter soll berechnet werden, wie
viele Mitarbeiter der gleichen Abteilung ein Gehalt im Bereich von +/- 1000 Dollar des
aktuellen Datensatzes haben. Die entsprechende SQL-Abfrage mit Verwendung der
analytischen Funktion COUNT sieht folgendermassen aus:
SELECT
,
,
,
,
deptno
ename
hiredate
sal
COUNT(*) OVER (PARTITION BY deptno
ORDER BY sal
RANGE BETWEEN 1000 PRECEDING
AND 1000 FOLLOWING) count_sal
FROM emp
Für jeden Datensatz der Resultatmenge wird nun die Spalte COUNT_SAL berechnet,
ausgehend von der aktuellen Partition und dem aktuellen Window. Zu beachten ist dabei, dass
die Partition und das Window „wandern“, d.h. für den jeweils aktuellen Datensatz (Current
Row) neu festgelegt werden.
Abb. 1: Partition, Window und Current Rows der analytischen Funktion
Anwendungsbeispiel: Kumulierte Umsatzzahlen
Eine typische Anwendung von analytischen Funktionen sind kumulierte Summierungen
(„Year-to-date“ Abfragen), wie sie häufig in Data Warehouses vorkommen: Neben dem
Umsatz pro Monat soll jeweils der gesamte Jahresumsatz bis zum entsprechenden Monat
angezeigt werden. Das nachfolgende Beispiel zeigt, wie diese Problemstellung mittels einer
analytischen Funktion gelöst werden kann. Dazu gruppieren wir die Umsatzzahlen nach
Kalendermonat. Alle Monatsumsätze eines Jahres werden zu einer Partition zusammengefasst
(PARTITION BY calendar_year) und nochmals summiert. Die Datensätze werden dazu
nach Monat aufsteigend sortiert (ORDER BY calendar_month_desc). Dabei nützen wir die
Tatsache aus, dass das für die Summierung verwendete Window jeweils alle Datensätze vom
Anfang der Partition (Januar des jeweiligen Jahres) bis zur Current Row (aktueller Monat)
enthält.
SELECT
,
,
,
t.calendar_year year
t.calendar_month_desc month
SUM(s.amount_sold)/1000000 millions
SUM(SUM(s.amount_sold)/1000000)
OVER (PARTITION BY t.calendar_year
ORDER BY t.calendar_month_desc) millions_ytd
FROM sales s, times t
WHERE s.time_id = t.time_id
GROUP BY t.calendar_year, t.calendar_month_desc
Als Resultat wird jeweils der Monatsumsatz (Spalte MILLIONS) und der aufsummierte
(=kumulierte) Umsatz innerhalb des Jahres (MILLIONS_YTD) ausgegeben:
YEAR
---------1998
1998
1998
1998
1998
1998
1998
1998
1998
1998
1998
1998
1999
1999
...
MONTH
MILLIONS MILLIONS_YTD
-------- -------- -----------1998-01
2.28
2.28
1998-02
2.37
4.65
1998-03
1.83
6.48
1998-04
1.98
8.46
1998-05
1.75
10.20
1998-06
1.87
12.07
1998-07
1.93
14.01
1998-08
1.97
15.98
1998-09
2.17
18.15
1998-10
2.24
20.38
1998-11
1.96
22.34
1998-12
1.74
24.08
1999-01
2.08
2.08
1999-02
2.36
4.44
Das Beispiel basiert auf dem “Sales History” Beispielschema von Oracle10g mit rund einer
Million Einträgen in der SALES-Tabelle. Hier sind nun bereits deutliche
Performanceunterschiede messbar. Der Execution Plan des ausgeführten SELECT-Befehls
zeigt, dass die Tabelle SALES nur einmal gelesen und mit TIMES gejoined werden musste.
Die Berechnung der analytischen Funktion wird am Schluss (nach dem GROUP BY)
ausgeführt und ist im Execution Plan als WINDOW BUFFER dargestellt.
call
count
------- -----Parse
1
Execute
1
Fetch
5
------- -----total
7
Rows
------48
48
918843
1826
918843
918843
cpu
elapsed
disk
query
current
-------- ---------- ---------- ---------- ---------0.00
0.00
0
0
0
0.00
0.01
0
0
0
1.81
1.80
0
1773
0
-------- ---------- ---------- ---------- ---------1.81
1.82
0
1773
0
rows
---------0
0
48
---------48
Row Source Operation
--------------------------------------------------WINDOW BUFFER (cr=1773 pr=0 pw=0 time=1803746 us)
SORT GROUP BY (cr=1773 pr=0 pw=0 time=1803741 us)
HASH JOIN (cr=1773 pr=0 pw=0 time=11948131 us)
TABLE ACCESS FULL TIMES (cr=56 pr=0 pw=0 time=3896 us)
PARTITION RANGE ALL PARTITION: 1 28 (cr=1717 pr=0 pw=0 time=5513218 us)
TABLE ACCESS FULL SALES PARTITION: 1 28 (cr=1717 pr=0 pw=0 time=1838601 us)
Im Vergleich dazu dauert die entsprechende Abfrage ohne Verwendung von analytischen
Funktionen rund 10 mal länger und führt zu einem viel aufwändigeren Execution Plan, da
mehrfach auf die Tabelle SALES zugegriffen werden muss:
SELECT
,
,
,
t.calendar_year year
t.calendar_month_desc month
SUM(s.amount_sold)/1000000 millions
(SELECT SUM(s1.amount_sold)/1000000
FROM sales s1,
times t1
WHERE s1.time_id = t1.time_id
AND t1.calendar_year = t.calendar_year
AND t1.calendar_month_desc <= t.calendar_month_desc) millions_ytd
FROM sales s, times t
WHERE s.time_id = t.time_id
GROUP BY t.calendar_year, t.calendar_month_desc
ORDER BY t.calendar_year, t.calendar_month_desc
call
count
------- -----Parse
1
Execute
1
Fetch
5
------- -----total
7
Rows
------48
5911266
5920777
9463
5911266
5911266
9454
48
918843
1826
918843
918843
cpu
elapsed
disk
query
current
-------- ---------- ---------- ---------- ---------0.01
0.00
0
0
0
0.00
0.00
0
0
0
18.14
18.15
0
300193
0
-------- ---------- ---------- ---------- ---------18.15
18.16
0
300193
0
rows
---------0
0
48
---------48
Row Source Operation
--------------------------------------------------SORT AGGREGATE (cr=298420 pr=0 pw=0 time=16352528 us)
TABLE ACCESS BY LOCAL INDEX ROWID SALES (cr=298420 pr=0 pw=0 time=82781550 us)
NESTED LOOPS (cr=21875 pr=0 pw=0 time=17885399 us)
TABLE ACCESS FULL TIMES (cr=2688 pr=0 pw=0 time=57388 us)
PARTITION RANGE ITERATOR PARTITION: KEY KEY (cr=19187 pr=0 pw=0 time=30093878 us)
BITMAP CONVERSION TO ROWIDS (cr=19187 pr=0 pw=0 time=12109898 us)
BITMAP INDEX SINGLE VALUE SALES_TIME_BIX PARTITION: KEY KEY (cr=19187 pr=0 …)
SORT GROUP BY (cr=1773 pr=0 pw=0 time=1800321 us)
HASH JOIN (cr=1773 pr=0 pw=0 time=12866998 us)
TABLE ACCESS FULL TIMES (cr=56 pr=0 pw=0 time=3736 us)
PARTITION RANGE ALL PARTITION: 1 28 (cr=1717 pr=0 pw=0 time=5513208 us)
TABLE ACCESS FULL SALES PARTITION: 1 28 (cr=1717 pr=0 pw=0 time=1838592 us)
Ranking-Funktionen
Als analytische Funktionen können nicht nur die klassischen Gruppenfunktionen wie SUM,
COUNT, MIN, MAX, AVG etc. verwendet werden, sondern es gibt auch weitere
Funktionen, die speziell für die Verwendungs als analytische Funktionen konzipiert wurden.
Dazu gehört die Familie der Ranking-Funktionen, mit denen beispielsweise Ranglisten oder
Top-n-Abfragen formuliert werden können.
Die Funktionen RANK und DENSE_RANK berechnen die Ranglisten-Nummer für jede
Zeile der Ausgabe, basierend auf der Sortierung innerhalb der Partition. Der Unterschied
zwischen den beiden Funktionen besteht darin, wie nach mehreren gleichplatzierten
Datensätzen die Nummerierung fortgesetzt wird:
SELECT
,
,
FROM
WHERE
deptno, ename, sal
RANK() OVER (ORDER BY sal DESC) rank
DENSE_RANK() OVER (ORDER BY sal DESC) dense_rank
emp
deptno IN (10, 20)
DEPTNO
---------10
20
20
20
10
10
20
20
ENAME
SAL
RANK DENSE_RANK
---------- ---------- ---------- ---------KING
5000
1
1
SCOTT
3000
2
2
FORD
3000
2
2
JONES
2975
4
3
CLARK
2450
5
4
MILLER
1300
6
5
ADAMS
1100
7
6
SMITH
800
8
7
Solche Ranglisten lassen sich über die ganze Datenmenge oder pro Partition berechnen, und
das in einer einzigen Abfrage. Im nachfolgenden Beispiel wird anhand der Verkaufszahlen
pro Produkt eine Rangliste über alle Produkte gebildet, und gleichzeitig eine Rangliste
innerhalb jeder Produktkategorie. Unabhängig davon, wie viele verschiedene analytische
Funktionen berechnet werden, die Basistabelle – in diesem Fall die Tabelle PROD_SALES
muss immer nur einmal gelesen werden.
SELECT
,
,
,
,
prod_category
prod_name
amount
RANK() OVER (ORDER BY amount DESC) rank_total
RANK() OVER (PARTITION BY prod_category
ORDER BY amount DESC) rank_category
FROM prod_sales
ORDER BY prod_category
Bei der ersten RANK-Funktion wurde kein PARTITION BY angegeben. Somit bezieht sich
die Rangliste auf alle Daten. Im zweiten Fall wird durch Angabe „PARTITION BY
prod_category“ erreicht, dass für jede Produktkategorie eine separate Rangliste erstellt wird.
Dieses Beispiel erweitern wir nun folgendermassen: Wir möchten für jede Produktkategorie
das erfolgreichste Produkt mit seinem Umsatz anzeigen. Dazu kann wiederum die Funktion
RANK verwendet werden. Der naheliegenste Ansatz wäre, die analytische Funktion in der
WHERE-Klausel zu verwenden:
WHERE RANK() OVER (PARTITION BY … ORDER BY …) = 1
Hier stossen wir allerdings auf eine Einschränkung: Analytische Funktionen können innerhalb
eines SQL-Befehls im SELECT- und im ORDER BY-Teil verwendet werden, nicht jedoch in
der WHERE- oder HAVING-Bedingung. Der Versuch führt zu einer Fehlermeldung:
ORA-30483: window
functions are not allowed here
Die Einschränkung lässt sich aber einfach umgehen, indem die analytischen Funktionen in
eine Inline-View „verpackt“ werden, auf welche dann die Einschränkung erfolgt:
SELECT prod_category, prod_name, amount
FROM (SELECT prod_category
, prod_name
, amount
, RANK() OVER (PARTITION BY prod_category
ORDER BY amount DESC) ranking
FROM prod_sales)
WHERE ranking = 1
Sind wir nun wieder gleich weit wie am Anfang? Ein Vorteil von analytischen Funktionen
besteht ja darin, dass man dadurch Inline-Views vermeiden kann. Und nun benötigen wir eine
Inline-View, um die analytischen Funktionen anzuwenden. Wo soll da der Vorteil sein?
Im Gegensatz zum Beispiel am Anfang dieses Beitrages muss hier die Basistabelle nur einmal
gelesen werden, und auf das Resultat dieses SELECT-Befehls erfolgt dann die
Einschränkung. Zwar wäre es aus Performancegründen noch besser, wenn die Einschränkung
zu Beginn, also vor der Auswertung der gesamten Inline-View erfolgen könnte. Dies ist aber
deshalb nicht möglich, weil für die Berechnung der Produkte mit dem höchsten Umsatz die
Verkaufszahlen entsprechend aggregiert und sortiert werden müssen, bevor die
Einschränkung erfolgen kann.
Weitere Funktionen
Das gleiche Resultat lässt sich auch ohne Inline-View erreichen, und zwar mit einer
Erweiterung, die mit Oracle9i eingeführt wurde: FIRST bzw. LAST können verwendet
werden, wenn eine analytische Funktion nur auf den ersten bzw. letzten Wert der
Resultatmenge angewendet werden soll. In unserem Beispiel heisst das, dass der Name des
ersten Produkts ermittelt werden soll, das aus einer sortierten Liste nach absteigendem
Umsatz zurückgegeben wird. Weil wir die gesamte Abfrage nach Produktkategorie
gruppieren, muss zusätzlich eine Gruppenfunktion (z.B. MIN oder MAX) angegeben werden.
SELECT prod_category
, MIN(prod_name) KEEP (DENSE_RANK FIRST ORDER BY amount DESC)
, MAX(amount)
FROM prod_sales
GROUP BY prod_category
Weitere interessante analytische Funktionen sind LAG und LEAD. Damit ist es möglich,
ausgehend vom aktuellen Datensatz auf vorhergehende oder nachfolgende Datensätze
zuzugreifen:
SELECT
,
,
FROM
ename, hiredate, sal
LAG(sal, 2) OVER (ORDER BY hiredate) lag_sal
LEAD(sal, 3) OVER (ORDER BY hiredate) lead_sal
emp
Das Resultat dieser Abfrage zeigt für jeden Mitarbeiter nicht nur sein Gehalt an, sondern auch
das Gehalt des vorletzten Mitarbeiters, der eingestellt wurde und jenes des Mitarbeiters drei
Anstellungsdaten später:
Abb. 2: Analytische Funktionen LAG und LEAD
Anwendungsbeispiel: Vorjahresvergleich
Immer wieder besteht die Anforderung, dass zum Beispiel Verkaufszahlen mit den
entsprechenden Werten der Vorperiode verglichen werden sollen. Im nachfolgenden Beispiel
soll der Umsatz des aktuellen Monats dem Umsatz des gleichen Monats im Vorjahr
gegenübergestellt werden. Dazu wird jeweils die Summe der Umsätze aus der 12-letzten Zeile
übernommen. Das funktioniert natürlich nur, wenn in jedem Monat Verkäufe gemacht
wurden.
SELECT t.calendar_month_desc
, SUM(s.amount_sold)/1000000 millions
, LAG(SUM(s.amount_sold), 12)
OVER (ORDER BY t.calendar_month_desc)/1000000 millions_ly
FROM sales s, times t
WHERE s.time_id = t.time_id
GROUP BY t.calendar_month_desc
Neben den hier vorgestellten analytischen Funktionen gibt es zahlreiche weitere
Möglichkeiten, insbesondere statistische Funktionen, auf die hier nicht im Detail eingegangen
wird. Einen Eindruck von der Mächtigkeit der analytischen Funktionen soll das letzte Beispiel
geben, in welchem verschiedene Funktionen kombiniert in einer SQL-Abfrage verwendet
werden. Pro Produktkategorie und Jahr werden folgende Werte berechnet:
•
•
•
•
•
TOTAL_AMOUNT: Gesamtumsatz der Produktkategorie im entsprechenden Jahr
CHANGE_LY: Prozentuale Veränderung gegenüber Vorjahresumsatz der Kategorie
PCT_YEAR: Prozentualer Anteil der Produktkategorie am gesamten Jahresumsatz
CAT_RANK: Platzierung des Jahres innerhalb der Produktkategorie
YEAR_RANK: Platzierung der Produktkategorie innerhalb des Jahres
SELECT
,
,
,
p.prod_category
t.calendar_year
SUM(s.amount_sold)
100 * SUM(s.amount_sold) / LAG(SUM(s.amount_sold))
OVER (PARTITION BY p.prod_category
ORDER BY t.calendar_year) -100
, 100 * RATIO_TO_REPORT (SUM(s.amount_sold))
OVER (PARTITION BY t.calendar_year)
, RANK ()
OVER (PARTITION BY t.calendar_year
ORDER BY SUM(s.amount_sold) DESC)
, RANK ()
OVER (PARTITION BY p.prod_category
ORDER BY SUM(s.amount_sold) DESC)
FROM sales s
JOIN times t USING (time_id)
JOIN products p USING (prod_id)
GROUP BY t.calendar_year, p.prod_category
ORDER BY p.prod_category, t.calendar_year
category
year
total_amount
change_ly
pct_year
cat_rank
year_rank
Sollten Sie nun der Meinung sein, dass dieser SQL-Befehl kompliziert ist, versuchen Sie die
gleiche Abfrage ohne Verwendung von analytischen Funktionen zu implementieren.
Spätestens danach werden Sie die Einfachheit der analytischen Funktionen zu schätzen
wissen.
Fazit
Mit den analytischen Funktionen hat Oracle eine SQL-Erweiterung eingeführt, die sehr
vielseitig und hilfreich ist, wenn es um die Formulierung von komplexen Auswertungen und
Berechnungen geht. Aufgrund der guten Performance eignen sie sich insbesondere auch für
Abfragen auf grossen Datenmengen – hauptsächlich, aber nicht nur im Umfeld von Data
Warehouses. Hat man die vielfältigen Möglichkeiten von analytischen Funktionen einmal
entdeckt, möchte man dieses Oracle-Feature nicht mehr missen.
Literatur
•
•
•
•
Oracle9i SQL Reference, Release 2 (9.2), Part No. A96540-02, Chapter 6
Oracle9i Data Warehousing Guide, Release 2 (9.2), Part No. A96520-01, Chapter 19
Oracle Database SQL Reference 10g Release 1 (10.1), Part No. B10759-01, Chapter 7
Oracle Database Data Warehousing Guide, 10g Release 1 (10.1), Part No. B10736-01, Chapter 21
Weitere Informationen zu analytischen Funktionen werden in den Trivadis-Kursen SQL-B
(SQL für Fortgeschrittene) und O-DWH (Data Warehousing mit Oracle) vermittelt.
Kontaktadresse:
Dani Schnider
Trivadis AG
Europa-Strasse 5
CH-8152 Glattbrugg
Telefon:
Fax:
E-Mail
Internet:
+41(0)1-808 70 20
+49(0)1-808 70 21
[email protected]
www.trivadis.com
Herunterladen