Data Cartridges für Oracle 8i am Beispiel räumlicher Daten Notizen zum Begleitkurs zur Vorlesung Datenbanksysteme II Sommersemester 2000 Universität Hannover Institut für Informatik Dr. H. H. Brüggemann Carsten Kleiner 1 1 Einführung Objekt-relationale Datenbanksysteme bieten einige der Vorteile von objekt-orientierten Systemen (benutzerdefinierte Datentypen, Objektidentität, Vererbung, Polymorphismus über late binding) ohne die Vorteile von traditionellen relationalen Datenbanksystemen aufzugeben. Ausführlich werden objekt-relationale DBMS u. a. in [SB99] beschrieben. Ab der Version 8.1.5 (wird im allgemeinen kurz Oracle 8i genannt) bietet das DBMS Oracle einige objekt-relationale Konzepte an. Es kann jedoch (noch) keine Vererbung und damit auch kein Polymorphismus verwendet werden. Man hat jedoch die Möglichkeit, eigene Datentypen zu definieren und deren effiziente Verwaltung direkt in den Kern des Datenbankmanagementsystems zu integrieren (durch benutzerdefinierte Indexstrukturen und Anfragebearbeitung). Einen Datentyp (objekt-orientiert auch Klasse genannt) zusammen mit seinen Operationen (Methoden) und Informationen zur effizienten Verwaltung realisiert man mithilfe eines sogenannten Data Cartridge. Dies ist eine Sammlung von SQL-Skripten und Programmen in PL/SQL und möglicherweise weiteren Programmiersprachen, die die o. g. Aufgaben erfüllen. Die genaue Realisierung ist in [Ora99f] beschrieben, woraus auch Teile der folgenden Kapitel stammen. 2 Data Cartridges für komplexe Datentypen Um komplexe Objekte der realen Welt (das Spektrum reicht von einfachen 2-dimensionalen Punkten bis zu komplexen Sound- oder Bildsequenzen) in einer Datenbank effizient verwalten zu können, benötigt man komplexe Datentypen sowie Möglichkeiten, mit diesen Datentypen zu operieren. Dazu dienen in Oracle 8i sogenannte Data Cartridges (sinngemässe Übersetzung: abstrakte Datentypen). Die Inhalte werden direkt in den Datenbank-Server integriert, um diesen für die neuen Datentypen zu erweitern. Abbildung 1: Data Cartridge als Erweiterung des DBMS-Servers Ein Data Cartridge wird als Paket (package) deklariert (mit create package) und später implementiert (mit create package body). 2 create [or replace] package <Paketname> fauthid [current userjdefiner]g as <Paketdeklaration>; create [or replace] package body <Paketname> as <Paketdefinition>; Innerhalb eines solchen Paketes müssen alle erforderlichen Routinen für den entsprechenden Datentyp vereinbart werden. In der Deklaration können dabei Funktionen, Prozeduren, Objekttypen und andere Datenbank-Objekte deklariert werden. Diese können entweder mit den Zugriffsrechten des ausführenden Benutzers (current user) oder des Programmierers (definer) ausgeführt werden. In der Deklaration werden die Prototypen der Funktionen und Prozeduren angegeben, die in der Implementierung (create package body) dann implementiert werden müssen. In Oracle 8i gibt es einige vordefinierte neue Datentypen, um objekt-relationale Konzepte zu realisieren (siehe Vorlesung DBS I: Kollektionen [VARRAY, NESTED TABLE], Referenzen [REF], Typen für grosse unstrukturierte Objekte [BLOB, CLOB]), sowie benutzerdefinierte Datentypen, die hier den Schwerpunkt bilden. 2.1 Komponenten eines Data Cartridge Ein benutzer-definierter Objekttyp kapselt die zu einem Objekt der realen Welt gehörigen persistenten Daten (Attribute) und auf diesen Daten definierte Operationen (Methoden) mit ihrer Signatur. create [or replace] type <Typname> fauthid [current userjdefiner]g as object (<Elementliste>); Analog zur Deklaration eines Pakets können die Methoden mit den Zugriffsrechten des Anwenders oder des Programmierers ausgeführt werden. Die vereinbarten Methoden zu einem Typ müssen dann in einem sogenannten Rumpf (type body) implementiert werden. create [or replace] type body <Typname> as f<Funktionsdefinition>j<Prozedurdefinition>g end Hierbei ist zu beachten, dass es möglich ist, Methoden in anderen Progammiersprachen (zur Zeit C und Java) zu implementieren und im PL/SQL-Rumpf lediglich den genauen Aufruf dieser Methode zu spezifizieren. Je nach gewählter Programmiersprache sind noch weitere Aktionen erforderlich, um die externen Funktionen dem Datenbank-Server bekannt zu machen (siehe [Ora99j] für C-Funktionen und [Ora99g] für Java-Methoden). Es ist ebenfalls möglich, Methoden durch externe Bibliotheken (external library) zu implementieren. Um den Zugriff auf die neuen Datentypen zu beschleunigen, können mithilfe des extensible indexing benutzerdefinierte Indexe auf diesen Datentypen angelegt (siehe Kapitel 3.3) sowie mithilfe des extensible optimizer Methoden zur benutzerunterstützten Anfrageoptimierung vereinbart werden (siehe Kapitel 3.4). Letzteres ist vor allem sinnvoll, wenn an das System Anfragen über mehrere Domains gestellt werden sollen. 3 2.2 Grobe Schritte bei der Entwicklung eines Data Cartridge Genrell erfolgt die Entwicklung eines Data Cartridge nach dem Diagramm in Abbildung 2. Abbildung 2: Schema zur Entwicklung eines Data Cartridge ([Ora99f]) Je nach Komplexität des zu modellierenden Objekttyps können also maximal die folgenden Schritte erforderlich sein: • Definition von Objekttypen (create type, siehe 3.1) • Implementierung der Methoden der Objekttypen (create type body, siehe 3.2) • Definition und Implementierung eines Index (create indextype, siehe 3.3) • Definition und Implementierung von Funktionen für eine erweiterte Anfrageoptimierung (analyze, siehe 3.4) 4 Diese einzelnen Schritte werden im nächsten Kapitel näher untersucht und beschrieben. 3 Entwicklung eines Data Cartridge 3.1 Definition von Objekttypen Zunächst sind alle Objekttypen mit ihren gewünschten Attributen und Methoden zu definieren. create [or replace] type <Typname> fauthid [current userjdefiner]g as object (<Attributvereinbarung>f,<Attributvereinbarung>g f,<Methodendeklaration>g f,<Ordnungsfunktionsdeklaration>g f,<Pragmadefinition>g ); / Als Datentyp für die Attributvereinbarungen können Standarddatentypen oder vorher deklarierte benutzer-definierte Typen verwendet werden (zirkuläre Abhängigkeiten von Typen können durch Vorwärtsverweise (create type <Typname>;) aufgelöst werden). Eine Attributvereinbarung hat die gleiche Syntax wie eine Spaltenvereinbarung einer relationalen Tabelle. Die Methodendeklarationen geben nur die Signatur der Methoden an: member function <Funktionsname> [(<Parameter>f,<Parameter>g)] return <Typ> member procedure <Prozedurname> [( <Parameter> f,<Parameter> g)] Um Objekte benutzer-definierter Datentypen auch strukturell (und nicht nur auf Gleichheit der Adressen) vergleichen und ordnen zu können, gibt es spezielle mapund order-Methoden, die spezielle inhaltliche Vergleiche implementieren können. Zusätzlich zu objektbezogenen Methoden, die auf das zugehörige Objekt mit self zugreifen können, ist auch die Vereinbarung statischer Objekttypmethoden möglich: es ist dann member durch static zu ersetzen. Um Funktionen optimal ausführen zu können, ist es hilfreich dem Datenbank-Server mitzuteilen, ob und welche Seiteneffekte eine Methode hat. Dies geschieht in der Typdeklaration durch Angabe von sogenannten Pragmas zusätzlich zu den Methodendeklarationen: pragma restrict references (<Methodenname>,f[wnds,wnps,rnds,rnps,trust]g) Durch Angabe von wnds garantiert man beispielsweise, dass die Methode keine schreibenden Datenbankzugriffe ausführt (writes no database state). Näheres dazu findet man in [Ora99j]. Wie solche selbstdefinierten Typen bei der Definition von Tabellen eingesetzt und später in SQL angefragt werden können, wurde bereits im Kurs zu Datenbanksysteme I besprochen. Konstruktoren zum Erstellen von Objekten eines Objekttyps werden von Oracle 8i automatisch erzeugt. Ein Objekt wird mit <Typname>(<Attributwerteliste>) erzeugt. Dabei müssen exakt so viele Argumente angegeben sein, wie 5 der Objekttyp Attribute besitzt; außerdem müssen die Werte den deklarierten Datentypen der Attribute in der Reihenfolge der Deklaration entsprechen. NULL-Werte sind zulässig, sofern sie nicht ausdrücklich ausgeschlossen wurden. CREATE OR REPLACE TYPE point AS OBJECT ( x1 REAL, x2 REAL, MEMBER FUNCTION getcoord(dim INTEGER) RETURN REAL, MEMBER PROCEDURE setcoord(dim INTEGER, value REAL), MEMBER FUNCTION distance(frompoint POINT) RETURN REAL, STATIC FUNCTION dimension RETURN INTEGER, MAP MEMBER FUNCTION x then y RETURN REAL); / Abbildung 3: Deklaration des selbstdefinierten Objekttyps point Soll ein einmal definierter Objekttyp gelöscht werden (oder in wesentlichen Teilen geändert), so kann man drop type <Typname> [force]; verwenden. Falls es von diesem Typ abhängige Datenbankobjekte gibt, so kann der Typ nicht gelöscht werden, es sei denn, es wird die Option force angegeben. Dann kann allerdings ein konsistenter Datenbankzustand nicht mehr garantiert werden. Um einen Typ zu verändern verwendet man alter type. Allerdings sind die Möglichkeiten der Änderung begrenzt (i. w. können Methoden hinzugefügt werden). Bei grösseren Änderungen bietet sich ein drop type mit anschliessendem modifizierten create type an. 3.2 Implementierung von Methoden Die in einer Typdeklaration deklarierten Funktionen und Prozeduren müssen im Typrumpf implementiert werden. Dabei besteht die Möglichkeit, die Methoden direkt durch einen PL/SQL-Block zu implementieren, oder sie können durch die Spezifikation des Aufrufs einer Java-Methode oder einer externen C-Funktion implementiert werden. 3.2.1 Implementierung in PL/SQL Wird eine Methode direkt implementiert, so ist eine herkömmliche PL/SQL-Funktion bzw. -Prozedur zu schreiben (siehe [Ora99j] und Datenbanksysteme I). [memberjstatic] procedure <Prozedurname> (<Parameterliste>) is <PL/SQL-Block> [memberjstatic] function <Funktionsname> (<Parameterliste>) return <Typ> is <PL/SQL-Block> 6 Es sind in diesen Funktionen alle Datentypen auch benutzer-definierte (einschliesslich dem gerade implementierten) erlaubt. Ebenso können Kollektionen (⇒ DBS I) eingesetzt werden. Die PL/SQL-Syntax ist etwas gewöhnungsbedürftig; PL/SQL bietet aber den Vorteil einer sehr engen Integration in das DBMS, so dass sehr elegant auf die verwalteten Datenbestände zugegriffen werden kann. Diese Art der Implementierung bietet sich also für Funktionen mit Zugriff auf grössere Teile des Datenbestandes an. CREATE LIBRARY geolib AS ’/home/ck/geolib.so’; / ALTER JAVA CLASS “GeoPackage.Point“ COMPILE; CREATE OR REPLACE TYPE BODY point AS MEMBER FUNCTION getcoord(dim INTEGER) RETURN REAL IS BEGIN IF (dim=1) THEN RETURN x1; ELSIF (dim=2) THEN RETURN x2; END IF; END; MEMBER PROCEDURE setcoord(dim INTEGER, value REAL) IS BEGIN IF (dim=1) THEN x1:=value; ELSIF (dim=2) THEN x2:=value; END IF; END; MEMBER FUNCTION distance(frompoint POINT) RETURN REAL is language java name ’GeoPackage.Point.distance(GeoPackage.Point) return float’; STATIC FUNCTION dimension RETURN INTEGER BEGIN RETURN 2; END; MAP MEMBER FUNCTION x then y RETURN REAL is language c name “map2to1“ library geolib with context; END; / Abbildung 4: Implementierung des Objekttyp point 3.2.2 Implementierung in Java Seit kurzem ist auch eine Implementierung von Methoden durch den Aufruf von Java-Methoden möglich. Syntaktisch ist dies relativ einfach: [memberjstatic] procedure <Prozedurname> (<Parameterliste>) is language java name ’<Java-Methodenaufruf>’ Für Methoden mit Rückgabewert ist eine analoge Implementierung über function möglich. 7 Semantisch ist die Verwendung solcher Methoden jedoch recht komplex: sollen nicht nur Standard-Datentypen sondern auch Klassen verwendet werden, so sind in der Java-Klasse entsprechende Interfaces zu implementieren, die die SQL-Datentypen der Argumente und Rückgabewerte auf Java-Datentypen abbilden. Im Beispiel der Funktion distance in Abbildung 4 ist eine mögliche Variante, dass die Klasse GeoPackage.Point das Interface SQLData implementiert (siehe Abbildung 5). Dieses von Oracle mitgelieferte Interface umfasst drei Methoden und ein Attrribut: das Attribut sql type für den entsprechenden Typnamen in SQL mit einer get-Methode sowie die Methoden writeSQL bzw. readSQL, die die Attribute des SQL-Objekttypen auf Attribute der Java-Klasse abbilden. Weitere Details findet man in [Ora99g]. Dort werden auch andere Besonderheiten beim Aufruf von Java-Konstruktoren behandelt. package GeoClass; import oracle.jdbc2.*; public class Point implements SQLData f private float x; private float y; . . . weitere Attribute . . . public float distance (Point otherpoint) f . . . g // implements SQLData String sql type; public String getSQLTypeName() throws SQLException f return sql type; g public void readSQL(SQLInput stream, String typeName) throws SQLException f sql type = typeName; x = stream.readFloat(); y = stream.readFloat(); g public void writeSQL(SQLOutput stream) throws SQLException f stream.writeFloat(x); stream.writeFloat(y); g g Abbildung 5: Fragment einer Java-Klasse zur Verwendung in Datenbankmethoden Auf jeden Fall müssen die Java-Sourcen der aufgerufenen Klassen (oder die .classDateien) vor der Benutzung auf der Shell-Ebene mit loadjava -u <Oracle-Connect-String> -v -r [-oci8jt] <Java-Klasse>.[classjjava] (Details hierzu siehe ebenfalls [Ora99g]) in die Datenbank geladen werden. Danach muss das Datenbankobjekt für die Java-Klasse noch in SQL-Plus mit alter java class “<Java-Klasse>“ compile; kompiliert werden, damit es dann aufgerufen werden kann. Bei der Ausführung von Java-Methoden als Implementierung einer Methode eines Datenbank-Objekttyps ist 8 es auch möglich wieder auf die Datenbank zuzugreifen. Dabei bedient man sich innerhalb der Java-Methode der herkömmlichen Datenbank-Anbindung über JDBC (siehe auch [Ora99h]). Eine Datenbankverbindung braucht in diesem Falle nicht mehr hergestellt zu werden (die Methode wird ja in der Datenbank ausgeführt, in der der Objekttyp existiert); syntaktisch erreicht man dies in Java durch Verwendung von Connection <Java-Variablenname> = new oracle.jdbc.driver.OracleDriver().defaultConnection(); um innerhalb des Java-Codes die Datenbankverbindung identifizieren zu können. 3.2.3 Implementierung in C Um Methoden eines Objekttyps in C zu implementieren, ist eine externe Bibliothek zu erstellen. In diese Bibliothek sind die fertig kompilierten C-Routinen aufzunehmen. Die Bibliothek ist der Datenbank mithilfe von create [or replace] library <DBS-Bibliothekname> as <Bibliothek-Dateiname>; bekanntzugeben. Dabei muss der Dateiname mit vollständigem Pfad angegeben werden. Um schliesslich einzelne Methoden im Typrumpf zu implementieren, wird [memberjstatic] procedure <Prozedurname> (<Parameterliste>) is language c [name “<C-Funktionsname>“] library <DBS-Bibliothekname> [with context] [parameters (<C-Parameterliste>)] angegeben. Dabei ist der C-Funktionsname der Name der C-Routine in der externen Bibliothek. Um aus der C-Routine Datenbankzugriffe ausführen zu können, ist die Klausel with context anzugeben. Sie gibt der Routine Zugriff auf die Datenbankumgebung und ihre Dienste. Diese Zugriffe erfolgen über das Oracle Call Interface (OCI) und sind recht komplex, daher werden sie hier nicht näher beschrieben (Details siehe [Ora99e]). Mit der optionalen zweiten Parameterliste können Informationen für Parameter beim Aufruf der C-Routine angegeben werden. Hier können beispielsweise Datentypen der Datenbank auf Datentypen in C abgebildet werden. Ein wesentlicher Unterschied zwischen diesen echt externen Funktionen und den im letzten Abschnitt beschriebenen gespeicherten Funktionen (stored procedures) ist, dass externe Funktionen in einem anderen Adressraum als der Datenbankserver ausgeführt werden, während gespeicherte Funktionen im gleichen Adressraum ausgeführt werden. Möglicherweise ergeben sich dadurch Vorteile in der Leistungsfähigkeit für gespeicherte Funktionen. 3.3 Operatoren und Indexe für spezielle Domains Hat man wie in den vorherigen Abschnitten beschrieben einen neuen Objekttyp angelegt, so erfolgt der Zugriff auf eine Spalte dieses Typs in einer Datenbanktabelle sequentiell. Da das DBMS die interne Struktur des neuen Objekttyps nicht einschätzen kann, sind zunächst keine typspezifischen Anfragen möglich. Hat man z. B. einen Objekttyp Rectangle angelegt, so möchte man Anfragen nach allen Rechtecken einer Tabelle, die ein gegebenes Rechteck überlappen (d. h. in einem gegebenen Bereich 9 liegen), effizient beantworten können. Funktionen, die auf solchen selbstdefinierten Datentypen arbeiten, nennt man in Oracle 8i auch Operatoren. 3.3.1 Operatoren Operatoren werden in zwei Schritten implementiert. Zunächst ist eine PL/SQL-Funktion zu schreiben, die die gewünschte Funktion ausführt. So ist z. B. eine Funktion overlaps(Rectangle, Rectangle) return boolean zu schreiben, die überprüft, ob sich zwei Objekte vom Typ Rectangle überlappen. Anschliessend ist ein Datenbankobjekt vom Typ Operator anzulegen, das dem Datenbank-Server die definierte Funktion als Operator auf dem neuen Datentyp erklärt. create operator <Operatorname> binding (<Datentypliste>) return <Datentyp> using <Funktionsname>; Der Funktionsname ist dabei der Name der zuvor definierten PL/SQL-Routine, deren Signatur natürlich mit der hier angegebenen übereinstimmen muss. Diese Deklaration ist erforderlich, damit benutzer-definierte Indexe und Anfrageoptimierung vom Datenbank-Server verwendet werden können: diese werden nämlich nur für whereKlauseln der Formen • <Operator> <RelOp> <Konstante>, • <Konstante> <RelOp> <Operator> sowie • <Operator> like <Konstante> eingesetzt. Daher ist die Verwendung einer benutzer-definierten Funktion in der where-Klausel einer Anfrage nie index-unterstützt. CREATE FUNCTION pointdistance (first point, second point) RETURN real AS BEGIN RETURN sqrt(((first.x1 - second.x1)**2)+((first.x2 - second.x2)**2)); END; / CREATE OPERATOR opdistance BINDING (point, point) RETURN real USING pointdistance; / Abbildung 6: Operator für Punktabstand 3.3.2 Indexe Nach der Definition von Operatoren auf einem neuen Datentyp, können Anfragen an Spalten dieses Datentyps nur sequentiell beantwortet werden: um alle Rechtecke zu finden, die ein gegebenes Rechteck überlappen, muss für alle Rechtecke der Tabelle der Operator overlaps ausgeführt werden. Danach werden alle Zeilen, für deren Rechtecke der Operator wahr zurückgibt ausgegeben. Diese Methodik kann bei relativ komplexen Funktionen (z. B. Überlappung bei allgemeinen Polygonen) zu langen Antwortzeiten führen. Um diese Zeiten zu verbessern, bietet sich die Einführung von 10 Indexen für spezielle Operatoren an. Beispielsweise könnte man den gesamten Raum aller Rechtecke in kleine Quadrate einteilen und die Überlappungsoperation nur für Rechtecke ausführen, die ein gemeinsames Quadrat mit dem gesuchten Bereich besitzen. Um diese sogenannten Kandidaten herauszufinden, bietet sich ein Index an, der schnell zu einem gegebenen Quadrat alle überlappenden Rechtecke bestimmen kann. Um einen Operator indexunterstützt auswerten zu können, sind mehrere Schritte erforderlich. Zunächst ist ein benutzerdefinerter Objekttyp anzulegen, der das Interface ODCIIndex (Oracle Data Cartridge Interface) implementiert. Dazu gehört die Implementierung zahlreicher Funktionen zur Implementierung des Index (Details siehe Kapitel 15 in [Ora99f]). Beispielsweise dient die Funktion ODCIIndexCreate der Initialisierung eines benutzer-definierten Index (z. B. durch Erzeugung der notwendigen Datenstrukturen, da auch die physische Speicherung der Indexeinträge zu implementieren ist). Sie wird vom DBMS bei Erzeugung des Index automatisch ausgeführt. Die Funktion ODCIIndexFetch soll gefundene Objekte zurückgeben. CREATE OR REPLACE TYPE treetable AUTHID CURRENT USER AS OBJECT ( name VARCHAR2(60), MEMBER PROCEDURE createtable(SELF IN treetable), MEMBER FUNCTION getnode(rid ROWID) RETURN rsnode, MEMBER FUNCTION createnode(node rsnode) RETURN ROWID, ... ) / CREATE OR REPLACE TYPE BODY treetable AS MEMBER PROCEDURE createtable(SELF IN treetable) IS ... END; / Abbildung 7: Objekttyp für benutzerdefinierte Indextabelle Die Implementierung eines Indextyps besteht dabei wiederum aus einem create type sowie einer Implementierung mit create type body. Da der Programmierer jedoch für die Verwaltung, Speicherung und den Zugriff auf die eigentlichen Indexeinträge selbst verantwortlich ist, wird häufig ein weiterer Objekttyp definiert, der diese Aufgaben übernimmt. Im Beispiel stellt der Objekttyp treetable eine Tabelle zur Speicherung allgemeiner Informationen über den Index sowie eine Tabelle für die eigentlichen Indexeinträge zur Verfügung. Methoden dieses Objekttyps werden dann im eigentlichen Objekttyp für den Index (rstree ind) verwendet. Beide Typen werden durch das weiter oben erklärte Verfahren mit create type und create type body implementiert. Schliesslich ist der Index-Objekttyp im DBS mittels create indextype <DBS-Indextypname> for <Operatorname>(<Parameterliste>) using <Typname>; als Indextyp zu erklären. Nun muss zur Verwendung des Index nur noch auf der Spalte einer Tabelle des zu indexierenden Datentyps mit create index <Indexname> on <Tabellenname>(<Spaltenname>) 11 indextype is <DBS-Indextypname>; ein Index angelegt werden. Das DBMS erzeugt mithilfe der implementierten ODCIIndex-Routinen nun automatisch den gewählten Index. Außerdem wird der Index bei allen Anfragen, in denen in der where-Klausel der oben definierte Operator verwendet wird, zur Effizienzsteigerung eingesetzt. Das gilt jedoch nur, wenn die indexierte Spalte als erstes Argument des Operators verwendet wird. Zum Ändern und Löschen von Indexen gibt es die Operationen alter index <Indexname> <Indexänderungsklausel>; drop index <Indexname> [force]; Details zur Indexänderungsklausel findet man in [Ora99i]. CREATE OR REPLACE TYPE rstree ind AUTHID CURRENT USER AS OBJECT ( rstree treetable, cmpvalue Point, opname VARCHAR2(30), retval NUMBER, STATIC FUNCTION ODCIGetInterfaces(ifclist OUT sys.ODCIObjectList) RETURN NUMBER, STATIC FUNCTION ODCIIndexCreate (ia sys.ODCIindexinfo, parms VARCHAR2) RETURN NUMBER, STATIC FUNCTION ODCIIndexInsert(ia sys.ODCIindexinfo, rid VARCHAR2, newval Point) RETURN NUMBER, STATIC FUNCTION ODCIIndexUpdate(ia sys.ODCIindexinfo, rid VARCHAR2, oldval Point, newval Point) RETURN NUMBER, MEMBER FUNCTION ODCIIndexFetch(SELF IN OUT rstree ind, nrows NUMBER, rids OUT sys.ODCIridlist) RETURN NUMBER, ... indextyp-spezifische Methoden . . . ); / CREATE OR REPLACE TYPE BODY rstree ind AS ... / CREATE INDEXTYPE rstree FOR opdistance(point, point) USING rstree ind; CREATE TABLE city (name VARCHAR2(40), location point); CREATE INDEX idx loc city ON city(location) INDEXTYPE IS rstree; Abbildung 8: Benutzerdefinierter Indextyp rstree 3.4 Anfrageoptimierung Um Anfragen an die Datenbank wirklich effizient zu bearbeiten, reichen Indexe jedoch nicht aus. So ist beispielsweise das Durchsuchen eines Index zur Ermittlung von Kandidaten für ein Anfrageergebnis sinnlos, wenn alle Einträge zu Kandidaten werden. In diesem Falle hätte man die Zeit für das Durchsuchen des Index sparen 12 können. Die effiziente Bearbeitung einer Anfrage hängt also auch von deren Selektivität und den Kosten für die Indexauswertung ab. Um möglichst viele dieser Faktoren in die Optimierung der Anfragebearbeitung einbauen zu können, verwendet Oracle 8i eine kostenbasierte Anfrageoptimierung. Das DBMS kann natürlich bei benutzerdefinierten Operatoren keine Abschätzung dieser Faktoren vornehmen. Daher kann der Programmierer selbst durch Implementierung bestimmter Methoden dem System Hilfen bei der Kostenabschätzung geben. Der (ältere) regelbasierte Optimierer von Oracle 8i kann nicht erweitert werden. Da die gesamte Thematik sehr komplex ist, sollen hier nur die prinzipiellen Grundlagen vorgestellt werden. Für benutzerdefinierte Indexe und Spalten mit Objekttypen können zusätzlich zu den systemgenerierten Standard-Statistiken noch benutzerdefinierte Statistiken erstellt werden. Dabei ist der Programmierer für deren Verwaltung und Speicherung verantwortlich. Weiterhin können für Prädikate (d. h. Klauseln im where-Teil einer Anfrage bzw. anderer DML-Operationen) der Formen • <Operator>(<Parameterliste>) <RelOp> <Konstante>, • <Konstante> <RelOp> <Operator>(<Parameterliste>) sowie • <Operator>(<Parameterliste>) like <Konstante> benutzerdefinierte Selektivitätsschätzungsfunktionen sowie Kostenfunktionen definiert werden. Diese wiederum können system- und benutzerdefinierte Statistiken verwenden. Dazu werden die erforderlichen Methoden in einem benutzerdefinierten Objekttyp implementiert. Dessen Methoden müssen gemäss dem Interface ODCIStats implementiert werden. Alle Funktionen dieses Interface, die realisiert wurden, werden automatisch vom Optimierer des Systems verwendet. Es gibt beispielsweise die Funktionen ODCIStatsCollect, ODCIStatsSelectivity, ODCIStatsFunctionCost. Die Details hierzu sind in Kapitel 8 bzw. 16 von [Ora99f] beschrieben. Benutzt werden die selbstdefinierten Statistiken, wenn nach Definition des Statistikdatentyps (siehe oben) dieser mit einem Datenbankobjekt in Verbindung gesetzt wird. Dies geschieht für Tabellenspalten mit associate statistics with columns <Tabellenname>.<Spaltenname> using <Statistiktypname>; und für andere Datenbankobjekte (Funktionen, Pakete, Typen, Indexe, Indextypen) mit associate statistics with <Assoziationsklausel>; Die genaue Syntax der Assoziationsklausel findet sich in [Ora99i]. Um Statistiken für ein Schemaobjekt erstellen zu lassen, wird das Kommando analyze <Schemaobjektart> <Schemaobjektname> compute statistics; verwendet. Dies aktualisiert unter Verwendung der vom Benutzer implementierten Routinen die zugehörigen Statistikeinträge. Diese werden dann bei der Anfragebearbeitung verwendet. Eine möglicherweise implementierte Selektivitätsschätzung sollte auf den so berechneten Statistiken operieren. 13 3.5 Weitere Konzepte beim Entwurf Einige weitere Dinge sind bei der Erstellung eines Data Cartridge von Bedeutung und werden hier kurz diskutiert. Realisierung von Vererbung Da es im Objektmodell von Oracle 8i keine Vererbung gibt, müssen Objekt-Hierarchien bei der Definition von Objekttypen auf andere Weise simuliert werden. Dazu gibt es i. w. drei Möglichkeiten: • Unterklasse enthält Oberklasse: Es werden Objekttypen für Unter- und Oberklassen angelegt. Die Unterklassen enthalten ein Attribut des Typs der Oberklasse. Alle Methoden der Oberklasse müssen in allen Unterklassen deklariert und definiert werden, die Oberklasse hat keine Methoden. Die Oberklasse wird nicht nach außen zur Verfügung gestellt. Diese Variante eignet sich vor allem für Oberklassen mit wenigen Methoden und Assoziationen bzw. Unterklassen mit zahlreichen Assoziationen. • Oberklasse enthält alle Unterklassen: Es werden Objekttypen für Unter- und Oberklassen angelegt. Die Oberklasse enthält ein Attribut für alle Unterklassen, wobei sichergestellt wird, dass für jedes Objekt nur genau eines dieser Attribute gesetzt ist. Die Oberklasse deklariert und definiert alle Methoden sämtlicher Unterklassen. Die Unterklassen sind nicht nach außen sichtbar. Dieses Modell eignet sich für wenige Unterklassen mit möglichst wenigen Methoden und für Klassen mit vielen 1:n-Beziehungen. • Duale Referenzen: Es werden Objekttypen für Unter- und Oberklassen angelegt. Die Oberklasse enthält ein referenziertes Attribut für alle Unterklassen, die Unterklassen enthalten ein referenziertes Attribut für die Oberklasse. In der Oberklasse wird sichergestellt, dass für jedes Objekt nur genau eines dieser Attribute gesetzt ist. Jede Klasse definiert die zu ihr gehörigen Methoden. Alle Klassen sind nach außen sichtbar. Diese Variante eignet sich besonders für komplexe Modelle, für die keines der beiden anderen Verfahren geeignet ist. Schwieriger ist hier die Implementierung, da mit vielen Referenzen gearbeitet wird. Die Wahl der richtigen Alternative ist nicht einfach; in einer späteren Version von Oracle ist mit einer internen Realisierung von Vererbung zu rechnen. Wahl der Implementierungssprache Eine Methode in C zu implementieren bietet sich immer dann an, wenn die Methode CPU-intensiv ist. Der zusätzliche Aufwand für den externen Aufruf muss durch eine kürzere Berechnungszeit der eigentlichen Funktion wettgemacht werden. PL/SQL hat Vorteile bei Methoden mit geringer CPU-Last und vielen Datenbankzugriffen (I/O). Java ist eine Art Zwitter: es wird im Adressraum des Servers ausgeführt und hat daher keinen Aufruf-Überhang, ist aber auch nicht unbedingt für CPU-intensive Berechnungen geeignet. Generell lohnt sich eine Implementierung in einer Sprache, in der schon grössere Mengen von Code zur Implementierung bereit stehen. 14 Performanz Um die Performanz eines selbstdefinierten Cartridges (und von SQL-Anfragen) zu untersuchen, steht die tkprof-Utility zur Verfügung. Details finden sich in [Ora99d]. Allgemeines Objekttypen sollten sehr sorgfältig entworfen werden, da eine Änderung sehr kompliziert ist, wenn das Cartridge bereits im Einsatz ist. Wenn eine grössere Zahl von Tabellen, Sichten und Objekten innerhalb des Cartridge verwendet werden sollen, bietet sich das Anlegen einer Metadatentabelle an, um die Wartbarkeit zu erhöhen. 3.6 Datentypen für Multimedia Um Mengen von relativ grossen, für das DBMS unstrukturierten Daten (wie z. B. Bilder, Sound- und Videoclips) abzuspeichern, bietet Oracle 8i einen Datentyp large object (LOB) an. Es könne Tabellenspalten von diesem Typ angelegt werden und mithilfe von sogenannten locators kann sogar auf bestimmte Stellen in diesen Daten zugegriffen werden (Details in [Ora99a]). Weiterhin gibt es ein fertiges PL/SQL-Paket für die Arbeit mit grossen Objekten (DBMS LOB), das in [Ora99c] ausführlich beschrieben wird. 4 Beispiel: Räumliche Daten In diesem Kapitel soll die Integration objekt-relationaler Konzepte in ein DBMS am Beispiel von räumlichen Daten vorgestellt werden. Zunächst wird dabei ein vom DBMS-Hersteller Oracle bereitgestelltes Cartridge für zweidimensionale Objekte vorgestellt. Im zweiten Teil wird als Alternative dazu einen benutzer-definierte Variante eines solchen Cartridge für die gleichen Objekte präsentiert. 4.1 Oracle Spatial In der aktuellen Version von Oracle 8i wird sowohl eine relationale Erweiterung für räumliche Daten aus früheren Versionen als auch eine Erweiterung unter Verwendung der objekt-relationalen Technik unterstützt. In diesem Abschnitt gehen wir nur auf die objekt-relationalen Konzepte ein, da das relationale Modell nur aus Kompatibilitätsgründen weiterhin zur Verfügung steht. Teile dieses Unterabschnitts folgen der Beschreibung in [Pfa00]. 4.1.1 Objekttyp SDO GEOMETRY Für die Speicherung von Objekten mit einer räumlichen Komponente im objektrelationalen Schema von Oracle8i wird eine beliebige vom Benutzer anzulegende Tabelle benötigt, die eine Spalte vom Objekttyp MDSYS.SDO GEOMETRY1 besitzt (MDSYS ist ein Pseudo-Benutzer, in dessen Schema Oracle 8i Spatial installiert wird). In dieser 1 SDO steht aus historischen Gründen für Spatial Data Option; so hieß ein Vorgänger des heutigen Spatial Cartridge in Oracle 7 15