Oracle

Large Objects (LOB)
Pipelined Functions
Package DBMS_OBFUSCATION_TOOLKIT und MD5 Checksummen
Ist ein Datensatz schon vorhanden oder nicht
Unbekanntere Feinheiten beim LIKE Operator
Neuerungen in Oracle 9i oder 9iR2
Inserts können kompakt mit Record-Variablen geschrieben werden
PL/SQL Tables können assoziative Arrays sein
Merge
Temporäre Tabellen
Index organisierte Tabellen
Externe Tabellen
SQL*Loader und FILLER
Common Table Expressions mit WITH
Hierarchische Abfragen
Views im Data Dictionary
Quoting von Strings ohne umständliches Escapen
listagg in Oracle 11g R2
XML Verarbeitung in Oracle
Import von XML-Daten
XML bearbeitende Zeilenfunktionen

Large Objects (LOB)

Wenn es nicht gerade darum geht binär Dateien in der Datenbank zu speichern dann wird man in den meisten Fällen zu den Spalten Typen clob oder nclob greifen. Die sind für Inhalte geeignet, die in anderen Datenbanken als text gespeichert würden. Allerdings ist die Umsetzung in Oracle ein bisschen komplexer und erfordert deshalb vom Nutzer die Bereitschaft mit zu denken.

LOBs speichern nämlich typischerweise einen Zeiger in der LOB Spalte. Wenn dieser Zeiger nicht da ist, hat die Spalte den Wert NULL, also keinen oder einen undefinierten Wert. Wenn er aber da ist, kann er immer noch auf etwas Leeres zeigen, oder eben auf eine echte Bytesequenz mit Daten.

CREATE TABLE demo (
   id    NUMBER(10),
   lob   CLOB
);

INSERT INTO demo (id) VALUES (1);

Nach diesem insert ist die Spalte noch NULL, sowie man es bei jedem anderen Datentyp auch kennt und erwartet.

UPDATE demo SET lob = empty_clob() WHERE id = 1;

Jetzt gibt eine Abfrage mit WHERE lob is null keine Zeilen mehr zurück. Statt dessen können wir aber mit den gängigen String-Funktionen überprüfen, was in dem LOB gespeichert ist:

SELECT id, length(lob) as std_length, dbms_lob.getlength(lob) as other_length
  FROM demo WHERE id = 1;

In beiden Fällen wird die Länge 0 sein.

Unabhängig davon, ob die LOB Spalte jetzt NULL ist oder ein Zeiger auf einen leeren Bereich, kann man die Spalte einfach wie eine VARCHAR Spalte per UPDATE (oder auch per INSERT) mit einem Wert versehen. Etwas anders verhalten sich LOB Variablen in PL/SQL.

UPDATE demo SET lob = 'Text would also fit in a VARCHAR' where id = 1;

DECLARE
  locator  CLOB;
  content  VARCHAR2(100);
BEGIN
  INSERT INTO demo (id,lob) VALUES (2, empty_clob())
    RETURNING lob INTO locator;
  content := 'Das ist der Inhalt';
  -- Inhalt in den lob-Zeiger (Variable locator) schreiben
  dbms_lob.write( locator, length(content), 1, content);
  COMMIT;
END;

Wichtig ist dabei, zu verstehen, dass sich locator insofern von einer normalen PL/SQL Variablen unterscheidet, dass der Wert der Variablen locator sich nicht in der SGA mit allen anderen PL/SQL Variablen befindet (die Zuweisung passiert ja auch nicht mit dem Operator :=), sondern direkt im Tablespace, auf den diese Zeigervariable verweist (die Zuweisung mit der Funktion dbms_lob.write macht eben genau diesen Unterschied). Die Variable bzw. deren Inhalt muss also nicht mehr per UPDATE set lob = locator in die Tabelle gebracht werden, wohl aber braucht es ein COMMIT nach dem Aufruf der Funktion dbms_lob.write, denn die gehört noch zu der Transaktion, die mit der INSERT SQL-Anweisung begonnen wurde, dazu.

DECLARE
  lob  CLOB;
  text VARCHAR2(100) := 'Ein bisschen Text';
BEGIN
  dbms_lob.write(lob, length(text), 1, text);
END;

Error: Invalid LOB locator specified: ORA-22275

Da die Variable lob nicht durch eine Abfrage auf eine LOB Spalte ihren Wert bekommen hat (der auch ein leerer Zeiger sein könnte), ist sie noch nicht initialisiert. Das muss also separat in PL/SQL passieren und zwar vor dem dbms_lob.write Statement:

  dbms_lob.createtemporary(lob, true, dbms_lob.session);

Sobald eine LOB-Variable mit SQL-Semantik bearbeitet wird, also nicht mit dem dbms_lob Paket, wird von der Zeiger-Variable eine Kopie in der SGA erstellt. Und zwar nicht nur von der Zeiger-Variablen, sondern eben auch von dem Daten Block im Hauptspeicher auf den sie verweist.

DECLARE
  locator CLOB;
  v_lob   CLOB;
  text    VARCHAR2(100) := ' Ein bisschen Text anhängen';
BEGIN
  SELECT lob INTO locator FROM demo
   WHERE id = 1 FOR UPDATE OF lob;
  dbms_lob.writeappend(locator, length(text), 1, text);
  v_lob := upper(locator);    -- v_lob ist jetzt neue unabhängige Variable
  -- deshalb muss sie per Update in die Datenbank-Tabelle geschrieben werden
  UPDATE demo SET lob = v_lob WHERE id = 1;
END;

Pipelined Functions

Prozeduren oder Funktionen, die Daten, z.B in einer Schleife, berechnen, tun dies im Normalfall komplett, bevor sie das Ergebnis zurück liefern. Bekanntestes Beispiel dafür ist vielleicht dbms_output, dessen Ausgabe erst zu sehen ist, wenn die Prozedur komplett zu Ende ist. Das schränkt natürlich ihren Nutzen als Ablaufverfolgung beim Debugging erheblich ein. Wenn es große Datenmengen sind, die da durch PL/SQL durch geschleust werden, kann das auch dazu führen, dass es zu einer nicht unerheblichen Speicherbelastung kommt, da der Speicher, der im Verlauf der Prozedur genutzt wird, nur am Ende auf einen Schlag, nicht aber inkrementell frei gegeben werden kann.

Solche Wartezeiten sind auch nicht im Sinne einer parallelen Weiterverarbeitung der Daten. Wenn es eine Unmenge von Daten gibt, könnte die nächste Verarbeitungsstufe ja schon mal mit dem ersten Teil anfangen, anstatt warten zu müssen, bis auch der 25. und letzte Teil durch ist. Solche Modelle kennt man von der klassischen Unix Pipe in der Shell und konsequenterweise heißt dann auch die Lösung, mit der Oracle für seine Datenbank aufwartet, pipelined Functions oder Tabellenfunktionen.

Der zweite Namen, Tabellenfunktion, kommt daher, weil solche Funktionen immer dort einsetzbar sind, wo ansonsten eine View oder eine Tabelle selber stehen würde:

SELECT INTO other_table SELECT * FROM pipelined_function;

Das ist das, was PostgreSQL schon ziemlich lange mit seinen RETURN SETOF praktiziert. Eine derartige Funktion kann tatsächlich wie eine Tabelle oder wie eine View benutzt werden.

Während es bei PostgreSQL erforderlich ist, dass man einen Record als Basis-Rückgabe-Typ definiert, muss bei Oracle sogar eine Collection definiert werden. Außerdem darf es auch keine Index-by PL/SQL Tabelle sein, sondern es muss eine nested table sein. Aus dem gleichen Grund wie auch eine User-Defined PL/SQL Funktion keinen boolean Wert zurück geben darf, wenn sie direkt in einer SQL Anweisung verwendet werden soll, weil der Typ boolean als Spaltentyp nicht erlaubt ist und damit auch nicht in einer SQL-Abfrage vorkommen darf. Das gleiche gilt für eine Index-by PL/SQL Tabelle. Der Typ, um den die Collection gewickelt ist, kann entweder ein einfacher skalarer Typ sein (NUMBER, VARCHAR2) oder ein Objekt-Typ. Ein Record artiger Typ ist nicht möglich — zumindest nicht in Oracle 9i. In 11g scheint sich das etwas vereinfacht zu haben.

Die folgende Funktion baut in Oracle nach, was PostgreSQL mit der built-in Funktion generate_series schon von Hause aus mit bringt: einen Generator von Zahlen:

CREATE TYPE numset_t IS TABLE OF NUMBER;

