24. März 2011

Table Functions, der Optimizer und Statistiken ...

Table Functions, the Optimizer and Statistics ...
Heute möchte ich mich mal dem Thema Table Functions widmen - ich werde aber nicht schreiben, wie man eine Table Function bauen kann, sondern vielmehr, wie man dem Query Opimizer ein wenig Information über die Table Function geben kann - wichtig wäre vor allem, dass der Optimizer weiss, wieviele Zeilen die Table-Function zurückgeben wird, denn das hat Einfluß auf seine Entscheidungen. So richtet sich die Join-Strategie doch erheblich nach der Anzahl Zeilen in den beteiligten Tabellen. Wir beginnen mit einem Beispiel - eine sehr einfache Table Function ...
Today I'd like to write about Table Functions. But this posting will not be about writing these (there is enough information available) - this posting is about how we can tell the Query Optimizer a bit about our table function. An important information would be how many rows the actual table function call will return - this would impact the optimizer decisions for e.g. the join strategy. But let's take the topic step by step. We'll start with a very simple table function example ...
create type tf_t as object(
  col1       number
);
/

create type tf_ct as table of tf_t
/ 

create or replace function tf (p_cnt in number)
return tf_ct pipelined as
begin
  for i in 1..p_cnt loop
    pipe row (tf_t(i*2));
  end loop;
  return;
end;
/
sho err

SQL> select * from table(tf(10));

      COL1
----------
         2
         4
         6
         8
        10
         :
So weit so gut - nun werfen wir mal einen Blick auf den Ausführungsplan dieser einfachen Abfrage ... und danach erstellen wir den Ausführungsplan nochmal mit dem Argument 100000. Also einen Ausführungsplan für 10 zurückgegebene Zeilen und einen für 100000 zurückgegebene Zeilen ...
So far, so good. Now we'll see what the query optimizer thinks about our function. We'll look into the execution plan for a invokation with "10" as well as for "100000" as parameter. So the table function will return 10 rows in the first and 100000 rows in the second case.
SQL> explain plan for 
  2  select * from table(tf(10))
  3  /

SQL> select * from table(dbms_xplan.display())
  2  /

------------------------------------------------------------------------------------------
| Id  | Operation                         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                  |      |  8168 | 16336 |    29   (0)| 00:00:01 |
|   1 |  COLLECTION ITERATOR PICKLER FETCH| TF   |  8168 | 16336 |    29   (0)| 00:00:01 |
------------------------------------------------------------------------------------------

SQL> explain plan for 
  2  select * from table(tf(100000))
  3  /

SQL> select * from table(dbms_xplan.display())
  2  /

------------------------------------------------------------------------------------------
| Id  | Operation                         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                  |      |  8168 | 16336 |    29   (0)| 00:00:01 |
|   1 |  COLLECTION ITERATOR PICKLER FETCH| TF   |  8168 | 16336 |    29   (0)| 00:00:01 |
------------------------------------------------------------------------------------------
Egal, was Ihr als Parameter angebt: die Anzahl Zeilen, die der Optimizer als Ergebnismenge der Table Function annimmt, ist immer gleich. Woher soll er es auch wissen? Es gibt eben hier keine Tabelle mit Statistiken. Aber: Die Datenbank erlaubt durchaus, dem Optimizer diese Information zu geben. Das ist auch in der Dokumentation zum Extensible Optimizer Interface beschrieben. Das ist übrigens Teil des Data Cartridge Developers Guide, einem meiner Meinung nach hochinteressanten Handbuch. Wir müssen demnach einen neuen Objekttypen implementieren. Und dieser Objekttyp muss zwei Funktionen enthalten: ODCIGetInterfaces und ODCIStatsTableFunction. Also brauchen wir zuerst den Object Type als solchen ...
The plan always looks the same, regardless of the given table function argument. Anyway: how should the optimizer know? There is not table and so there are no statistics.
But, the Oracle database allows to privide information for the query optimizer. This is documented in the Extensible Optimizer Interface, which is part of the Data Cartridge Developers Guide (IMHO one of the most interesting documentation handbooks). So we have to implement another object type for the "communication" with the optimizer. And this object type must contain two functions: ODCIGetInterfaces and ODCIStatsTableFunction. So we start the the type declaration ...
CREATE TYPE tfstats as object (
  dummy number,                          -- object types need attributes - here is one.
  static FUNCTION ODCIGetInterfaces(
    ifclist  OUT ODCIObjectList
  ) RETURN NUMBER,
  STATIC FUNCTION ODCIStatsTableFunction(
   func      IN  SYS.ODCIFuncInfo, 
   outStats  OUT SYS.ODCITabFuncStats, 
   argDesc   IN  SYS.ODCIArgDescList, 
   argument  in number                   -- list all table function arguments here
  ) RETURN NUMBER
);
/
sho err
Das war einfach - nun kommt die konkrete Implementierung. Für unser Beispiel ist die aber nicht besonders schwierig. Der Parameter der Table Function gibt die Anzahl Zeilen ja an; also geben wir ihn einfach wieder zurück. Genau dieses Detail ist in der Praxis mit Sicherheit am schwierigsten, denn aufwändige Berechnungen und Abfragen sind hier fehl am Platz - man muss mit möglichst wenig Aufwand eine Abschätzung finden ...
... and then we continue with the actual implementation. Our example is very simple. The table function arguments determines exactly the amount of returned rows. So we simply return the argument as the optimizer information in ODCIStatsTableFunction. In practice this will be the most difficult part of the task. Complex and expensive calculations or queries are not appropriate here (note that this will be called when the optimizer generated the execution plan). So you will have to find a good approximation with as less efforts as possible ...
create or replace type body tfstats as
  static FUNCTION ODCIGetInterfaces(
    ifclist  OUT ODCIObjectList
  ) RETURN NUMBER is
  begin
    -- Always return SYS.ODCISTATS2 here
    ifclist := ODCIObjectList(ODCIObject('SYS','ODCISTATS2'));
  return ODCIConst.Success;
  end;

  STATIC FUNCTION ODCIStatsTableFunction(
    func      IN SYS.ODCIFuncInfo, 
    outStats OUT SYS.ODCITabFuncStats, 
    argDesc   IN SYS.ODCIArgDescList, 
    argument  in NUMBER 
  ) RETURN NUMBER is
  begin
    outStats := SYS.ODCITabFuncStats(argument);
    return ODCIConst.Success;
  end;
