Implementation von R*-Bäumen als benutzerdefinierte Indexstruktur in Oracle 8i Sascha Klopp 14. Dezember 1999 Zusammenfassung In dieser Studienarbeit wurde der in [BKSS90] beschriebene R*-Baum implementiert. Es handelt sich um eine Weiterentwicklung des R-Baums, einer Indexstruktur für mehrdimensionale Daten [Gut84]. Dazu wurde in Oracle 8i ein neuer Indextyp mit Hilfe des Extensible Indexing Interface angelegt. Dabei wurden die neuen objektorientierten Features der Programmiersprache PL/SQL erprobt, die es erlauben, komplexe Datentypen mit eigenen Zugriffs- und Manipulationsmethoden zu definieren. Inhaltsverzeichnis 1 R*-Bäume 1.1 Motivation . . . . . . . . 1.2 Aufbau . . . . . . . . . . 1.3 Algorithmen . . . . . . . 1.3.1 Einfügen . . . . . ChooseSubtree . Split . . . . . . . Forced Reinsert 1.3.2 Suchen . . . . . . 1.3.3 Löschen . . . . . . . . . . . . . . 4 4 7 7 8 8 9 11 12 13 2 Oracle 8i 2.1 Klassen in Oracle 8i . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Das „Extensible Indexing Interface” . . . . . . . . . . . . . . . 15 15 16 16 3 Implementation 3.1 Die Klasse Point . . . . . . . . . . 3.2 Die Klasse Rect . . . . . . . . . . . 3.3 Operatoren . . . . . . . . . . . . . 3.4 Die Klasse rectpointer . . . . . . . 3.5 Der Datentyp rsnode . . . . . . . 3.6 Die Klassen inspath und selpath 3.7 Die Klasse treetable . . . . . . . . 3.8 Implementierung des R*-Baums . . . . . . . . 20 20 21 22 22 23 23 24 26 4 Benutzung 4.1 Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Benutzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 32 33 . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Tests 5.1 Ergebnisse . . . . . . . . . . 5.1.1 Zufällige Rechtecke 5.1.2 Geodaten . . . . . . 5.2 Kommentierung . . . . . . . . . . A Die Eingabe für den SQL*Loader 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 35 36 36 37 38 Kapitel 1 R*-Bäume Nach einigen einführenden Bemerkungen werden die grundlegende Struktur des R*-Baums und die zugehörigen Algorithmen erläutert. Insbesondere werden die Unterschiede zum R-Baum verdeutlicht. 1.1 Motivation Datenbankmanagementsysteme bieten i. a. eine Vielzahl vordefinierter Datentypen an, die als Typdefinition in Relationen dienen können. In den allermeisten Fällen handelt es sich hierbei um skalare Datentypen, auf denen in sinnvoller Weise eine Ordnung definiert werden kann oder bereits definiert ist. Rowid i (integer) 1 2 3 4 5 6 7 8 47 13 42 11 88 9 56 0 Die Tupel können nach der Spalte i sortiert werden: 8, 6, 4, 2, 3, 1, 7, 5. Für den schnellen Zugriff auf solche Daten mittels eines Indexes benötigt man eine Datenstruktur, die es ermöglicht, ohne langes Suchen festzustellen, ob ein bestimmtes Datum in einer Relation vorhanden ist und wenn ja, wo es sich befindet. Hierzu wird i. a. der B*-Baum benutzt. Dieser benötigt allerdings die bereits erwähnte Ordnung auf dem Datentyp. 4 0..13 42..88 Basierend auf der i-Spalte kann ein 0 9 11 13 42 47 56 88 8 6 4 2 3 1 7 5 B*-Baum aufgebaut werden. Fängt man nun an, neue Datentypen zu entwickeln, gibt es oft mehrere sinnvolle, evtl. sogar gleichberechtigte Arten, eine Ordnung auf ihnen zu definieren. Dies ist z. B. der Fall bei mehrdimensionalen Datentypen wie Punkten im Raum oder in einer Ebene. Diese können nach jeder Dimension unabhängig sortiert werden. y 7 3 Es gibt zwei mögliche Sortierungen: 4 1 2, 1, 5, 4, 6, 3, 7 oder 6 1, 3, 2, 4, 5, 6, 7. 5 2 x Werden sogar ausgedehnte Objekte betrachtet, im einfachsten Fall Tupel von Intervallen, also Rechtecke oder Quader, gibt es zu jeder Dimension die Möglichkeit, entweder nach der unteren Grenze oder nach der oberen, oder auch nach dem Mittelpunkt, zu sortieren. Es ist klar, dass ein schneller Zugriff auf solche Daten nicht mit einem B*-Baum realisiert werden kann. Auch das Anlegen eines Indexes pro Dimension ist nicht sinnvoll, da dies kombinierte Anfragen, wie zum Beispiel die Suche nach allen Objekten in einem bestimmten Gebiet, nicht ausreichend unterstützt. Eine Datenstruktur, die diese neuen Anforderungen bewältigt, ist der sogenannte R-Baum [Gut84]. Anschaulich gesehen, fasst er benachbarte Objekte zu neuen, übergordneten Objekten zusammen. Im Prinzip ähnelt er dabei stark den bisherigen B*-Bäumen, allerdings benötigt er aufwendigere Algorithmen zum Einfügen und Suchen (s. 1.3). 5 y Rechtecke werden zu neuen, übergeorneten Rechtecken gebündelt. In dieser Darstellung wird aus Gründen der Übersichtlichkeit nicht das kleinste gewählt. x Man beachte die Tatsache, dass Überschneidungen nicht zu vermeiden sind. Dies liegt daran, dass es sich um ausgedehnte Objekte handelt. Sei zum Beispiel eine Menge von Rechtecken gegeben, die sich wie folgt überlappen: y x Dann kann man diese nicht so in zwei Gruppen einteilen, dass sich die übergordneten Rechtecke nicht überlappen. Diese Eigenschaft verkompliziert den Suchalgorithmus stark (s. 1.3.2). Der R*-Baum, um den es in dieser Arbeit geht, ist eine Weiterentwicklung hinsichtlich des Einfügealgorithmus [BKSS90]. Es hat sich nämlich gezeigt, dass z. B. die Entscheidung, in welchen Knoten neue Objekte am besten eingefügt werden, damit die Suche in dem Baum möglichst schnell geht, von vielen Parametern abhängt, die in der älteren Version zum Teil nicht berücksichtigt wurden. Es wurde ebenfalls festgestellt, dass der R-Baum mehr als der B*-Baum durch die ersten Eintragungen geprägt wird. Ein unglückliche Reihenfolge der Einfügungen kann also die Performance stark beeinträchtigen. Diese Erfahrungen wurden in der Entwicklung des R*-Baums berücksichtigt. 6 1.2 Aufbau An der Baumstruktur wurde gegenüber dem R-Baum nichts geändert, es folgt also nur ein kurzer Überblick: • Verweise auf Daten befinden sich nur in Blättern, die sich, wie im B*-Baum, alle auf der gleichen Höhe befinden. • Alle anderen Knoten enthalten Verweise auf Kindknoten zusammen mit dem kleinsten Objekt, das noch alle Objekte in dem referenzierten Knoten enthält. Dieses Objekt wird im Folgenden Verzeichnisrechteck genannt, obwohl diese Bezeichnung zweidimensionale Objekte suggeriert. Analog dazu heißen die Daten auch Datenrechtecke. • Jeder Knoten kann nur eine beschränkte Anzahl von Einträgen fassen. Diese Anzahl ist ein Parameter des Baums und sollte aus Effizienzgründen so groß gewählt sein, dass ein Knoten gerade in einem Pufferblock Platz hat. Nach allgemeiner Konvention wird diese Zahl M genannt. Weiterhin wird eine Mindestanzahl vereinbart. Diese ist in allen Knoten bis auf den Wurzelknoten wirksam und wird mit m bezeichnet. Natürlich ist m kleiner als M, und damit der Split-Algorithmus (1.3.1) aus einem überfüllten Knoten zwei Knoten machen kann, gilt sogar: 1 2≤m≤ M 2 Ein kleines m unterstützt eine höhere Verzweigung des Baums, da die Knoten dann unter Umständen nicht mehr so voll werden. Ein grosses m sorgt dagegen für eine bessere Speicherauslastung. Ein optimaler Wert kann nur im Rahmen eines Testverfahrens gefunden werden. Es wurde gezeigt, dass m = 0.4 · M die beste Wahl ist [BKSS90]. 1.3 Algorithmen Es folgt eine Beschreibung der Algorithmen zum Einfügen, Löschen und Suchen in einem R*-Baum. Ein Update besteht aus dem Löschen des alten Eintrags und dem Einfügen des neuen. Auf eine andere Weise kann dies nicht vernünftig, ð/ so, dass das Suchen im Baum effizient bleibt, definiert werden. 7 1.3.1 Einfügen Der Einfügealgorithmus ist der wesentliche Unterschied zwischen R-Bäumen und R*-Bäumen. Es werden neue Verfahren zum Suchen des optimalen Blattes (ChooseSubtree), in das eingefügt werden soll, sowie zum Aufteilen von überfüllten Knoten (Split) verwendet. Auf die Unterschiede zum R-Baum wird bei den einzelnen Verfahren eingegangen. Neu ist das Konzept des „Forced Reinsert”, das im letzten Abschnitt beschrieben wird. ChooseSubtree Das erste Problem beim Einfügen in einen Baum ist das Finden des optimalen Blattes, das den neuen Eintrag aufnehmen soll. Drei Werte werden für die Wahl des nächsttieferen Knoten berücksichtigt: • Der neue Flächeninhalt des Kindknotens, würde er den neuen Eintrag aufnehmen. • Die Differenz zwischen aktuellem Flächeninhalt und neuem Flächeninhalt. • Die sogenannte Überlappung, die wie folgt definiert ist: Über lappung(Rk ) := n X F läche(Rk ∩ Ri ) , 1 ≤ k ≤ n i=1,i≠k wobei n die Knotengröße und Ri , 1 ≤ i ≤ n, die Rechtecke des aktuellen Knotens bezeichnen. Der ChooseSubtree-Algorithmus: 1. Setze K auf den Wurzelknoten. 2. Falls K ein Blatt ist, gebe K zurück, sonst: Falls die Einträge in K auf Blätter zeigen, wähle den Eintrag aus K, bei dem die kleinste Vergrößerung der Überlappung auftreten würde, falls der neue Eintrag hinzukäme. Bei Gleichstand wähle den Eintrag mit dem geringsten Flächenzuwachs, und danach den mit der kleinsten Fläche. Falls die Einträge in K nicht auf Blätter zeigen, wähle den Eintrag mit dem kleinsten Flächenzuwachs, falls der neue Eintrag hinzukommt, bei Gleichstand den mit der kleinsten Fläche. 8 3. Setze K auf den Knoten, auf den der gewählte Eintrag zeigt und fahre mit Punkt 2 fort. 15 2 10 5 3 1 5 4 0 6 5 10 15 20 In diesem Beispiel würde das kleine Quadrat in das Verzeichnisrechteck 4 eingefügt werden, falls die Rechtecke 1..6 Verzeichnisrechtecke von Blattknoten sind. Andernfalls würde ,,ChooseSubtree’’ das Rechteck Nummer 1 wählen. Der Algorithmus unterscheidet sich bis auf die Strategie kurz vor der Blattebene nicht von dem herkömmlichen „ChooseLeaf”-Algorithmus in [Gut84], dort wird auch das Blatt mit Hilfe des Flächenzuwachses bestimmt, die Überlappung wird nicht benutzt. Anzumerken ist, daß die Entscheidung, welche Knoten in diesem Algorithmus als Blätter betrachtet werden, davon abhängt, was für Daten eingefügt werden, denn falls während eines „Forced Reinsert” ein ganzer Teilbaum neu eingefügt wird, wird er als Blatt angesehen und alle Teilbäume dieser Höhe ebenfalls, so dass hinterher alle echten Blätter wieder die gleiche Höhe haben. Split Falls es notwendig wird, einen Knoten wegen Überfüllung zu teilen, wird der Splitalgorithmus verwendet. Ist ein Knoten überfüllt, besitzt er M + 1 Einträge. Ein Aufteilungsalgorithmus muss zwei Teilmengen liefern, die jeweils mindestens m Elemente beinhalten. 9 Der Algorithmus des R-Baums sucht aus den Einträgen zwei heraus, die geometrisch möglichst weit voneinander entfernt liegen und benutzt diese als Starteinträge für die neuen Teilknoten. Danach wird in einer Schleife jeweils einer der restlichen Einträge so einer der beiden Gruppen zugeordnet, dass ein möglichst kleiner Flächenzuwachs des zugehörigen Verzeichnisrechtecks entsteht. Das neue Verfahren benutzt als weitere Parameter die Randlänge und auch die Überlappung der entstehenden Teilknoten. Zur Vorbereitung des Aufteilvorgangs werden für jede Achse die Einträge einmal nach der kleineren Koordinate und einmal nach der größeren sortiert. Jede dieser 2n Sortierungen (n sei die Dimension) kann auf M − 2m + 2 Weisen geteilt werden, so dass jede Teilmenge noch mindestens m Elemente enthält. Die k-te Aufteilung sei diejenige, bei der die ersten (m-1)+k Einträge in der ersten Gruppe sind und die restlichen in der zweiten (1 ≤ k ≤ M − 2m + 2). 1 2 3 4 5 6 Ein Knoten mit 11 Einträgen kann bei einer Mindestgröße von 3 auf sechs verschiedene Arten pro Sortierung aufgeteilt werden. Eine dieser 2n(M − 2m + 2) Aufteilungen wird nun vom Splitalgorithmus ausgewählt. Dazu können für jede von ihnen drei Werte berechnet werden: • Flächenwert: Fläche[Box(erste Gruppe)] + Fläche[Box(zweite Gruppe)] • Randwert: Rand[Box(erste Gruppe)] + Rand[Box(zweite Gruppe)] • Überlappungswert: Fläche[Box(erste Gruppe) ∩ Box(zweite Gruppe)] Der Split-Algorithmus: 1. Sortiere die Einträge des Knotens, der geteilt werden soll, bezüglich jeder Dimension, sowohl nach der unteren Grenze als auch nach der oberen. 2. Bestimme für jede Sortierung die M − 2m + 2 Aufteilungen. 10 3. Berechne für jede Dimension die Summe S aller Randwerte von Aufteilungen entlang dieser Dimension. 4. Wähle aus den Aufteilungen derjenigen Dimension mit dem kleinsten Wert S diejenige aus, die den kleinsten Überlappungswert besitzt. Bei Gleichstand wähle diejenige mit dem kleinsten Flächenwert. Bekanntlich ist bei vorgegebener Fläche ein Quadrat das Rechteck mit der kleinsten Randlänge. Die Berücksichtigung des Randparameters bewirkt also, dass die Teilknoten quadratischer werden. Da sich Quadrate leichter packen lassen als beliebige Rechtecke, enthalten die übergordneten Verzeichnisrechtecke weniger Raum, der nicht von einem Eintrag in ihnen abgedeckt wird. Das Suchen wird also effizienter, da erfolglose Abstiege in diese Knoten vermieden werden. Forced Reinsert Wie bereits in der Einführung angemerkt, hängt die Suchperformance im R-Baum stark von der Einfügereihenfolge ab. Um nun diese Abhängigkeit ein wenig abzuschwächen, werden bei einem Überlauf eines Knotens die (geometrisch) äußeren Einträge aus dem Knoten entfernt und von ganz oben neu in den Baum eingefügt. Dies bewirkt eventuell ein Verschieben dieser Einträge in benachbarte Knoten. Dazu werden die Einträge nach dem Abstand ihres Mittelpunkts vom Mittelpunkt des Verzeichnisrechtecks sortiert. Die p Rechtecke, die am weitesten entfernt sind, werden aus dem Knoten gelöscht und neu in den Baum eingefügt. Es wurde in [BKSS90] festgestellt, dass p am besten 30 Prozent von M betragen sollte, weiterhin sollten die inneren Einträge vor den äußeren neu eingefügt werden. Falls während dieses Neueinfügens wiederum ein Knoten in der Blattebene überläuft, wird dieser Knoten allerdings aufgeteilt (mit dem Split-Algorithmus). Dabei kann der übergeordnete Knoten wiederum überlaufen, was wiederum ein „Forced Reinsert” anstößt, diesmal in der übergeordneten Ebene. Ein weiterer Überlauf in dieser Ebene während einer weiteren der p Neueinfügungen löst allerdings kein weiteres „Forced Reinsert” aus. Es wird also pro Ebene einmal versucht, einige Einträge aus dem zu vollen Knoten auf andere Knoten aufzuteilen, erst wenn das nicht klappt, wird ein Split vollzogen. Trivialerweise wird für den Wurzelknoten beim Überlauf kein „Forced Reinsert” versucht, sondern es wird gleich eine 11 neue Wurzel generiert, und die beiden Teilknoten, die der Aufteilalgorithmus liefert, werden als Kinder in diese neue Wurzel eingetragen. 1.3.2 Suchen Wird an einen B*-Baum eine Suchanfrage gestellt, muss lediglich einmal von oben nach unten in dem Baum hinabgestiegen werden, um zu erfahren, ob der gesuchte Wert enthalten ist oder nicht. Die Möglichkeit zur Bereichsanfrage kann leicht durch eine Verkettung der Blätter realisiert werden. Der allgemeine Suchalgorithmus für einen R-Baum ist komplexer als der eines B*-Baums, da beim Absteigen im Baum evtl. mehrere Pfade ein Ergebnis liefern können. Nach dem Abarbeiten eines Blattknotens kann es also nötig sein, in einem höheren Knoten weitere Kindknoten zu durchsuchen. Grundsätzlich werden vier Arten von Anfragen von einem R-Baum unterstützt, die jeweils alle ein Rechteck, das sog. Suchrechteck, als Argument erwarten. Diese sind im einzelnen: Finde alle Rechtecke, die 1. . . . dem gegebenen Rechteck entsprechen, 2. . . . das gegebene Rechteck schneiden, 3. . . . in dem gegebenen Rechteck enthalten sind, 4. . . . das gegebene Rechteck überdecken. Prinzipiell unterscheiden sich die zugehörigen Suchalgorithmen in der Art, wie die weiter zu betrachtenden Kindknoten in einem Oberknoten ausgewählt werden. Werden zum Beispiel alle in dem übergebenen Rechteck enthaltenen Rechtecke gesucht, müssen auch die Knoten betrachtet werden, deren Verzeichnisrechtecke das gegebene Rechteck nur schneiden. Für die anderen Arten der Suchanfragen gelten ähnliche Überlegungen. Beispielhaft wird der Suchalgorithmus beschrieben, der alle Datenrechtecke in dem übergebenen Teilbaum findet, die von dem Suchrechteck überdeckt werden, also für den Fall Nummer 3: • Falls der übergebene Knoten ein innerer Knoten ist, rufe diesen Algorithmus mit allen seinen Kindknoten auf, deren Verzeichnisrechteck das Suchrechteck schneiden. 12 Andernfalls füge alle Elemente des Knotens in die Ergebnismenge ein, die von dem Suchrechteck überdeckt werden (Dies entspricht dem Suchkriterium). Will man nun alle Objekte finden, die das Suchrechteck überdecken (Fall 4), muss nur das Entscheidungsverfahren, in welchen Kindknoten der Algorithmus weitersuchen soll, so geändert werden, dass nur noch die Teilbäume betrachtet werden, deren Verzeichnisrechtecke das Suchrechteck überdecken (Und natürlich das Auswahlverfahren für die Blätter, das ist klar). Für den ersten und zweiten Fall muss nur das Verfahren in den Blättern geändert werden, im Inneren läuft die Suche genauso wie im dritten Fall ab. Zur Optimierung des Suchalgorithmus für den Fall 3 kann nach der Wahl des nächsten zu durchsuchenden Teilbaum auch geprüft werden, ob sein Verzeichnisrechteck ganz im Suchrechteck enthalten ist, dann sind nämlich alle darunterligenden Objekte auch im Suchrechteck enthalten und es wird die weitere Überprüfung gespart. 1.3.3 Löschen Zunächst muss der Eintrag im Baum gefunden werden, der dem zu löschenden Datum entspricht. Dazu wird eine Suche nach der exakten Übereinstimmung angestossen (Fall 1 in 1.3.2). Ist die Ergebnismenge leer, bricht der Algorithmus ab. Andernfalls muss überprüft werden, ob einer der gefundenen Einträge dem zu löschenden Eintrag entspricht. Ist dies nicht der Fall, bricht der Algorithmus ebenfalls ab. Der gefundene Eintrag kann nun aus seinem Knoten entfernt werden. Hat der verbleibende Knoten noch mindestens m Elemente oder ist es der Wurzelknoten, so ist das Löschen hiermit beendet (aber noch nicht der Algorithmus), ansonsten muss eine Unterlaufbehandlung angestossen werden, wenn man den das Suchen im Baum effizient halten will. Dazu werden alle verbleibenden Einträge ebenfalls aus dem Knoten entfernt und in eine Menge D eingefügt. Der nun leere Knoten kann jetzt aus dem übergeordneten Knoten entfernt werden. Ein nun möglicher Unterlauf in diesem Knoten wird auf die gleiche Weise behandelt, bis man auf die Wurzel stösst. Die Verzeichnisrechtecke im Löschpfad werden angepasst. Die Elemente der Menge D werden neu in den Baum eingefügt (mit dem 13 Einfüge-Algorithmus (1.3.1)), so dass hinterher wieder alle Blätter auf der gleichen Höhe liegen. Es muss also die Höhe der Teilbäume, die die Elemente in D repräsentieren, beachtet werden. Zum Schluss wird noch geprüft, ob die Wurzel nur noch ein Element enthält und kein Blatt ist. In diesem Fall wird die Wurzel aufgelöst und durch den von ihr referenzierten Knoten ersetzt. Dies reduziert die Baumhöhe um eins. 14 Kapitel 2 Oracle 8i Mit der Version 8.1.5 (kurz: 8i) des Oracle-DBMS wurden objektorientierte Ansätze in die Programmiersprache PL/SQL eingebaut, wie z. B. Objekte mit eigenen Methoden oder Überladung von Funktionen. Dies sind aber erst Ansätze, so kann man etwa keine eigenen Konstruktoren definieren und Vererbung ist hier völlig unbekannt. Relationale Datenbankmanagementsysteme (RDBMSe), die vom Benutzer definierte Datentypen einbinden können, nennt man auch objektrelationale DBMSe. 2.1 Klassen in Oracle 8i Eine Klasse in Oracle 8i enthält eine oder mehrere Instanzenvariablen und, optional, Memberfunktionen und/oder -prozeduren, also Funktionen bzw. Prozeduren, die zu dieser Klasse gehören und es erlauben, deren Inhalt abzufragen, zu verändern oder sonstige Manipulationen vorzunehmen, die mit diesem Objekt in Verbindung stehen. Des weiteren existiert die Möglichkeit, statische Funktionen bzw. Prozeduren zu definieren, diese sind dann nicht an eine bestimmte Instanz dieser Klasse gebunden, sondern nur an die Klasse selbst. Sie kann also Methoden bereitstellen, die den gesamten Datentyp betreffen ([SQLPlus]). Im Weiteren wird im Text der Begriff „Methode” als Oberbegriff für Memberfunktionen und Memberprozeduren verwendet, sowohl statische als auch nicht-statische. Wie bereits erwähnt ist es nicht möglich, den Konstruktor zu überladen. Der Default-Konstruktor erwartet als Argumente Werte für die Instanzenvariablen in der Reihenfolge ihres Auftretens in der Klassendefinition. 15 Andere Methoden oder auch einfache Funktionen oder Prozeduren können lt. Handbuch überladen werden, dies ist nach eigenen Erfahrungen allerdings nicht empfehlenswert. Insbesondere das Überladen von vordefinierten Funktionen und Prozeduren ist problematisch, denn die alte Definition wird von der neuen überdeckt und nicht von ihr erweitert. Eine Klassenbeschreibung besteht in PL/SQL aus dem Deklarationsteil und dem Implementationsteil (ähnlich dem Prototyping in C/C++). Eine Klasse kann erst redefiniert werden, wenn keine weiteren Tabellen oder Objekttypen auf diese Klasse Bezug nehmen. Auf diese Art selbstdefinierte Datentypen können wie die vordefinierten Datentypen als Spalten von Relationen definiert werden. 2.2 Operatoren Es ist ebenfalls möglich, Operatoren zu definieren, die auf der SQL-Ebene benutzt werden können, z. B. in Anfragen. Beispiele für vordefinierte Operatoren sind ’=’, ’<’, oder auch das LIKE für Zeichenketten. Leider ist es nicht möglich, diese Operatoren zu überladen. Stattdessen muss zur Definition eines Operators eine PL/SQL-Funktion geschrieben werden, die das gewünschte leistet. Diese wird dann als Argument eines create operator-Aufrufs angegeben. Anzumerken ist, dass diese Funktion nur Oracle-SQL-Datentypen zurückliefern darf, darunter fällt zum Beispiel nicht der boolean-Typ, der hier äußerst nützlich wäre, da ein Operator wie equals natürlicherweise einen Wahrheitswert liefern sollte. Stattdessen müssen die Werte true und false z. B. mit den Integerwerten 1 und 0 umschrieben werden. 2.3 Das „Extensible Indexing Interface” Hat man nun eine Relation erstellt, mit einer Spalte von einem selbstdefinierten Typ, so ergibt evtl. sich sehr schnell die Anforderung nach einem Index auf dieser Spalte. Der von Oracle bereitgestellte B*-Baum leistet hier unter Umständen nicht das gewünschte (s. 1.1). 1 Aus diesem Grund stellt Oracle 8i ein Konzept namens „Extensible Indexing” bereit. Mit seiner Hilfe kann ein neuer Indextyp geschaffen werden, der ganz auf die Bedürfnisse des neuen Datentyps abgestimmt ist. 1 Insbesondere ist es nicht möglich, mit Oracle auf einer Spalte solchen Typs einen herkömmlichen Index anzulegen. 16 Dieses Konzept ist Teil der objekt-relationalen Fähigkeiten, die es erlauben, sogenannte „Data Cartridges” zu definieren. Darunter versteht man eine Einheit von neuen Datentypen, zugehörigen Indextypen und, bei Bedarf, auch Erweiterungen des Anfrage-Optimierers. Die Installation eines „Data Cartridge” erweitert die Fähigkeiten des Oracle-Servers in einer bestimmten, vom Benutzer gewünschten Richtung ([DCDevG]). Für einen neuen Indextyp muss das „Extensible Indexing Interface” implementiert werden, indem eine Klasse definiert wird, die gewisse Methoden bereithält. Weiterhin muss eine Menge von Operatoren definiert werden, die auf dem neuen Datentyp arbeiten und für die der Indextyp geschaffen wurde. Zum Beispiel könnte es einen Indextyp für Zeichenketten geben, der Anfragen nach Enthaltensein einer Teilzeichenkette unterstützt. Dieses Enthaltensein müsste von einem Operator entscheidbar sein. Wenn in der where-Klausel einer select-Anfrage einer dieser Operatoren benutzt wird und als erstes Argument eine mit dem passenden Indextyp indexierte Spalte benutzt wird, dann wählt der Optimierer den Zugriffspfad über diesen Index. Im Folgenden werden die zu implementierenden Methoden aufgeführt. Bis auf ODCIGetInterfaces bekommen alle statischen Methoden einen Parameter vom Typ sys.odciindexinfo übergeben. Diese vordefinierte Struktur enthält alle notwendigen Informationen über die indexierte Spalte. Außerdem geben alle ODCI-Funktionen eine Zahl zurück. Diese muß im Erfolgsfall die Konstante ODCIConst.Success sein und sonst ODCIConst.Error. Im Übrigen müssen nicht alle Methoden definiert sein. Sobald eine benötigte Funktion nicht vorhanden ist, meldet Oracle einen Fehler und markiert den Index als ungültig. Er kann dann nur noch gelöscht werden. Die Methode müssen zum Teil mit Argumenten von einem implementationsabhängigen Typ definiert werden. Es bedeutet <icoltype> der Typ, der mit diesem Indextyp indexiert werden kann, <impltype> ist der Name der Klasse, die dieses Interface implementiert und <opbndtype> ist der Typ des Rückgabewerts des Operators. • static function ODCIGetInterfaces(ifclist out sys.ODCIObjectList) return number Beim Erzeugen eines Indextyps mit create indextype ruft Oracle diese Funktion auf, um zu überprüfen, ob das geforderte Interface implementiert wurde. Alle von dieser Klasse implementierten Interfaces müssen in ifclist deklariert werden. 17 • static function ODCIIndexCreate(ia sys.odciindexinfo, parms varchar2) return number Wann immer ein Index dieses Typs erstellt werden soll, wird diese Methode von Oracle augerufen. Mit erfolgreicher Ausführung dieser Methode muss ein funktionsfähiger Index auf der indexierten Spalte bestehen, ð/ insbesondere, dass alle bereits vorhandenen Daten im Index enthalten sein müssen. • static function ODCIIndexAlter(ia sys.odciindexinfo, parms varchar2, alter_option number) return number Mit dieser Methode kann eine Änderung der Indexparameter bearbeitet werden oder der Name des Indexes geändert. • static function ODCIIndexDrop(ia sys.odciindexinfo) return number Wenn mit drop index ein Index, der wie soeben beschrieben erstellt worden ist, gelöscht wird, ist diese Methode für das Löschen der Struktur zuständig. Implizit wird diese Methode auch beim Löschen einer ganzen Tabelle, die auf einer oder mehrerer ihrer Spalten einen solchen Index besaß, aufgerufen. • static function ODCIIndexTruncate(ia sys.odciindexinfo) return number Es besteht die Möglichkeit, eine Tabelle vollständig zu leeren, ohne einen delete-Befehl ohne where-Klausel zu benutzen, nämlich mit dem truncate table-Befehl. Dann wird bei Bedarf diese Methode aufgerufen, sie muss folglich den Index auf dieser Spalte leeren. • static function ODCIIndexInsert(ia sys.odciindexinfo, rid varchar2, newval <icoltype>) return number Bei jedem Einfügen eines Datums in eine mit diesem Indextypen indexierte Spalte wird diese Methode aufgerufen. Sie bekommt u. a. das Datum und die zugehörige Rowid in Form einer äquivalenten Zeichenkette übergeben. • static function ODCIIndexDelete(ia sys.odciindexinfo, rid varchar2, oldval <icoltype>) return number Diese Methode wird von Oracle aufgerufen, wenn ein Datum aus dem Index gelöscht werden soll. • static function ODCIIndexUpdate(ia sys.odciindexinfo, rid varchar2, oldval <icoltype>, newval <icoltype>) return number 18 Ein Update einer indexierten Tabelle bewirkt für jede geänderte Zeile den Aufruf dieser Methode. • static function ODCIIndexStart(sctx in out <impltype>, ia sys.odciindexinfo, op sys.odciPredInfo, qi sys.ODCIQueryInfo, strt <opbndtype>, stop <opbndtype>, <valargs>) return number Ein Suchdurchlauf durch den Baum wird mit dieser Methode initialisiert. Im Wesentlichen sind der Operatorname (in op) und die restlichen Argumente des Operatoraufrufs (in <valargs>) übergeben worden. Es wird eine Instanz der Klasse zurückgegeben, die dieses Interface implementiert. Mit Hilfe dieser Instanz, die als eine Art Cursor betrachtet werden kann, und der ODCIIndexFetchMethode wird die Anfrage von der Datenbank abgearbeitet. Falls es Operatoren gibt, die einen unterschiedlichen Rückgabewert und/oder unterschiedliche Parameterlisten besitzen, muss für jede Kombination eine eigene ODCIIndexStart-Funktion geschrieben werden. Operatoren mit verschiedenen Namen, aber gleichen Parameterlisten und Rückgabetypen benötigen keine unterschiedlichen Startmethoden. • member function ODCIIndexFetch(self in out <impltype>, nrows number, rids out sys.odciridlist) return number Nachdem mit ODCIIndexStart ein Cursor vorbereitet wurde, kann mit dieser Methode eine gewisse Anzahl von Ergebnissen geholt werden. Diese Anzahl wird in dem Parameter nrows übergeben. Während die bisherige Methoden alle als statisch definiert werden müssen, sind diese und die ODCIIndexClose-Methode Instanzenmethoden, da sie auf einem bestimmten Cursor, also einer Instanz, arbeiten, der zur Laufzeit zur Verfügung stehen mußs. Die Deklaration des self-Parameters (Er entspricht dem this-Zeiger in C++) als in out ist notwendig, da diese Memberfunktion den Inhalt dieser Klasse ändern können muss. Defaultmäßig können nur Memberprozeduren den Klasseninhalt ändern, nicht aber Memberfunktionen. • member function ODCIIndexClose return number Nach Beendigung der Anfragebearbeitung wird mit dieser Methode der Cursor geschlossen. Danach ist diese Instanz nicht mehr zu gebrauchen und kann gelöscht werden. Falls z. B. die Implementation mit Hilfe einer externen Bibliothek durchgeführt wurde, können hier nicht mehr benötigte Ressourcen freigegeben werden. 19 Kapitel 3 Implementation Die gesamte Implementation ist in PL/SQL geschrieben, der Programmiersprache, die Oracle bereithält. Sie erlaubt die direkte Anbindung an die Datenbank, da die Programme in dem DBMS-Kern abgearbeitet werden. Teile der Implementation, wie z. B. die Bestimmung des Überlappungswerts oder das Aufteilen von überfüllten Knoten, könnten in einer Funktionsbibliothek implementiert werden, die auf Maschinenebene läuft. Dies würde wahrscheinlich die Geschwindigkeit enorm steigern. 3.1 Die Klasse Point Zur Aufnahme eines Punktes im n-dimensionalen Raum wurde die Klasse Point definiert. Sie enthält lediglich die Punktkoordinaten als Instanzenvariablen vom Typ number und set/get-Zugriffsmethoden, die die Klasse nach außen als ein Array von Zahlen erscheinen lassen.1 Die Methoden erwarten also die Nummer der Achse als Argument für den Zugriff auf die entsprechende Koordinate. Weiterhin gibt es eine Memberfunktion distance(p1 Point, p2 Point), die das Quadrat des Abstands zweier Punkte berechnet und zurückgibt. Dies ist ausreichend, da der Abstand nur für die Sortierung beim „Forced Reinsert” benötigt wird. In der vorliegenden Version sieht ein Point so aus: 1 Ursprünglich war beabsichtigt, ein Punkt als ein Array von Zahlen zu definieren (mit Hilfe des Datentyps varray), da aber die Version 8.1.5 von Oracle keine geschachtelten Arrays erlaubt und ein Knoten des Baums (s. u.) ein Array ist, ist dies wohl erst in einer späteren Version möglich. 20 Point x1 real; x2 real; 3.2 Die Klasse Rect Mit Hilfe von zwei Objekten der Klasse Point als Instanzenvariablen wird ein n-dimensionales Rechteck, Rect, definiert, sie bilden die unteren bzw. oberen Grenzen der Intervalle, die die Ausdehnung in jeder Dimension beschreiben. In der Ebene entspricht dies der linken, unteren bzw. der rechten, oberen Ecke eines Rechtecks. Rect lower_point Point; upper_point Point; In der Klasse Rect sind einige Methoden definiert: • member procedure makevalid Da es nur den Default-Konstruktor gibt, muß überprüft werden, ob die Punkte wirklich die kleineren bzw. größeren Koordinaten beinhalten. Bei Bedarf tauscht diese Methode die Werte aus. Wenn in einer späteren Oracle-Version Konstruktoren selbst definiert werden können, kann dies ein Teil eines solchen werden. • member function intersects(r Rect) return boolean Gibt true zurück, falls r dieses Rechteck schneidet, und false sonst. • member function contains(r Rect) return boolean Gibt true zurück, falls r in diesem Rechteck enthalten ist, und false sonst. • member function iscontainedby(r Rect) return boolean Gibt true zurück, falls r dieses Rechteck enthält, und false sonst. • member function area return number Gibt den Flächeninhalt dieses Rechtecks zurück. • member function halfmargin return number Gibt die Summe der Intervalllängen dieses Rechtecks zurück. Da die Randlänge nur für Sortierungen gebraucht wird, wird die echte Randlänge benötigt. 21 • member function center return point Gibt den Mittelpunkt dieses Rechtecks als Point-Objekt zurück. Logisch zur Klasse Rect gehören noch die drei Funktionen intersection(r Rect, q Rect), bb(r Rect, q Rect) und recttochar(r Rect). Die erste berechnet den Durchschnitt der beiden Rechtecke und gibt ihn als Rect-Objekt zurück, die zweite berechnet analog das kleinste umgebende Rechteck und gibt es zurück. Die dritte gibt eine Stringdarstellung des Rechtecks zurück, und zwar so, daß sie in SQL als Konstruktor für dieses Rechteck benutzt werden kann. 3.3 Operatoren In diesem Fall gibt es drei Operatoren, die den Methoden contains, intersects und iscontainedby der Klasse Rect entsprechen. In der vorliegenden Implementation gibt es noch eine Zwischenstufe: Da Operatoren nicht mit Hilfe von Klassenmethoden, sondern nur mit Funktionen definiert werden können, gibt es noch drei Dummy-Funktionen, die lediglich die Methoden benutzen und als Implementation von entsprechenden SQL-Operatoren deklariert werden. Die Operatoren können wie jeder andere auch ohne Index benutzt werden: select id from hann where intersects(r,Rect(Point(515050,4407),Point(517868,7263)))=1; Diese Anfrage liefert die Id’s von allen Objekten, dessen umgebende Rechtecke das gegebene Rechteck schneiden. (Die referenzierte Beispieltabelle kann wie in 4 beschrieben erstellt werden.) 3.4 Die Klasse rectpointer Um ein Verzeichnisrechteck mit dem Verweis auf den Kindknoten zu einer Einheit zu verschmelzen, wurde die Klasse rectpointer geschaffen. Sie enthält keine Methoden, lediglich zwei Instanzenvariablen: ein Rect und eine rowid. Dieser Typ wird auch zum Speichern der Datenrechtecke benutzt, die rowid zeigt dann auf das entsprechende Tupel in der indexierten Relation. 22 rectpointer rct Rect; rid rowid; 3.5 Der Datentyp rsnode Zur Darstellung eines Knotens des R*-Baums wurde ein varray definiert, das 51 Einträge vom Typ rectpointer aufnehmen kann, also eine maximale Knotengröße von 50 unterstützt. Der überzählige Platz wird zum Abfangen von Überläufen benutzt. Ein varray ist in der PL/SQL-Terminologie keine Klasse, allerdings bekommt jeder varray-Typ einen eigenen Namen. Dieser hier heisst rsnode. rsnode varray(51) of rectptr; 3.6 Die Klassen inspath und selpath Beim Einfügen eines neuen Eintrags in den Baum berechnet die chooseSubtree-Methode das optimale Blatt zum Einfügen. Damit in der Tabelle keine Verweise auf den Vaterknoten nötig sind und auch, um die Zugriffe auf die Tabelle gering zu halten, wird der Pfad zu diesem Blatt in einer Struktur gehalten, die für den gesamten Einfügealgorithmus von Bedeutung ist. inspath ll integer; node1 rsnode; index1 integer; nodeid1 varchar2(18); node2 rsnode; index2 integer; nodeid2 varchar2(18); .. . node8 rsnode; index8 integer; nodeid8 varchar2(18); 23 Der Eintrag indexn bezieht sich auf den Index, den der Eintrag nodeid(n + 1) im Knoten noden hat. Der Einträge node1 und nodeid1 beziehen sich stets auf den Wurzelknoten. Aus der Beschränkung, dass man keine varrays von varrays definieren kann, ergibt sich die Notwendigkeit, für jede Ebene explizit die Variablen zu definieren. Es können also so nur Bäume bis zur Höhe 8 bearbeitet werden. Eine Erweiterung erforderte lediglich das Hinzufügen weiterer Instanzenvariablen und die Erweiterung der Zugriffsmethoden. Diese Höhe sollte aber für die meisten Anwendungen ausreichen, es können auf diese Weise maximal M 8 Datenrechtecke verwaltet werden, allerdings ist der Baum in der Praxis nie vollständig gefüllt, so dass schon früher ein Überlauf eintritt. Wie bei der Klasse Point sind auch auf diesem Typ Zugriffsmethoden definiert, die ein Array simulieren. Bei der Anfragebearbeitung wird ebenfalls ein Pfad durch den Baum benötigt. Aus Effizienzgründen wurde ein neuer Typ, selpath, definiert, der aus inspath durch Weglassen der nodeid hervorgeht. Diese wird dort nicht benötigt und somit wird die Struktur kleiner. 3.7 Die Klasse treetable Diese Klasse stellt eine Repräsentation eines Baums, der in einer Tabelle abgespeichert ist, dar. Mit Hilfe eines Objekts dieser Klasse, die im Übrigen als einzige Instanzenvariable den Namen des Baums enthält, kann eine Tabelle unter diesem Namen angelegt, gefüllt und auch wieder gelöscht werden. Die Tabelle enthält nur eine Spalte, und zwar vom Typ rsnode. Diese Klasse überwacht in keiner Weise die Einhaltung der Baumstruktur, es können im Wesentlichen nur Knoten eingefügt, gelesen und verändert werden. Weiterhin wird eine Hilfstabelle angelegt, die die aktuelle Höhe des Baums bereithält, sowie die Rowid des Wurzelknotens. Diese Daten werden von treetable in einer internen Struktur vom Typ treeinfo bereitgestellt. (Dies ist eine Klasse, die genau diese Werte speichern kann und keine weiteren Methoden besitzt.) Eine Ausprägung dieser beiden Tabellen könnte so aussehen: (Die Rechteckdaten wurden aus Platzgründen entfernt und die Darstellung der RowIds wurde vereinfacht. I n bezieht sich auf die Indextabelle, D n entsprechend auf die Datentabelle (hier nicht abgebildet).) 24 RowId I1 I2 I3 I4 I5 I6 I7 I8 I9 I 10 I 11 I 12 I 13 node rsnode I 6, I10 I 5, I 7, I 8,I 11 I 1, I 2, I 4 I 9, I 12, I 13 D 14, D 25, D 2, D 7 D 10, D 27, D 26 D 5, D 11, D 3 D 16, D 22, D 17, D 8 D 1, D 9 D 13, D 19, D 20 D 4, D 15, D 12 D 24, D 6, D 21 D 18, D 23 ll integer 3 rootid rowid I3 Der Wurzelknoten steht also in Tupel I 3 und die Knoten, die Verweise auf Daten enthalten, liegen in der Ebene 3. Im Einzelnen werden folgende Methoden von treetable bereitgestellt: • member procedure createtable Legt einen neuen Baum unter dem Namen der Instanz an. Ein Baum besteht aus zwei Tabellen: Der Haupttabelle, die die Knoten enthält, und eine Hilfstabelle, die die Rowid des Wurzelknotens und die Höhe des Baums enthält. Die initiale Höhe des Baums ist 1, sie muss erhöht werden, sobald die Wurzel gespalten wird, bzw. erniedrigt, wenn die Wurzel nur noch einen Eintrag enthält und somit durch ihren Nachfolger ersetzt wird. (Dieser Fall tritt hier nicht auf, weil kein Löschen implementiert wurde.) • member procedure droptable Die beiden Tabellen werden aus der Datenbank entfernt. Der Baum ist damit gelöscht. Weitere Versuche, Knoten zu erzeugen, schlagen fehl. • member procedure truncatetable Leert den Baum, ð/ es wird droptable und danach createtable aufgerufen. • member function gettreeinfo return treeinfo Gibt den Inhalt der Hilfstabelle in einer treeinfo-Struktur zurück. • member procedure setrootid(rid rowid) Erklärt die übergebene Rowid zur Rowid des Wurzelknotens, indem 25 sie in die Hilfstabelle eingetragen wird. Aus Effizienzgründen findet keine Prüfung statt, ob diese Rowid tatsächlich in der Tabelle existiert. • member procedure increasell Erhöht den Leaflevel, ð/ die Höhe des Baums, um eins, indem in der Hilfstabelle der entsprechende Wert erhöht wird. • member function createnode(node rsnode) return rowid Legt einen neuen Knoten mit dem übergebenen Inhalt an und gibt die Rowid dieser neuen Zeile zurück. • member function getnode(rid rowid) return rsnode Holt den Knoten, der unter der übergebenen Rowid in der Tabelle liegt. • member procedure updatenode(rid rowid, node rsnode) Ersetzt den Knoten unter der übergebenen Rowid durch einen neuen, ebenfalls übergebenen, Knoten. 3.8 Implementierung des R*-Baums Zur Einbindung eines R*-Baums in Oracle8i wurde das in 2.3 beschriebene „Extensible Indexing Interface” implementiert. In der vorliegenden Implementierung werden die folgenden Methoden in der Klasse rstree_im bereitgestellt, jedoch ist das Löschen nicht implementiert, ð/ die Methoden ODCIIndexDelete und ODCIIndexUpdate geben eine Warnung aus und einen Fehler an Oracle zurück. In einem nahezu statischen Datenbestand ist eine Löschmethode nicht unbedingt notwendig, da bei Bedarf der Index auch neu aufgebaut werden kann. dies kann unter Umständen einen Vorteil bei der Suche verschaffen. • static function ODCIGetInterfaces(ifclist out sys.ODCIObjectList) return number Im Ausgabeparameter ifclist wird angezeigt, dass das von dieser Klasse das „Extensible Indexing Interface” implementiert wird. • static function ODCIIndexCreate(ia sys.odciindexinfo, parms varchar2) return number Erzeugt ein treetable-Objekt und lässt dieses den Baum anlegen. 26 Danach werden alle bereits vorhandenen Daten in den Baum eingefügt. Dabei wird jedes Datenrechteck mit makevalid normiert, so dass die restlichen Methoden davon ausgehen können, dass im ersten Punkt alle Koordinaten kleiner oder gleich der entsprechenden Koordinate im zweiten Punkt sind. • static function ODCIIndexDrop(ia sys.odciindexinfo) return number Erzeugt lediglich ein passendes treetable-Objekt und lässt dieses den Baum löschen. • static function ODCIIndexTruncate(ia sys.odciindexinfo) return number Ruft nacheinander droptable und createtable von einem passenden treetable-Objekt auf. Damit ist der Index geleert. • static function ODCIIndexInsert(ia sys.odciindexinfo, rid varchar2, newval Rect) return number Zunächst wird wiederum das neue Rechteck mit makevalid normiert. Danach wird die interne Methode InsertData mit einem Reinsertion-Level, der der Höhe des Baums entspricht, aufgerufen. Seine Bedeutung wird in der Beschreibung der InsertData-Methode verdeutlicht. • static function ODCIIndexStart(sctx in out rstree_im, ia sys.odciindexinfo, op sys.odciPredInfo, qi sys.ODCIQueryInfo, strt number, stop number, cmpval rect) return number Stellt einen Cursor zur Bearbeitung der Anfrage bereit, indem eine Instanz vom Typ rstree_im konstruiert wird. Der Inhalt des Cursors ist zunächst ein Suchpfad von der Wurzel bis in die Blattebene, der auf das erste zurückzugebende Rechteck zeigt. Es wird also in dieser Methode bereits das erste Ergebnis geholt (mit getNext). Da alle Operatoren zwei Argumente vom Typ Rect erwarten und einen number-Wert zurückliefern, gibt es nur eine ODCIIndexStartFunktion. • member function ODCIIndexFetch(self in out rstree_im, nrows number, rids out sys.odciridlist) return number Holt nrows Ergebnisse der Anfrage und legt sie in rids ab. • member function ODCIIndexClose return number Gibt eine Erfolgsmeldung, ð/ odciconst.success, zurück. Eine Freigabe belegter Ressourcen o. ä. ist hier nicht notwendig. 27 Dies waren die Methoden, die das Interface implementieren. Die Klasse rstree_im enthält noch weitere Methoden, die zur Verwaltung des R*Baums nötig sind: • static function ChooseSubtree(rstree treetable, newval Rect, rootid rowid, ll integer) return inspath Das Suchen des optimalen Blattknotens in dem Baum wird mit Hilfe dieser Methode bewältigt. Sie ist eine Implementation des in Kapitel 1 vorgestellten Algorithmus. Rückgabewert ist eine Struktur, die den gesamten Pfad von der Wurzel bis zum Blatt beinhaltet. Diese Struktur wird von der Einfügeroutine benutzt, um die Vorgängerknoten zu finden, ohne nochmal auf die Tabelle zugreifen zu müssen. • static procedure InsertData(rstree treetable, rid varchar2, newval Rect, rootid in out rowid, lvl integer, reinsertion in out integer) Diese Methode stellt den eigentlichen Einfügealgorithmus dar. Er wird benutzt, um ein Rechteck (newval) in einen R*-Baum (rstree) einzufügen, zusammen mit der Referenz rid, die im Falle eines Datenrechtecks auf ein Tupel der indexierten Tabelle zeigt, und im Falle eines Verzeichnisrechtecks auf den Wurzelknoten des zugehörigen Teilbaums. Die Ebene der Knoten, in die eingefügt werden soll, wird in lvl übergeben und die Ebene, in der ein „Forced Reinsert” als nächstes stattfinden darf, in reinsertion. Zunächst wird mit der ChooseSubtree-Methode der beste Knoten bestimmt, in das dieses Datum eingefügt werden kann. Falls dieses Blatt noch nicht voll ist, wird das neue Rechteck eingefügt und die Funktion ist beendet. Andernfalls wird die Überlauf-Behandlung angestoßen: Wenn das Datum nicht in die Wurzel einzufügen ist (also lvl > 1) und der aktuelle Reinsertion-Level dieser Einfügehöhe entspricht, dann wird ein „Forced Reinsert” begonnen. Dazu werden die Einträge in dem gewählten Knoten nach dem Abstand ihres Mittelpunkts vom Mittelpunkt des Verzeichnisrechtecks sortiert und die InsertData-Methode rekursiv mit den äußeren p Einträgen, beginnend mit dem inneren von ihnen, aufgerufen. Vorher wird jedoch der Reinsertion-Level um eins erniedrigt, so dass in dieser Ebene kein weiteres „Forced Reinsert” stattfindet. Falls die Bedingungen für das „Forced Reinsert” nicht zutreffen, wird der Aufteilvorgang angestoßen: Mit der Split-Methode wird eine Aufteilung des überfüllten Knotens ermittelt. Falls es sich nicht um die Wurzel handelte, werden diese neuen Knoten an Stelle des alten in den übergeordneten Knoten eingefügt. Dadurch wird die 28 Anzahl der Einträge in diesem Knoten um eins erhöht. Falls es sich um die Wurzel handelte, wird ein weiterer neuer Knoten erzeugt. Dieser wird die neue Wurzel und erhält die beiden Teilknoten als Einträge. Schließlich wird die Überlaufbehandlung wird bei Bedarf in der übergeordneten Ebene fortgesetzt. Der Algorithmus entspricht damit der Beschreibung in 1.3.1. Beim Aufruf dieser Methode von ODCIIndexInsert aus wird der Reinsertion-Level dort auf die Baumhöhe gesetzt (s. dort). Mit jedem „Forced Reinsert” wird dieser Level um eins erniedrigt, so dass während des Einfügens eines Datenrechtecks pro Ebene nur ein Wiedereinfügen versucht wird. Technisch wird der Reinsertion-Level durch einen sogenannten in out-Parameter realisiert, also ein Parameter, der sowohl zur Übergabe in die Funktion, als auch zur Rückgabe an den Aufrufer benutzt wird. Dies bewirkt, dass er während einer Einfügephase eines Datenrechtecks global ist. • static function Split(node in out rsnode) return integer Zusammen mit der nächsten kann mit dieser Methode ein überfüllter Knoten in zwei Knoten aufgeteilt werden. In dieser Funktion wird die optimale Splitachse bestimmt, entlang der die Sortierung nach der oberen bzw. der unteren Grenze (s. Kapitel 1) aufgeteilt werden kann. Danach wird mit Hilfe von ChooseSplitIndex die Sortierung und der Index ausgewählt, die die optimale Aufteilung bestimmen. Die notwendigen Sortierungen werden von den Funktionen sortbylower und sortbyupper bereitgestellt. Die ausgewählte Sortierung wird in dem Ausgabeparameter node abgelegt und der größte Index in diesem Knoten, der noch zu dem ersten Teilknoten gehört, ist der Rückgabewert dieser Funktion. • static function ChooseSplitIndex(lsort rsnode, usort rsnode) return integer Wie von der vorherigen Methode benötigt, berechnet diese Funktion die optimale Teilstelle aus den 2(M −2m +2) Möglichkeiten, die durch die beiden übergebenen Sortierungen bestehen, wie in Kapitel 1 beschrieben. Liegt diese Teilstelle in der Sortierung usort, so wird der negative Index zurückgeliefert, der die optimale Teilstelle beschreibt, liegt er in lsort, so ist der Rückgabewert positiv. Auf diese Weise wird also die Auswahl, welche Sortierung zum Teilen benutzt werden 29 soll, mitgeteilt. • static procedure adjustbbs(rstree treetable, path inspath, lvl integer) Wann immer ein Knoten verändert wird, kann sich auch das Verzeichnisrechteck zu diesem Knoten ändern. Falls es sich nicht um die Wurzel handelt, muss diese Änderung im übergeordneten Knoten vermerkt werden. Damit ändert sich auch dieser Knoten und die Behandlung muss eine Ebene höher fortgesetzt werden.. Diese Methode passt die Verzeichnisrechtecke im Einfügepfad inspath an. Dabei wird nur so lange in dem Pfad nach oben gestiegen, bis keine Änderung mehr aufgetreten ist, damit keine unnötigen Dateizugriffe stattfinden. • static function overlap_enlargement(node rsnode, k integer, new rect) return number Berechnet die Vergrößerung der Überlappung des Eintrags Nummer k im Knoten node, falls in diesem das Rechteck new eingefügt würde. Dieser Wert wird von der ChooseSubtree-Methode benötigt. • static function area_enlargement(rct rect, new rect) return number Berechnet die Differenz zwischen dem Flächeninhalt von rct und dem kleinsten umgebenden Rechteck von rct und new, also die Flächenvergrößerung, die das Verzeichnisrechteck rct erführe, falls das neue Rechteck new in den zugehörigen Knoten eingefügt würde. Dieser Wert wird ebenfalls von der ChooseSubtree-Methode benötigt. • static function nodebbox(node rsnode, lo integer, hi integer) return rect Berechnet kleinste umgebende Rechteck zu den Einträgen Nummer lo bis Nummer hi des Knotens node. • static function margin_values(node rsnode) return number Berechnet die Summe aller Randlängen der Verzeichnisrechtecke, die jeweils entstünden, wenn der Knoten node an den M − 2m + 2 möglichen Stellen geteilt würde. Diese Funktion wird von Split-Methode benutzt, um die Summe S, wie in 1.3.1, beschrieben, zu berechnen. • static function overlap_value(node rsnode, k integer) return number 30 Berechnet den Überlappungswert (s. 1.3.1) der Aufteilung Nummer k des Knotens node. • static function area_value(node rsnode, k integer) return number Berechnet den Flächenwert (s. 1.3.1) der Aufteilung Nummer k des Knotens node. • static procedure sortbylower(node in out rsnode, dim integer), static procedure sortbyupper(node in out rsnode, dim integer), static procedure sortbynumber(numarr in out numarray) Diese Methoden implementieren die benötigten Sortierfunktionen. Der Algorithmus ist überall der gleiche (Bubblesort), nur die Vergleichsfunktion variiert. Die ersten beiden Prozeduren sortieren den Knoten node nach der unteren bzw. oberen Grenze des Intervalls zu der Dimension dim. Die dritte Methode sortiert ein Array von number-Variablen. Sie wird benutzt, um Knoteneinträge nach ihrem Abstand vom Mittelpunkt zu sortieren (s. 1.3.1). • member procedure getnext(lvl integer) Setzt den Cursor um einen Schritt weiter, so dass ODCIIndexFetch ein neues Datum in die Liste der Rückgabewerte eintragen kann. Zur Bearbeitung einer Suchanfrage an den Index ergibt diese Methode zusammen mit den Open- und Fetch-Methoden des Interfaces die Implementation des Suchalgorithmus (1.3.2). Der Parameter lvl gibt an, in welcher Ebene der nächste Eintrag geholt werden soll. Ist ein Knoten in einer Ebene vollständig bearbeitet, ruft sich dise Methode selbst auf, um den nächsten Knoten in der nächsthöheren Ebene zu holen. Ist aber die Wurzel vollständig durchlaufen, so wird der Cursor als vollständig bearbeitet gekennzeichnet, so dass die Anfrage beendet werden kann. Die Klasse enthält noch einige Instanzenvariablen, die den Cursor darstellen. Unter anderem gibt es eine Struktur, die den Suchpfad beinhaltet. Sie enthält als ersten Eintrag den Wurzelknoten und einen Index, der die Position des Teilbaums bezeichnet, der gerade durchsucht wird. Dieses setzt sich rekursiv fort bis die Blattebene erreicht ist. Weiterhin gibt es Variable, die den Namen des Operators enthält, um die benötigte Art des Suchens zu bestimmen, und eine Kopie des Suchrechtecks. Außerdem existiert hier noch das treetable-Objekt, das den zu durchsuchenden R*-Baum bereithält. 31 Kapitel 4 Benutzung Es wird die Installation und Benutzung des Oracle-Indextyps „RSTree” beschrieben. Dazu ist die Version 8i des Oracle-DBMS notwendig. 4.1 Installation Zur Installation des Indextyps „RSTree” in Ihrem DB-Schema führen Sie bitte folgendes aus: 1. Kopieren Sie die Dateien Rectangle.sql, Operators.sql, RS_Supp.sql und RSTree.sql in ein Verzeichnis ihrer Wahl. 2. Wechseln Sie in dieses Verzeichnis. 3. Starten Sie sqlplus (Teil der Oracle-Installation). 4. Geben Sie hierdrin @Rectangle ein. Damit wird die entsprechende Datei eingelesen. Danach gibt es die Datentypen Point und Rect in ihrem Schema, wobei die Punkte die Dimension 2 haben. 5. Danach müssen mit @Operators die Operatoren für die Interaktion mit SQL definiert werden. Wie in 3.3 beschrieben, können jetzt, auch ohne Index, die neuen Vergleichsoperatoren für Rechtecke in SQL-Statements genutzt werden. 6. Die Hilfsklassen für den R*-Baum müssen mit @RS_Supp geladen werden. 7. Schliesslich kann jetzt mit @RSTree der neue Indextyp definiert werden. Es erscheinen dabei mehrere Eingabeaufforderungen: 32 • Der Name des Indextyps identifiziert ihn, ð/, wenn Sie später den gleichen Namen an dieser Stelle verwenden, wird der alte Indextyp mit diesem Namen gelöscht. • Der Bedeutung des Parameters M wird in 1.2 beschrieben. • Ebenso die Parameter m und p. Es wird allerdings nicht kontrolliert, ob die Werte den Begrenzungen genügen. Bei falscher Wahl wird ein damit erzeugter Index nicht funktionieren. Der letzte Schritt kann beliebig oft wiederholt werden, um R*-BaumIndextypen mit verschiedenen Parametern zu erzeugen. Die Beispieldaten können mit folgenden Schritten geladen werden: 1. Falls es den Datentyp Rect in Ihrem Schema noch nicht gibt, dann erstellen Sie ihn wie oben beschrieben. 2. Legen Sie folgendermaßen eine Oracle-Tabelle an: create table hann(id varchar2(21), r Rect); 3. Kopieren Sie die Dateien loadhann.ctl und hannover_poly.lst in ein Verzeichnis ihrer Wahl. 4. Wechseln Sie in dieses Verzeichnis. 5. Starten Sie den SQL-Lader sqlldr mit loadhann als Argument. Die Tabelle wird jetzt mit den Beispieldaten gefüllt. 4.2 Benutzung Angenommen, Sie haben den Indextyp rstree genannt, dann kann auf einer Spalte vom Typ Rect jetzt ein Index angelegt werden: create index <Name> on <Spalte> indextype is rstree; Achtung: Diese Operation dauert unter Umständen einige Minuten (s. 5). Sei nun als Beispiel auf der Spalte hann(r) ein Index angelegt worden. Jetzt kann mit einer Anfrage, die einen passenden Operator benutzt, dieser Index benuzt werden: select id from hann where intersects(r,Rect(Point(515050, 4407), Point(517868, 7263)))=1; Diese Anfrage liefert alle Objekte, die das gegebene Rechteck schneiden. Den Beweis, dass der Index (in diesem Fall) korrekt arbeitet, erhält man, wenn man die Parameter des Operators vertauscht. Dann wählt Oracle 33 nämlich nicht den Zugriffspfad über den Index, sondern startet eine lineare Suche. Das Anfrageergebnis ist in beiden Fällen gleich, bis auf die Reihenfolge in der Ausgabe. select id from hann where intersects(Rect(Point(515050, 4407, 517868, 7263)),r)=1; Zum Testen der anderen beiden Operatoren müssen zusätzlich zur Vertauschung der Parameter auch der Operator vertauscht werden. Es ist nämlich contains(a,b)=iscontainedby(b,a). 34 Kapitel 5 Tests Zum Testen des neuen Indextyps wurden folgende Beispieldatenbestände benutzt: • 5000 zufällige Rechtecke, deren linke, untere Ecken im Bereich von 0 bis 255 liegen, mit Höhen bzw. Breiten im Bereich von 0 bis 7. • Geodaten aus dem Raum Hannover, bestehend aus 3590 Rechtecken. Es folgt eine tabellarische Aufstellung der Ergebnisse und eine Kommentierung. 5.1 Ergebnisse Die folgenden Tabellen enthalten die durchschnittlichen Suchzeiten in Sekunden für Anfragen ohne Index und mit Indexen mit unterschiedlicher Knotengröße M. Zusätzlich ist die benötigte Zeit zur Erstellung des R*-Baums angegeben, die Anzahl seiner Knoten und seine Höhe. Die Zeiten wurden mit der timing-Option von SQL*Plus gemessen. Diese misst die Gesamtzeit vom Absetzen des SQL-Statements bis zur Ausgabe. Die Gesamtleistung des Systems und die momentane Auslastung fliessen also in die Messung ein. Weiterhin ist nicht garantiert, dass die Anfragen repräsentativ sind. Falls z. B. ein Suchrechteck gewählt wird, das zufällig von sehr vielen Verzeichnisrechtecken geschnitten wird, sind eventuell viele vergebliche Abstiege, ð/ ohne Ergebnis, nötig. 35 5.1.1 Zufällige Rechtecke Der Index wurde mit folgende Anfragen getestet: 1. select * from rects where intersects(r,rect(point(1,1),point(5,5)))=1; Diese Anfrage liefert 2 Zeilen als Ergebnis, also 0.04% des Datenbestands. 2. select * from rects where contains(r,rect(point(100,100),point(100,100)))=1; Diese Anfrage liefert 3 Zeilen als Ergebnis, also 0.06% des Datenbestands. ohne Index M = 5, m = 2, p = 2 M = 10, m = 4, p = 3 M = 15, m = 6, p = 5 M = 20, m = 8, p = 6 5.1.2 create nodes ll 1412 1870 3006 4394 1838 774 497 368 7 5 4 4 1 2.77 0.44 0.40 0.43 0.50 2 2.60 0.62 1.17 1.56 1.43 Geodaten Der Index wurde mit folgende Anfragen getestet: 1. select id from hann where intersects(r,rect(point(449532,18178),point(450152,18789)))=1; Diese Anfrage liefert 5 Zeilen als Ergebnis, also 0.14% des Datenbestands. 2. select id from hann where iscontainedby(r,rect(point(449532,18178),point(450152,18789)))=1; Diese Anfrage liefert 1 Zeile als Ergebnis, also 0.028% des Datenbestands. ohne Index M = 5, m = 2, p = 2 M = 10, m = 4, p = 3 M = 15, m = 6, p = 5 M = 20, m = 8, p = 6 M = 30, m = 12, p = 9 create nodes ll 1102 1425 2253 3305 4961 1527 608 394 300 186 7 4 4 3 3 36 1 1.95 1.10 0.80 0.77 0.81 0.65 2 1.91 0.82 0.61 0.57 0.66 0.45 5.2 Kommentierung Zunächst kann man beaobachten, dass die Anfragen mit Hilfe des Indexes (bis auf eine Ausnahme) mindestens doppelt so schnell abgearbeitet werden, der hier beobachtete Höchstfall zeigt eine knapp siebenfache Geschwindigkeit. Interessant zu beobachten ist die Tatsache, dass der R*-Baum anscheinend die zufällig generierten, gleichverteilten Rechtecke, zumindest für die intersects-Anfrage, besser indexiert als die (realen) Geodaten. Obwohl die Knoten bei M = 20 schon zu groß sind, um in einen Pufferblock zu passen, ist keine signifikante Abnahme der Suchleistung an diesem Schwellwert festzustellen. Die Geschwindigkeit des Indexaufbaus sinkt mit der Erhöhung der maximalen Knotengröße, dies hängt vermutlich damit zusammen, dass hier oft größere Datenbereiche im Speicher verschoben werden. Ein größerer Wert für M (30 bzw. 50) bewirkte bei dem vorliegenden System einen Überlauf des Rollback-Segments während der Indexerzeugung. Insgesamt scheint nach diesen Tests die beste Wahl für den Parameter M 10 zu sein, denn damit werden die Indexe noch möglichst schnell erzeugt. Die Suchgeschwindigkeit scheint die Wahl dieses Werts nicht zu beeinflussen. 37 Anhang A Die Eingabe für den SQL*Loader Um die Beispieldaten in eine Tabelle zu laden, wurde der SQL*Loader benutzt ([O8iUtil]). Die Geodaten von Hannover lagen in folgender Form vor: Eine 21 Zeichen lange Zeichenkette, gefolgt von 10 Leerzeichen und 4 7-stelligen Zahlen, getrennt mit jeweils 2 Leerzeichen, beschreiben ein Rechteck auf einer Zeile. Insgesamt gibt es 3590 Zeilen in der Datei in dieser Form. Zum Einladen in eine Oracle-Tabelle muss ein sogenanntes Kontrollfile erstellt werden: LOAD DATA INFILE ’hannover_poly.lst’ INTO TABLE hann TRUNCATE FIELDS TERMINATED BY WHITESPACE ( id POSITION(01:21) CHAR(21), r COLUMN OBJECT ( lower_point COLUMN OBJECT (x1 POSITION(32:38) x2 POSITION(41:47) upper_point COLUMN OBJECT (x1 POSITION(50:56) x2 POSITION(59:65) ) ) INTEGER EXTERNAL, INTEGER EXTERNAL), INTEGER EXTERNAL, INTEGER EXTERNAL) Mit Hilfe dieses Kontrollfiles können die Beispieldaten in die Tabelle hann geladen werden. Die Zufallsrechtecke wurden mit einen einfachen C-Programm erzeugt: 38 #include <stdio.h> #include <stdlib.h> int main(void) { FILE *sf; int i; int x,y,dx,dy; sf=fopen("rectangles.dat","w"); for (i=0;i<5000;i++) { x=random() & 255; y=random() & 255; dx=random() & 7; dy=random() & 7; fprintf(sf,"%3d %3d %3d %3d\n",x,y,x+dx,y+dy); } return 0; } Das Kontrollfile für diese Daten sieht so aus: LOAD DATA INFILE ’rectangles.dat’ INTO TABLE rects TRUNCATE FIELDS TERMINATED BY WHITESPACE ( r COLUMN OBJECT ( lower_point COLUMN OBJECT (x1 POSITION(01:03) x2 POSITION(05:07) upper_point COLUMN OBJECT (x1 POSITION(09:11) x2 POSITION(13:15) ) ) 39 INTEGER EXTERNAL, INTEGER EXTERNAL), INTEGER EXTERNAL, INTEGER EXTERNAL) Literaturverzeichnis [BKSS90] N. Beckmann, H.-P. Kriegel, R. Schneider, B. Seeger: The R∗ Tree: An Efficient and Robust Access Method for Points and Rectangles. In H. Garcia-Molina, H. V. Jagadish (eds.), Proceedings of the 1990 ACM SIGMOD International Conference on Management of Data, SIGMOD Record 2, ACM Press, New York, 1990, 322–331. [DCDevG] D. Raphaely, C. Murray: Data Cartridge Developer’s Guide, Release 8.1.5. Part-No. A68002-01, Oracle Corporation, 1999. [GMS95] M. Goossens, F. Mittelbach, A. Samarin: Der LaTeX Begleiter. Addison-Wesley (Deutschland), Bonn, 1995. [Gut84] A. Guttman: R-Trees: A Dynamic Index Structure for Spatial Searching. In B. Yormark (ed.), Proceedings of the 1984 ACM SIGMOD International Conference on Management of Data, SIGMOD Record 2, ACM Press, New York, 1984, 47–57. [Knu97] D. E. Knuth: The Art of Computer Programming - Vol.I: Fundamentals Algorithms (Third Edition), 3rd edition. AddisonWesley, Reading, MA, 1997. [O8iUtil] J. Durbin Oracle 8i Utilities, Release 8.1.5. Part-No. A67792-01, Oracle Corporation, 1999. [PLSQL] T. Portfolio: PL/SQL User’s Guide and Reference, Release 8.1.5. Part-No. A67842-01, Oracle Corporation, 1999. [SQLPlus] F. Rovitto: SQL*Plus User’s Guide and Reference, Release 8.1.5. Part-No. A66736-01, Oracle Corporation, 1999. 40