CREATE OR REPLACE
FUNCTION generate_range(p_max IN NUMBER)
  RETURN numset_t
  PIPELINED
IS
  i NUMBER(10) := 0;
BEGIN
  LOOP
     i := i + 1;
     EXIT WHEN i > p_max;
     PIPE ROW(i);
  END LOOP;

  RETURN;
END;

Das obige Beispiel, das zeigt, wie eine pipelined Funktion schlicht und einfach an Stelle eines Views verwendet werden kann, funktioniert allerdings nur im PL/SQL Kontext (erkennbar am SELECT INTO). Wenn die Funktion innerhalb von SQL verwendet werden soll, muss das Set, das sie in die PL/SQL Variable schreibt, in eine n x m ResultSet Struktur entpackt werden. Für so was ist die CAST-Funktion TABLE( .. ) zuständig.

SELECT * FROM TABLE(pipelined_function);
-- oder mit der eben angelegten Funktion und mehr Praxisbezug:
SELECT sysdate + column_value AS next_week FROM table(generate_range(7));

Ohne pipelined Funktionen, die es seit 9i gibt, kann man das mit der gleichen Funktionssignatur implementieren. Allerdings muss die Collection-Table innerhalb des Funktionsrumpfes komplett aufgebaut werden und als ganzes in einem return Statement zurück gegeben werden. Das ist zwar näher an dem, was man von der allgemeinen prozeduralen Programmierung kennt, und deshalb einfacher zu verstehen, aber die Vorteile der eventuellen parallelen Verarbeitung und des geringeren Speicherverbrauchs sind weg.

CREATE OR REPLACE
FUNCTION generate_range(p_max IN NUMBER)
  RETURN numset_t
IS
  INIT_SIZE CONSTANT NUMBER(10) := 1;
  i NUMBER(10) := INIT_SIZE + 1;
  series numset_t;
BEGIN
  -- Initialize the collection with the constructor (and INIT_SIZE elements)
  series := numset_t(1);

  IF p_max > INIT_SIZE
  THEN
     -- Extend the nested table to the requested size
     series.EXTEND(p_max - INIT_SIZE);
     -- ... and fill the additional elements with sequential numbers in a loop
     WHILE i <= p_max
     LOOP
        series(i) := i;
        i := i + 1;
     END LOOP;
  END IF;

  RETURN series;
END;

Das next week Anwendungsbeispiel sollte als Inspiration ausreichen, um den Nutzen solch einer series pipelined Funktion zu sehen. Sie kann immer da verwendet werden, wo man es bei der Pseudo-Tabelle dual bedauert hat, dass sie nur eine Zeile als Result Set hat und nicht mehrere. Ich habe schon häufiger Datenbanken gesehen in der es eine super_dual Tabelle gab, die mit 10 oder 100 sequenziellen Zahlen befüllt war.

Bei allem Spot über super_dual Tabellen sollte man sich aber doch manchmal auch an die eigene Nase fassen. Wer sich ein bisschen eingehender mit den Oracle Erweiterungen befasst hat, der kennt die CONNECT BY Klausel mit der sich ohne den PL/SQL Umweg direkt (und performant) in SQL ein Range erzeugen lässt:

SELECT level AS num_in_range FROM dual CONNECT BY level <= 10;

Seit Oracle 11g R2 kann man das auch durch eine rekursive Common Table Expression erreichen. Rekursive With Klauseln oder CTEs brauchen zwingend eine Vorab-Deklaration der Spalten, da der Parser die Spalten wohl nicht aus dem rekursiven UNION ALL Sub-Select herauslesen kann (was übrigens bei allen anderen Datenbanken, die rekursive CTEs unterstützen auch so ist). Im nachfolgenden Beispiel ist das numbers(n). Und das stellt einen kombinierten Tabellen und Spalten Alias da, mit numbers als Tabellenname und n als Spaltenname. Aber solche kombinierten Aliase wurden von Oracle vor 11g nicht unterstützt:

WITH numbers(n) AS
(
 SELECT 1 FROM dual
 UNION ALL
 SELECT n + 1 FROM numbers WHERE n <= 10
)
SELECT * FROM numbers;

Der beschränkte Einsatz von Record Types in den pipelined Functions ist aber durchaus zu verschmerzen, denn die Deklaration eines Object-Typs unterscheidet sich nicht sonderlich von der eines Record-artigen. Eine Record artige Variable muss lediglich initialisiert werden, während für die Objekt-Variable eine Instanzierung zwingend notwendig ist.

CREATE TYPE MyType AS OBJECT
(
  num_var  NUMBER(10),
  date_var DATE,
  text_var VARCHAR2(30)
);
CREATE TYPE MyType_set_t AS TABLE OF MyType;

BEGIN
  my_data MyType := MyType(NULL,NULL,NULL);
END;
/

Sinnvoll nutzbar ist so was zum Beispiel beim Einbinden einer where in Klausel, ohne dafür auf dynamisches SQL zurückgreifen zu müssen, wenn ich nicht im voraus weiß, wie die Elemente in meiner IN Liste heißen, bzw. wie viele es sind: IN (:A, :B, :C) gingen zwar schon, aber flexibel ist das nicht, denn weder den Fall einer Liste mit 2 Elementen, noch den einer mit 4 Elementen kann ich damit abbilden:

Unser Ziel ist:

SELECT t.* FROM the_table t
            INNER JOIN table(string_to_tab(csv_liste)) tf ON t.col_a = tf.column_value;


SELECT t.* FROM the_table
 WHERE t.col_a IN (select column_value from table(string_to_tab(csv_liste)))

Eine Anmerkung ist aber noch wichtig: Pipelined Functions oder generell Table Functions finden zwar im Kontext einer SQL-Abfrage statt (man kann ja von ihnen Zeilen selektieren wie von einer Tabelle oder von einer View), aber deshalb ist sie nicht Read-Commited so wie das etwa eine umfangreiche Report Abfrage mit etlichen Sub-Selects wäre. Innerhalb der Funktion können ja durchaus Kontext-Wechsel zischen SQL Engine und PL/SQL Engine auftreten.

Die angesprochene umfangreiche Select-Abfrage über 20 oder 30 Zeilen findet man aber gar nicht so oft. Typischer ist eine Report-Erstellung, die am Anfang einen 15 Zeilen Cursor ausführt, um alle relevanten Datensätze, die für den Report gebraucht werden, zu selektieren. Diese Abfrage des Cursors, ist allerdings Read-Committed, was bedeutet, dass auch wenn die Verarbeitung eines Cursor-Datensatzes 5 Sekunden dauert, die 24.selektierte Zeile dieses Cursors immer noch so aussieht, wie sie vor 120 Sekunden beim Öffnen des Cursors ausgesehen hätte.

Das Problem dabei ist nur, dass in den PL/SQL Statements zwischen den einzelnen Fetches vom Cursor Loop unter Umständen auch weitere Abfragen an die Datenbank stattfinden können. Diese Abfragen sind nicht im gleichen Kontext, wie die ursprüngliche Abfrage und daher aus dem "Read Committed" Kontext ausgeschlossen. Wenn aus der Tabelle, auf die der Cursor zugreift, — zwischen dem 2, und 3, Fetch — noch mal gelesen wird, kann das durchaus ein anderes ResultSet ergeben als kalkuliert, da just in dieser Zwischenzeit eine andere Session die 4 oder 5 Zeilen gelöscht haben könnte.

Package DBMS_OBFUSCATION_TOOLKIT und MD5 Checksummen

Das Paket DBMS_OBFUSCATION_TOOLKIT, das seit Oracle 8i unterstützt wird, bietet eine Funktion MD5, die einen String als Parameter erwartet und ihr Ergebnis auch als String zurück gibt. Also versucht man sich an dem naheliegenden:

SELECT dbms_obfuscation_toolkit.md5('123456') FROM dual;

Aber das bricht mit einem ORA-06553: PLS-307: too many declarations of 'MD5' match this call ab. Die typische Kontrolle mit DESC dbms_obfuscation_toolkit zeigt genau die erwartete Funktion:

FUNCTION MD5 RETURNS VARCHAR2(16)
 Argument Name                  Type                    In/Out
 ------------------------------ ----------------------- ------
 INPUT_STRING                   VARCHAR2                IN

FUNCTION MD5 RETURNS RAW(16)
 Argument Name                  Type                    In/Out
 ------------------------------ ----------------------- ------
 INPUT                          RAW                     IN

