Document

Werbung
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
Herunterladen