Oracle native json Support Erste Schritte 1 Ausgangslage Als erster Schritt erstellen wir eine Tabelle, die wir für den weiteren Verlauf brauchen werden. Die Felder Id, Created und Username sind „normale“ Attribute, das Feld Extended wird später die dynamischen Properties beinhalten. Aus SQL Sicht ist es prinzipiell eine normale Spalte, die einen String-Wert beinhalten kann. Die dynamische Struktur wird im Verlauf dieses Artikels komplexer, die Tabelle muss hierfür aber nicht angepasst werden. Wir haben eine Tabelle SAMPLE1 mit folgendem Inhalt: Name Type Id NUMBER Created TIMESTAMP Username VARCHAR2 Extended CLOB -- Table CREATE TABLE SAMPLE1 ( Id NUMBER(10) NOT NULL, CREATED TIMESTAMP (0), USERNAME VARCHAR2(20 BYTE), EXTENDED CLOB CONSTRAINT valid_json CHECK (Extended IS JSON), PRIMARY KEY (Id)) --Trigger CREATE SEQUENCE Sample1_seq START WITH 1 INCREMENT BY 1; CREATE OR REPLACE TRIGGER Sample1_seq_tr BEFORE INSERT ON Sample1 FOR EACH ROW WHEN (NEW.Id IS NULL) BEGIN SELECT Sample1_seq.NEXTVAL INTO :NEW.Id FROM DUAL; END; / Die Spalte Extended nutzen wir um den JSON-String abzulegen. Hier setzen wir einen Constraint „valid_json“, der überprüft, ob die Eingabe einem gültigen JSON-String entspricht. Diese Überprüfung ist wichtig, andernfalls können die Felder nicht abgefragt werden. 1.1 Abfrage von Feldern mit JSON Zuerst legen wir Beispiel-Daten an: INSERT INTO Sample1 (Created, Username, Extended) VALUES (TO_DATE('2016-11-17 17:26:30', 'yyyy-mm-dd hh24:mi:ss'), 'Test', '{ "name": "Nineteen Eighty-Four", "author": "George Orwell", "genre": "Political fiction", "price": 14.99, "pages": 328 }') INSERT INTO Sample1 (Created, Username, Extended) VALUES (TO_DATE('2008-01-28 21:28:27', 'yyyy-mm-dd hh24:mi:ss'), 'Test', '{ "name": "The Metamorphosis", "author": "Franz Kafka", Oracle native json Support - Erste Schritte Seite 2 von 8 "genre": "Short Story", "price": 11.99, "pages": 201 }') Die „klassischen“ Attribute können wie gewohnt über eine Query abgefragt werden: SELECT * FROM Sample1 WHERE Username = 'Test' Aber wie werden die zusätzlichen Felder in der Spalte Extended abgefragt? Hierfür gibt es mehrere Möglichkeiten, zwei davon werde ich hier vorstellen: Die einfachste Methode ist die Dot-Notation. Diese eignet sich für simple Aufgaben, kommt aber, bei beispielsweise Arrays, schnell an ihre Grenzen. SELECT s.Extended.name FROM sample1 s Hier werden beide Buchtitel ausgegeben: SELECT s.Extended.name FROM sample1 s WHERE s.Extended.pages > 300 Oder nur das Buch ausgegeben, das mehr als 300 Seiten umfasst: Eine andere Vorgehensweise ist der Einsatz von JSON_TABLE: SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$' COLUMNS ( name VARCHAR2(100) PATH '$.name', author VARCHAR2(100) PATH '$.author', price NUMBER(10,2) PATH '$.price', pages NUMBER(10) PATH '$.pages' ) ) as jt Hier werden die JSON-Properties als relationale Tabellen-Spalten projiziert. Es kann natürlich auch hier gefiltert werden: SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$' COLUMNS ( name VARCHAR2(100) PATH '$.name', Oracle native json Support - Erste Schritte Seite 3 von 8 author VARCHAR2(100) PATH '$.author', price NUMBER(10,2) PATH '$.price', pages NUMBER(10) PATH '$.pages' ) ) as jt WHERE price > 12 AND Username = 'Test' Ein paar Erklärungen zum Syntax: $ bezieht sich auf das Root-Element, hier ist Root „ganz oben“ also das komplette Dokument. Danach werden die Spalten aufgelistet, die ausgewertet werden sollen. Es ist wichtig den richtigen Datentyp zu wählen - oder falls dies nicht möglich ist, VARCHAR2 nehmen. Mit VARCHAR2 können dann natürlich keine Operatoren (Vergleiche, logische oder numerische Operatoren) benutzt werden. Um das Beispiel etwas zu erweitern, importieren wir mehr Datensätze, die diesmal komplexer aufgebaut sind bzw. nicht mehr flach sind. Das Property address beinhaltet ebenfalls ein UnterObjekt und phoneNumber ist ein Array von mehreren Objekten. INSERT INTO Sample1 (Created, Username, Extended) VALUES (TO_DATE('1970-05-08 04:58:52', 'yyyy-mm-dd hh24:mi:ss'), 'Test2', '{ "firstName" : "Robin", "lastName" : "Hood", "age" : 22, "address" : { "street" : "Wood 12", "city" : "London", "zip" : "1000" }, "phoneNumber" : [{ "type" : "home", "number" : "111 222-3333" }, { "type" : "fax", "number" : "111 222-4444" } ] }') INSERT INTO Sample1 (Created, Username, Extended) VALUES (TO_DATE('1995-06-10 10:52:45', 'yyyy-mm-dd hh24:mi:ss'), 'Test2', '{ "firstName" : "Bruce", "lastName" : "Lee", "age" : 31, "address" : { "street" : "Street 42", "city" : "Hong Kong", "zip" : "1234" }, "phoneNumber" : [{ "type" : "home", "number" : "123456789" }, { "type" : "fax", "number" : "987654321" } ] }') Oracle native json Support - Erste Schritte Seite 4 von 8 Führen wir die Select Queries aus dem letzten Schritt aus, bekommen wir dieselben Resultate. Das ist auch zu erwarten, denn die zwei neuen Datensätze enthalten kein Feld mit dem Titel name oder price. Fragen wir aber das Feld age ab, ergibt sich ein anderes Bild: SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$' COLUMNS ( firstName VARCHAR2(100) PATH '$.firstName', lastName VARCHAR2(100) PATH '$.lastName', age NUMBER(10,2) PATH '$.age' ) ) as jt WHERE age > 18 Die Properties address und phoneNumber sind komplexer: Wie bereits erwähnt, enthält die adress-Property weitere Unter-Properties, die zusammen wieder ein Objekt ergeben: street, city und zip. In der folgenden Query werden wir den Wert city ausgeben. Die anderen Properties funktionieren analog dazu. SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$' COLUMNS ( firstName VARCHAR2(100) PATH '$.firstName', lastName VARCHAR2(100) PATH '$.lastName', age NUMBER(10,2) PATH '$.age', city VARCHAR2(100) PATH '$.address.city' ) ) as jt WHERE age > 18 Hier wird einfach mittels der Dot-Notation eine weitere Spalte angezeigt ($.address.city). Das ganze kann auch ohne Hilfe der JSON_TABLE Funktion verwendet werden: SELECT s.Extended.address.city FROM sample1 s WHERE s.Extended.age > 18 Das Property phoneNumber beinhaltet einen Array, es funktioniert deshalb etwas anders als bisher. Mit der Dot-Notation kann zwar auf die Property zugegriffen werden, Oracle verbindet aber die Werte, was wahrscheinlich oft nicht das gewünschte Resultat liefert: SELECT s.Extended.phoneNumber."number" FROM sample1 s WHERE s.Extended.age > 18 Oracle native json Support - Erste Schritte Seite 5 von 8 Hinweis: number ist ein reserviertes Oracle Keyword und muss escaped werden. Mit JSON_TABLE kann das Ganze detaillierter aufgelistet werden: SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$' COLUMNS ( firstName VARCHAR2(100) PATH '$.firstName', lastName VARCHAR2(100) PATH '$.lastName', age NUMBER(10,2) PATH '$.age', NESTED PATH '$.phoneNumber[*]' COLUMNS ( phoneType VARCHAR2(20) PATH '$.type', phoneNumber VARCHAR2(50) PATH '$.number' ) ) ) as jt WHERE age > 18 Spannend ist hier das Keyword NESTED im Zusammenhang mit dem Pfad $.phoneNumber[*] das den nachfolgenden Array zeilenweise ausgibt. Das Stern-Symbol (*) bezieht sich hier auf alle Einträge. Stünde beispielsweise stattdessen eine 0 in den Klammern, würde es nur jeweils den ersten Eintrag ausgeben. Hier sieht man aber sehr gut, dass die Datensätze doppelt ausgegeben werden, weil das phoneNumber Array auch jeweils zwei Einträge besitzt. Natürlich kann auch hier gefiltert werden: [...] WHERE phoneType = 'fax' Im letzten Beispiel verschieben wir das Root-Element der Abfrage: SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$.phoneNumber[*]' COLUMNS ( phoneType VARCHAR2(100) PATH '$.type', phoneNumber VARCHAR2(100) PATH '$.number' ) ) as jt Hier wird direkt auf die Property phoneNumber verwiesen und die beiden Unter-Properties type und number werden ausgegeben. Die restlichen Properties wie firstName oder age können bei diesem Ansatz aber nicht ausgegeben werden. Je nach Aufgabenstellung ist dies eventuell auch nicht nötig. Oracle native json Support - Erste Schritte Seite 6 von 8 1.2 Performance Neben der reinen Abfragetechniken, möchten wir auch eine Aussage zur Performance machen. Dieser Test gibt einen kleinen Überblick, wie sich die Performance mit JSON-Date in Oracle Datenbanken auswirkt. In unserer Testdatenbank befinden sich 500'000 Datensätze mit zweifach verschachtelten JSON-Daten. Die Tests wurden auf einen Desktop-Rechner mit i7-3770 CPU (3.4 Ghz), 16 GB RAM, SDD auf Windows 8.1 ausgeführt Das Feld Extended besitzt einen Index: CREATE INDEX SAMPLE_JSON_IDX ON SAMPLE1 (Extended) INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS('section group CTXSYS.JSON_SECTION_GROUP SYNC (ON COMMIT)'); String Length Query ohne JSON-Bezug: SELECT COUNT(*) FROM Sample1 WHERE length(Username) > 0 Antwortzeit: 0.09s String Length Query mit JSON-Bezug: SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$' COLUMNS ( firstName VARCHAR2(100) PATH '$.firstName', lastName VARCHAR2(100) PATH '$.lastName', age NUMBER(10,2) PATH '$.age' ) ) as jt WHERE length(lastName) > 0 Antwortzeit: 2.4s Integer Vergleich ohne JSON-Bezug: SELECT COUNT(*) FROM Sample1 WHERE Id >= 0 Antwortzeit: 0.085s Integer Vergleich mit JSON-Bezug: SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$' COLUMNS ( firstName VARCHAR2(100) PATH '$.firstName', lastName VARCHAR2(100) PATH '$.lastName', age NUMBER(10,2) PATH '$.age' ) ) as jt WHERE age >= 0 Antwortzeit: 2.25s JSON-Table Join mit normalen Tabelle: SELECT sample1.*, jt.* FROM sample1, JSON_TABLE(Extended, '$' COLUMNS ( firstName VARCHAR2(100) PATH '$.firstName', lastName VARCHAR2(100) PATH '$.lastName', age NUMBER(10,2) PATH '$.age' ) ) as jt INNER JOIN person ON age = person.age Oracle native json Support - Erste Schritte Seite 7 von 8 Antwortzeit: 2.28s Hinweis: Hier wird eine zweite Tabelle referenziert, die nicht Teil dieses Artikels ist. 1.3 Fazit Mittels der JSON-Erweiterung für Oracle können viele Vorteile der dokumentbasierten Datenbanken auch in relationalen Oracle Datenbanken verwendet werden. Auf die gewohnte SQL Syntax muss hierbei nicht verzichtet werden. Damit wird für viele Anwender, die auf NoSQL Datenbanken wechseln (müssen) ein Hindernis auf die Seite geräumt. An einigen Stellen merkt man allerdings, dass hier zwei Parteien miteinander verheiratet werden, die eigentlich gar nicht füreinander bestimmt sind. Zum Beispiel sind JSON-Properties nicht typisiert, der SQL Standard verlangt diese aber. Ein anderes Fass macht man mit OR-Mappern auf: Sie unterstützen die erweiterten Abfragen (noch) nicht, der Entwickler ist also gezwungen einen eigenen Datenbank-Layer zu entwickeln. In Punkto Performance ergeben sich zwar Einbussen aber diese sind verhältnismässig gering. Wir werden den Ansatz auf jeden Fall bei der Realisierung von dynamischen Datenmodellen weiterverfolgen. Oracle native json Support - Erste Schritte Seite 8 von 8