Etwaige Cast-Versuche, um Oracle von einem bestimmten Parameter-Typen zu überzeugen, helfen auch nicht weiter. Die einzige Möglichkeit, um die Exception zu umgehen ist, die Funktion mit benannten Parametern aufzurufen. Aber damit steckt man dann in der Catch-22 Situation, denn Funktionsaufrufe mit benannten Parametern sind direkt in SQL nicht erlaubt (mit Version 11g hat sich das geändert).

Da die MD5-Checksumme aber typischerweise 7-bit ASCII clean sein soll, wird man den Wert, den dbms_obfuscation_toolkit.md5 zurück gibt, ohnehin nach bearbeiten. rawtohex gibt den hexadezimalen String mit Großbuchstaben zurück, was meistens nicht so gewollt ist und nochmal einen weiteren Wrapper mit lower erfordert.

DECLARE
  v_result VARCHAR2(255);
BEGIN
  v_result := dbms_obfuscation_toolkit.md5(input_string => :v_input);
  dbms_output.put_line('MD5-Sum: ' || v_result);
  v_result := rawtohex( utl_raw.cast_to_raw(v_result) );
  dbms_output.put_line('MD5-Sum: ' || v_result);
END;

Ist ein Datensatz schon vorhanden oder nicht

Wenn die Fragestellung ist, ob ein bestimmter Datensatz in einer Tabelle schon existiert, ist der erste Ansatz in PL/SQL meistens:

  SELECT count(*) INTO has_rows FROM the_table
   WHERE column_a = 34 AND column_b = v_attribute_b;
  IF has_rows > 0
  THEN
     -- code if the data already exists
  ELSE
     -- other code
  END IF;

SELECT count(*) ist eine dankbare Abfrage, weil es weder eine NO_DATA_FOUND noch eine TOO_MANY_ROWS Exception verursachen kann und außerdem auch im Gegensatz zu sum(1) nie NULL als Wert zurück gibt.

Das Problem dabei ist aber, dass die PL/SQL Variable in die selektiert wird, zwar has_rows heisst und damit ihren logischen Zweck beschreibt, aber die SQL-Abfrage nicht dasselbe logische Ergebnis liefert, sondern die Anzahl der Zeilen, die auf die Bedingung passen, also count_of_rows. Bei einer umfangreichen Tabelle bedeutet das, dass die Datenbank nach dem ersten passenden Datensatz die Tabelle noch weiter durchlaufen wird, um nach weiteren passenden Zeilen zu suchen, die alle von der Aggregats-Funktion count(*) verarbeitet werden müssen. Die Verarbeitung in dieser Aggregats-Funktion ist zwar denkbar einfach und verursacht keine nennenswerte CPU Last, der Umstand, dass die Tabelle aber weiter durchlaufen werden muss, ohne dass sich am Ergebnis etwas ändert, kostet aber sehr wohl Ressourcen.

Der alternative, wenn auch etwas schreibaufwendigere Ansatz ist, einen expliziten Cursor zu verwenden. Auch der vermeidet das zusätzliche Exception Handling für NO_DATA_FOUND und TOO_MANY_ROWS:

  DECLARE
    CURSOR cur(p_attribute VARCHAR2) IS SELECT 1 FROM the_table
                   WHERE column_a = 34 AND column_b = p_attribute;
    has_rows  NUMBER;
  BEGIN
     OPEN cur('a_string_value');
     FETCH cur INTO has_rows;
     CLOSE cur;
     IF has_rows = 1
     THEN
        ...
     END IF;
  END;

Der Cursor wird geöffnet (und dabei das SQL Statement ausgeführt) und dann wird der erste Datensatz in die Variable has_rows kopiert. Das war's.

Bei umfangreichen Tabellen ist das sicherlich eine effizientere Methode, aber die beste Lösung ist, die anfängliche Fragestellung in adäquates SQL umzusetzen. Denn es ist einfach nicht der richtige Weg, eine Prüfung auf Existenz über die Anweisung count(*) durchzuführen. Dafür gibt es das etwas umständlich anmutende, aber effiziente EXISTS Konstrukt:

  SELECT 1 INTO has_rows FROM dual
   WHERE EXISTS (select 1 from the_table
                  where column_a = 34 AND column_b = v_attribute_b);

Üblicherweise steht die Frage, ob ein Datensatz schon vorhanden ist, nicht einfach nur so im Raum und will auch nicht nur rein theoretisch beantwortet werden. Sondern sie steht in einem bestimmten Kontext und der ist eben oft genug die Frage, ob ich, um meine Daten in die Tabelle zu bringen, einen vorhandenen Datensatz (mit dem gleichen Schlüssel) updaten muss oder ob ich einen neuen Datensatz einfügen muss. Und da ist count(*) allemal, aber auch EXISTS der falsche Ansatz, denn in diesem Fall brauche ich die Abfrage atomar zusammen mit meiner Änderungsanweisung. In einer Datenbank, als klassischem Multiuser Environment, muss ich immer damit rechnen, dass etwas anderes parallel die Daten ändert: Ein "Datensatz mit diesem Schlüssel existiert nicht" kann in der nächsten hunderstel Sekunde schon falsch sein, da gerade eben jemand anderes solch einen Datensatz eingefügt hat. Von daher ist es sinnvoll, einfach mal ein UPDATE abzusetzen und auf die SQL%ROWCOUNT zu achten, die die Information enthält, ob Zeilen vom UPDATE betroffen waren, oder nicht. Falls ja, ist nichts mehr zu tun. Falls nein, muss eben noch ein INSERT (leider auch kein atomares) hinterher geschoben werden. Umgekehrt, sich erst am INSERT zu probieren und bei einer Dublicate Key Exception, doch noch ein UPDATE zu versuchen, ist natürlich auch möglich und je nach Datenlage günstiger, weil vielleicht (zu Beginn) die Wahrscheinlichkeit auf doppelte Schlüssel zu treffen, sehr gering ist.

Unbekanntere Feinheiten beim LIKE Operator

Im Gegensatz zu PostgreSQL kennt Oracle kein ILIKE zur case-insensitiven Suche, da muss man sich mit einem zusätzlichen upper oder lower, das um den Feldnamen gewickelt wird behelfen. Anders sieht das beim mit 9i neuen Vergleichsoperator mit regulären Ausdrücken regexp_like aus, der akzeptiert einen dritten optionalen Parameter 'i' für case-insensitive Vergleiche:

  ... WHERE regexp_like(col_name,'^H[a-e]','i')

Allein vom Hinschauen sollte man schon erkennen, dass auf so ein komplex anmutendes Konstrukt kein Index angewandt werden kann, wie das ja beim normalen LIKE möglich ist, wenn der gesuchte Ausdruck nicht mit einer Wildcard beginnt. Ich bin mir ziemlich sicher, dass die neueren Funktions basierten Indizes von Oracle bei den regulären Ausdrücken auch an ihre Grenzen kommen.

Was die Wildcards anbetrifft, muss man übrigens ziemlich lange suchen, bis man mal herausfindet, wie nach einen Wildcard-Zeichen selber zu suchen ist. Ich habe jedenfalls schon etliche Oracle-Bücher in der Hand gehabt, in denen dazu nichts zu finden war. Meistens muss man schon froh sein, wenn wenigstens noch erwähnt wird, dass auch der Unterstrich als Wildcard fungiert. Die typische Suche nach einem Prefix im Data-Dictionary macht nämlich nicht immer, was sie soll:

  SELECT * FROM cat WHERE table_name like 'V_%';

Der Unterstrich wird nämlich nicht als Teil vom Prefix angesehen, sondern es wird alles ausgegeben, was mit V anfängt.

Nachdem man die üblichen Verdächtigen wie Backslash oder — analog zum verdoppelten Quote um einfache Quotes in literalen Strings unterzubringen — ein verdoppeltes Prozentzeichen erfolglos probiert hat, ist man versucht aufzugeben. Richtig, wenn auch sehr umständlich, ist das Escape-Zeichen in einer eigenen Klausel explizit mit anzugeben:

  WHERE col_name LIKE 'C%\%' ESCAPE '\'

Neuerungen in Oracle 9i oder 9iR2

Inserts können kompakt mit Record-Variablen geschrieben werden

Der typische Anfänger wird seine INSERTs schreiben, wie es einigermaßen naheliegend ist:

INSERT INTO the_table (col_A, col_b, col_C) VALUES (23,'text',sysdate);

Das INSERT INTO ... SELECT x, y, ... FROM some_table ist am Anfang einfach zu befremdlich für jemanden, der aus der prozeduralen Programmierung kommt. Für die Profis oder Maniacs hingegen — wie auch immer man das sehen will — ist das gegen die reine Lehre und sie bauen auch die literalen Werte in ein SELECT FROM ein (es soll ja schließlich Mengen artig aussehen):

  INSERT INTO the_table (col_A, col_b, col_C)
   SELECT 23,'text',sysdate FROM dual
   UNION ALL
   SELECT 24,'oder',sysdate -1/20 FROM dual;

