PL/SQL- Funktionen von SQL aus aufrufen - beim O'Reilly Verlag
PL/SQL- Funktionen von SQL aus aufrufen - beim O'Reilly Verlag
PL/SQL- Funktionen von SQL aus aufrufen - beim O'Reilly Verlag
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
In diesem Kapitel:<br />
• Das Problem<br />
• Die Syntax für den Aufruf<br />
gespeicherter <strong>Funktionen</strong> in<br />
<strong>SQL</strong><br />
• Bedingungen an gespeicherte<br />
<strong>Funktionen</strong> in <strong>SQL</strong><br />
• Einschränkungen <strong>von</strong><br />
<strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong><br />
• <strong>Funktionen</strong> in Packages <strong>von</strong><br />
<strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
• Die Präzedenz <strong>von</strong> Spaltenund<br />
Funktionsnamen<br />
• Die rauhe Wirklichkeit:<br />
<strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong><br />
<strong>aufrufen</strong><br />
• Beispiele eingebetteten<br />
<strong>PL</strong>/<strong>SQL</strong>-Codes<br />
17<br />
<strong>PL</strong>/<strong>SQL</strong>-<br />
<strong>Funktionen</strong> <strong>von</strong><br />
<strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
<strong>PL</strong>/<strong>SQL</strong> ist eine prozedurale Spracherweiterung zu <strong>SQL</strong>. Daher können Sie <strong>aus</strong> Ihren<br />
<strong>PL</strong>/<strong>SQL</strong>-Programmen auch native <strong>SQL</strong>-Anweisungen wie SELECT, INSERT oder UPDATE<br />
<strong>aufrufen</strong>. Bis zur Version 2.1 <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> (die zur Oracle-Datenbank 7.1 gehört) konnten<br />
Sie allerdings keine eigenen <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in einer <strong>SQL</strong>-Anweisung verwenden.<br />
HINWEIS<br />
Die in diesem Kapitel beschriebenen Möglichkeiten stehen erst ab <strong>PL</strong>/<strong>SQL</strong> Version<br />
2.1 zur Verfügung.<br />
Das Problem<br />
Die fehlende Möglichkeit, <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong>-Anweisungen verwenden zu<br />
können, führte oft zu häßlichen <strong>SQL</strong>-Anweisungen und redundanten Implementierungen<br />
<strong>von</strong> Geschäftsregeln. Nehmen wir beispielsweise an, Sie müssen das Gesamtgehalt<br />
eines Mitarbeiters sowohl in nativem <strong>SQL</strong> als auch in Ihren Formularen berechnen. Die<br />
Berechnung selbst ist einfach genug:<br />
Total compensation = salary + bonus<br />
Meine <strong>SQL</strong>-Anweisung würde diese Formel enthalten<br />
SELECT employee_name, salary + NVL (bonus, 0)<br />
FROM employee;<br />
579
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
während mein Post-Query-Trigger in meiner Oracle Forms-Anwendung den folgenden<br />
<strong>PL</strong>/<strong>SQL</strong>-Code verwenden würde:<br />
:employee.total_comp := :employee.salary + NVL (:employee.bonus, 0);<br />
In diesem Fall ist die Berechnung sehr einfach, aber trotzdem müssen Sie all diese hartkodierten<br />
Berechnungen sowohl in den <strong>SQL</strong>-Anweisungen als auch in den Anwendungskomponenten<br />
im Frontend einzeln ändern, wenn sich die Formel zur Berechnung<br />
des Gesamtgehalts <strong>aus</strong> irgendeinem Grunde ändern sollte.<br />
Sehr viel besser ist es, eine Funktion zu schreiben, die das Gesamtgehalt zurückgibt:<br />
FUNCTION total_comp<br />
(salary_in IN employee.salary%TYPE, bonus_in IN employee.bonus%TYPE)<br />
RETURN NUMBER<br />
IS<br />
BEGIN<br />
RETURN salary_in + NVL (bonus_in, 0);<br />
END;<br />
Diese Formel kann ich dann folgendermaßen in meinem Code einsetzen:<br />
SELECT employee_name, total_comp (salary, bonus)<br />
FROM employee;<br />
:employee.total_comp := total_comp (:employee.salary, :employee.bonus);<br />
Vor der Version 2.1 <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> führte das zu folgendem Fehler<br />
ORA-00919: invalid function<br />
weil es in <strong>SQL</strong> keinen Mechanismus gab, Referenzen auf in der Datenbank abgespeicherte,<br />
vom Programmierer definierte <strong>Funktionen</strong> aufzulösen. Inzwischen ist die Verbindung<br />
zwischen <strong>PL</strong>/<strong>SQL</strong> und <strong>SQL</strong> aber <strong>aus</strong>geglichener. Das ist auch sinnvoll so, denn<br />
die <strong>Funktionen</strong> werden ohnehin in der Datenbank abgelegt (wie nicht anders zu erwarten,<br />
in einer Tabelle) und stehen daher der <strong>SQL</strong>-Schicht über eine SELECT-Anweisung<br />
zur Verfügung.<br />
Seit der Version 2.1 <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> können Sie jetzt an jeder Stelle in einer <strong>SQL</strong>-Anweisung,<br />
in der ein Ausdruck erlaubt ist, gespeicherte <strong>Funktionen</strong> <strong>aufrufen</strong>, darunter in den<br />
SELECT-, WHERE-, START WITH-, GROUP BY-, HAVING-, ORDER BY-, SET- und<br />
VALUES-Kl<strong>aus</strong>eln (weil gespeicherte Prozeduren selbst <strong>aus</strong>führbare <strong>PL</strong>/<strong>SQL</strong>-Anweisungen<br />
sind, können diese nicht in eine <strong>SQL</strong>-Anweisung eingebettet werden).<br />
Sie können Ihre eigenen <strong>Funktionen</strong> gen<strong>aus</strong>o wie die Built-in- <strong>SQL</strong>-<strong>Funktionen</strong><br />
wie etwa TO_DATE, SUBSTR oder LENGTH verwenden. Auf der Begleitdiskette<br />
ist ein Package namens ps.parse (bestehend <strong>aus</strong> den Dateien psparse.sps und<br />
psparse.spb) enthalten, in dem es eine Funktion gibt, welche die Anzahl der Atome<br />
(Worte und/oder Begrenzungszeichen) in einem String zurückgibt. Das kann ich direkt<br />
in einer <strong>SQL</strong>-Anweisung verwenden, um die Verteilung der Worte in einer Reihe <strong>von</strong><br />
Textnotizen anzuzeigen:<br />
580
Das Problem<br />
SELECT line_number,<br />
ps_parse.number_of_atomics (line_text) AS num_words<br />
FROM notes<br />
ORDER BY num_words DESC;<br />
Beachten Sie, daß ich in diesem Fall dem Funktionsaufruf mit der »AS«-Syntax einen<br />
Spaltenalias zugewiesen habe. Diesen kann ich dann in ORDER BY verwenden, ohne<br />
die Syntax des Funktionsaufrufs selbst erneut angeben zu müssen.<br />
Die Möglichkeit, programmiererdefinierte <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> zu verwenden, ist<br />
eine sehr mächtige Erweiterung der Oracle-Entwicklungsumgebung. Mit diesen <strong>Funktionen</strong><br />
können Sie folgendes tun:<br />
• Die Logik <strong>von</strong> Geschäftsregeln in einer kleinen Anzahl optimierter und leicht wartbarer<br />
<strong>Funktionen</strong> zusammenfassen. Sie müssen diese Logik nicht über die einzelnen<br />
<strong>SQL</strong>-Anweisungen und <strong>PL</strong>/<strong>SQL</strong>-Programme verteilen. Dieser Punkt ist wahrscheinlich<br />
der weitreichendste und wichtigste Vorteil bei der Verwendung <strong>von</strong><br />
<strong>Funktionen</strong>.<br />
• Die Performanz Ihrer <strong>SQL</strong>-Anweisungen verbessern. <strong>SQL</strong> ist eine nichtprozedurale<br />
Sprache, aber die Anforderungen der Anwendung bedingen doch oft prozedurale<br />
Logik in Ihrem <strong>SQL</strong>-Code. <strong>SQL</strong> ist robust genug, so daß Sie immer irgendwie an die<br />
Antwort kommen können, aber in manchen Situationen ist das zu ineffizient. Eingebettetes<br />
<strong>PL</strong>/<strong>SQL</strong> kann den Job manchmal schneller erledigen. Natürlich kostet es<br />
auch ein wenig, diese <strong>Funktionen</strong> aufzurufen, weswegen Sie sorgfältig abwägen<br />
und <strong>aus</strong>probieren müssen, wo und wann <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> am meisten<br />
nützen.<br />
• Ihre <strong>SQL</strong>-Anweisungen vereinfachen. All die Gründe, <strong>aus</strong> denen sie <strong>PL</strong>/<strong>SQL</strong>-Code<br />
verwenden sollten, gelten auch für <strong>SQL</strong>, insbesondere wenn es darum geht, komplizierte<br />
Ausdrücke und komplizierte Logik hinter einer Funktionsspezifikation zu<br />
verstecken. Von der DECODE-Anweisung bis zu verschachtelten, korrelierten Subselects<br />
können programmiererdefinierte <strong>Funktionen</strong> die Lesbarkeit vieler <strong>SQL</strong>-<br />
Anweisungen verbessern.<br />
• Aktionen in <strong>SQL</strong> durchführen, die sonst unmöglich wären. <strong>SQL</strong> ist eine Sprache,<br />
die immer eine Menge zur Zeit bearbeitet, die <strong>aus</strong> Zeilen besteht, auf die dann<br />
irgendwelche Aktionen angewendet werden. Iterative Verarbeitung über einzelne<br />
Spaltenwerte ist nicht möglich. Wenn Sie beispielsweise die Anzahl der Vorkommnisse<br />
eines Substrings in den Namen <strong>von</strong> Firmen ermitteln wollen, dann geht das<br />
mit reinem <strong>SQL</strong> nicht. Sie können aber in einer SELECT-Liste eine <strong>PL</strong>/<strong>SQL</strong>-Funktion<br />
über den Firmennamen laufen lassen, der diese Art <strong>von</strong> iterativer Verarbeitung<br />
durchführen kann.<br />
Sie können <strong>Funktionen</strong> in einer VALUES-Liste, einer SET-Kl<strong>aus</strong>el oder einer GROUP<br />
BY-Kl<strong>aus</strong>el verwenden. Hier folgen ein paar Beispiele dazu:<br />
• VALUES-Liste. Schauen Sie sich das folgende Beispiel an:<br />
INSERT INTO notes<br />
(call_id, line_text, line_number)<br />
581
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
VALUES<br />
(:call.call_id, :note.text, next_line_number (:call.call_id));<br />
Die Funktion next_line_number holt die nächste laufende Nummer der Notizen<br />
zu diesem Vorfall (Sie könnten zwar einen Folgenummerngenerator verwenden,<br />
um die nächste eindeutige Vorfallnummer zu erhalten, aber die line_number für<br />
Notizen fängt immer wieder bei 1 an, so daß man hier keinen Folgenummerngenerator<br />
verwenden kann).<br />
• SET-Kl<strong>aus</strong>el. Die Funktion max_compensation gibt das höchstmögliche Gehalt in<br />
der Abteilung eines Angestellten wieder.<br />
UPDATE employee SET salary = max_compensation (department_id)<br />
WHERE employee_id = 1005;<br />
In diesem Fall ersetzt die Funktion max_compensation ein Subselect, das die<br />
Funktion AVG verwenden würde, um den Wert zu berechnen.<br />
• GROUP BY-Kl<strong>aus</strong>el. Meine Firma hat die Vereinheitlichung der Titel nicht besonders<br />
gut hinbekommen; es gibt 15 verschiedene Versionen <strong>von</strong> stellvertretenden<br />
Geschäftsführern, 20 verschiedene Manager usw. Die folgende SELECT-Anweisung<br />
räumt mit dieser Verwirrung auf und zeigt das Gesamtgehalt für jede Job-»Kategorie«<br />
an.<br />
SELECT job_category (job_title_id) as title, SUM (salary)<br />
FROM employee<br />
GROUP BY title;<br />
Die Funktion wird sowohl in der SELECT-Liste als auch in der GROUP BY-Kl<strong>aus</strong>el<br />
verwendet.<br />
Die Syntax für den Aufruf gespeicherter<br />
<strong>Funktionen</strong> in <strong>SQL</strong><br />
Eine gespeicherte Funktion wird <strong>aus</strong> einem <strong>SQL</strong>-Ausdruck mit der gleichen Syntax wie<br />
in einem <strong>PL</strong>/<strong>SQL</strong>-Ausdruck aufgerufen:<br />
[schema_name.][Package_name.][funktions_name[@db_link_name][parameter_liste]<br />
Hierbei ist schema_name der optionale Name des Datenbankschemas, in dem die Funktion<br />
definiert ist (üblicherweise Ihr Oracle-Benutzerkonto), Package_name ist der<br />
optionale Name des Packages, in dem die Funktion definiert ist (wenn es sich nicht um<br />
eine freistehende Funktion handelt), funktions_name ist der Name der Funktion,<br />
db_link_name ist der optionale Name der Datenbankverbindung, wenn Sie einen entfernten<br />
Prozeduraufruf (remote procedure call) <strong>aus</strong>führen, und parameter_liste ist<br />
die optionale Liste der Parameter der Funktion.<br />
Wenn die Funktion calc_sales folgendermaßen definiert ist<br />
582
Bedingungen an gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong><br />
FUNCTION calc_sales<br />
(company_id_in IN company.company_id%TYPE,<br />
status_in IN order.status_code%TYPE := NULL)<br />
RETURN NUMBER;<br />
dann kann diese Funktion <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> wie folgt aufgerufen werden:<br />
• Als freistehende Funktion:<br />
SELECT calc_sales (1001, 'O')<br />
FROM orders;<br />
• Als Funktion in einem Package:<br />
SELECT sales_pkg.calc_sales (1001, 'O')<br />
FROM orders;<br />
• Als entfernter Aufruf einer Funktion in einem Package:<br />
SELECT sales_pkg.calc_sales@NEW_YORK (1001, 'O')<br />
FROM orders;<br />
• Als freistehende Funktion in einem bestimmten Schema:<br />
SELECT scott.calc_sales (1001, 'O')<br />
FROM orders;<br />
<strong>SQL</strong> kann all diese Variationen korrekt parsen, aber Sie sollten es vermeiden, das<br />
Schema des Moduls und die Datenbankverbindung in Ihren <strong>SQL</strong>-Anweisungen hart zu<br />
kodieren (wie es im dritten und vierten Punkt gemacht wurde). Statt dessen sollten Sie<br />
Synonyme dafür anlegen, die diese Information verstecken. Damit müssen Sie nur das<br />
Synonym ändern, und nicht alle einzelnen <strong>SQL</strong>-Anweisungen, die diese Funktion <strong>aufrufen</strong>,<br />
wenn Sie jemals den Eigentümer der Funktion ändern oder zu einer anderen<br />
Datenbankinstanz wechseln sollten.<br />
Wenn Sie eine gespeicherte Funktion in einer <strong>SQL</strong>-Anweisung verwenden, dann müssen<br />
Sie die Zuordnung über Positionen verwenden, das Mischen der Zuordnung über<br />
den Namen und über die Position ist nicht erlaubt. Sie können calc_sales nur durch<br />
Angabe der beiden Argumente in der richtigen Reihenfolge <strong>aufrufen</strong>.<br />
Bedingungen an gespeicherte <strong>Funktionen</strong><br />
in <strong>SQL</strong><br />
Es gibt eine Reihe <strong>von</strong> Bedingungen, die eine programmiererdefinierte <strong>PL</strong>/<strong>SQL</strong>-Funktion<br />
erfüllen muß, damit sie <strong>von</strong> einer <strong>SQL</strong>-Anweisung <strong>aus</strong> aufgerufen werden kann.<br />
• Die Funktion muß in der Datenbank gespeichert sein. Eine Funktion, die in einer<br />
<strong>PL</strong>/<strong>SQL</strong>-Bibliothek <strong>von</strong> Oracle Developer/2000 oder einem einzelnen Formular<br />
gespeichert ist, kann nicht <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufen werden. <strong>SQL</strong> hat keine Möglichkeit,<br />
die Referenz auf eine solche Funktion aufzulösen.<br />
583
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
• Die Funktion muß eine zeilenspezifische Funktion sein, keine Spalten- oder Gruppenfunktion.<br />
Die Funktion kann nur auf eine einzelne Zeile mit Daten angewendet<br />
werden, nicht auf eine gesamte Spalte, die sich über mehrere Zeilen erstreckt.<br />
• Alle Funktionsparameter müssen den IN-Modus verwenden. Weder IN OUT- noch<br />
OUT-Parameter sind in gespeicherten <strong>Funktionen</strong> erlaubt, die <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufen<br />
werden sollen; aber Sie sollten ohnehin nie IN OUT- oder OUT-Parameter in<br />
<strong>Funktionen</strong> verwenden. Egal ob Sie die Funktion in einer <strong>SQL</strong>-Anweisung verwenden<br />
oder nicht, erzeugen solche Parameter Seiteneffekte zusätzlich zum Hauptzweck<br />
der Funktion, der ja die Rückgabe eines einzigen Wertes ist.<br />
• Die Datentypen der Parameter der Funktion wie auch der Datentyp der RETURN-<br />
Kl<strong>aus</strong>el müssen Oracle Server bekannt sein. Während alle Datentypen <strong>von</strong> Oracle<br />
Server in <strong>PL</strong>/<strong>SQL</strong> gültig sind, hat <strong>PL</strong>/<strong>SQL</strong> neue Datentypen hinzugefügt, die <strong>von</strong> der<br />
Datenbank (noch) nicht unterstützt werden. Zu diesen Datentypen gehören BOO-<br />
LEAN, BINARY_INTEGER, <strong>PL</strong>/<strong>SQL</strong>-Tabellen, <strong>PL</strong>/<strong>SQL</strong>-Datensätze und programmiererdefinierte<br />
Subtypen.<br />
• <strong>Funktionen</strong>, die in Packages definiert sind, müssen das Pragma RESTRICT_<br />
REFERENCES verwenden. Wenn Sie <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> eine in einem Package definierte<br />
Funktion <strong>aufrufen</strong> wollen, müssen Sie der Package-Spezifikation ein Pragma hinzufügen,<br />
das explizit angibt, daß diese Funktion zur Ausführung <strong>aus</strong> <strong>SQL</strong>-Anweisungen<br />
her<strong>aus</strong> zugelassen ist. Der Abschnitt »<strong>Funktionen</strong> in Packages <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> <strong>aus</strong><br />
<strong>aufrufen</strong>« enthält nähere Informationen hierzu.<br />
Die folgenden Funktionsspezifikationen würden zurückgewiesen werden, wenn sie in<br />
einer <strong>SQL</strong>-Anweisung verwendet würden:<br />
/* <strong>SQL</strong> weiss nichts ueber <strong>PL</strong>/<strong>SQL</strong>-Tabellen */<br />
TYPE string_tabtype IS TABLE OF VARCHAR2(30) INDEX BY BINARY_INTEGER;<br />
FUNCTION temp_table RETURN string_tabtype;<br />
/* <strong>SQL</strong> weiss nichts ueber Boolesche Werte */<br />
FUNCTION call_is_open (call_id_in IN call.call_id%TYPE) RETURN BOOLEAN;<br />
FUNCTION calc_sales<br />
(company_id_in IN NUMBER, use_closed_orders_in IN BOOLEAN)<br />
RETURN NUMBER;<br />
Einschränkungen <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong><br />
in <strong>SQL</strong><br />
Gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong> sind sehr mächtig. Aber Sie können sich vielleicht denken,<br />
daß diese Mächtigkeit auch die Möglichkeit zum Mißbrauch und die Notwendigkeit<br />
verantwortungsvollen Handelns mit sich bringt. Im Zusammenhang mit <strong>SQL</strong><br />
besteht der Mißbrauch in der Verwendung <strong>von</strong> Seiteneffekten in einer Funktion.<br />
Schauen Sie sich die folgende Funktion an:<br />
584
Einschränkungen <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong><br />
FUNCTION total_comp<br />
(salary_in IN employee.salary%TYPE, bonus_in IN employee.bonus%TYPE)<br />
RETURN NUMBER<br />
IS<br />
BEGIN<br />
UPDATE employee SET salary = salary_in / 2;<br />
RETURN salary_in + NVL (bonus_in, 0);<br />
END;<br />
Diese kleine Berechnung, die ich am Anfang des Kapitels schon eingeführt hatte, aktualisiert<br />
das Gehalt aller Mitarbeiter auf die Hälfte des angegebenen Wertes. Dieser Vorgang<br />
beeinflußt die Ergebnisse der Abfrage, <strong>aus</strong> der total_comp aufgerufen werden<br />
könnte, aber schlimmer noch, er beeinflußt alle anderen <strong>SQL</strong>-Anweisungen in dieser<br />
Sitzung.<br />
Neben der Modifikation <strong>von</strong> Datenbanktabellen ist die Modifikation <strong>von</strong> Package-Variablen<br />
ein anderer Seiteneffekt <strong>von</strong> gespeicherten <strong>Funktionen</strong> in <strong>SQL</strong>. Package-Variablen<br />
dienen in der jeweiligen Sitzung als globale Variablen. Eine Funktion, die eine Package-<br />
Variable verändert, könnte andere gespeicherte <strong>Funktionen</strong> oder Prozeduren beeinflussen,<br />
was wiederum die <strong>SQL</strong>-Anweisung beeinflußt, welche die gespeicherte Funktion<br />
verwendet.<br />
Eine <strong>PL</strong>/<strong>SQL</strong>-Funktion könnte auch einen Seiteneffekt in der WHERE-Kl<strong>aus</strong>el einer<br />
Abfrage verursachen. Der Anfrageoptimierer kann die Reihenfolge der Prädikate in der<br />
WHERE-Kl<strong>aus</strong>el umstellen, um die Anzahl der zu verarbeitenden Zeilen zu minimieren.<br />
Eine Funktion, die in dieser Kl<strong>aus</strong>el <strong>aus</strong>geführt wird, könnte also den Optimierungsprozeß<br />
der Anfrage unterlaufen.<br />
Ich würde ganz allgemein empfehlen, daß die Funktion sich ganz eng darauf beschränken<br />
sollte, einen Wert zu berechnen und zurückzugeben. Aber eine Empfehlung ist<br />
nicht genug, wenn es um die Integrität der Datenbank geht: Um sich gegen häßliche<br />
Seiteneffekte und unvorhersagbares Verhalten zu schützen, läßt der Oracle Server die<br />
folgenden Aktionen in Ihren gespeicherten <strong>Funktionen</strong> nicht zu:<br />
• Die gespeicherte Funktion darf keine Datenbanktabellen verändern. Sie kann<br />
keine INSERT-, DELETE- oder UPDATE-Anweisung <strong>aus</strong>führen.<br />
• Eine gespeicherte Funktion, die entfernt oder parallelisiert aufgerufen wird, darf<br />
weder lesend noch schreibend auf Package-Variablen zugreifen. Der Oracle Server<br />
erlaubt keine Seiteneffekte, welche die Grenzen der aktuellen Benutzersitzung<br />
überschreiten.<br />
• Eine gespeicherte Funktion kann die Werte <strong>von</strong> Package-Variablen nur dann<br />
ändern, wenn diese Funktion in einer SELECT-, VALUES- und SET-Kl<strong>aus</strong>el aufgerufen<br />
wird. Wenn diese Funktion in einer WHERE- oder GROUP BY-Kl<strong>aus</strong>el aufgerufen<br />
wird, kann sie keine Package-Variablen beschreiben.<br />
• Vor Oracle 8 konnten Sie nur sehr wenige Programme in Built-in Packages in einer<br />
<strong>von</strong> <strong>SQL</strong> <strong>aus</strong> verwendeten Funktion <strong>aufrufen</strong>. Zu den verbotenen Programmen<br />
bzw. Packages gehörten beispielsweise DBMS_OUTPUT.PUT_LINE, DBMS_PIPE und<br />
585
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
DBMS_<strong>SQL</strong>, um nur einige zu nennen. Das ist auch manchmal ganz gut so. Wenn<br />
Sie keine UPDATE-Anweisung <strong>aus</strong>führen dürfen, sollten Sie auch nicht über<br />
DBMS_<strong>SQL</strong> eine UPDATE-Operation »an der Zensur« vorbei einschleusen dürfen.<br />
Aber bei einigen anderen Packages sind diese Einschränkungen unnötig und nur<br />
vorhanden gewesen, weil Oracle diese Programme nicht aktiviert hatte. In Oracle8<br />
wurden einige dieser Einschränkungen aufgehoben. Sie können jetzt DBMS_<br />
OUTPUT.PUT_LINE <strong>aus</strong> einer <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufenen Funktion <strong>aufrufen</strong> und<br />
sogar Informationen durch Datenbank-Pipes schicken. 1<br />
• Vor Oracle8 konnten Sie RAISE_AP<strong>PL</strong>ICATION_ERROR nicht <strong>aus</strong> einer gespeicherten<br />
Funktion <strong>aufrufen</strong>.<br />
• In Oracle Server 7.3 können Sie keine <strong>PL</strong>/<strong>SQL</strong>-Tabellenmethoden (COUNT, FIRST,<br />
LAST, NEXT, PRIOR usw.) in einer gespeicherten Funktion verwenden, die <strong>von</strong><br />
<strong>SQL</strong> <strong>aus</strong> verwendet wird (das ist ein »bekannter Bug«, der in Oracle8 behoben<br />
wurde). Beispielsweise kann eine Funktion, die den folgenden Code enthält, in<br />
<strong>SQL</strong> nicht verwendet werden:<br />
DECLARE<br />
TYPE emptabtype IS TABLE of emp%ROWTYPE INDEX BY BINARY_INTEGER;<br />
emptab emptabtype;<br />
BEGIN<br />
IF emptab.COUNT > 0 THEN -- Wird in <strong>SQL</strong> zurueckgewiesen<br />
• Die gespeicherte Funktion darf kein anderes Modul (gespeicherte Prozedur oder<br />
Funktion) verwenden, das eine der obigen Regeln verletzt. Eine Funktion ist nur so<br />
rein wie alle Module, die sie selber aufruft.<br />
• Die gespeicherte Funktion darf keine Sicht verwenden, die eine der obigen Regeln<br />
verletzt. Eine Sicht ist eine gespeicherte SELECT-Anweisung; die SELECT-Anweisung<br />
der Sicht kann gespeicherte <strong>Funktionen</strong> verwenden.<br />
Wenn Ihre Funktion gegen eine dieser Regeln verstößt oder eine Funktion <strong>aus</strong> einem<br />
Package ist, in dem das Pragma RESTRICT_REFERENCES fehlt, bekommen Sie den<br />
gefürchteten Fehler ORA-06571:<br />
ORA-06571: Function TOTAL_COMP does not guarantee not to update database<br />
Wie im Abschnitt »Die rauhe Wirklichkeit: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> <strong>aufrufen</strong>«<br />
erwähnt, kann es manchmal sehr schwierig (oder schlichtweg unmöglich) sein, diesen<br />
Fehler zu vermeiden. In anderen Situationen gibt es aber einfache Lösungen (schauen<br />
Sie aber auf jeden Fall durch die obige Liste der Beschränkungen).<br />
1 Mein Buch Oracle Built-In Packages, 1998, O’Reilly & Associates, enthält eine umfassende Besprechung<br />
der <strong>Funktionen</strong> <strong>aus</strong> Packages, die <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> verwendet werden können.<br />
586
<strong>Funktionen</strong> in Packages <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
<strong>Funktionen</strong> in Packages <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
Wie in Kapitel 16, Packages, beschrieben, sind die Spezifikation und der Rumpf eines<br />
Packages getrennt <strong>von</strong>einander; eine Spezifikation kann (und muß) existieren, bevor<br />
der Rumpf definiert wird. Dieses Feature <strong>von</strong> Packages macht das Leben schwieriger,<br />
wenn es darum geht, <strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufzurufen. Wenn eine SELECT-Anweisung<br />
eine Funktion <strong>aus</strong> einem Package aufruft, dann ist nur die Information <strong>aus</strong> der<br />
Spezifikation des Packages verfügbar. Andererseits ist es aber der Inhalt des Package-<br />
Rumpfes, der festlegt, ob eine Funktion <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aus</strong>geführt werden darf. Als Konsequenz<br />
<strong>aus</strong> dieser Situation ergibt sich, daß Sie Code zu Ihrer Package-Spezifikation<br />
hinzufügen müssen, um eine Funktion <strong>aus</strong> einem Package <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufrufbar zu<br />
machen.<br />
Man spricht da<strong>von</strong>, daß Sie den »Reinheitsgrad« einer gespeicherten Funktion (der Grad,<br />
in dem die Funktion frei <strong>von</strong> Seiteneffekten ist) in der Package-Spezifikation »zusichern«<br />
müssen (assert the purity level). Der Oracle Server kann damit <strong>beim</strong> Kompilieren des<br />
Package-Rumpfes bestimmen, ob die Funktion diesen »Reinheitsgrad« erfüllt oder nicht.<br />
Erfüllt sie ihn nicht, wird ein Fehler gemeldet, und Sie stehen der manchmal gewaltigen<br />
Aufgabe gegenüber, her<strong>aus</strong>zufinden, wo und wie diese Verletzung vorkommt.<br />
Die Zusicherung des Reinheitsgrades einer Funktion geschieht mit dem Pragma<br />
RESTRICT_REFERENCES, das wir uns im nächsten Abschnitt ansehen werden.<br />
Das Pragma RESTRICT_REFERENCES<br />
Wie ich bereits erwähnt habe, ist ein Pragma eine Direktive für den <strong>PL</strong>/<strong>SQL</strong>-Compiler.<br />
Wenn Sie jemals eine programmiererdefinierte, benannte Ausnahme verwendet haben,<br />
dann sind Sie Ihrem ersten Pragma schon über den Weg gelaufen. Im Falle des Pragmas<br />
RESTRICT_REFERENCES teilen Sie dem Compiler den Reinheitsgrad mit, <strong>von</strong> dem Sie<br />
glauben, daß ihn Ihre Funktion mindestens erreicht.<br />
Jede Funktion im Package, die Sie in einer <strong>SQL</strong>-Anweisung verwenden wollen, benötigt<br />
ein eigenes Pragma, das nach der Funktionsdeklaration in der Package-Spezifikation<br />
stehen muß (im Package-Rumpf muß das Pragma dagegen nicht angegeben werden).<br />
Um den Reinheitsgrad mit dem Pragma zuzusichern, verwenden Sie die folgende Syntax:<br />
PRAGMA RESTRICT_REFERENCES<br />
(funktions_name, WNDS [, WNPS] [, RNDS] [, RNPS])<br />
Hierbei ist funktions_name der Name der Funktion, deren Reinheitsgrad Sie zusichern<br />
wollen. Die vier verschiedenen Codes haben die folgenden Bedeutungen:<br />
WNDS<br />
Writes No Database State. Sichert zu, daß die Funktion keine Datenbanktabellen<br />
verändert.<br />
WNPS<br />
Writes No Package State. Sichert zu, daß die Funktion keine Package-Variablen verändert.<br />
587
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
RNDS<br />
Reads No Database State. Sichert zu, daß die Funktion nicht <strong>aus</strong> Datenbanktabellen<br />
liest.<br />
RNPS<br />
Reads No Package State. Sichert zu, daß die Funktion nicht <strong>aus</strong> Package-Variablen<br />
liest.<br />
Nur WNDS muß im Pragma stehen. Das ist auch konsistent mit der Beschränkung, daß<br />
gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong> keine UPDATE-, INSERT- oder DELETE-Anweisung<br />
<strong>aus</strong>führen dürfen. Alle anderen Zusicherungen sind optional. Sie können Sie in beliebiger<br />
Reihenfolge aufführen, aber WNDS muß immer dabei sein. Kein Argument folgt <strong>aus</strong><br />
einem der anderen. Ich kann in die Datenbank schreiben, ohne <strong>aus</strong> ihr zu lesen, und<br />
<strong>aus</strong> einer Package-Variable lesen, ohne darauf zu schreiben.<br />
Hier ein Beispiel für zwei verschiedene Zusicherungen der Reinheit <strong>von</strong> <strong>Funktionen</strong> im<br />
Package company_financials:<br />
PACKAGE company_financials<br />
IS<br />
FUNCTION company_type (type_code_in IN VARCHAR2)<br />
RETURN VARCHAR2;<br />
FUNCTION company_name (company_id_in IN company.company_id%TYPE)<br />
RETURN VARCHAR2;<br />
PRAGMA RESTRICT_REFERENCES (company_type, WNDS, RNDS, WNPS, RNPS);<br />
PRAGMA RESTRICT_REFERENCES (company_name, WNDS, WNPS, RNPS);<br />
END company_financials;<br />
In diesem Package liest die Funktion company_name <strong>aus</strong> der Datenbank, um den<br />
Namen der angegebenen Firma zu ermitteln. Beachten Sie, daß ich beide Pragmas an<br />
das Ende der Package-Spezifikation gestellt habe, die Pragmas müssen den Funktionsspezifikationen<br />
nicht unmittelbar folgen. Ich habe mir außerdem die Mühe gemacht, die<br />
Argumente WNPS und RNPS bei beiden <strong>Funktionen</strong> anzugeben. Oracle Corporation<br />
empfiehlt, immer den höchstmöglichen Reinheitsgrad anzugeben, so daß der Compiler<br />
die Funktion nie unnötig zurückweist.<br />
HINWEIS<br />
Wenn eine Funktion, die Sie <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong> wollen, eine Prozedur in einem<br />
Package aufruft, dann muß auch für diese Prozedur das Pragma RESTRICT_<br />
REFERENCES angegeben werden. Sie können die Prozedur zwar nicht direkt <strong>von</strong><br />
<strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong>, aber wenn sie indirekt <strong>von</strong> <strong>SQL</strong> aufgerufen werden soll, dann<br />
muß sie sich ebenfalls an die Regeln halten.<br />
Fehler bei Pragmaverstößen<br />
Wenn Ihre Funktion gegen ihr Pragma verstößt, wird der Fehler <strong>PL</strong>S-00542 gemeldet.<br />
Wenn der Rumpf des Packages company_financials folgendermaßen <strong>aus</strong>sieht<br />
588
<strong>Funktionen</strong> in Packages <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
CREATE OR RE<strong>PL</strong>ACE PACKAGE BODY company_financials<br />
IS<br />
FUNCTION company_type (type_code_in IN VARCHAR2)<br />
RETURN VARCHAR2<br />
IS<br />
v_sal NUMBER;<br />
BEGIN<br />
SELECT sal INTO v_sal FROM emp WHERE empno = 1;<br />
RETURN 'bigone';<br />
END;<br />
FUNCTION company_name (company_id_in IN company.company_id%TYPE)<br />
RETURN VARCHAR2<br />
IS<br />
BEGIN<br />
UPDATE emp SET sal = 0;<br />
RETURN 'bigone';<br />
END;<br />
END company_financials;<br />
/<br />
dann bekomme ich <strong>beim</strong> Kompilieren den folgenden Fehler:<br />
3/4 <strong>PL</strong>S-00452: Subprogram 'COMPANY_TYPE' violates its associated pragma<br />
Der Grund dafür ist, daß die Funktion company_type <strong>aus</strong> der Datenbank liest, obwohl<br />
ich den Reinheitsgrad RNDS angegeben habe. Auch wenn ich diese dumme SELECT-<br />
Anweisung entferne, bekomme ich immer noch den folgenden Fehler<br />
11/4 <strong>PL</strong>S-00452: Subprogram 'COMPANY_NAME' violates its associated pragma<br />
weil die Funktion company_name die Daten in der Datenbank aktualisiert, obwohl ich<br />
WNDS angegeben habe. Manchmal werden Sie auf Ihre Funktion starren und sagen, »He,<br />
diesmal verletzte ich den angegebenen Reinheitsgrad wirklich nicht. Hier ist ja gar kein<br />
UPDATE, DELETE oder INSERT«. Vielleicht haben Sie recht. Aber es ist nicht unwahrscheinlich,<br />
daß Sie ein Built-in Package aufgerufen oder auf andere Weise gegen die<br />
Regeln verstoßen haben.<br />
Zusichern des Reinheitsgrades im Initialisierungsabschnitt des<br />
Packages<br />
Wenn Ihr Package einen Initialisierungsabschnitt (<strong>aus</strong>führbare Anweisungen nach einer<br />
BEGIN-Anweisung im Package-Rumpf) enthält, dann müssen Sie auch den Reinheitsgrad<br />
dieses Abschnitts zusichern. Der Initialisierungsabschnitt wird automatisch <strong>aus</strong>geführt,<br />
wenn das erste Mal auf ein Objekt <strong>aus</strong> dem Package zugegriffen wird. Wenn eine<br />
Funktion <strong>aus</strong> einem solchen Package also in einer <strong>SQL</strong>-Anweisung <strong>aus</strong>geführt wird,<br />
kann sie die Ausführung dieses Codes bewirken. Wenn der Initialisierungsabschnitt<br />
Package-Variablen oder Daten in der Datenbank modifiziert, dann muß dies dem Compiler<br />
über das Pragma mitgeteilt werden.<br />
589
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
Sie können den Reinheitsgrad des Initialisierungsabschnitts entweder explizit oder<br />
implizit zusichern. Für die explizite Zusicherung verwenden Sie die folgende Variation<br />
des Pragmas RESTRICT_REFERENCES:<br />
PRAGMA RESTRICT_REFERENCES<br />
(Package_name, WNDS, [, WNPS] [, RNDS] [, RNPS])<br />
Anstelle des Namens der Funktion geben Sie hier den Namen des Packages selbst an,<br />
worauf wieder die Zustandsargumente folgen. Im folgenden Beispiel sichere ich nur<br />
WNDS und WNPS zu, weil der Initialisierungsabschnitt Daten <strong>aus</strong> der Konfigurationstabelle<br />
und <strong>aus</strong> einer globalen Variable eines anderen Packages (session_pkg.<br />
user_id) liest.<br />
PACKAGE configure<br />
IS<br />
PRAGMA RESTRICT_REFERENCES (configure, WNDS, WNPS);<br />
user_name VARCHAR2(100);<br />
END configure;<br />
PACKAGE BODY configure<br />
IS<br />
BEGIN<br />
SELECT lname || ', ' || fname INTO user_name<br />
FROM user_table<br />
WHERE user_id = session_pkg.user_id;<br />
END configure;<br />
Warum kann ich hier WNPS zusichern, obwohl ich die Package-Variable user_name<br />
beschreibe? Die Antwort ist, daß es sich um eine Variable <strong>aus</strong> dem gleichen Package<br />
handelt, weswegen diese Aktion nicht als Seiteneffekt zählt.<br />
Sie können den Reinheitsgrad des Initialisierungsabschnitts des Packages auch implizit<br />
zusichern, indem Sie den Compiler den Reinheitsgrad <strong>aus</strong> den Reinheitsgraden aller<br />
Pragmas der einzelnen <strong>Funktionen</strong> im Package ableiten lassen. In der folgenden Version<br />
des Packages company kann der Oracle Server <strong>aus</strong> den beiden Pragmas für die<br />
<strong>Funktionen</strong> einen Reinheitsgrad <strong>von</strong> RNDS und WNPS für den Initialisierungsabschnitt<br />
ableiten. Der Initialisierungsabschnitt kann also nicht <strong>aus</strong> der Datenbank lesen und<br />
keine Package-Variablen beschreiben.<br />
PACKAGE company<br />
IS<br />
FUNCTION get_company (company_id_in IN VARCHAR2)<br />
RETURN company%ROWTYPE;<br />
FUNCTION deactivate_company (company_id_in IN company.company_id%TYPE)<br />
RETURN VARCHAR2;<br />
PRAGMA RESTRICT_REFERENCES (get_company, RNDS, WNPS);<br />
PRAGMA RESTRICT_REFERENCES (deactivate_name, WNPS);<br />
END company;<br />
590
Die Präzedenz <strong>von</strong> Spalten- und Funktionsnamen<br />
Üblicherweise ist es besser, den Reinheitsgrad des Initialisierungsabschnitts explizit<br />
anzugeben. Das macht es denjenigen, die das Package später einmal warten müssen,<br />
leichter, Ihre Absichten und Ihr Verständnis des Packages zu durchschauen.<br />
Die Präzedenz <strong>von</strong> Spalten- und<br />
Funktionsnamen<br />
Wenn Ihre Funktion den gleichen Namen wie eine Tabellenspalte in einer SELECT-<br />
Anweisung und keine Parameter hat, dann hat die Spalte Vorrang vor der Funktion.<br />
Die Tabelle employee habe eine Spalte namens salary. Wenn Sie nun auch eine<br />
Funktion namens salary erzeugen<br />
CREATE TABLE employee (employee_id NUMBER, ... , salary NUMBER, ...);<br />
FUNCTION salary RETURN NUMBER;<br />
dann bezieht sich salary in einer SELECT-Anweisung immer auf die Spalte und nicht<br />
auf die Funktion:<br />
SELECT salary INTO calculated_salary FROM employee;<br />
Wenn Sie diese Präzedenz der Spalte umgehen wollen, müssen Sie den Namen der<br />
Funktion mit dem Namen des Schemas, dem diese Funktion gehört, qualifizieren:<br />
SELECT scott.salary INTO calculated_salary FROM employee;<br />
Damit wird die Funktion <strong>aus</strong>geführt, anstatt den Spaltenwert zu holen.<br />
Die rauhe Wirklichkeit: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in<br />
<strong>SQL</strong> <strong>aufrufen</strong><br />
Die Möglichkeit, <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> aufzurufen, gibt es schon seit der Version<br />
2.1, aber trotzdem kann das in vielerlei Hinsicht (zumindest vor dem Erscheinen <strong>von</strong><br />
Oracle8) als »brandneue« Technologie betrachtet werden. Warum ist das so?<br />
• Sie müssen in Ihrem Code die RESTRICT_REFERENCES-Pragmas <strong>von</strong> Hand eintragen,<br />
und Sie müssen selbst her<strong>aus</strong>finden, wo diese nötig sind. Wie man das macht,<br />
wird in einem späteren Abschnitt beschrieben.<br />
• Die <strong>Funktionen</strong> werden außerhalb des Lesekonsistenzmodells der Oracle-Datenbank<br />
<strong>aus</strong>geführt (!). Auch das wird in einem späteren Abschnitt näher erläutert.<br />
• Es ist immer noch teuer, eine Funktion <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufzurufen. Die genauen<br />
Kosten eines Funktionsaufrufs (verglichen beispielsweise mit Inline-<strong>SQL</strong>-Code)<br />
sind schwer zu bestimmen, sie unterscheiden sich <strong>von</strong> Computer zu Computer und<br />
sogar <strong>von</strong> Instanz zu Instanz oder <strong>von</strong> aufgerufener Funktion zu aufgerufener<br />
591
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
Funktion. Ich habe <strong>von</strong> einer zusätzlichen halben Sekunde bis zu erstaunlichen<br />
zusätzlichen fünfzig Sekunden gehört (auch wenn ich im letzteren Falle vorschlagen<br />
würde, ein wenig mehr zu analysieren und zu debuggen). Aber egal wie<br />
schnell der Aufruf wirklich ist, die Verzögerung kann für den Anwender spürbar<br />
sein. Sie müssen sie also in Ihren Design- und Testplänen berücksichtigen.<br />
• Tuning-Mechanismen wie EX<strong>PL</strong>AIN <strong>PL</strong>AN berücksichtigen den <strong>SQL</strong>-Code nicht,<br />
der in <strong>Funktionen</strong> aufgerufen werden könnte, die wiederum <strong>von</strong> Ihren <strong>SQL</strong>-<br />
Anweisungen aufgerufen werden. <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> werden <strong>von</strong> EX<strong>PL</strong>AIN <strong>PL</strong>AN<br />
nicht berücksichtigt. Damit wird es schwierig, die Flaschenhälse zu erkennen, die<br />
man zur Performanzsteigerung beseitigen muß.<br />
• Ein großer Teil der Oracle-Technologie, und dabei wiederum insbesondere jener<br />
Teil, der in Built-in Packages steckt, ist für <strong>Funktionen</strong> in <strong>SQL</strong> verboten. Denken<br />
Sie nur an DBMS_OUTPUT. Sie können dieses Package nicht in <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufenen<br />
<strong>Funktionen</strong> verwenden. Normalerweise möchten Sie aber die gleichen <strong>Funktionen</strong><br />
in <strong>SQL</strong> und <strong>PL</strong>/<strong>SQL</strong> verwenden. Auch fügen Sie zum Debuggen möglicherweise<br />
am Anfang und am Ende Ihrer <strong>Funktionen</strong> und Prozeduren Aufrufe ein und<br />
verwenden sehr wahrscheinlich DBMS_OUTPUT (oder UTIL_FILE oder DBMS_PIPE,<br />
was Sie wollen, diese <strong>Funktionen</strong> sind alle – zumindest vor Oracle8 – verboten).<br />
Tja, tut mir leid. Diese <strong>Funktionen</strong> können Sie <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> nicht verwenden. Also<br />
müssen Sie Code <strong>aus</strong> der Funktion her<strong>aus</strong>nehmen. Am Ende haben Sie zwei Versionen<br />
Ihres Codes: eine für <strong>PL</strong>/<strong>SQL</strong>, eine für <strong>SQL</strong>, ein Alptraum für Software-Entwickler.<br />
Schauen wir uns jetzt zwei dieser Probleme genauer an.<br />
Manuelles Eintragen der Pragmas<br />
Sie müssen die RESTRICT_REFERENCES-Pragmas überall in Ihrem Code eintragen und<br />
auch her<strong>aus</strong>finden, wo diese hingehören. Dieser Vorgang ähnelt oft einer detektivischen<br />
Spurensuche, Sherlock Holmes läßt grüßen! Sie kompilieren ein Package und<br />
erhalten eine Meldung über einen Verstoß gegen ein Pragma. Der Grund dafür kann<br />
sein, daß Ihr Programm eine Regel verletzt (indem es beispielsweise Daten verändert),<br />
oder andere Programme aufruft, die eine Regel verletzen. In letzterem Fall stehen Sie<br />
womöglich vor fünf oder sechs anderen <strong>Funktionen</strong> oder Prozeduren, müssen also in<br />
all diesen ebenfalls die Pragmas eintragen. Damit sichern Sie aber Reinheitsgrade zu,<br />
wo das vorher nicht geschah, und handeln sich damit im besten Fall weitere Fehler ein,<br />
im schlechtesten Fall aber weitreichende Konsequenzen für die Architektur Ihrer<br />
Anwendung.<br />
Nehmen wir beispielsweise an, daß Sie auf einmal einer Prozedur im Package X ein<br />
Pragma anbeistellen müssen. Dieses Package hat aber einen Initialisierungsabschnitt, so<br />
daß Sie auch diesen mit Pragmas versehen müssen. In diesen Abschnitten werden oft<br />
<strong>PL</strong>/<strong>SQL</strong>-Tabellen für die Datenmanipulation im Speicher eingerichtet. Wenn Sie aber<br />
<strong>PL</strong>/<strong>SQL</strong>-Tabellenmethoden für die Initialisierung solcher Tabellen verwenden, wird das<br />
Pragma fehlschlagen.<br />
592
Die rauhe Wirklichkeit: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> <strong>aufrufen</strong><br />
Dieser Vorgang kann ziemlich frustrierend sein und manchmal dazu führen, daß Sie am<br />
Ende darauf verzichten, eine Funktion <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufzurufen. Ich habe die Erfahrung<br />
gemacht, daß man so weit wie möglich im vornherein die Bereiche der Anwendung<br />
festlegen sollte, die <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufen werden sollen. Sie können dann diesen<br />
Code besonders »sauber« und beschränkt auf die eigentliche Aufgabe halten und die<br />
Verstrickungen mit anderen Packages sowie die Verwendung Built-in Packages so<br />
gering wie möglich halten. Das ist weder einfach noch besonders spaßig.<br />
Komplikationen mit dem Lesekonsistenzmodell<br />
Ja, es ist kaum zu glauben, aber wahr: Wenn Sie nicht besondere Vorsichtsmaßnahmen<br />
treffen, ist es gut möglich, daß Ihre <strong>SQL</strong>-Abfrage das Lesekonsistenzmodell des Oracle-<br />
RDBMS verletzt, ein seit Jahren geheiligtes Territorium in Oracle. Um dieses Problem zu<br />
verstehen, werfen Sie bitte einen Blick auf die folgende Abfrage und die <strong>von</strong> ihr aufgerufene<br />
Funktion:<br />
SELECT name, total_sales (account_id)<br />
FROM account<br />
WHERE status = 'ACTIVE';<br />
FUNCTION total_sales (id_in IN account.account_id%TYPE)<br />
RETURN NUMBER<br />
IS<br />
CURSOR tot_cur<br />
IS<br />
SELECT SUM (sales) total<br />
FROM orders<br />
WHERE account_id = id_in<br />
AND year = TO_NUMBER (TO_CHAR (SYSDATE, 'YYYY'));<br />
tot_rec tot_cur%ROWTYPE;<br />
BEGIN<br />
OPEN tot_cur;<br />
FETCH tot_cur INTO tot_rec;<br />
RETURN tot_rec.total;<br />
END;<br />
Die Tabelle account hat fünf Millionen aktive Zeilen (offensichtlich ein sehr erfolgreiches<br />
Unternehmen!). Die Tabelle orders hat zwanzig Millionen Zeilen. Ich starte die<br />
Abfrage um 11 Uhr morgens und rechne damit, daß sie etwa eine Stunde dauert. Um<br />
viertel vor zwölf kommt jemand mit der entsprechenden Berechtigung, löscht alle Zeilen<br />
<strong>aus</strong> der Tabelle orders und gibt die Änderung frei. Nach dem Lesekonsistenzmodell<br />
<strong>von</strong> Oracle sollte die Sitzung, welche die Abfrage <strong>aus</strong>führt, all diese gelöschten Zeilen<br />
sehen, bis die Abfrage abgeschlossen ist. Aber wenn das nächste Mal die Funktion<br />
total_sales <strong>aus</strong> der Abfrage her<strong>aus</strong> aufgerufen wird, findet Sie keine Zeilen in<br />
orders mehr vor und gibt NULL zurück – und das auch bei allen weiteren Aufrufen.<br />
Sie müssen sich also über solche Lesekonsistenzprobleme im klaren sein, wenn Sie in<br />
<strong>Funktionen</strong>, die <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufen werden, wiederum Abfragen durchführen.<br />
Wenn diese <strong>Funktionen</strong> <strong>von</strong> lange laufenden Abfragen oder Transaktionen aufgerufen<br />
593
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
werden, sollten Sie vielleicht zwischen den <strong>SQL</strong>-Anweisungen der aktuellen Transaktion<br />
den folgenden Befehl eingeben, um die Lesekonsistenz zu erzwingen:<br />
SET TRANSACTION READ ONLY<br />
Mehr dazu finden Sie in Kapitel 6, Interaktion mit der Datenbank und Cursoren.<br />
Das Arbeiten mit <strong>Funktionen</strong> in <strong>SQL</strong> ist schwieriger und komplizierter, als Sie es sich<br />
vielleicht auf den ersten Blick vorstellen können. Das hätte man sich eigentlich auch<br />
denken können, denn man kann das über fast jeden Aspekt der Oracle-Technologie<br />
sagen, insbesondere über die kürzlich hinzugekommenen Neuerungen. Ich hoffe, daß<br />
uns Oracle mit der Zeit das Leben leichter machen wird (und in Oracle 8.0 gibt es einige<br />
deutliche Verbesserungen). Was wir letztendlich brauchen, ist ein Hilfsprogramm, mit<br />
dem ein Entwickler auf eine Funktion verweisen und verlangen kann, daß »diese Funktion<br />
für <strong>SQL</strong> nutzbar gemacht wird«. Dieses Hilfsprogramm trägt dann alle Pragmas ein<br />
oder erstellt zumindest einen Bericht über die notwendigen Schritte.<br />
Man darf ja schließlich träumen, oder?<br />
Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />
Je mehr Sie über gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong> nachdenken, um so mehr Möglichkeiten<br />
werden Ihnen einfallen, diese in jeder einzelnen Ihrer Anwendungen zu verwenden.<br />
Um Ihrer Kreativität eine kleine Starthilfe zu geben, zeige ich Ihnen hier einige<br />
Beispiele, wie gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong>-Anweisungen Ihre Oracle-basierten<br />
Systeme verändern können.<br />
Eingekapselte Berechnungen<br />
In so ziemlich jeder Anwendung müssen Sie immer wieder die gleichen Berechnungen<br />
durchführen. Egal ob es sich dabei um die Berechnung des Bruttobetrags, des Standes<br />
der Hypothek, des Abstands zweier Punkte auf einer kartesischen Ebene oder einer statistischen<br />
Varianz handelt, mit nativem <strong>SQL</strong> müssen Sie diese Berechnungen in jeder<br />
<strong>SQL</strong>-Anweisung, in der sie benötigt werden, wieder neu schreiben.<br />
Für diese Redundanz zahlen Sie einen hohen Preis. Der Code, der Ihre Geschäftsregeln<br />
implementiert, ist über die gesamte Anwendung verstreut und wird immer wieder wiederholt.<br />
Sogar wenn sich diese Regeln selbst nicht ändern, wird sich wahrscheinlich die<br />
Implementierung dieser Regeln irgendwann ändern müssen. Wenn sich die Regel selbst<br />
ändert, ist die Lage natürlich noch übler, weil das oft zu ziemlich umfassenden Änderungen<br />
im Code führen kann.<br />
Um dieses Problem zu lösen, können Sie alle Formeln und Berechnungen in gespeicherten<br />
<strong>Funktionen</strong> verstecken. Diese <strong>Funktionen</strong> können dann sowohl <strong>aus</strong> <strong>SQL</strong>-<br />
Anweisungen als auch <strong>aus</strong> <strong>PL</strong>/<strong>SQL</strong>-Programmen her<strong>aus</strong> aufgerufen werden.<br />
Ein schönes Beispiel für die Nützlichkeit eingekapselter Berechnungen entstand, als<br />
eine Versicherung Analysen ihrer Konten auf Basis <strong>von</strong> Datumsangaben durchführen<br />
594
Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />
mußte. Der letzte Tag im Monat ist natürlich für die meisten Finanzinstitutionen ein sehr<br />
wichtiges Datum. Um die Datumsangaben zu manipulieren, sah die IT-Abteilung der<br />
Firma vor, die Built-in-Funktion LAST_DAY zu verwenden, um den letzten Tag eines<br />
Monats zu bekommen, und mit ADD_MONTHS <strong>von</strong> einem Monat zum nächsten zu springen.<br />
Bald fiel ihnen aber eine sehr interessante Nuance in der Funktion <strong>von</strong> ADD_<br />
MONTHS auf: Wenn Sie einen Tag an ADD_MONTH übergeben, welcher der letzte Tag<br />
des jeweiligen Monats ist, dann gibt <strong>SQL</strong> immer den letzten Tag im dar<strong>aus</strong> resultierenden<br />
Monat zurück, egal, wie viele Tage jeder der einzelnen Monate wirklich hat:<br />
Anders <strong>aus</strong>gedrückt:<br />
ADD_MONTHS ('28-FEB-1994', 2) ==> 30-APR-1993<br />
Das ist vielleicht bei manchen Anwendungen und Abfragen auch sinnvoll. Bei der Versicherung<br />
war die Anforderung aber, daß man <strong>beim</strong> Springen <strong>von</strong> Monat zu Monat<br />
immer am gleichen Tag des Monats ankommen muß (oder am letzten Tag, wenn der<br />
Tag im Ausgangsmonat nach dem letzten Tag des Zielmonats lag). Ohne gespeicherte<br />
<strong>Funktionen</strong> hätte man folgende <strong>SQL</strong>-Anweisung verwenden müssen:<br />
SELECT<br />
DECODE (payment_date,<br />
LAST_DAY (payment_date),<br />
LEAST (ADD_MONTHS (payment_date, 1),<br />
TO_DATE (TO_CHAR (ADD_MONTHS (payment_date, 1),<br />
'MMYYYY') ||<br />
TO_CHAR (payment, 'DD'),<br />
'MMYYYYDD')),<br />
ADD_MONTHS (payment_date, 1))<br />
FROM premium_payments;<br />
Auf deutsch heißt das: »Wenn das Datum der letzten Zahlung auf den letzten Tag des<br />
Monats fällt, dann gib als Datum der nächsten Zahlung entweder das Ergebnis der Addition<br />
eines Monats zum Datum der letzten Zahlung (mit ADD_MONTHS) oder den gleichen<br />
Tag wie den der letzten Zahlung im neuen Monat zurück. Falls die letzte Zahlung nicht<br />
am letzten Tag des Monats getätigt wurde, dann verwende zur Bestimmung des nächsten<br />
Zahlungsdatums einfach ADD_MONTHS.«<br />
Das ist nicht nur schwierig zu verstehen, sondern benötigt auch drei Aufrufe der Builtin-Funktion<br />
ADD_MONTHS. Stellen Sie sich jetzt noch vor, daß diese komplexe <strong>SQL</strong>-<br />
Anweisung in jeder SELECT-Liste hätte wiederholt werden müssen, in der ADD_MONTHS<br />
verwendet wurde, um die Datumsangaben herauf- oder herunterzuzählen. Sie können<br />
sich da wahrscheinlich gut vorstellen, wie froh die Programmierer in dieser Firma<br />
waren, als sie die Version 7.1 <strong>von</strong> Oracle Server installierten und die folgende Funktion<br />
in ihren <strong>SQL</strong>-Anweisungen verwenden konnten (eine vollständige Erklärung der Logik<br />
dieser Funktion finden Sie in Kapitel 12, Datumsfunktionen):<br />
FUNCTION new_add_months (date_in IN DATE, months_shift IN NUMBER)<br />
RETURN DATE<br />
IS<br />
return_value DATE;<br />
595
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
BEGIN<br />
return_value := ADD_MONTHS (date_in, months_shift);<br />
IF date_in = LAST_DAY (date_in)<br />
THEN<br />
return_value :=<br />
LEAST (return_value,<br />
TO_DATE (TO_CHAR (return_value, 'MMYYYY') ||<br />
TO_CHAR (date_in, 'DD') ,<br />
'MMYYYYDD'));<br />
END IF;<br />
RETURN return_value;<br />
END new_add_months;<br />
Damit wird die SELECT-Anweisung zur Bestimmung des nächsten Zahlungsdatums sehr<br />
einfach:<br />
SELECT new_add_months (payment_date,1)<br />
FROM premium_payments;<br />
Je mehr Sie Ihre <strong>SQL</strong>-Anweisungen durchsehen, um so mehr Möglichkeiten für den Einsatz<br />
gespeicherter <strong>Funktionen</strong> werden Sie finden, mit denen Sie Berechnungen verstekken<br />
und damit auf lange Sicht die Stabilität Ihres Codes verbessern können. Es ist zwar<br />
wenig wahrscheinlich, daß Sie die Zeit und die Ressourcen haben, um das gesamte<br />
<strong>SQL</strong>-Rückgrat Ihrer Anwendung neu zu schreiben, aber Sie können zumindest entsprechende<br />
<strong>Funktionen</strong> entwickeln und diese in Neuentwicklungen einsetzen.<br />
Kombinieren skalarer und aggregierter Werte<br />
Die folgende einfache Frage ist in <strong>SQL</strong> schwer zu beantworten: »Zeige mir den Namen<br />
und das Gehalt des Mitarbeiters jeder Abteilung, der in dieser Abteilung das höchste<br />
Gehalt hat, sowie das Gesamtgehalt aller Mitarbeiter der jeweiligen Abteilungen.«<br />
Wenn man diese Frage in zwei Teile aufteilt, ist das nicht schwer. Hier ist der erste Teil:<br />
SELECT department_id, last_name, salary<br />
FROM employee E1<br />
WHERE salary = (SELECT MAX (salary)<br />
FROM employee E2<br />
WHERE E.department_id = E2.department_id)<br />
GROUP BY department_id;<br />
Und hier der zweite:<br />
SELECT department_id, SUM (salary)<br />
FROM employee<br />
GROUP BY department_id;<br />
Es ist aber nicht so einfach, diese beiden zu kombinieren, weil man dabei sowohl skalare<br />
Werte (eine einzelne Zeile) als auch aggregierte (sich über mehrere Zeilen erstrekkende)<br />
Werte <strong>aus</strong> ein und derselben Tabelle abfragen müßte. Wie soll ich meine<br />
FROM-, WHERE- und GROUP BY-Kl<strong>aus</strong>eln schreiben, um sowohl das Gehalt eines einzelnen<br />
als auch das aller Abteilungsmitglieder zusammengenommen anzuzeigen?<br />
596
Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />
SELECT department_id, last_name, salary, SUM (salary)<br />
FROM ...?<br />
WHERE ...?<br />
GROUP BY ...?<br />
Vor der Version 2.1 <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> war die naheliegendste Lösung, eine Sicht zu erzeugen,<br />
die das Gehalt jeder Abteilung schon einmal »im vor<strong>aus</strong>« zusammenfaßte:<br />
CREATE VIEW dept_salary<br />
AS<br />
SELECT department_id, SUM (salary) total_salary<br />
FROM employee<br />
GROUP BY department_id;<br />
Mit dieser Sicht kann ich die Antwort mit einer einzigen <strong>SQL</strong>-Anweisung bekommen:<br />
SELECT E.department_id, last_name, salary, total_salary<br />
FROM employee E, dept_salary DS<br />
WHERE E.department_id = DS.department_id<br />
AND salary = (SELECT MAX (salary)<br />
FROM employee E2<br />
WHERE E.department_id = E2.department_id);<br />
Diese Lösung sieht ja gar nicht so schlecht <strong>aus</strong>, aber Sie müssen jedesmal, wenn Sie<br />
eine solche Berechnung durchführen wollen, eine spezielle Sicht erzeugen. Abgesehen<br />
da<strong>von</strong>, ist dieser <strong>SQL</strong>-Code auch für viele Programmierer nicht so besonders offensichtlich.<br />
Eine bessere Lösung ist es, eine gespeicherte Funktion zu verwenden. Anstatt eine Sicht<br />
zu erzeugen, schreiben wir eine Funktion, die genau die gleiche Berechnung durchführt,<br />
aber dieses Mal nur für die angegebene Abteilung:<br />
FUNCTION total_salary (dept_id_in IN department.department_id%TYPE)<br />
RETURN NUMBER<br />
IS<br />
CURSOR grp_cur<br />
IS<br />
SELECT SUM (salary)<br />
FROM employee<br />
WHERE department_id = dept_id_in;<br />
return_value NUMBER;<br />
BEGIN<br />
OPEN grp_cur;<br />
FETCH grp_cur INTO return_value;<br />
CLOSE grp_cur;<br />
RETURN return_value;<br />
END;<br />
Wenn eine Abteilung nicht existiert, gebe ich NULL zurück. Wenn eine Abteilung keine<br />
Mitarbeiter hat, gebe ich 0 zurück, ansonsten die Summe der Gehälter aller Mitarbeiter<br />
in dieser Abteilung.<br />
597
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
Meine Abfrage muß jetzt keine Sicht verwenden, die ein GROUP BY enthält. Statt dessen<br />
ruft sie einfach die Funktion total_salary auf und übergibt die ID-Nummer der<br />
Abteilung des Mitarbeiters als Parameter:<br />
SELECT E.department_id, last_name, salary, total_salary (E.department_id)<br />
FROM employee E<br />
WHERE salary = (SELECT MAX (salary)<br />
FROM employee E2<br />
WHERE E.department_id = E2.department_id);<br />
Die dar<strong>aus</strong> resultierende <strong>SQL</strong>-Anweisung ist nicht nur leichter zu lesen, sondern ist<br />
auch schneller, insbesondere bei größeren Tabellen. Ich könnte die <strong>SQL</strong>-Anweisung<br />
noch weiter vereinfachen, indem ich eine Funktion schreibe, die das maximale Gehalt<br />
in einer bestimmten Abteilung zurückgibt. Die SELECT-Anweisung lautet dann einfach<br />
nur:<br />
SELECT department_id, last_name, salary, total_salary (department_id)<br />
FROM employee E<br />
WHERE salary = max_sal_in_dept (department_id);<br />
Ersetzung korrelierter Subabfragen<br />
Mit gespeicherten <strong>Funktionen</strong> in <strong>SQL</strong>-Anweisungen kann man auch korrelierte Subabfragen<br />
ersetzen. Das sind SELECT-Anweisungen innerhalb der WHERE-Kl<strong>aus</strong>el einer<br />
<strong>SQL</strong>-Anweisung (SELECT, INSERT oder DELETE), die sich auf eine oder mehrere Spalten<br />
der umgebenden <strong>SQL</strong>-Anweisung beziehen. Im letzten Abschnitt habe ich eine korrelierte<br />
Subabfrage verwendet, um den Mitarbeiter mit dem höchsten Gehalt in jeder<br />
Abteilung zu bestimmen:<br />
SELECT E.department_id, last_name, salary, total_salary (E.department_id)<br />
FROM employee E<br />
WHERE salary = (SELECT MAX (salary)<br />
FROM employee E2<br />
WHERE E.department_id = E2.department_id);<br />
Die letzten drei Zeilen der Abfrage enthalten eine SELECT-Anweisung, in der die Abteilungsnummer<br />
des »inneren« Mitarbeiters (E2) mit der Abteilungsnummer in der »äußeren«<br />
Mitarbeitertabelle (E1) übereinstimmen soll. Die innere Abfrage wird für jede mit<br />
der äußeren Abfrage ermittelte Zeile einmal <strong>aus</strong>geführt.<br />
Korrelierte Subabfragen sind ein sehr mächtiges Feature <strong>von</strong> <strong>SQL</strong>, weil sie den verschachtelten<br />
Schleifen prozeduraler Programmiersprachen wie in<br />
LOOP<br />
LOOP<br />
END LOOP;<br />
END LOOP;<br />
entsprechen. Sie haben aber zwei Nachteile:<br />
• Die Logik kann ziemlich kompliziert werden.<br />
• Die resultierende <strong>SQL</strong>-Anweisung kann schwierig zu verstehen und zu verfolgen<br />
sein.<br />
598
Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />
Sie können diese Nachteile vermeiden, indem Sie gespeicherte <strong>Funktionen</strong> anstelle der<br />
korrelierten Subabfragen verwenden. Im obigen Beispiel bräuchte ich eine Funktion,<br />
die das höchste Gehalt in einer bestimmten Abteilung berechnet:<br />
FUNCTION max_salary (dept_id_in IN department.department_id%TYPE)<br />
RETURN NUMBER<br />
IS<br />
CURSOR grp_cur<br />
IS<br />
SELECT MAX (salary)<br />
FROM employee<br />
WHERE department_id = dept_id_in;<br />
return_value NUMBER;<br />
BEGIN<br />
OPEN grp_cur;<br />
FETCH grp_cur INTO return_value;<br />
CLOSE grp_cur;<br />
RETURN return_value;<br />
END;<br />
Jetzt kann ich sowohl total_salary als auch max_salary in meiner SELECT-Anweisung<br />
verwenden. Auf deutsch übersetzt würde diese etwa lauten: »Zeige mir den<br />
Namen und das Gehalt des Mitarbeiters, der in der jeweiligen Abteilung das höchste<br />
Gehalt hat, sowie das Gesamtgehalt aller Mitarbeiter in der Abteilung dieses Mitarbeiters«:<br />
SELECT E.department_id, last_name, salary, total_salary (department_id)<br />
FROM employee<br />
WHERE salary = max_salary (department_id);<br />
Vergleichen Sie dieses einfache und selbstdokumentierende Stück <strong>SQL</strong> mit der Version,<br />
die eine Sicht und eine korrelierte Subabfrage benötigt:<br />
CREATE VIEW dept_salary<br />
AS<br />
SELECT department_id, SUM (salary) total_salary<br />
FROM employee<br />
GROUP BY department_id;<br />
SELECT E.department_id, last_name, salary, total_salary<br />
FROM employee E, dept_salary DS<br />
WHERE E.department_id = DS.department_id<br />
AND salary = (SELECT MAX (salary)<br />
FROM employee E2<br />
WHERE E.department_id = E2.department_id);<br />
Ich bin sicher, Sie werden mir zustimmen, daß gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong> Ihr<br />
Leben einfacher machen können.<br />
Ihnen ist vielleicht aufgefallen, daß die Funktion total_salary <strong>aus</strong> dem letzten<br />
Abschnitt der Funktion max_salary <strong>aus</strong> diesem Abschnitt sehr ähnlich sieht. Der einzige<br />
Unterschied zwischen den beiden ist der, daß der Cursor in total_salary die<br />
599
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
Gruppierungsfunktion SUM und der Cursor in max_salary die Gruppierungsfunktion<br />
MAX verwendet. Wenn Sie immer fanatisch darauf <strong>aus</strong> sind, möglichst viel Ihres Codes<br />
in einer möglichst kleinen Anzahl unterschiedlicher »beweglicher Teile« zusammenzufassen,<br />
dann könnten Sie auch eine einzige Funktion dar<strong>aus</strong> machen, die abhängig <strong>von</strong><br />
einem zweiten Parameter, verschiedene Statistiken auf Gruppenebene zurückliefert:<br />
FUNCTION salary_stat<br />
(dept_id_in IN department.department_id%TYPE,<br />
stat_type_in IN VARCHAR2)<br />
RETURN NUMBER<br />
IS<br />
v_stat_type VARCHAR2(20) := UPPER (stat_type_in);<br />
CURSOR grp_cur<br />
IS<br />
SELECT SUM (salary) sumsal,<br />
MAX (salary) maxsal,<br />
MIN (salary) minsal,<br />
AVG (salary) avgsal,<br />
COUNT (DISTINCT salary) countsal,<br />
FROM employee<br />
WHERE department_id = dept_id_in;<br />
grp_rec grp_cur%ROWTYPE;<br />
retval NUMBER;<br />
BEGIN<br />
OPEN grp_cur;<br />
FETCH grp_cur INTO grp_rec;<br />
CLOSE grp_cur;<br />
IF v_stat_type = 'SUM'<br />
THEN<br />
retval := grp_rec.sumsal;<br />
ELSIF v_stat_type = 'MAX'<br />
THEN<br />
retval := grp_rec.maxsal;<br />
ELSIF v_stat_type = 'MIN'<br />
THEN<br />
retval := grp_rec.minsal;<br />
ELSIF v_stat_type = 'COUNT'<br />
THEN<br />
retval := grp_rec.countsal;<br />
ELSIF v_stat_type = 'AVG'<br />
THEN<br />
retval := grp_rec.avgsal;<br />
END IF;<br />
RETURN retval;<br />
END;<br />
600
Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />
Die zusätzlichen Kosten, die durch diese Ausdrücke in der SELECT-Liste entstehen –<br />
und die Verarbeitung der IF-Anweisung – sind vernachlässigbar. Mit diesem neuen,<br />
generischen Werkzeug sieht meine Gehaltsanalyse nun so <strong>aus</strong>:<br />
SELECT E.department_id, last_name, salary, salary_stat (department_id, 'sum')<br />
FROM employee<br />
WHERE salary = salary_stat (department_id, 'max');<br />
Wenn ich jemals den <strong>SQL</strong>-Code, mit dem man die Statistiken über Abteilungen erhält,<br />
ändern muß, dann muß ich nur diese eine Funktion anfassen.<br />
Ersetzen <strong>von</strong> DECODE durch IF-Anweisungen<br />
Die DECODE-Funktion stellt IF-ähnliche Möglichkeiten in der nichtprozeduralen <strong>SQL</strong>-<br />
Umgebung des Oracle Server bereit. Sie können die DECODE-Syntax verwenden, um<br />
Matrixberichte mit einer festen Anzahl <strong>von</strong> Spalten zu erzeugen, aber auch, um in einer<br />
Abfrage komplexe IF-THEN-ELSE-Logik einzubauen. Betrachten Sie das folgende Beispiel<br />
für DECODE, in dem bestimmt wird, ob ein Datum innerhalb eines vorgeschriebenen<br />
Bereichs liegt und das, wenn ja, die Anzahl der Zeilen, die diese Bedingung erfüllen,<br />
inkrementiert:<br />
SELECT FC.year_number,<br />
SUM (DECODE (GREATEST (ship_date, FC.q1_sdate),<br />
ship_date,<br />
DECODE (LEAST (ship_date, FC.q1_edate),<br />
ship_date, 1,<br />
0),<br />
0)) Q1_results,<br />
SUM (DECODE (GREATEST (ship_date, FC.q2_sdate),<br />
ship_date,<br />
DECODE (LEAST (ship_date, FC.q2_edate),<br />
ship_date, 1,<br />
0),<br />
0)) Q2_results,<br />
SUM (DECODE (GREATEST (ship_date, FC.q3_sdate),<br />
ship_date,<br />
DECODE (LEAST (ship_date, FC.q3_edate),<br />
ship_date, 1,<br />
0),<br />
0)) Q3_results,<br />
SUM (DECODE (GREATEST (ship_date, FC.q4_sdate),<br />
ship_date,<br />
DECODE (LEAST (ship_date, FC.q4_edate),<br />
ship_date, 1,<br />
0),<br />
0)) Q4_results<br />
FROM orders O,<br />
fiscal_calendar FC<br />
GROUP BY year_number;<br />
Die Ergebnismenge der Abfrage könnte so <strong>aus</strong>sehen:<br />
601
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
YEAR NUMBER Q1 RESULTS Q2 RESULTS Q3 RESULTS Q4 RESULTS<br />
------------ ---------- ---------- ---------- ----------<br />
1993 12000 14005 22000 40000<br />
1994 10000 15000 21000 55004<br />
Es ist zwar sehr nützlich, DECODE für solche Berichte zu verwenden, aber die dafür<br />
notwendige <strong>SQL</strong>-Anweisung ist doch etwas einschüchternd. Das DECODE für Q1<br />
RESULTS läßt sich etwa so übersetzen: » Wenn das Versanddatum größer oder gleich<br />
dem ersten Datum im ersten Quartal und kleiner oder gleich dem letzten Datum im<br />
ersten Quartal ist, dann addiere eins zur Summe aller Bestellungen, die in diesem Quartal<br />
<strong>aus</strong>geliefert worden sind. Ansonsten addiere 0.«<br />
Unglücklicherweise werden Sie die vertrackten <strong>SQL</strong>-Anweisungen nur mit viel Erfahrung<br />
mit DECODE entschlüsseln können. Auch die Wiederholungen im SELECT<br />
schreien geradezu nach einer Modularisierung. Diese erreichen wir mit der folgenden<br />
gespeicherten Funktion (incr_in_range steht für increment if in the range (inkrementieren,<br />
wenn im Bereich)):<br />
FUNCTION incr_in_range<br />
(ship_date_in IN DATE, sdate_in IN DATE, edate_in IN DATE)<br />
RETURN INTEGER<br />
IS<br />
BEGIN<br />
IF ship_date_in BETWEEN sdate_in AND edate_in<br />
THEN<br />
RETURN 1;<br />
ELSE<br />
RETURN 0;<br />
END IF;<br />
END;<br />
Ja, das ist wirklich schon alles! Mit der Funktion incr_in_range wird die lange und<br />
verworrene <strong>SQL</strong>-Anweisung zu einem einfachen:<br />
SELECT FC.year_number,<br />
SUM (incr_in_range (ship_date, q1_sdate, q1_edate)) Q1_results,<br />
SUM (incr_in_range (ship_date, q2_sdate, q2_edate)) Q2_results,<br />
SUM (incr_in_range (ship_date, q3_sdate, q3_edate)) Q3_results,<br />
SUM (incr_in_range (ship_date, q4_sdate, q4_edate)) Q4_results<br />
FROM orders O,<br />
fiscal_calendar FC<br />
GROUP BY year_number;<br />
Die gespeicherte Funktion beseitigt die Code-Redundanz und macht die <strong>SQL</strong>-Anweisung<br />
sehr viel lesbarer. Außerdem könnte diese Funktion auch noch in anderen <strong>SQL</strong>-<br />
Anweisungen verwendet werden, wo die gleiche Logik benötigt wird.<br />
Partielle Spaltenwerte mit GROUP BY<br />
Die GROUP BY-Kl<strong>aus</strong>el in einer SELECT-Anweisung ermöglicht es Ihnen, Daten über<br />
mehrere Datensätze hinweg zu sammeln und anhand einer oder mehrerer Spalten zu<br />
gruppieren. Die folgende SELECT-Anweisung berechnet beispielsweise den Gesamtbe-<br />
602
Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />
trag der Gehälter, die an alle Mitarbeiter mit jeweils dem gleichen Titel <strong>aus</strong>gezahlt werden:<br />
SELECT job_title_desc, SUM (salary)<br />
FROM employee E, job_title JT<br />
WHERE E.job_title_id = JT.job_title_id<br />
GROUP BY job_title_desc;<br />
Ein Auszug <strong>aus</strong> der Ergebnismenge dieser Abfrage könnte so <strong>aus</strong>sehen:<br />
ACCOUNT MANAGER 32000<br />
OFFICE CLERK 4000<br />
PROJECT MANAGER 10006<br />
SHIPPING CLERK 4200<br />
Was wäre aber, wenn Sie eine Gesamtsumme der Gehälter für jeden Typ oder jede<br />
Titelkategorie sehen wollten? Wieviel verdienen die verschiedenen Angestellten in der<br />
Firma? Und wie ist es mit den verschiedenen Managern und Geschäftsführern?<br />
Mit nativem <strong>SQL</strong> ist so etwas ziemlich schwierig zu schreiben. Was Sie eigentlich wollen,<br />
ist, mit GROUP BY die verschiedenen Kategorien zusammenzufassen, aber diese<br />
Kategorien gibt es nicht als separate Tabelle. Sie sind in die Titelbezeichnungen denormalisiert<br />
worden, die GROUP BY-Kl<strong>aus</strong>el kann aber nur auf alle vollständigen Spaltenwerte<br />
angewendet werden. Glücklicherweise bieten gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong><br />
eine naheliegende Lösung für dieses Problem. Die folgende Funktion job_category<br />
erwartet als ihren Parameter den Primärschlüssel der Titeltabelle und liefert die Jobkategorie<br />
zurück. Das geschieht mittels einer LIKE-Operation über den Text dieses Jobtitels<br />
– etwas, was man in einer GROUP BY-Kl<strong>aus</strong>el nicht direkt machen kann.<br />
FUNCTION job_category (title_id_in IN job_title.job_title_id%TYPE)<br />
RETURN VARCHAR2<br />
IS<br />
CURSOR title_cur IS<br />
SELECT job_title_desc FROM job_title<br />
WHERE job_title_id = title_id_in;<br />
title_rec title_cur%ROWTYPE;<br />
BEGIN<br />
OPEN title_cur; FETCH title_cur INTO title_rec;<br />
IF title_cur%NOTFOUND<br />
THEN<br />
CLOSE title_cur;<br />
RETURN NULL;<br />
ELSE<br />
CLOSE title_cur;<br />
IF title_rec.job_title_desc LIKE '%CLERK%'<br />
THEN<br />
RETURN 'CLERK';<br />
ELSIF title_rec.job_title_desc LIKE '%VICE PRESIDENT%'<br />
THEN<br />
RETURN 'VICE PRESIDENT';<br />
ELSIF title_rec.job_title_desc LIKE '%MANAGER%'<br />
THEN<br />
603
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
RETURN 'MANAGER';<br />
END IF;<br />
END IF;<br />
END;<br />
Jetzt kann ich meine Abfrage mit der Funktion job_category neu schreiben und all die<br />
Konfusionen mit meinen denormalisierten Stellenbezeichnungen vermeiden:<br />
SELECT job_category (job_title_id) as title, SUM (salary)<br />
FROM employee<br />
GROUP BY title;<br />
Ich verwende diese Funktion sowohl in der SELECT-Liste als auch in der GROUP BY-<br />
Kl<strong>aus</strong>el. Mit dieser Abfrage kann ich nun etwa folgendes Resultat bekommen:<br />
CLERK 8200<br />
MANAGER 42006<br />
VICE PRESIDENT 75000<br />
Sequentielle Verarbeitung eines Spaltenwerts<br />
Es ist sehr leicht, mit der Funktion INSTR her<strong>aus</strong>zufinden, ob ein bestimmtes Wort oder<br />
eine Menge <strong>von</strong> Zeichen in einem String steht. Es ist ebenfalls leicht, in <strong>SQL</strong> zu bestimmen,<br />
welche Zeilen eine Spalte haben, die ein bestimmtes Wort enthält. Aber es ist sehr<br />
viel schwieriger, nur mit <strong>SQL</strong> her<strong>aus</strong>zufinden, wie oft ein bestimmtes Wort oder eine<br />
Menge <strong>von</strong> Zeichen in einem String in einer einzigen Zeile auftaucht. Die eingebaute<br />
mengenbezogene Verarbeitung <strong>von</strong> <strong>SQL</strong> kennt kein iteratives oder Schleifenverhalten<br />
innerhalb einer Zeile – nur über Zeilen hinweg.<br />
Wenn ich eine iterative Analyse oder Berechnung auf einem bestimmten Wert in einer<br />
einzigen Zeile <strong>aus</strong>führen möchte, dann muß ich eine <strong>PL</strong>/<strong>SQL</strong>-Funktion verwenden.<br />
Schauen wir uns an, wie man das macht.<br />
Zunächst der <strong>SQL</strong>-Code, der mir alle Zeilen in der Tabelle text <strong>aus</strong>gibt, die das Wort<br />
»der« enthalten:<br />
SELECT text<br />
FROM notes<br />
WHERE INSTR (text, 'der') > 0;<br />
Ich kann sogar INSTR benutzen, um alle Zeilen her<strong>aus</strong>zufinden, die das Wort »der« mindestens<br />
dreimal enthalten:<br />
SELECT text<br />
FROM notes<br />
WHERE INSTR (text, 'der', 1, 3) > 0;<br />
Und bevor man mich schlägt, könnte ich auch alle Zeilen ermitteln, die das Wort »der«<br />
genau dreimal enthalten:<br />
SELECT text<br />
FROM notes<br />
604
Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />
WHERE INSTR (text, 'der', 1, 3) != 0<br />
AND INSTR (text, 'der', 1, 4) = 0;<br />
Wenn INSTR also einen <strong>von</strong> Null verschiedenen Wert für das dritte Auftreten <strong>von</strong> »der«,<br />
aber 0 für das vierte Auftreten zurückgibt, dann heißt das, daß der Teilstring genau dreimal<br />
im String vorkommt.<br />
Wenn ich aber einen Bericht benötige, der die Anzahl <strong>von</strong> »der«-Vorkommnissen in<br />
jeder meiner Notizen aufführt, oder wenn ich die Anzahl der Vorkommnisse <strong>von</strong> »der«<br />
mit GROUP BY zusammenfassen möchte, dann läßt <strong>SQL</strong> mich im Stich. Ich kann meinen<br />
Wunsch höchstens in Pseudocode <strong>aus</strong>drücken:<br />
SELECT anzahl-<strong>von</strong>-der-im-text, text<br />
FROM notes<br />
GROUP BY anzahl-<strong>von</strong>-der-im-text;<br />
Als nichtprozedurale Sprache, die mengenorientiert (und zwar in Bezug auf Mengen<br />
<strong>von</strong> Zeilen) ist, kennt <strong>SQL</strong> keine Möglichkeit, programmatisch den Inhalt einer<br />
bestimmten Spalte analysieren zu lassen. <strong>PL</strong>/<strong>SQL</strong> ist dagegen perfekt dafür <strong>aus</strong>gerüstet,<br />
um diese Art <strong>von</strong> Problemen zu lösen. Das Package ps.parse auf der Diskette zeigt,<br />
wie man eine Funktion schreibt, welche die Anzahl <strong>von</strong> Atomen (Wörter und/oder<br />
Begrenzungszeichen) in einem String ermittelt. Ich werde die Implementierung dieses<br />
Packages hier nicht wiederholen, aber Ihnen zumindest zeigen, wie man diese Funktion<br />
in <strong>SQL</strong> einsetzen könnte:<br />
• Anzeigen, wie oft »der« in jeder Zeile <strong>von</strong> text vorkommt:<br />
SELECT text,<br />
ps_parse.number_of_atomics (text, 'der')<br />
FROM notes;<br />
• Anzeigen, wie oft das Wort »dringend« über die Tage der Woche verteilt ist, aber<br />
nur die Texte berücksichtigen, in denen dieses Wort mindestens einmal auftaucht:<br />
SELECT TO_CHAR (note_date, 'DAY')day,<br />
SUM (ps_parse.number_of_atomics (text, 'dringend')) urgent_count<br />
FROM notes<br />
WHERE urgent_count > 0;<br />
GROUP BY TO_CHAR (note_date, 'DAY');<br />
Rekursive Verarbeitung in einer <strong>SQL</strong>-Anweisung<br />
<strong>SQL</strong> unterstützt keine Rekursion, auch wenn diese mächtige Programmiertechnik<br />
manchmal doch sehr wichtig für die Lösung eines Problems ist. <strong>PL</strong>/<strong>SQL</strong> erlaubt aber das<br />
rekursive Aufrufen <strong>von</strong> <strong>Funktionen</strong>, so daß Sie die rekursiven Aufrufe bei Bedarf in eine<br />
Funktion stecken können, wenn Sie in <strong>SQL</strong> Rekursion benötigen.<br />
Nehmen wir an, Sie schreiben eine Anwendung, die Schecks <strong>aus</strong>druckt. Ein Scheck enthält<br />
sowohl die numerische Betragsangabe (beispielsweise DM 99,70) als auch die <strong>aus</strong>geschriebene<br />
Version des Betrags (neunundneunzig Mark und siebzig Pfennige). Mit<br />
<strong>SQL</strong> können Sie den numerischen Scheckbetrag (der in der Datenbank steht) nicht in<br />
die <strong>aus</strong>geschriebene Version umsetzen. Mit <strong>PL</strong>/<strong>SQL</strong> geht das aber, auch wenn wir dafür<br />
605
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
ein bißchen tiefer in die Trickkiste greifen müssen. Dabei bekommen wir Rekursion,<br />
globale Daten und <strong>PL</strong>/<strong>SQL</strong>-Tabellen zu fassen und können damit eine sehr elegante<br />
Lösung finden.<br />
Das untenstehende Package checks ermöglicht genau diese Konvertierung. 2<br />
/* Dateiname auf der Begleitdiskette: checks.spp */<br />
PACKAGE checks<br />
IS<br />
/* Zahl in Worte konvertieren */<br />
FUNCTION num2words (number_in IN NUMBER) RETURN VARCHAR2;<br />
/* Das ist ein Package, ich muss den Reinheitsgrad zusichern */<br />
PRAGMA RESTRICT_REFERENCES (num2words, WNDS);<br />
END checks;<br />
PACKAGE BODY checks<br />
IS<br />
/* Tabellenstruktur, welche die Namen der numerischen Komponenten enthaelt. */<br />
TYPE numwords_tabtype IS TABLE OF VARCHAR2(20)<br />
INDEX BY BINARY_INTEGER;<br />
words_table numwords_tabtype;<br />
/* Wird im Initialisierungsabschnitt verwendet */<br />
v_date DATE;<br />
FUNCTION num2words (number_in IN NUMBER) RETURN VARCHAR2<br />
IS<br />
my_number NUMBER;<br />
BEGIN<br />
/* Sorry, aber wir kuemmern uns hier nicht um Pfennige! */<br />
my_number := FLOOR (number_in);<br />
/* $1000+ */<br />
IF my_number >= 1000<br />
THEN<br />
/* In zwei rekursive Aufrufe <strong>von</strong> num2words aufteilen */<br />
RETURN num2words (my_number/1000) ||<br />
' Thousand ' ||<br />
num2words (MOD (my_number, 1000));<br />
END IF;<br />
/* $100-999 */<br />
IF my_number >= 100<br />
THEN<br />
/* In zwei rekursive Aufrufe <strong>von</strong> num2words aufteilen */<br />
RETURN num2words (my_number/100) ||<br />
' Hundred ' ||<br />
2 Diese Implementierung <strong>von</strong> number-to-words wurde zuerst <strong>von</strong> Mike Burnside <strong>von</strong> Oracle Australia auf<br />
der International Oracle User's Week in San Francisco im September 1994 vorgestellt. Ich habe einige kleinere<br />
Änderungen vorgenommen.<br />
606
Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />
END IF;<br />
num2words (MOD (my_number, 100));<br />
/* $20-$99 */<br />
IF my_number >= 20<br />
THEN<br />
/* In das Zehnerwort und den letztlichen Dollarbetrag aufteilen */<br />
RETURN words_table (FLOOR (my_number/10)) ||<br />
' ' ||<br />
num2words (MOD (my_number, 10));<br />
END IF;<br />
/* Wir sind bei 19 oder weniger. Wort <strong>aus</strong> dem "oberen Register" der Tabelle<br />
holen */<br />
RETURN words_table (my_number + 10);<br />
END num2words;<br />
BEGIN<br />
/* Initialisierungsabschnitt des Packages, wird nur einmal pro Sitzung <strong>aus</strong>gefuehrt<br />
*/<br />
/* Manuell die Zehnernamen konstruieren */<br />
words_table (1) := 'Ten';<br />
words_table (2) := 'Twenty';<br />
words_table (3) := 'Thirty';<br />
words_table (4) := 'Forty';<br />
words_table (5) := 'Fifty';<br />
words_table (6) := 'Sixty';<br />
words_table (7) := 'Seventy';<br />
words_table (8) := 'Eighty';<br />
words_table (9) := 'Ninety';<br />
/* Bei 0 NULL zurueckgeben */<br />
words_table (10) := NULL;<br />
/* Zahlnamen <strong>von</strong> eins bis neunzehn erzeugen */<br />
FOR day_index IN 1 .. 19<br />
LOOP<br />
v_date := TO_DATE (to_char(day_index) || '-JAN-94');<br />
words_table (day_index+10) :=<br />
INITCAP (TO_CHAR (v_date, 'DDSP'));<br />
END LOOP;<br />
END checks;<br />
Hier einige Beispiele der Konvertierung <strong>von</strong> ganzen Zahlen in Worte:<br />
checks.num2words (99) ==> Ninety Nine<br />
checks.num2words (12345) ==> Twelve Thousand Three Hundred Forty Five<br />
checks.num2words (5) ==> Five<br />
607
Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />
Ich kann diese Package-Funktion auch in meiner <strong>SQL</strong>-Anweisung verwenden, um alle<br />
Informationen, die ich zum Drucken des Schecks benötige, <strong>aus</strong> der Datenbank zu<br />
holen:<br />
SELECT TO_CHAR (SYSDATE, 'Month DD, YYYY'),<br />
payee,<br />
amount,<br />
checks.num2words (amount),<br />
comment<br />
FROM bill<br />
WHERE bill_status = 'UNPAID'<br />
608