end;
/
sho err 
Nun ist der Objekttyp implementiert - fehlt noch die Verbindung zur konkreten Table Function. Und das geht so.
Now, as the implementation is complete, we need to associate this with the table function. The following call will "tell" the optimizer where to look for statistics information regarding a particular table function.
SQL> ASSOCIATE STATISTICS WITH FUNCTIONS tf USING tfstats;
Dann schauen wir uns die obigen Ausführungspläne nochmals an ...
That's it - let's look into the execution plan ...
SQL> explain plan for 
  2  select * from table(tf(10))
  3  /

SQL> select * from table(dbms_xplan.display())
  2  /

------------------------------------------------------------------------------------------
| Id  | Operation                         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                  |      |    10 |    20 |    29   (0)| 00:00:01 |
|   1 |  COLLECTION ITERATOR PICKLER FETCH| TF   |    10 |    20 |    29   (0)| 00:00:01 |
------------------------------------------------------------------------------------------

SQL> explain plan for 
  2  select * from table(tf(100000))
  3  /

SQL> select * from table(dbms_xplan.display())
  2  /

--------------------------------------------------------------------------------------------
| Id  | Operation                         | Name | Rows   |  Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                  |      | 100000 | 200000 |    29   (0)| 00:00:01 |
|   1 |  COLLECTION ITERATOR PICKLER FETCH| TF   | 100000 | 200000 |    29   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------
Folgt nun noch ein Test, was der Optimizer daraus macht. Ich habe für mein Beispiel den Session-Parameter OPTIMIZER_INDEX_COST_ADJ auf "1" gestellt, damit Indexzugriffe "billig" sind und der Optimizer schneller auf Nested Loops umschaltet ... Und den Ausführungsplan berechne ich nun mal für folgende Query:
Now I'd like to see what the optimizer does with that information. For the following example I set OPTIMIZER_INDEX_COST_ADJ to "1" - that makes index access "cheap" and the optimizer will tend to the "nested loops" join stategy faster. And now I'll generate the execution plan for the following query ...
explain plan for 
select s.prod_id
from sh.sales s, table(tf({argument})) t
where t.col1 = s.prod_id
/
Ohne dieses Verfahren sieht der Ausführungsplan stets gleich aus. Der Optimizer geht von den oben schon dargestellten ca. 8000 Zeilen aus und entscheidet sich wie folgt.
Without our approach the optimizer always thinks that the table function will return about 8000 rows. So the execution plan always looks the same:
-----------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                |   104M|   596M|   620  (91)| 00:00:08 |
|*  1 |  HASH JOIN                         |                |   104M|   596M|   620  (91)| 00:00:08 |
|   2 |   COLLECTION ITERATOR PICKLER FETCH| TF             |  8168 | 16336 |    29   (0)| 00:00:01 |
|   3 |   PARTITION RANGE ALL              |                |   918K|  3589K|    29   (0)| 00:00:01 |
|   4 |    BITMAP CONVERSION TO ROWIDS     |                |   918K|  3589K|    29   (0)| 00:00:01 |
|   5 |     BITMAP INDEX FAST FULL SCAN    | SALES_PROD_BIX |       |       |            |          |
-----------------------------------------------------------------------------------------------------
Mit dem Verfahren (und dem Parameter OPTIMIZER_INDEX_COST_ADJ = 1 sieht der Plan mit dem Parameter 100000 in der Table Function so aus ...
With the additional information the optimizer has the choice - and it uses it. When the table function returns many rows (here: 100000) the join strategy is HASH JOIN ...
-------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                |  1276M|  7302M|       |  7677  (90)| 00:01:33 |
|*  1 |  HASH JOIN                         |                |  1276M|  7302M|  1368K|  7677  (90)| 00:01:33 |
|   2 |   COLLECTION ITERATOR PICKLER FETCH| TF             |   100K|   195K|       |    29   (0)| 00:00:01 |
|   3 |   PARTITION RANGE ALL              |                |   918K|  3589K|       |     1   (0)| 00:00:01 |
|   4 |    BITMAP CONVERSION TO ROWIDS     |                |   918K|  3589K|       |     1   (0)| 00:00:01 |
|   5 |     BITMAP INDEX FULL SCAN         | SALES_PROD_BIX |       |       |       |            |          |
-------------------------------------------------------------------------------------------------------------
... und mit dem Parameter 20 so.
... and for less rows (here: 20) the optimizer decides to do NESTED LOOPS.
-----------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                |   255K|  1495K|    35   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                      |                |   255K|  1495K|    35   (0)| 00:00:01 |
|   2 |   COLLECTION ITERATOR PICKLER FETCH| TF             |    20 |    40 |    29   (0)| 00:00:01 |
|   3 |   PARTITION RANGE ALL              |                | 12762 | 51048 |    35   (0)| 00:00:01 |
|   4 |    BITMAP CONVERSION TO ROWIDS     |                | 12762 | 51048 |    35   (0)| 00:00:01 |
|*  5 |     BITMAP INDEX SINGLE VALUE      | SALES_PROD_BIX |       |       |            |          |
-------------------------------------------------------------------------------------------------------------
Das finde ich eine ganz interessante Möglichkeit - Wenn Table Functions stark schwankende Ergebnismengen liefern und sich diese einfach abschätzen lassen, dann ist das eine recht interessante Herangehensweise ...
Probably it is not applicably for each and every table function; but if a table function returns different amounts of rows and the amount of rows can be estimated easily ... this might be an interesting approach.

8. März 2011

Noch ein Szenario mit "User Defined Aggregates"

Another scenario for 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 ...
In the previous but one blog posting I talked about user defined aggregates. Today I will again write about this topic ... since those functions are applicable in more situations than you might think.
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 ...
When reading the documentation and creating some example functions it seems that user defined aggregates can only aggregate scalar values. But this is not true - with the utilization of object types we can aggregate even complex datasets. To illustrate this I have created the example of a savings account. On this account we have transactions (debit and credit). And now (at the end of the year) we have to calculate interest. And the interest depends on the transactions' amount as well as on its date. So, we create the transaction table first.
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.
To calculate intrerest we use the "interest number" method (german wikipedia article here - did not found an english one). The interest number aggregates the information about the transaction amount and date. From the interest number we can then calculate the amount of interest.
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".
So we need the transaction amount and date in order to calculate the interest number - so we need two values. The user defined aggregate allows only one parameter. We solve this situation by encapsulating the two values in one object type as a "transport object".
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.
Next we'll need some global data applicable for all table rows. This is the interest rate as well as the calculation date. Most often this will be the 31st of December - but any date is possible. To hold those global values we'll use a PL/SQL package. The following code creates it as well as some get- and set- methods.
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.
Now comes the user defined aggregate implementation. Note that the type BUCHUNG_T is used in the ODCIAggregateIterate method - so the method gets the amount as well as the date. The "global" data as the calculation date and the interest rate is being retrieved from the PL/SQL package. The ODCIAggregateIterate method aggregates the interest numbers and the ODCIAggregateTerminate method finally calculates interest using the aggregated interest number. If the interest rate is set to zero the function will return the interest number itself. And - as usual in the financial industry - every month has 30 days.
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.
Then we crate the top-level aggregate function based on the above implementation. Again, the input parameter is of type 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.
And that's it. Now you can test the function. First set the "global" values ...
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.
And then you can calculate the interest amount with just a SQL query. Note that you (syntactically) first create a BUCHUNG_T object using the transaction date and amount. This object is passed to the aggregate function.
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.
Set the interest rate to zero - and you will get the interest numbers.
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.
So we have seen that user defined aggregates can also do a bit more complex things: Financial math might be an interesting application area. Yield calculation might also be done with a user defined aggregate. The good but about a user defined aggregate is that the implemented functionality is being leveraged to the SQL layer - a simple SQL query with a GROUP BY or an analytic clause then does the trick.

Beliebte Postings