Die Variante mit den Record-Variablen lehnt sich eher an den 1.Ansatz an, ist aber im Gegensatz zu den beiden anderen Methoden nur in PL/SQL nutzbar, da reines SQL nichts von Records weiß. Irgendwo her muss die Record Variable ja auch mit Werten befüllt werden und da braucht es nun meistens doch eine sehr herkömmliche PL/SQL Zuweisung.

DECLARE
  rec_row  the_table%ROWTYPE;
BEGIN
  rec_row.col_A := 21;
  rec_row.col_B := 'Some Text';
  rec_row.col_c := sysdate;
  INSERT INTO the_table VALUES rec_row;
-- or
  UPDATE dept SET ROW = rec_row WHERE deptno = 30;
END;

PL/SQL Tables können assoziative Arrays sein

Die Einschränkung, dass eine integer oder zählbare Variable als Index für die PL/SQL Tabellen herhalten muss, ist mit 9iR2 weggefallen. Der Inhalt einer PL/SQL Tabelle konnte ja schon immer alles sein (VARCHAR, NUMBER, DATE oder ein RECORD Typ). Dadurch, dass strings als Index genutzt werden können, sind sie auch wie Perl's Hash Implementierungen nutzbar. Mit dict.FIRST und dict.NEXT('stringIndex') kann die dünn besetzte Tabelle in einer While-Schleife immer noch durchlaufen werden. Nicht mehr aber in einer FOR-Schleife, denn der Index ist ja nicht mehr abzählbar.

FOR i IN index_by.FIRST .. index_by.LAST
LOOP
 ...
END LOOP;

Diese Art von For-Schleife war auch früher schon sehr heikel, denn bei einem dünn besetzten Array gibt es keine Garantie, dass nach dem Index 4, der Index 5 auch tatsächlich vorhanden ist.

Merge

MERGE ist Oracle's Analogon zu REPLACE in MySQL. Allerdings ist merge sehr viel komplexer, aber auch flexibler als replace. In MySQL wird einfach das Schlüsselwort INSERT durch REPLACE ersetzt, was dazu führt, dass alle Zeilen eingefügt werden, bis auf die, die schon vorhanden sind. Die entscheidende Frage ist nun: Was ist das Kriterium dafür, ob eine Zeile vorhanden ist oder nicht. Die Antwort von MySQL darauf ist schlicht und einfach: Wenn die Schlüsselspalten gleich sind. Die Dokumentation schreibt dazu:

  1. Try to insert the new row into the table
  2. While the insertion fails because a duplicate-key error occurs for a primary or unique key

Oracle ist da flexibler, denn es lässt den Anwender selber bestimmen, in welchen Spalten die auf Gleichheit zu testenden Zeilen übereinstimmen sollen. Dabei wird die von Joins bekannte Syntax "ON (a.col = b.col)" verwendet (und wie bei den Joins müssen das natürlich nicht zwei gleichnamige Spalten sein und es gibt auch keine Bedingung, dass sie zum Unique Key gehören müssen):

MERGE INTO emp e
USING emp_dept20 v
   ON (e.empno = v.empno)
 WHEN MATCHED THEN update
    set e.sal = v.sal
 WHEN NOT MATCHED THEN insert
   (empno, ename, mgr, job, salury, deptno)
 VALUES (v.empno, v.ename, v.mgr, b.job, v.sal, 20);

Interessanterweise kennt auch Microsofts MS-SQL eine merge Anweisung und deren Syntax ist nahezu identisch mit der von Oracle. MATCHED hat anscheinend eine Sonderstellung, denn in einer normalen Filter-Klausel müssten die zwei mit OR verknüpften Filter-Bedingungen in Klammern eingeschlossen werden.

MERGE merge_table AS m
USING merge_delta AS d
   ON (m.ID = d.ID)
 WHEN MATCHED AND m.field1 <> d.field1 OR m.field2 <> d.field2
   THEN UPDATE SET m.field1 = d.field1, m.field2 = d.field2
 WHEN NOT MATCHED
   THEN INSERT (ID, field1, field2)
          VALUES (d.ID, d.field1, d.field2);

Temporäre Tabellen

Temporäre Tabellen sind bei Oracle globale Objekte. Wenn in einer anderen Session eine temporary table mit gleichem Namen angelegt wird, dann wird dieser Versuch fehlschlagen. Lediglich die Inhalte der temporären Tabellen sind spezifisch für den jeweiligen User bzw. bei Verwendung von DELETE ROWS sogar nur in der aktuellen Transaktion sichtbar.

Diese Auftrennung zwischen Container (table) und Inhalt (rows) mutet etwas merkwürdig an, weil User A etwas anderes in der Tabelle sieht als User B. Auf den zweiten Blick kann man darin aber auch die konsequente Fortsetzung des Isolationsprinzip bei Transaktionen sehen. Dabei sieht ja User A auch nichts von den Änderungen, die User B vorgenommen hat, solange dessen Änderungen nicht per COMMIT festgeschrieben werden.

Ein Vergleich mit einer verschachtelten Transaktion ist dabei hilfreich. Verschachtelte Transaktionen findet man ja fast nur beim MS SQL-Server, fast alle anderen Datenbank Engines halten sie für Teufelszeug. Oracle auch. Das Verhalten der temporären Tabellen bei Oracle lässt sich ganz gut mit zwei verschachtelten Transaktionen erklären:

  Session
  +-------- hidden outer transaction only for temporary tables
  |
  |  +------ Inner normal transaction for all tables
  |  |
  :  :
  |  |
  |  +----- COMMIT OR ROLLBACK
  |
  |
  |  :  other transactions, statements, etc
  |  :
  |
  +----- End of Session with silent outer rollback

Die Transaktion ist erst dann komplett wenn beide angefangenen Transaktionen mit COMMIT enden. Die äußere für die temporären Tabellen mit PRESERVE ROWS endet mit der Session, aber in einem ROLLBACK und damit sind die Daten dann weg.

Bei DELETE ROWS ist die Verschachtelung umgekehrt. Die versteckte für die temporäre Tabelle liegt innerhalb einer normalen Transaktionsklammer und wird unmittelbar vor dem Beenden der normalen Transaktion auch mit beendet, aber mit einem ROLLBACK. Und damit sind die Daten verschwunden. Für alle, auch für die in der Session. Naja, und für die außerhalb der Session waren sie ja noch nicht (richtig) da, also verschwinden sie nicht, sondern "verbleiben im nichts".

Dieses Verhalten passt auch zu der Philosophie von Oracle im prozeduralen Kontext (also in anonymen PL/SQL Blöcken oder in Prozeduren) keine DDL Anweisungen zu erlauben. "Mal schnell eine temporäre Tabelle anlegen, weil ich die vier oder 5 Anweisungen später wieder brauche" wird von Oracle nicht gefördert. Dafür sind PL/SQL Tables vorgesehen, die ja auch Records als Elemente haben können und ihre Inhalte per FORALL bulk Binding in herkömmliche Tabellen schreiben können.

CREATE GLOBAL TEMPORARY TABLE temp_copy
  ON COMMIT PRESERVE ROWS
AS
 SELECT * FROM another_table WHERE 1 = 0;

CREATE GLOBAL TEMPORARY TABLE temp_explicit
(
  x       INT,
  a_date  DATE
)
 ON COMMIT DELETE ROWS;

https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:48812348054

Was passiert, wenn die temporäre Tabelle mit einem nicht leeren SELECT angelegt wird? Bei DELETE ROWS wären die Inhalte ja eigentlich sofort wieder weg, da eine DDL Anweisung (was CREATE TEMPORARY TABLE ja ist) bei Oracle bekanntlich in ein implizites COMMIT davor und danach eingebettet ist.

Und genau so ist es. Eine nicht-leere temporäre Tabelle anzulegen, ist also nur mit der Option PRESERVE ROWS sinnvoll.

Gelöscht wird eine temporäre Tabelle ohne die zusätzlichen Attribute GLOBAL und TEMPORARY:

DROP TABLE temp_copy;

Index organisierte Tabellen

