Graph Datenmodelle und Graph Datenbanken haben in den letzten Jahren immer mehr an Bedeutung gewonnen, weil sie auf die Abbildung und das Analysieren von hochgradig vernetzen Informationen spezialisiert sind. Beispiele dafür sind semantische Netze, die insbesondere in der Wissenspräsentation verbreitet sind oder auch soziale Netzwerke.
Die Oracle Datenbank ist eine multimodale Datenbank. Sie kennt also nicht nur das relationale Datenmodell sondern unterstützt andere Datenmodelle. In Bezug auf Netzwerk- oder Graphen werden entsprechende Datenmodelle bereits seit Version 10.2 unterstützt. Das gilt für räumliche (z.B. Straßen- oder Leitungsnetze) als auch semantische Netze ("Linked Data"). Mit Version 12.2 der Oracle DB wurde neu die Unterstützung für das Property Graph Datenmodell eingeführt.
Dieser Beitrag fokussiert sich darauf, wie Property Graphs in der Oracle DB abgebildet und geladen werden können. Dazu werde ich das relational modelierte HR Schema in ein Graph-Datenmodell übertragen. Auch eine erste Analyse mit Hilfe des Dijkstra−Algorithmus für "Kürzeste−Wege" Berechnungen werde ich zeigen. Alles, was Sie zum Verständnis benötigen, sind SQL Kenntnisse.
Auf die sehr umfassenden Möglichkeiten der Graphen-Analyse mittels der im Oracle Home bereitgestellten Java API sowie das Visualisieren von Graphen mit Open Source Werkzeugen werde ich in einem zweiten Blog-Beitrag zu späterer Zeit eingehen.
Property Graphs kurz erklärt
Allen bekannten und von Graph-Datenbanken unterstützten Datenmodellen liegt ein Netzwerk von Knoten und Kanten zugrunde. Ein solches Datenmodell legt den Schwerpunkt auf die Beziehungen zwischen Informationen und deren Analyse mittels Graph-Algorithmen oder auch einer deklarativen Graph-Anfragesprache.
Beim Property Graph, der generischsten Art von Graphen, repräsentieren die Knoten Informationstypen. Diese sind über Kanten mit anderen Knoten verbunden. Beiden, sowohl Knoten als auch Kanten, können eine beliebige Anzahl von Attributen, engl. Properties genannt, zugeordnet werden [Abb. 1]. Typischerweise werden diese Attribute als Key-Value-Paare abgebildet und gespeichert. Die Attribute von Knoten oder Kanten müssen nicht identisch sein, weder in der Anzahl noch in ihren Inhalten. In der Praxis werden jedoch Knoten gleicher Informationstypen gleichartige Attribute aufweisen ebenso wie die Attribute von Kanten, die in der Regel mindestens ein gleiches Etikett (engl. Label) tragen. Etiketten sind eine zusätzliche Eigenschaft, die Kanten tragen können. Knoten können keine Etikette zugeordnet werden. Es ist jedoch möglich, ein Attribut als Etikett zu deklarieren.
Abb. 1: Ausschnitt aus dem HR Beispielschema der Oracle Datenbank − Modellierung als Property Graph
Alle Funktionalität rund um Property Graphs ist Teil der Datenbank Option Spatial and Graph. Voraussetzung ist eine Enterprise Edition in der Version 12.2 oder höher. Zusätzlich benötigt wird die Option Partitioning. Für eine Evaluierung können Sie entweder
nutzen. Für diesen Blog-Beitrag habe ich eine Oracle DB 18c genutzt, bereitgestellt als Cloud Service über die Oracle Cloud [Abb. 2].
Abb. 2: Oracle DB 18c Instanz in der Oracle Cloud
Um mit Property Graphs in der Datenbank zu arbeiten, müssen folgende technischen Voraussetzungen erfüllt sein:
Der Default Wert für MAX_STRING_SIZE ist "standard". Daher folgt hier eine kurze Beschreibung, wie die Änderung auf "extended" erfolgt. Bitte beachten Sie zusätzliche Hinweise zum Umschalten des Parameters in der Dokumentation.
prompt$ sqlplus / as sysdba Connected. SQL> alter session set container=CDB$ROOT; Session altered. SQL> alter system set max_string_size=extended scope=SPFILE; System altered. SQL> shutdown immediate; Database closed. Database dismounted. ORACLE instance shut down. SQL> startup upgrade; ORACLE instance started. ... Database mounted. SQL> alter pluggable database all open upgrade; Pluggable database altered. SQL> quit prompt$ cd $ORACLE_HOME/rdbms/admin prompt$ mkdir /home/oracle/utl32k_cdb_pdbs_output prompt$ $ORACLE_HOME/perl/bin/perl $ORACLE_HOME/rdbms/admin/catcon.pl -u SYS -d $ORACLE_HOME/rdbms/admin -l '/home/oracle/utl32k_cdb_pdbs_output' -b utl32k_cdb_pdbs_output utl32k.sql ... Enter Password: catcon.pl: completed successfully prompt$ sqlplus / as sysdba SQL> shutdown immediate; SQL> startup SQL> alter pluggable database all open read write; Pluggable database altered. SQL> quit prompt$ mkdir /home/oracle/utlrp_cdb_pdbs_output prompt$ $ORACLE_HOME/perl/bin/perl $ORACLE_HOME/rdbms/admin/catcon.pl -u SYS -d $ORACLE_HOME/rdbms/admin -l '/home/oracle/utlrp_cdb_pdbs_output' -b utlrp_cdb_pdbs_output utlrp.sql ... Enter Password: ... catcon.pl: completed successfully prompt$ sqlplus / as sysdba SQL> alter session set container=PDB1; Session altered. SQL> show parameters max_string;
Nicht zwingend notwendig aber empfehlenswert sind zusätzlich das Anlegen:
prompt$ sqlplus / as sysdba SQL> alter session set container=PDB1; SQL> create bigfile tablespace pgtbs datafile '+data' size 512m reuse autoextend on next 128M maxsize unlimited extent management local; SQL> select * from v$tablespace; SQL> create user pguser identified by Welcome123#; SQL> grant connect, resource, alter session to pguser; SQL> alter user pguser default tablespace pgtbs; SQL> grant unlimited tablespace to pguser;
Einen Property Graph erzeugen
Bevor Knoten und Kanten geladen werden können, muß der Graph definiert werden. Dafür wird die Prozedur CREATE_PG im PL/SQL Paket OPG_APIS aufgerufen.
SQL> conn pguser/Welcome123#@pdb1 SQL> exec opg_apis.create_pg(graph_name => 'pgtest', SQL> dop => 4, SQL> num_hash_ptns => 8, SQL> tbs => 'PGTBS'); SQL> select table_name from user_tables;
Die einzelnen Parameter für den Aufruf der Prozedur beziehen sich dabei auf:
Optional kann auch noch angegeben werden, daß die Daten komprimiert gespeichert werden. Im Aufruf der Prozedur weiter oben wurde dieser Parameter nicht gesetzt.
Die Prozedur legt mehrere Tabellen mit dem Präfix PGTEST an [Abb. 3]. In der Tabelle mit der Endung VT$ werden die Knoten (engl. Vertices) mit ihren Attributen abgelegt, in der Tabelle mit der Endung GE$ die Kanten (engl. Edges) inlusive ihrer Attribute.
Abb. 3: Tabellen für den Graph PGTEST dargestellt im Oracle SQL Developer
An dieser Stelle soll ein kurzer Blick auf die Definition dieser beiden Tabellen geworfen werden.
SQL> desc pgtestVT$; Name Null? Type ---- -------- --------------------------- VID NOT NULL NUMBER K NVARCHAR2(3100) T NUMBER(38) V NVARCHAR2(15000) VN NUMBER VT TIMESTAMP(6) WITH TIME ZONE SL NUMBER VTS DATE VTE DATE FE NVARCHAR2(4000) SQL> desc pgtestGE$; Name Null? Type ---- -------- --------------------------- EID NOT NULL NUMBER SVID NOT NULL NUMBER DVID NOT NULL NUMBER EL NVARCHAR2(3100) K NVARCHAR2(3100) T NUMBER(38) V NVARCHAR2(15000) VN NUMBER VT TIMESTAMP(6) WITH TIME ZONE SL NUMBER VTS DATE VTE DATE FE NVARCHAR2(4000)
Die Definition orientiert sich am sogenannten Oracle Flat File Format für Property Graphs.
Eine Zeile in der Tabelle für Knoten ist repräsentiert durch:
Eine Zeile in der Tabelle für Kanten ist repräsentiert durch:
Einen Property Graph laden
Für das Laden der Knoten und Kanten gibt es mehrere Möglichkeiten. Je nach Format der Ausgangsdaten ist es möglicherweise notwendig, diese in ein Datenformat zu transformieren, welches von Spatial and Graph unterstützt wird. Das sind neben dem Oracle Flat File Format auch GraphSON, GraphML oder GML.
Das nachfolgende Beispiel ist die einfachste Form des Ladens, mittels INSERT. Dieses Vorgehen ist jedoch nur dann sinnvoll, wenn die Anzahl der zu ladenden Daten überschaubar ist. Typischerweise enthalten Graphen jedoch eine sehr große Anzahl an Knoten und Kanten. Dann empfehlen sich bekannte und bewährte Werkzeuge, wie SQL*Loader oder das Laden über External Tables.
truncate table pgtestVT$; insert into pgtestVT$(vid,k,t,v,vn,vt) values(1,'type',1,'City',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(1,'city_name',1,'Zürich',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(1,'city_lon',4,null,8.54,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(1,'city_lat',4,null,47.38,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(1,'city_ranking',2,null,0,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(2,'type',1,'City',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(2,'city_name',1,'Genève',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(2,'city_lat',4,null,46.21,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(2,'city_ranking',2,null,1,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(2,'city_member_count',2,null,1225,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(3,'type',1,'City',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(3,'city_name',1,'Lausanne',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(3,'city_lon',4,null,6.62,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(3,'city_lat',4,null,46.52,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(3,'city_ranking',2,null,2,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(3,'city_member_count',2,null,635,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(4,'type',1,'City',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(4,'city_name',1,'Bern',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(4,'city_lon',4,null,7.44,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(4,'city_lat',4,null,46.95,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(4,'city_ranking',2,null,3,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(5,'type',1,'City',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(5,'city_name',1,'Vienna',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(5,'city_lon',4,null,16.37,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(5,'city_lat',4,null,48.22,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(5,'city_ranking',2,null,0,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(5,'city_member_count',2,null,1367,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(6,'type',1,'Country',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(6,'country_name',1,'Switzerland',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(6,'country_code',1,'ch',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(7,'type',1,'Country',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(7,'country_name',1,'Austria',null,null); insert into pgtestVT$(vid,k,t,v,vn,vt) values(7,'country_code',1,'at',null,null); commit; truncate table pgtestGE$; insert into pgtestGE$(EID,SVID,DVID,EL,K,T,V,VN,VT) values(1000,1,6,'locatedIn','weight',null,3,1.0,null); insert into pgtestGE$(EID,SVID,DVID,EL,K,T,V,VN,VT) values(1001,2,6,'locatedIn','weight',null,3,1.0,null); insert into pgtestGE$(EID,SVID,DVID,EL,K,T,V,VN,VT) values(1002,3,6,'locatedIn','weight',null,3,1.0,null); insert into pgtestGE$(EID,SVID,DVID,EL,K,T,V,VN,VT) values(1003,4,6,'locatedIn','weight',null,3,1.0,null); insert into pgtestGE$(EID,SVID,DVID,EL,K,T,V,VN,VT) values(1004,5,7,'locatedIn','weight',null,3,1.0,null); commit; select * from pgtestVT$; select * from pgtestGE$;
Das HR Schema als Property Graph
Zusätzlich soll ein zweiter Graph erstellt werden. Dieser wird mit Daten aus dem HR Schema aus den Oracle Database Examples gefüllt. Die Daten müssen in ein lesbares Format transferiert werden, hier auch wieder das Oracle Flat File Format.
Ein relationales Schema kann recht einfach in ein Property Graph Datenmodell umgewandelt werden. Hier sind die wesentlichen Schritte und zu beachtende Hinweise:
Die Transformation selbst ist einfaches SQL, also leicht verständlich. Das vollständige SQL Skript befindet sich in diesem GitHub Repository.
-- -- Schema/Nutzer: HR -- -- Schritt 1: Sequenzen, um Surrogate Keys zu erzeugen create sequence seq_nodes; create sequence seq_edges start with 800000; -- Zusätzliche Spalte mit Surrogate Key f�r Tabellen mit nicht-numerischem Primärschlüssel alter table jobs add (surrogate_key number(10)); update jobs set surrogate_key = seq_nodes.nextval; alter table countries add (surrogate_key number(10)); update countries set surrogate_key = seq_nodes.nextval; commit; -- Schritte 2 bis 4 create or replace view tmp_regions_nodes as select region_id + 100000 as vid , '_type' as k , 1 as t , 'region' as v , null as vn , null as vt from hr.regions union all select region_id + 100000 as vid , 'region_name' as k , 1 as t , region_name as v , null as vn , null as vt from hr.regions order by 1,2; create or replace view tmp_jobs_nodes as select surrogate_key + 200000 as vid , '_type' as k , 1 as t , 'job' as v , null as vn , null as vt from hr.jobs union all select surrogate_key + 200000 as vid , 'job_id' as k , 1 as t , job_id as v , null as vn , null as vt from hr.jobs union all select surrogate_key + 200000 as vid , 'job_title' as k , 1 as t , job_title as v , null as vn , null as vt from hr.jobs union all select surrogate_key + 200000 as vid , 'min_salary' as k , 2 as t , to_char(min_salary) as v , min_salary as vn , null as vt from hr.jobs union all select surrogate_key + 200000 as vid , 'max_salary' as k , 2 as t , to_char(max_salary) as v , max_salary as vn , null as vt from hr.jobs order by 1,2; -- Analog vorgehen für Tabellen LOCATIONS, EMPLOYEES, DEPARTMENTS, COUNTRIES -- Schritte 5 bis 8 create table tmp_countries_rel_regions as select seq_edges.nextval as eid , c.surrogate_key + 700000 as country_id , r.region_id + 100000 as region_id from hr.countries c join hr.regions r on c.region_id = r.region_id; create or replace view tmp_countries_regions_edges as select eid , country_id as svid , region_id as dvid , 'is_located_in' as el , 'weight' as k , 3 as t , null as v , 1.0 as vn , null as vt from tmp_countries_rel_regions order by 1,2,3; create table tmp_departments_rel_locations as select seq_edges.nextval as eid , d.department_id + 500000 as department_id , l.location_id + 300000 as location_id from hr.departments d join hr.locations l on d.location_id = l.location_id; create or replace view tmp_departments_locations_edges as select eid , department_id as svid , location_id as dvid , 'is_located_in' as el , 'weight' as k , 3 as t , null as v , 1.0 as vn , null as vt from tmp_departments_rel_locations order by 1,2,3; -- Analog vorgehen für 5 weitere Fremdschlüsselbeziehungen -- Rechte vergeben auf neu angelegten Tabellen und Views begin for x in ( select table_name from user_tables where upper(table_name) like 'TMP%' ) loop execute immediate 'grant select on ' || x.table_name || ' to pguser'; end loop; end; / begin for x in ( select view_name from user_views where upper(view_name) like 'TMP%' ) loop execute immediate 'grant select on ' || x.view_name || ' to pguser'; end loop; end; / -- -- Schema: PGUSER -- -- Graph definieren exec opg_apis.create_pg('hrgraph', 2, 2, 'PGTBS'); -- Knoten laden insert /*+ APPEND */ into HRGRAPHVT$ (vid,k,t,v,vn,vt) select * from hr.tmp_countries_nodes where v is not null; commit; insert /*+ APPEND */ into HRGRAPHVT$ (vid,k,t,v,vn,vt) select * from hr.tmp_departments_nodes where v is not null; commit; insert /*+ APPEND */ into HRGRAPHVT$ (vid,k,t,v,vn,vt) select * from hr.tmp_employees_nodes where v is not null; commit; insert /*+ APPEND */ into HRGRAPHVT$ (vid,k,t,v,vn,vt) select * from hr.tmp_jobs_nodes where v is not null; commit; insert /*+ APPEND */ into HRGRAPHVT$ (vid,k,t,v,vn,vt) select * from hr.tmp_locations_nodes where v is not null; commit; insert /*+ APPEND */ into HRGRAPHVT$ (vid,k,t,v,vn,vt) select * from hr.tmp_regions_nodes where v is not null; commit; select * from hrgraphvt$; -- Kanten laden select * from hrgraphge$; insert /*+ APPEND */ into hrgraphge$ (eid,svid,dvid,el,k,t,v,vn,vt) select * from hr.tmp_countries_regions_edges; commit; insert /*+ APPEND */ into hrgraphge$ (eid,svid,dvid,el,k,t,v,vn,vt) select * from hr.tmp_departments_locations_edges; commit; insert /*+ APPEND */ into hrgraphge$ (eid,svid,dvid,el,k,t,v,vn,vt) select * from hr.tmp_departments_managers_edges; commit; insert /*+ APPEND */ into hrgraphge$ (eid,svid,dvid,el,k,t,v,vn,vt) select * from hr.tmp_employees_departments_edges; commit; insert /*+ APPEND */ into hrgraphge$ (eid,svid,dvid,el,k,t,v,vn,vt) select * from hr.tmp_employees_jobs_edges; commit; insert /*+ APPEND */ into hrgraphge$ (eid,svid,dvid,el,k,t,v,vn,vt) select * from hr.tmp_employees_managers_edges; commit; insert /*+ APPEND */ into hrgraphge$ (eid,svid,dvid,el,k,t,v,vn,vt) select * from hr.tmp_locations_countries_edges; commit; select * from hrgraphge$;
Analyse eines Property Graph
Bei der Analyse eines Graphen steht häufig im Mittelpunkt, wie über die Knoten und Kanten traversiert werden kann oder welche Muster im Graph erkannt werden. Dafür gibt es eine Vielzahl von Algorithmen aus der Graphentheorie, die Anwendung finden.
Die Unterstützung von Property Graphs für die Oracle Datenbank sieht die Möglichkeit vor, solche Algorithmen als Prozeduren des PL/SQL Pakets OPG_APIS aufzurufen oder von außerhalb der Datenbank über die Java API. Letztere ist sehr umfangreich und beinhaltet vorimplementiert ungefähr 40 Algorithmen. Diese sind in einem Analyse-Framework zusammengefaßt. Die Algorithmen laden den Graph oder Teile davon in den Hauptspeicher und analysieren diesen dort parallel.
Hier und heute möchte mich in diesem Abschnitt darauf beschränken, eine "Kürzeste-Wege"-Berechnung (engl. Shortest Path) mittels des zuvor erwähnten PL/SQL Package durchzuführen. Der dafür verwendete Algorithmus ist Dijkstra. Dieser ermittelt die kürzeste Verbindung zwischen 2 Knoten unter Berücksichtung von Gewichtungen auf den Kanten und gibt diese aus. Als ein einfaches Beispiel nutze ich die hierarchische Struktur von EMPLOYEES und deren Beziehung zu DEPARTMENTS. Der Ausgangsknoten ist dabei das Department mit dem Namen "IT", der Zielknoten ist Employee "Stephen King".
declare wt1 varchar2(100); -- Arbeitstabelle n number; path varchar2(1000); weights varchar2(1000); begin -- Vorbereitender Schritt opg_apis.find_sp_prep('hrgraphGE$', wt1); dbms_output.put_line('Arbeitstabelle: ' || wt1); -- Berechnung opg_apis.find_sp( 'hrgraphGE$', 500060, -- Startknoten: Department mit ID 60 (plus Offset) 400100, -- Zielknoten: Employee mit ID 100 (plus Offset) wt1, -- Arbeitstabelle (für Dijkstra Algorithmus) dop => 1, -- Grad der Parallelisierung stats_freq=>1000, -- Frequenz zum Sammeln von Statistiken path_output => path, -- Kürzester Weg als Sequenz von Knoten (Shortest Path) weights_output => weights, -- Gewichtung der Kanten options => null ); dbms_output.put_line('Pfad: ' || path); dbms_output.put_line('Gewichtungen: ' || weights); -- Aufräumen opg_apis.find_sp_cleanup('hrgraphGE$', wt1); end; /
Das Ergebnis sieht dann folgermaßen aus:
Arbeitstabelle: "HRGRAPHGE$$TWFS205" Pfad: 500060 400103 400102 400100 Gewichtungen: 4 3 1 1 1
Eine solche Abfrage geht natürlich auch mit SQL über Joins und die CONNECT BY Klausel. Je entfernter jedoch die Knoten voneinander liegen, je unterschiedlicher die Informationstypen sind, umso aufwändiger ist die Berechnung in einem relationalen Datenmodell. Je höher der Grad der Komplexität eines Graphen ist, umso effizienter sind die Algorithmen der Graph-Analyse. Wie eingangs schon erwähnt, werde ich auf die Analyse und das Visualisieren von Graphen zu einem späteren Zeitpunkt detailliert eingehen. Als kleine Vorausschau will ich hier aber schon mal den erzeugten HRGRAPH visuell abbilden [Abb. 4].
Abb. 4: HRGRAPH dargestellt mit Cytoscape 3.7.1 (unter Nutzung eines Plugins für die Oracle Datenbank)
Lizenzhinweis
Property Graph ist ein Feature der Option Spatial and Graph, welche die Oracle Datenbank Enterprise Edition voraussetzt. Zusätzlich wird die Option Partitioning benötigt.
Weitere Informationen
Zurück zur Community-Seite