Noch ein Szenario mit "User Defined Aggregates"
Im vorletzten Blog Posting hatte ich ja vorgestellt, wie man sich eigene Aggregatsfunktionen mit den User Defined Aggregates bauen kann. Heute greife ich das Thema nochmals auf, denn diese Funktionen haben mehr denkbare Einsatzgebiete, als man sich denken mag ...
Wenn man die Dokumentation liest, scheint es, als ob User Defined Aggregates nur einzelne skalare Werte aggregieren können - aber bei genauerer
Betrachtung ist das nicht der Fall - man kann über den Umweg eines Objekttypen auch ganze "Datasets" aggregieren
lassen. Als Beispiel habe ich das mal mit einem "Sparbuch" probiert - auf diesem Sparkonto gibt es Ein-
und Auszahlungen - und mit einer Aggregatsfunktion sollen die Zinsen berechnet werden. Die hängen neben dem
Betrag der Transaktion auch von deren Datum ab.
Zuerst brauchen wir also unsere Tabelle für die Konten ...
create table tab_buchungen( id number, konto number, datum date, betrag number ) / alter session set nls_date_format='DD.MM.YYYY' / /* * Konto I */ insert into tab_buchungen values (1, 4711, '01.01.2010', 500); insert into tab_buchungen values (2, 4711, '09.01.2010', 1500); insert into tab_buchungen values (3, 4711, '15.02.2010', -912); insert into tab_buchungen values (4, 4711, '25.03.2010', 2500); insert into tab_buchungen values (5, 4711, '18.05.2010', 2000); insert into tab_buchungen values (6, 4711, '27.07.2010', -1500); insert into tab_buchungen values (7, 4711, '03.08.2010', 850); insert into tab_buchungen values (8, 4711, '19.10.2010', 1350); insert into tab_buchungen values (9, 4711, '28.11.2010', -2000); /* * Konto II */ insert into tab_buchungen values (10, 4712, '01.01.2010', 500); insert into tab_buchungen values (11, 4712, '09.02.2010', 1500); insert into tab_buchungen values (12, 4712, '28.02.2010', -912); insert into tab_buchungen values (13, 4712, '25.03.2010', 2500); insert into tab_buchungen values (14, 4712, '18.04.2010', 2000); insert into tab_buchungen values (15, 4712, '19.04.2010', 5000); insert into tab_buchungen values (16, 4712, '27.05.2010', -3500); insert into tab_buchungen values (17, 4712, '04.06.2010', 100); insert into tab_buchungen values (18, 4712, '03.08.2010', 152); insert into tab_buchungen values (19, 4712, '19.10.2010', 950); insert into tab_buchungen values (20, 4712, '28.11.2010', 4000); /* * Konto III */ insert into tab_buchungen values (21, 4713, '31.12.2009', 5000); commit /
Für die Zinsberechnung nehmen wir die Zinszahlen-Methode. Wer genaueres wissen möchte, schaut
am besten im Wikipedia-Artikel nach. Die
Aggregatsfunktion muss also während der Iteration die Zinszahlen "aggregieren" und zum
Abschluß aus der Zinszahl die Zinsen rechnen.
Um die Zinszahlen rechnen zu können, braucht man pro Transaktion eben den Betrag und das
Datum, also zwei Werte. Das User Defined Aggregate kann aber nur einen Parameter entgegennehmen - später
auf einer Spalte arbeiten. Diesen Gegensatz lösen wir auf, indem wir die beiden Werte in einen
Objekttypen kapseln - den Objekttypen verwenden wir quasi als "Transportobjekt".
create type t_buchung as object( buchungsdatum date, betrag number ) /
Als nächstes brauchen wir noch globale Daten wie den Zinssatz und das Abrechnungsdatum (meist der
31.12., könnte natürlich aber auch jedes andere sein. Da diese Angaben für alle Tabellenzeilen
gleich sind, packen wir sie in ein PL/SQL-Paket - direkt erstellt mit get- und set-Methoden.
create or replace package pkg_zinsen is
function get_zinssatz return number;
procedure set_zinssatz(p_zinssatz in number);
function get_abrechnungsdatum return date;
procedure set_abrechnungsdatum (p_datum in date);
end pkg_zinsen;
/
create or replace package body pkg_zinsen is
g_zinssatz number;
g_datum date;
function get_zinssatz return number is begin return g_zinssatz; end;
procedure set_zinssatz(p_zinssatz in number) is begin g_zinssatz := p_zinssatz; end;
function get_abrechnungsdatum return date is begin return g_datum; end;
procedure set_abrechnungsdatum (p_datum in date) is
begin
g_datum := p_datum;
if extract(DAY from g_datum) = 31 then
g_datum := to_date('30'||to_char(g_datum, 'MMYYYY'), 'DDMMYYYY');
end if;
if to_char(g_datum, 'DDMM') = '2802' or to_char(g_datum, 'DDMM') = '2902' then
g_datum := to_date('30'||to_char(g_datum, 'MMYYYY'), 'DDMMYYYY');
end if;
end set_abrechnungsdatum;
end pkg_zinsen;
/
Nun kommt dann die Implementierung der Aggregatsfunktion - achtet auf die Verwendung des Objekttypen
T_BUCHUNG. Die Funktion bekommt daher für jede Iteration eben nicht einen skalaren, sondern
dadurch gleich zwei Werte übergeben. Die anderen Daten nimmt sie aus dem PL/SQL-Paket und hat somit
alle Informationen beisammen. In der ODCIAggregateIterate-Methode werden die Zinszahlen
berechnet und in der ODCIAggregateTerminate werden die Zinsen ermittelt.Ist ein Zinssatz von
0% angegeben, werden die Zinszahlen selbst zurückgegeben. Wie im Bankwesen
üblich, wird ein Monat stets mit 30 Tagen gerechnet.
create type agg_zinsen_t as object(
v_agg_zinsen number,
static function ODCIAggregateInitialize(
sctx IN OUT agg_zinsen_t
) return number,
member function ODCIAggregateIterate(
self IN OUT agg_zinsen_t, value IN t_buchung
) return number,
member function ODCIAggregateTerminate(
self IN agg_zinsen_t, returnValue OUT number, flags IN number
) return number,
member function ODCIAggregateMerge(
self IN OUT agg_zinsen_t, ctx2 IN agg_zinsen_t
) return number
);
/
sho err
create or replace type body agg_zinsen_t is
static function ODCIAggregateInitialize(sctx IN OUT agg_zinsen_t)
return number is
begin
sctx := agg_zinsen_t(0);
return ODCIConst.Success;
end;
member function ODCIAggregateIterate(
self IN OUT agg_zinsen_t,
value IN t_buchung
) return number is
v_daysm number;
v_daysd number;
v_daysy number;
begin
v_daysy := (extract(YEAR from pkg_zinsen.get_abrechnungsdatum) - extract(YEAR from value.buchungsdatum)) * 360;
v_daysm := (extract(MONTH from pkg_zinsen.get_abrechnungsdatum) - extract(MONTH from value.buchungsdatum)) * 30;
if to_char(value.buchungsdatum, 'DD') = '31' or to_char(value.buchungsdatum, 'DDMM') in ('2802','2902') then
v_daysd := 30;
else
v_daysd := extract(DAY from value.buchungsdatum);
end if;
v_daysd := extract(DAY from pkg_zinsen.get_abrechnungsdatum) - v_daysd;
self.v_agg_zinsen := self.v_agg_zinsen + round((value.betrag * (v_daysy + v_daysd + v_daysm) / 100));
return ODCIConst.Success;
end;
member function ODCIAggregateTerminate(
self IN agg_zinsen_t,
returnValue OUT number,
flags IN number
) return number is
begin
if pkg_zinsen.get_zinssatz = 0 then
returnValue := self.v_agg_zinsen;
else
returnValue := self.v_agg_zinsen / (360 / pkg_zinsen.get_zinssatz);
end if;
return ODCIConst.Success;
end;
member function ODCIAggregateMerge(self IN OUT agg_zinsen_t, ctx2 IN agg_zinsen_t) return number is
begin
self.v_agg_zinsen := self.v_agg_zinsen + ctx2.v_agg_zinsen;
return ODCIConst.Success;
end;
end;
/
sho err
Nun kommt die Einrichtung der Aggregatsfunktion an sich - die auf der eben eingespielten Implementierung
basiert - wiederum nimmt sie keinen skalaren Datentypen entgegen, sondern BUCHUNG_T.
CREATE or replace FUNCTION agg_zinsen (input t_buchung) RETURN number PARALLEL_ENABLE AGGREGATE USING agg_zinsen_t; / sho err
Und das war's - nun kann man testen. Zuerst setzen wir die globalen Parameter im PL/SQL-Paket.
begin
pkg_zinsen.set_abrechnungsdatum(to_date('31122010', 'DDMMYYYY'));
pkg_zinsen.set_zinssatz(1.5);
end;
/
Und dann können wir die Zinsen eines Kontos abrechnen - mit nichts als einer SQL-Abfrage. Achtet darauf,
dass Ihr syntaktisch zuerst ein BUCHUNG_T-Objekt mit Buchungsdatum und Betrag erzeugt und dieses
dann an die Aggregatsfunktion übergebt.
select konto, agg_zinsen(t_buchung (datum, betrag)) zinsen from tab_buchungen
group by konto
/
KONTO ZINSEN
---------- ----------
4711 61,5666667
4712 96,95
4713 75
Ein Zinssatz von 0% gibt die Zinszahl direkt zurück.
begin
pkg_zinsen.set_abrechnungsdatum(to_date('31122010', 'DDMMYYYY'));
pkg_zinsen.set_zinssatz(0);
end;
/
select konto, agg_zinsen(t_buchung (datum, betrag)) zinsen from tab_buchungen
group by konto
/
KONTO ZINSEN
---------- ----------
4711 14776
4712 23268
4713 18000
Es lassen sich also auch komplexere Dinge mit Benutzerdefinierten Aggregaten erledigen. Interessant
sind hier viele Aufgaben aus der Finanzmathematik - Neben der einfachen Zinsrechnung ist auch jede
Form der Renditeermittlung interessant. Und wenn es als Aggregatsfunktion bereitsteht, erhöhen sich
die Nutzungsmöglichkeiten nochmals massiv - denn die Funktion steht per SQL-Abfrage bereit.
Kommentare:
Interessant, wie immer.
Aber die set_abrechnungsdatum-Funktion bildet für die letzten Februartage einen 30.02., was Oracle zu einer "ORA-01839: Datum für angegebenen Monat nicht gültig"-Meldung veranlassen sollte.
Gruß
Ralf
Hallo Ralf,
das ist richtig; in der Tat. Man muss immer wieder testen :-)
Muss mal sehen, dass ich das korrigiere ...
Beste Grüße
-Carsten
Kommentar veröffentlichen