Sinnvoll sind solche Tabellen für klassische Lookup-Tabellen, bei denen eine numerische Id oder eine Abkürzung als Schlüssel für eine (längere) Beschreibung genutzt wird. Den unique Index für die Id Spalte braucht es ohnehin. Da aber beim Lesen der Zeile (und das wird immer die vorrangige Nutzung eine Lookup Tabelle sein), erst der Index (von der Platte) gelesen und dann mit einem zweiten (gezielten) Zugriff der jeweilige Block mit den Tabellendaten ausgelesen wird, ist der zweite Zugriff, wenn auch gegenüber einem Full Table Scan weitaus effizienter, etwas abstrus, denn aus dem 4 oder 8 kByte Block werden lediglich 100 oder 200 Zeichen ausgelesen. Eine Index organisierte Tabelle macht nun das naheliegende, indem sie diese geringen Nutzdaten gleich im Speichersegment des Index mit ablegt und sich damit den zweiten Festplatten Zugriff auf den Block mit den Tabellen-Daten spart.

CREATE TABLE salutation
(
    salutation_id VARCHAR2(4),
    description   VARCHAR2(25 char),
  CONSTRAINT pk_salutation PRIMARY KEY (salutation_id)
)
ORGANIZATION INDEX;

Beim Anlegen der Tabelle ist da an der Syntax von CREATE TABLE nicht viel zu ändern. Es muss lediglich ein ORGANIZATION INDEX angehängt werden.

Externe Tabellen

External tables sind ein weiterer Tabellen-Typ, der wie eine normale Tabelle spezifiziert wird, aber dann ebenso wie die Index-Tabellen noch eine ORGANIZATION Klausel dazu bekommt. Bei den externen Tabellen ist die allerdings ein bisschen länger als bei den Index organisierten Tabellen. Das ist auch einleuchtend, denn in dieser zusätzlichen Klausel muss all das rein, was ansonsten im SQL*Loader Control-File steht.

Da ein Zugriff auf das Dateisystem statt findet, muss ein DIRECTORY Objekt, das für den Betriebssystem-Pfad mit den Dateien definiert ist, zwingend vorhanden sein. Und damit ist auch klar, dass es externe Tabellen nicht vor Oracle 9i gegeben haben kann, denn mit dieser Version wurden die Directories eingeführt.

CREATE TABLE ext_emp
(
    employee_id   NUMBER(10),
    last_name     VARCHAR2(60),
    first_name    VARCHAR2(60),
    hire_date     DATE
)
ORGANIZATION EXTERNAL (
   TYPE ORACLE_LOADER
   DEFAULT DIRECTORY ext_dir
   ACCESS PARAMETERS
   (
      -- Need UTF-8, but comments must be first among access parameters
      -- hence it can't be placed directly to the `characterset` line
      RECORDS delimited by newline
      characterset AL32UTF8
      badfile ext_dir: 'employee.bad'
      logfile ext_dir: 'employee.log'
      skip 1
      FIELDS terminated by ';'
      missing field values are null
      (
         employee_id   INTEGER EXTERNAL,
         last_name     CHAR,
         middle_name   CHAR,
         first_name    CHAR,
         hire_date     CHAR date_format DATE mask "yyyy-mm-dd"
      )
   )
   LOCATION ('employee.txt')
) REJECT LIMIT 40;

ALTER TABLE ext_emp REJECT LIMIT UNLIMITED;

Die "missing field values" Klausel gehört schon zur Field-List Spezifikation und muss daher direkt vor der Liste mit Feldern in der Datei kommen. Die Liste der Felder selber (in Klammer) kann dagegen weggelassen werden, wenn die Felder in der Datei mit den vorher angegebenen Tabellen-Spalten übereinstimmen. In diesem Fall hier würde man sich aber auf sehr heiklem Boden bewegen, weil man dann annehmen würde, dass Oracle schon das richtige Datums-Format verwenden wird. Die Regel ist also, sobald man eine Datumsspalte in der Datei mit dabei hat, muss man die Felder explizit mit angeben, und zwar alle, auch wenn nur eins von 20 Feldern ein Datum ist. Die Typ-Angaben kann man sich dagegen bei den 19 nicht-Datums-Feldern sparen, denn da errät Oracle den Typ und das Format immer richtig. Aber der Name des Feldes muss auf jeden Fall angegeben werden. Durch eine Änderung der Reihenfolge der Felder in der ACCESS PARAMETERS Klausel kann nämlich der Import auf eine andere Spaltenreihenfolge in der Datei angepasst werden. Und nicht nur die Reihenfolge kann angepasst werden, sondern es ist ein generelles Mapping zwischen Source und Destination möglich, bei dem auch bestimmte Felder übersprungen werden können. Das FILLER Attribut, das man von SQL*Loader her kennt, ist dabei nicht notwendig und auch gar nicht erlaubt.

Etwas unschön ist, dass es zwar auch ein ALTER TABLE für externe Tabellen gibt, aber das naheliegende, dass man dadurch nämlich die Datei, die "importiert" werden soll, auf eine andere umbiegen kann, funktioniert so nicht. Dafür muss die externe Tabelle neu angelegt werden. Wichtig ist auch zu verstehen, dass der Import, also der SQL*Loader Aufruf bei jedem lesenden Zugriff auf die externe Tabelle abläuft. Man wird also relativ schnell eine ganze Menge an Logfile Einträgen haben. Es sei denn man benutzt die Option NOLOGFILE anstelle der LOGFILE Klausel. Dann hängt man aber bei etwaigen Import-Fehlern ziemlich in der Luft.

Allerdings braucht man ein ALTER TABLE auch nicht unbedingt, um die externe Tabelle auf eine andere Daten-Datei umzubiegen. Die Vorgehensweise ist umgekehrt: Man schiebt der externen Tabelle eine andere Datendatei unter. Und das muss an noch nicht einmal mit Betriebsystem Befehlen bewerkstelligen, sondern man kann dafür die utl_file.fcopy oder utl_file.frename Funktionen nutzen. Da die externen Tabellen von Oracle als read-only behandelt werden, gibt es da auch meistens keine Locks oder andere Sperren, die ein Überschreiben der bisherigen Datei verhindern würden.

SQL*Loader und FILLER

SQL*Loader kennt seit 8i sogenannte FILLER Felder, die inbesondere bei csv artigen Daten hilfreich sind. Sie erlauben es nämlich Felder zu spezifizieren und gleichzeitig beim Insert in die Datenbank-Tabelle zu überspringen. Bei Daten mit fester Breite pro Feld ist das keine Verbesserung, denn da konnte man ja einfach das nächste relevanter Feld bei Zeichen 23 statt bei Zeichen 16 anfangen lassen und hat damit automatisch die Spalte (oder die Spalten) zwischen Zeichen 16 und 22 übersprungen. Bei csv Dateien hat man sich da bisher mit den typischen Unix Tools wie sed, cut oder awk beholfen und die Import-Dateien in einem vorangestellten Umformatierungsprozess entsprechend präpariert.

Es ist zwar ein bisschen gegen die UNIX Philosophie, die spezialisierten Tools nicht mehr das machen zu lassen, was sie am besten können und stattdessen dem einen fast schon "eierlegende Wolllmichsau" Programm diesen Task auch noch aufzubürden. Aber es hat den Vorteil, dass diese Spalte nicht weg ist, sondern schon noch geparst und gelesen wird und lediglich beim letztendlichen Insert nicht berücksichtigt wird. Ihr Wert kann aber benutzt werden, um Default-Werte in anderen Spalten festzulegen oder in WHEN Bedingungen darüber entscheiden, ob eine Zeile verarbeitet oder verworfen wird.

Die Verwendung in SQL Expressions, um csv Werte umzurechnen oder zu konvertieren, ist erst ab Oracle 9i möglich. Und auch nur dann, wenn das FILLER Feld im Control File nicht nur als FILLER, sondern als BOUND FILLER angegeben ist.

OPTIONS ( SKIP=1 )
LOAD DATA
  CHARACTERSET 'WE8ISO8859P1'
  INFILE 'employee.txt'
  APPEND INTO TABLE employees
  FIELDS TERMINATED BY ';'
(
    employee_id   INTEGER EXTERNAL,
    last_name     CHAR TERMINATED BY ',',
    middle_name   FILLER CHAR TERMINATED BY ',',
    first_name    CHAR,
    hire_date     DATE "yyyy-mm-dd",
    name_length   "greatest(:first_name,:last_name)"
)

Das "terminating" Zeichen wird nach INTO TABLE in einer FIELDS Clause angegeben und dabei in einfache Quotes eingeschlossen. "Zeichen" bedeutet aber nicht unbedingt, dass das nur ein einzelnes Zeichen sein darf. Es kann da durchaus auch ein ganzer String wie '|-|' oder '";"' angegeben werden. Wobei sich letztere Variante nicht lohnt, denn dafür gibt es (OPTIONALLY) ENCLOSED BY '"'. Eine spezielle Variante von ein ganzer String ist WHITESPACE, das genau so angegeben werden muss und nicht von einfachen Quotes eingeschlossen werden darf. Besonders ist es deshalb, weil die Länge variabel ist und Tabs und Spaces enthalten kann. Der Trenner kann auch durch die Angabe seines ASCII Codes spezifiziert werden, dazu muss der Byte-Code hexadecimal mit vorangestelltem X angegeben werden: X'20' Das wäre jetzt ein einfaches Leerzeichen, und zwar wirklich ein einzelnes und keine Sequenz, wie das bei WHITESPACE ist. Sinnvoll ist das, wenn Control File und Data File unterschiedliches Encoding haben. Dann kann man nämlich einfach den Pointcode für den Trenner, eben in der Codierung in der er in der Datendatei vorkommt, als Hexwert angbeben.

"TERMINATOR" ist ein mit Bedacht gewählter Name, denn eben das ist dieses Zeichen und kein Feldtrenner, wie am das von anderen Programmen gewohnt ist. Das wird dann wichtig, wenn das Terminierungszeichen direkt bei der Spalte angegeben wird und damit das generell für die Tabelle angegebene Zeichen überschreibt. Auf diese Weise sind auch verschachtelte Felder in eine flache Tabellen-Struktur zu konvertieren (in obigem Beispiel passiert das mit den 3 Namensanteilen, die sind durch Komma und nicht wie die restlichen Spalten durch Semikolon getrennt).

Common Table Expressions mit WITH

Die Syntax mit WITH ermöglicht es eine "inline" oder "virtuelle" View für die Dauer einer Abfrage zu erstellen.

WITH average AS (SELECT avg(e.salary) AS sal FROM department d
                  INNER JOIN employess e ON d.departementid = e.departementid)
SELECT d.department_name, avg(e.salary) AS avg_per_department, average.sal
  FROM department d INNER JOIN employess e ON d.departementid = e.departementid
       CROSS JOIN average
 GROUP BY d.department_name, average.sal
HAVING avg(e.salary) > average.sal

Statt CROSS JOIN average hätte hier natürlich auch die (Sub-)Select Anweisung nach WITH average AS direkt als Subselect nach CROSS JOIN geschrieben werden können. Wenn der Alias average in dem nachfolgenden Statement nicht nur einmal sondern mehrfach genutzt wird, dann ist ein Duplizieren des Subselect an mehreren Stellen nicht mehr schneller ... und sicherlich auch schwerer zu lesen.

Hierarchische Abfragen

Hierarchien werden üblicherweise durch verkettete Listen bzw. verbundene Baumstrukturen ausgedrückt, wobei jede Node ein weiteres Attribut "is-a-children-of" hat, das die Verbindung zur nächsten Ebene ausdrückt. In der einzelnen Node betrachtet, tut sich da noch nicht viel. Um ein Gesamtbild der Baumstruktur zu bekommen, muss man den "Graphen zeichnen" und dabei alle Nodes durchlaufen. Genau dazu kann man auch Oracle auffordern, allerdings zeichnet die Datenbank nicht wirklich einen Baum, sondern durchläuft ihn nur und gibt für jede Tabellenzeile mit Hilfe der Pseudospalte LEVEL den Hierarchie-Level aus. Die Klausel START WITH gibt an, von wo aus das Durchlaufen der Struktur beginnen soll.

Im folgenden Beispiel, wird der Root-Knoten als Startpunkt genommen, weil da eben am meisten zu sehen ist:

SELECT LEVEL, id, parent_id, last_name,
       SYS_CONNECT_BY_PATH(last_name,'/') as the_path
  FROM profile START WITH id = 1 CONNECT BY PRIOR id = parent_id;

  LEVEL      ID PARENT_ID LAST_NAME        THE_PATH
------- ------- --------- ---------------- --------------------------
      1       1           White            /White
      2       9         1 Bauer            /White/Bauer
      2      13         1 Ritter           /White/Ritter
      3      61        13 Erhard           /White/Ritter/Erhard
      3      81        13 Schröder         /White/Ritter/Schröder
      2      41         1 Reseller         /White/Reseller
      2      62         1 Maler            /White/Maler
      2      63         1 switchownerstaff /White/switchownerstaff
      2      64         1 Kiosk            /White/Kiosk

Außer der Pseudospalte LEVEL kommt auch noch eine spezielle Funktion zum Einsatz, die nur in hierarchischen Abfragen funktioniert: SYS_CONNECT_BY_PATH protokolliert in Form von Breadcrumbs den Weg mit, der vom Startpunkt bis zur aktuellen Node/Zeile durchlaufen worden ist.

PRIOR gehört übrigens nicht fix zur CONNECT BY Klausel, sondern ist ein Operator, der im konditionalen Ausdruck nach dem connect by auf einen der beiden Terme angewandt werden muss. Ein Vertauschen von PRIOR bewirkt im folgenden Fall, dass die Baumstruktur in umgekehrter Richtung durchlaufen wird. Damit dabei was sinnvolles zu sehen ist, kann als Startpunkt natürlich nicht die Wurzel genutzt werden, sondern idealerweise sollte das ein Blatt sein.

SELECT LEVEL, id, parent_id, last_name,
       SYS_CONNECT_BY_PATH(last_name,'/') as the_path
  FROM profile START WITH id = 61 CONNECT BY id = PRIOR parent_id;

  LEVEL      ID PARENT_ID LAST_NAME        THE_PATH
------- ------- --------- ---------------- -------------------------
      1      61        13 Erhard           /Erhard
      2      13         1 Ritter           /Erhard/Ritter
      3       1           White            /Erhard/Ritter/White

Views im Data Dictionary

Wie man an die Abfrage ran kommt, die mit der View hinterlegt ist, ist meistens bekannt. user_view ist nun wirklich nicht schwer zu erraten und nach einem desc user_view ist auch ziemlich klar, was in den 5 oder 6 Spalten zu erwarten ist.

Weniger bekannt ist, dass auch die Information wie und welche Spalten in einer View verändert werden können, im Data-Dictionary steckt. Dass eine group by Klausel und die Verwendung von Zeilen oder Aggregats-Funktionen zu read-only Views führt ist auch ziemlich schnell einleuchtend. Aber es gibt eben auch die anderen Fälle, bei denen es sehr verwirrend ist, weshalb da jetzt eine Spalte modifizierbar ist, die andere aber nicht. Da hilft ein Blick auf die Tabelle user_updatable_columns weiter:

SELECT table_name, column_name,
       updatable AS upd, insertable AS ins, deletable AS del
  FROM user_updatable_columns
 WHERE table_name in (select view_name from user_views);

Quoting von Strings ohne umständliches Escapen

Seit 10g gibt es in Oracle die Möglichkeit literale Strings mit einem Mechanismus zu Quoten, der an Here-Documents oder an Perl's q// Operator angelehnt ist. Dieses vereinfachte Quoten, bei dem auch einfache Quotes innerhalb des literalen Strings möglich sind, ist aber nur in PL/SQL verfügbar.

sql := q'[SELECT column_name FROM user_tab_columns WHERE table_name='A_TAB']';

listagg in Oracle 11g R2

SELECT deptno, listagg(ename, ';') within group (order by ename) AS liste
  FROM emp GROUP BY deptno;

Nimmt die Daten aus mehreren Zeilen und wendet den + String Operator, also eine Stringverkettung, statt den numerischen + an. Der zweite Parameter gibt ein Trennzeichen an, dass beim Verketten noch eingebaut wird und über die zusätzliche Klausel within group lässt sich die Reihenfolge, in der die gefundenen Felder verkettet werden sollen, festlegen. Die numerische Summe ist ja kommutativ, von daher ist bei ihr die Reihenfolge der Summanden egal.

Die Namensähnlichkeit zu xmlagg() ist auffällig und wohl auch gewollt, weil es auch bei der XML Variante um eine String-Verkettung geht.

XML Verarbeitung in Oracle

CREATE TABLE person (
    person_no   NUMBER,
    person_data XMLTYPE
);
INSERT INTO person (person_no, person_data)
 VALUES (1, XMLTYPE('<prsData><Name>Meier</Name></prsData>') );
INSERT INTO person (person_no, person_data)
 VALUES (2, XMLTYPE('<prs age="23">
<Name><vorname name="Peter"/><nachname name="Maier"/></Name></prs>') );
-- or
CREATE TABLE person_xml of XMLTYPE;
INSERT INTO person_xml SELECT person_data FROM person WHERE person_no = 1;

Aus Rudolf Jansens Oracle, Java, XML über die Speicherung von XML in der Datenbank:

Voraussetzung für die strukturierte Speicherung über objekt-relationale Tabellen, ist die Existenz einer XML Schema-Definition, auf der dann alle XML-Dokumente, die in der XMLType Spalte oder Tabelle abgelegt werden, beruhen. Ohne Einschränkung der Struktur der in der XMLType Spalte einzutragenden XML-Dokumente können dort alle validen XML-Dokumente abgelegt werden, mehr oder weniger als reiner CLOB Stream. Konsequenterweise gibt es auch keine Bedingung, dass jedes XML-Type Feld, die gleiche XML-Hierarchie enthalten muss. In der ersten Zeile dieser Tabelle kann zum Beispiel ein XML-String mit einem Root-Knoten und genau einem einzigen Child-Knoten im Root enthalten sein. In der zweiten Zeile dagegen ein Root-Knoten, der diesmal aber einen vollkommen anderen Tagnamen haben kann, und der 3 Kind-Elemente enthält, die jeweils eine unterschiedliche Anzahl an Attributen haben und eines dieser Kind-Element hat sogar noch weitere eigene Kind-Elemente.

Mit einer XSD-Schema Datei wäre das natürlich in einem bestimmten Maße auch möglich, wenn es viele Elemente gibt, die nicht verpflichtend sondern nur optional sind. Aber zumindest die Root-Elemente müssten dann in allen Zeilen gleich heißen.

Beim Umgang mit dem XMLTYPE ist es gut, sich klar zu machen, dass es sich dabei nur um einen speziellen, von Oracle schon vorab angelegten, Object Type handelt. Der intuitive Zugriff auf die Tabellen bringt zwar brauchbare Ergebnisse:

SQL> desc person
 Name                       Null?    Type
 -------------------------- -------- ------------
 PERSON_NO                           NUMBER
 PERSON_DATA                         XMLTYPE

SQL> desc person_xml
 Name                       Null?    Type
 -------------------------- -------- ------------
 TABLE of XMLTYPE

SQL> select person_data from person where person_no = 1;

PERSON_DATA
--------------------------------------------------------------
<prsData><Name>Meier</Name></prsData>

SQL> select * from person_xml;

SYS_NC_ROWINFO$
-------------------------------------------------------------
<prsData><Name>Meier</Name></prsData>

-- beside SYS_NC_ROWINFO$ there is a second hidden column SYS_NC_OID$

SQL> select SYS_NC_ROWINFO$, SYS_NC_OID$ FROM documents;

SYS_NC_ROWINFO$(DOC_ID, NAME, AUTHOR, URL, PUBLICATION_YEAR)           SYS_NC_OID$
---------------------------------------------------------------------- --------------------------------
DOC_T(1, 'Lügen im Heiligen Land', 'Peter Scholl-Latour', NULL, 1998)  660EF40881F54AEAE050A8C04D011F58
DOC_T(2, 'Anmerkungen zu Hitler', 'Sebastian Haffner', NULL, 1978)     660EF40881F64AEAE050A8C04D011F58

SYS_NC_ROWINFO$ verhält sich auch polymorph: Wenn es von einer Objekt-Tabelle basierend auf Objekten, die auch Attribute und nicht nur Methoden haben (sowie der XMLTYPE), gibt es in der Ergebnisdarstellung diese Attribute mit aus. Aber bei allem, was darüber hinaus geht, wird es schwierig:

SQL> select person_data.getClobVal() from person where person_no = 1;
select person_data.getClobVal() from person where person_no = 1
       *
FEHLER in Zeile 1:
ORA-00904: "PERSON_DATA"."GETCLOBVAL": invalid identifier

SQL> select getRootElement(person_data) from person where person_no = 1;
select getRootElement(person_data) from person where person_no = 1
       *
FEHLER in Zeile 1:
ORA-00904: "GETROOTELEMENT": invalid identifier

SQL> select extract(p,'//Name') from person_xml p;
select extract(p,'//Name') from person_xml p
               *
ERROR at line 1:
ORA-00904: "P": invalid identifier

Wichtig ist, dass zuerst mit dem VALUE Operator aus dem Objekt- bzw. XML-Typ eine Instanz erstellt wird:

SQL> SELECT VALUE(p) FROM person_xml p;

VALUE(P)
-------------------------------------------------------------
<prsData><Name>Meier</Name></prsData>

SQL> SELECT VALUE(d) FROM documents d WHERE doc_id = 2;

VALUE(D)(DOC_ID, NAME, AUTHOR, URL, PUBLICATION_YEAR)
----------------------------------------------------------------------
DOC_T(2, 'Anmerkungen zu Hitler', 'Sebastian Haffner', NULL, 1978)


SQL> select extractValue(value(p),'//Name') from person_xml p;

EXTRACTVALUE(VALUE(P),'//NAME')
-------------------------------------------------------------
Meier

SQL> select extractValue(value(p),'//Name/@age') from person_xml p;

EXTRACTVALUE(VALUE(P),'//NAME/@AGE')
-------------------------------------------------------------


SQL> select extractValue(value(p),'//Name/text()') from person_xml p;

EXTRACTVALUE(VALUE(P),'//NAME/TEXT()')
-------------------------------------------------------------
Meier

SQL> select extract(value(p),'//Name') from person_xml p;

EXTRACT(VALUE(P),'//NAME')
-------------------------------------------------------------
<Name>Meier</Name>

-- Was mit extract funktioniert, klappt aber nicht immer, z.B. mit getClobVal()
-- nicht
--
SQL> select getClobVal(value(p)) from person_xml p;
select getClobVal(value(p)) from person_xml p
       *
FEHLER in Zeile 1:
ORA-00904: "GETCLOBVAL": invalid identifier

-- Richtig ist, erst aus dem Objekt-Typ mit value() eine Instanz bauen und dann
-- für diese Instanz klassisch objekt orientiert die jeweilige Methode aufzurufen
--
SQL> select value(p).getClobVal() from person_xml p;

VALUE(P).GETCLOBVAL()
-------------------------------------------------------------
<prsData><Name>Meier</Name></prsData>

SQL> select value(p).extract('//Name') from person_xml p;

VALUE(P).EXTRACT('//NAME')
-------------------------------------------------------------
<Name>Meier</Name>

SQL> select value(p).getRootElement() from person_xml p;

VALUE(P).GETROOTELEMENT()
-------------------------------------------------------------
prsData

-- Ohne alias p für die Objekt-Tabelle geht interessanterweise auch nichts:
--
SQL> select value(person_xml).getRootElement() from person_xml;
select value(person_xml).getRootElement() from person_xml
             *
FEHLER in Zeile 1:
ORA-00904: "PERSON_XML": invalid identifier

Der Alias ist wirklich elementar, denn ohne den kommt auch mit der Tabelle mit dem XMLType in der Spalte nichts zustande. Es ist also hilfreich, sich die Objektmethoden als so eine Art korrelierte Unterabfragen vorzustellen, bei denen muss ja auch ein Tabellen-Alias in die Unterabfrage "hinab" gereicht werden.

SQL> select p.person_data.getRootElement() from person p;

P.PERSON_DATA.GETROOTELEMENT()
--------------------------------------------------
prsData
prs

SQL> select p.person_data.existsNode('//Name') from person p;

P.PERSON_DATA.EXISTSNODE('//NAME')
----------------------------------
                                 1
                                 1

SQL> UPDATE person p
   SET p.person_data = updateXML(p.person_data,'//Name/text()','Paul Meier')
 WHERE person_no = 1;

-- Aber:
select person.person_data.existsNode('//Name') from person where person_no = 1
       *
FEHLER in Zeile 1:
ORA-00904: "PERSON"."PERSON_DATA"."EXISTSNODE": invalid identifier

Import von XML-Daten

Bei umfangreicheren XML Daten wird es kaum reichen, den XMLTYPE Konstruktor mit einem VARCHAR2 als Parameter aufzurufen, literal schon mal gar nicht und eine Tabellenspalte vom Typ VARCHAR2 ist auch nur bedingt nutzbar, da sie auf 8000 Zeichen beschränkt ist. PL/SQL erlaubt zwar mehr Zeichen in einer VARCHAR2 Variable, aber wenn man schon von reinem SQL auf eine prozedurale Sprache umsteigt, dann sollte man es gleich richtig machen und von einem binary blob file laden. Bei Oracle läuft das unter dem Namen BFILE.

Wie bei allen Dateisystem-Zugriffen von Oracle (genauer vom Oracle-Server) aus, muss ein DIRECTORY Objekt mit den entsprechenden Berechtigungen angelegt werden, damit das funktioniert.

CREATE DIRECTORY XML_DATA AS '/os/path';

DECLARE
  bfl BFILE;
BEGIN
  /* attach  the XML document myDoc.xml to BFILE locator */
  bfl := BFILENAME('XML_DATA', 'myDoc.xml');

  INSERT INTO person (person_no, person_data)
    VALUES (3, XMLTYPE(bfl, nls_charset_id('WE8MSWIN1252')) );

  COMMIT;
END;

Die Nutzung des Directory Objekts bedingt, ebenso wie bei External Tables, dass die zu importierende Datei auf dem Oracle Datenbank-Server liegt. Typischerweise bedeutet das, dass derjenige, der den Import vornehmen will, auch Shell-Zugriff auf das Betriebssystem und das Dateisystem haben muss. Im Prinzip reichen auch ftp und WebDAV aus, aber bei diesen ftp bzw. Webserver basierten Methoden, kann man sich durchaus auch im Berechtigungssumpf verirren.

Ein alternativer Ansatz ist das Laden mit dem SQL*Loader, damit können auch binäre Daten vom Client zum Server geschaufelt werden:

CREATE TABLE demo_images
(
    inode     NUMBER(10),
    bindata   BLOB,
    file_size NUMBER(10),
    file_date DATE,
    mime_type VARCHAR2(30)
);
LOAD DATA
  CHARACTERSET 'WE8ISO8859P1'
  INFILE 'png-files.dat'
  APPEND INTO TABLE demo_images
  FIELDS TERMINATED BY X'09'
(
    image_file FILLER CHAR,
    bindata    LOBFILE(image_file) RAW TERMINATED BY EOF,
    file_size  INTEGER EXTERNAL,
    inode      INTEGER EXTERNAL,
    file_date  DATE "yyyy-mm-dd hh24:mi:ss",
    mime_type  CONSTANT 'image/png'
)

Die zu importierende Datendatei sieht dann wie folgt aus (und kann mit stat in der Shell oder etwas bequemer innerhalb von Perl generiert werden):

$ stat --printf='%n\t%s\t%i\t%y\n' *.png

Feinlayout-Anpassen.png	5804	654361	2008-03-25 02:10:04.000000000 +0100
Feinlayout-Drucker.png	3586	654403	2008-03-25 00:22:50.000000000 +0100
Feinlayout-FVREF.png	4135	654315	2008-03-25 02:05:04.000000000 +0100
Feinlayout-Index.png	3872	654405	2008-03-25 02:19:20.000000000 +0100
Feinlayout-Seiten.png	5665	654412	2008-03-25 02:04:46.000000000 +0100

SQL> SELECT inode, file_size, file_date, mime_type, length(bindata) FROM demo_images;

    INODE FILE_SIZE FILE_DATE           MIME_TYPE     LENGTH(BINDATA)
  ------- --------- ------------------- ------------- ---------------
   654361      5804 2008-03-25 02:10:04 image/png                5804
   654403      3586 2008-03-25 00:22:50 image/png                3586
   654315      4135 2008-03-25 02:05:04 image/png                4135
   654405      3872 2008-03-25 02:19:20 image/png                3872
   654412      5665 2008-03-25 02:04:46 image/png                5665

XML bearbeitende Zeilenfunktionen

SYS_XMLGEN

Die SYS_XMLGEN Funktion kann innerhalb einer SQL-Abfrage zur Generierung von XML-Konstrukten eingesetzt werden. Sie operiert auf Zeilenebene, d.h. für jede Zeile der Ergebnismenge wird die SYS_XMLGEN Funktion ausgeführt. Sie unterscheidet sich in dieser Tatsache vom DBMS_XMLGEN Package, dessen Funktionen immer auf das Ergebnis einer gesamten Abfrage zugreifen.

SELECT sys_xmlgen(person_no) FROM person;

SYS_XMLGEN(PERSON_NO)
---------------------------------
<?xml version="1.0"?>
<PERSON_NO>1</PERSON_NO>
<?xml version="1.0"?>
<PERSON_NO>2</PERSON_NO>

2 rows selected.

SELECT sys_xmlgen(count(person_no)) FROM person;

SYS_XMLGEN(COUNT(person_no))
----------------------------
<?xml version="1.0"?>
<ROW>2</ROW>

1 row selected.

Oder wenn man den Tagnamen des Elements selber festlegen will, kann man noch einen zweiten, optionalen Parameter bei sys_xmlgen mit angeben:

SELECT sys_xmlgen(person_no, xmlformat.createformat('personNumber'))
  FROM person;

Interessanter wird das, wenn eine XMLType Spalte an sys_xmlgen verfüttert wird:

SELECT sys_xmlgen(person_data, xmlformat.createformat('personInfo'))
  FROM person;

SYS_XMLGEN(PERSON_DATA,XMLFORMAT.CREATEFORMAT('PERSONINFO'))
--------------------------------------------------------------
<?xml version="1.0"?>
<personInfo>
<prsData>
  <Name>Meier</Name>
</prsData>
</personInfo>

<?xml version="1.0"?>
<personInfo>
<prs age="23">
  <Name>
    <vorname name="Peter"/>
    <nachname name="Maier"/>
  </Name>
</prs>
</personInfo>

Dabei wird nämlich das spezifizierte Tag-Element um den XML Content der Spalte gewickelt.

Standard konforme XML Zeilenfunktionen

Parallel zu sys_xmlgen kann das mehr am Standard SQLX orientierte XMLElement verwendet werden. Es erwartet 2 Parameter: Den Namen des Tags, das für die den XML-Knoten verwendet werden soll (da man dabei oft Tag-Namen haben will, die nicht nur aus Blockbuchstaben bestehen, wird dieser 1.Parameter sehr häufig in Anführungzeichen angebeben. Analog zu Spalten- oder Tabellennamen werden hier double quotes verwendet, single quotes als String Delimiter wären da vollkommen fehl am Platz) und als 2.Parameter einen Spaltennamen einer Tabelle. Im Unterschied zu sys_xmlgen wird aber kein XML Prolog mit der der XML Version ausgeben.

Die Zeilenfunktionen können dabei nicht nur Felder aus rein relationalen Tabellen in XML umwandeln, sondern sie akzeptieren auch Objekt-Spalten — typischerweise XMLType — als Parameter.

SELECT XMLElement(person_id,person_no) FROM person;

XMLELEMENT(PERSON_ID,PERSON_NO)
---------------------------------
<PERSON_ID>1</PERSON_ID>
<PERSON_ID>2</PERSON_ID>

2 rows selected.

SELECT XMLElement("PersonId",person_no) FROM person;

SELECT XMLElement( "PersonData",
                   XMLAttributes(person_no AS "id"),
                   XMLElement("data",person_data) ) AS xml_string FROM person;

XML_STRING
-------------------------------------------
<PersonData id="1"><data><prsData>
  <Name>Meier</Name>
</prsData>
</data></PersonData>

<PersonData id="2"><data><prs age="23">
  <Name>
    <vorname name="Peter"/>
    <nachname name="Maier"/>
  </Name>
</prs>
</data></PersonData>

Das Result-Set dieser Operationen hat aber immer so viele Zeilen, wie die abgefragte Tabelle oder View. Will man da einen gemeinsamen String daraus machen, hilft das, was bei den hinlänglich bekannten rein relationalen Tabellen auch genutzt wird: Aggregation.

Im folgenden wird das exemplarisch anhand der emp Tabelle aus dem Scott Schema gezeigt (das Beispiel ist übrigens aus Jürgen Siebens sehr lesenswertem Buch "Oracle SQL" entnommen).

  SELECT XMLElement("Mitarbeiterliste",
             XMLAttributes(deptno AS "abteilung"),
             xmlagg(
                XMLElement("Mitarbeiter",
                    XMLAttributes(empno AS "id", hiredate AS "EinstellTag"),
                    XMLForest(
                       initcap(ename) AS "Name",
                       initcap(job) AS "Beruf",
                       trim(to_char(sal,'999G990D00l')) AS "Gehalt"
                    )
                )
             )
         ) AS xml_string
   FROM emp
  GROUP BY deptno;

xmlagg aggregiert dabei die einzelnen Zeilen — bzw. das was die innere Zeilenfunktion XMLElement aus der Zeile (mit den Feldern empno, hiredate, ename, job und sal) macht — indem es die als Parameter jeweils übergebenen Elemente per String-Verkettung aneinander hängt.