- Fachgebiet Datenbanken und Informationssysteme

Werbung
Erweiterung eines objektrelationalen
Datenbankmanagementsystems um räumliche
Verbunde
Diplomarbeit im Rahmen des Studienganges
Mathematik mit Studienrichtung Informatik
vorgelegt von Oliver Schweer
Universität Hannover - Fachbereich Informatik
Institut für Informationssysteme
Fachgebiet Datenbanksysteme
Prüfer: Prof. Dr. U. Lipeck
Zweitprüfer: Prof. Dr. R. Parchmann
Hannover, 27. Juni 2003
Inhaltsverzeichnis
1 Einleitung
1
2 Verbunde in Datenbanken
2.1 Verbund ohne Index . . . . . . . . . . . . . . . . .
2.2 Indexe in Datenbanken . . . . . . . . . . . . . . . .
2.2.1 B*-Bäume . . . . . . . . . . . . . . . . . . .
2.2.2 Hash-Index . . . . . . . . . . . . . . . . . .
2.2.3 Bitmap-Listen . . . . . . . . . . . . . . . . .
2.2.4 Verallgemeinerte Zugriffspfadstruktur . . . .
2.3 Indexgestützter Verbund . . . . . . . . . . . . . . .
2.3.1 Verbundindexe . . . . . . . . . . . . . . . .
2.3.2 Verbunde in objektrelationalen Datenbanken
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
4
5
6
9
14
15
17
17
19
3 Spatial Join Algorithmen
3.1 indexed nested loops join . . . . . . . . . . . . . . . . . . . . .
3.2 R*-tree join . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.1 Aufbau eines R-Baumes . . . . . . . . . . . . . . . . .
3.2.2 Spatial Join . . . . . . . . . . . . . . . . . . . . . . . .
3.3 Seeded Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3.1 Seeded Tree Konstruktion . . . . . . . . . . . . . . . .
3.4 Spatial Hash-Join . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.1 Funktionsweise des Hash-Joins . . . . . . . . . . . . . .
3.4.2 Beispiele für Zuweisungs- und Ausdehnungsfunktionen
3.5 Z-Codes und Standard-Indexe . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
20
20
21
21
22
24
25
27
27
28
30
.
.
.
.
.
.
.
36
37
41
41
42
43
43
45
4 GiST-Indexe
4.1 Überblick über GiST . . . . . . . . . . . . . .
4.2 libgist der University of California at Berkeley
4.2.1 GiST-Erweiterungen . . . . . . . . . .
4.2.2 Funktionsübersicht . . . . . . . . . . .
4.2.3 Klassenübersicht . . . . . . . . . . . .
4.2.4 Suchbaumerweiterungen . . . . . . . .
4.2.5 Einschränkungen der libgist . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5 GiST Indexe für Oracle
5.1 Oracle - Extensible Indexing Interface . . . . . . . .
5.1.1 Methoden des Extensible Indexing Interface
5.2 OraGIST-Bibliothek . . . . . . . . . . . . . . . . .
5.2.1 Änderungen an der libgist . . . . . . . . . .
5.2.2 Struktur der Implementierung . . . . . . . .
5.2.3 Die Klasse OraGiST . . . . . . . . . . . . . .
5.3 Oracle - Pipelined Table Functions . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
47
47
48
50
50
51
52
54
6 Entwurf des Verbund-Algorithmus
6.1 Vorüberlegungen . . . . . . . . . . . . .
6.2 Verbundalgorithmen für GiST . . . . . .
6.2.1 Einfacher GiST-Verbund . . . . .
6.2.2 Zusätzliche Selektionen . . . . . .
6.2.3 Verbundalgorithmus für die libgist
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
57
57
58
59
60
60
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7 JoinGiST
7.1 Struktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2 Methoden der JoinGiST-Klassen . . . . . . . . . . . . . . . . .
7.2.1 JoinGiST parameters . . . . . . . . . . . . . . . . . . .
7.2.2 JoinGiST ext t . . . . . . . . . . . . . . . . . . . . . .
7.2.3 JoinGiST . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.4 odci2join - Schnittstelle zwischen JoinGiST und Oracle
7.3 Einschränkungen . . . . . . . . . . . . . . . . . . . . . . . . .
7.3.1 Cursorerweiterung . . . . . . . . . . . . . . . . . . . .
7.3.2 Compiler . . . . . . . . . . . . . . . . . . . . . . . . . .
7.3.3 Speicherort der Indexdaten . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
63
63
64
64
66
67
69
70
70
70
71
8 Anwendung und Performance
8.1 Anwendung . . . . . . . . . . . . . . . .
8.1.1 Formulierung von Anfragen . . .
8.1.2 Weitere Verbunde . . . . . . . . .
8.2 Praktisches Beispiel mit Performance . .
8.3 Performance-Analyse . . . . . . . . . . .
8.4 Ausblick und Verbesserungsmöglichkeiten
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
72
72
72
73
73
75
75
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
A Beispiel einer OraGiST-Erweiterung
77
B Beispiel einer JoinGiST-Erweiterung
92
Kapitel 1
Einleitung
In objektrelationalen Datenbankmanagementsystemen (ORDBMS) besteht die Möglichkeit, komplexe Datenstrukturen mit zugehörigen Methoden zu verwalten. So ist es unter
anderem möglich, geographische Daten in den Tabellen einer Datenbank abzulegen und
verschiedene Operationen darauf anzuwenden. Eine dieser Operationen ist der räumliche Verbund (spatial join), wo die Objekte zweier Relationen mittels eines geeigneten
Operators verglichen werden und Paare dieser Objekte als Ergebnis geliefert werden.
Als Beispiel stelle man sich in einer Relation Straßen, in einer anderen Flüsse vor. Eine
Anfrage, die alle Straßen liefern soll, die sich mit Flüssen kreuzen, muss zunächst einen
räumlichen Verbund der beiden Tabellen bilden. Dabei benutzt man einen Operator,
der überprüft, ob sich zwei Objekte überschneiden (intersection). Man erhält also alle
Straßen, deren Verlauf von dem der Flüsse geschnitten wird.
Um nicht alle Objekte der einen Relation mit allen Objekten der anderen Relation
vergleichen zu müssen (nested loops join), ist es hilfreich die Daten vorher zu partitionieren. Wenn es gelingt, Objekte, die räumlich nahe beieinander liegen, zu gruppieren,
können während des Join-Prozesses große Datenmengen schon ausgeschlossen werden, da
sie sich nicht in räumlicher Nähe des gerade betrachteten Objektes befinden.
Da die Anwendung des Vergleichs-Operators auf die konkreten Objekte der Datensätze
vergleichsweise rechenintensiv sein kann, unterteilt man die Verarbeitung der Verbundberechnung üblicherweise in zwei Stufen: In einer ersten Filterstufe wird mit Hilfe der
zu den Relationen gehörenden Index-Daten eine Kandidaten-Liste erstellt, die Objektpaare enthält, welche als Verbundpaar in Betracht gezogen werden müssen. Da in den
Index-Daten meistens nicht die tatsächliche Geometrie eines Objektes, sondern nur eine
Approximation davon aufgenommen ist, kann diese Filterstufe den Verbund nicht komplett berechnen, sondern nur dazu beitragen, bestimmte Kombinationen auszuschließen.
Die zweite Filterstufe verwendet dann diese Kandidatenliste, um die Verbund-Operation
anhand der tatsächlichen Datenobjekte der Kandidaten durchzuführen.
Ziel dieser Arbeit ist es, eine Möglichkeit zu finden, das objektrelationale Datenbankmanagementsystem Oracle 9i um die Berechnung räumlicher Verbunde auf Basis
benutzerdefinierter Indexstrukturen zu erweitern. Dazu wird eine im Rahmen einer anderen Diplomarbeit entwickelte Indexunterstützung verwendet, die die Datenbank um
benutzerdefinierte Indexe in der Form verallgemeinerter Suchbäume erweitert.
2
Auf diese Indexerweiterung (OraGiST) aufbauend, wird daher eine Bibliothek JoinGiST entwickelt, die es erlaubt, eine Verbundberechnung mittels zweier in verallgemeinerten Suchbäumen gespeicherten Indexstrukturen auszuführen.
Gliederung der Arbeit
Kapitel 2 beschreibt zunächst, was unter einem Verbund zweier Tabellen zu verstehen
ist. Da hier die Indexunterstützung bei einem Verbund von besonderem Interesse ist,
werden dazu einige der in Datenbanken verwendeten Indexierungsstrategien vorgestellt
und erläutert, wie diese bei der Berechnung eines Verbundes hilfreich sein können.
Speziell die Verbundalgorithmen mit den zugehörigen Indexierungsstrategien für räumliche Daten werden in Kapitel 3 betrachtet.
Kapitel 4 beschäftigt sich mit verallgemeinerten Suchbäumen, die als universelle Baumarchitektur eine Vielzahl verschiedener zur Verwendung als Index geeigneter Baumtypen
unterstützt.
Das folgende Kapitel betrachtet die Verwendung dieser Bäume als benutzerdefinierte
Indexstrukturen in Verbindung mit der Oracle-Datenbank. Die vorgestellte Bibliothek
OraGiST dient dabei als Bindeglied zwischen einer C++ -Implementierung verallgemeinerter Suchbäume und der Oracle-Datenbank. Zudem werden einige Schnittstellen der
Datenbank betrachtet, die im Zusammenhang mit dieser Arbeit oder den benutzerdefinierten Indexstrukturen von Bedeutung sind.
Im sechsten Kapitel wird schließlich der Algorithmus vorgestellt, der in dieser Arbeit
verwendet werden soll, um zwei verallgemeinerte Suchbäume miteinander zu verbinden.
Die darauf aufbauende Bibliothek JoinGiST zur Verbundberechnung zweier OraGiSTObjekte wird in Kapitel 7 vorgestellt.
Das letzte Kapitel betrachtet die Anwendung dieser Bibliothek mit einigen Beispieldaten und betrachtet das Abschneiden des Verbundalgorithmus bei der tatsächlichen
Berechnung darauf aufbauender Verbunde.
Wie die dazu benötigten OraGiST-Spezialisierungen angelegt werden, wird dazu nochmal kurz im Anhang A skizziert. Anhang B beinhaltet das dazugehörige Beispiel zur
Erstellung einer Erweiterung zu JoinGiST, um zwei Bäume dieses Typs zu verbinden.
Kapitel 2
Verbunde in Datenbanken
Wesentliches Ziel beim Einsatz moderner relationaler und objektrelationer Datenbanken
ist, die Speicherung redundanter Daten zu vermeiden oder zumindest zu kontrollieren,
soweit dies zur Sicherstellung des Erhalts der Datensicherheit oder zur PerformanceSteigerung notwendig ist. Dazu wird die Möglichkeit genutzt, Daten auf mehrere Tabellen
zu verteilen, so dass gleiche Informationen nicht für jeden Datensatz erneut gespeichert
werden müssen, sondern nur einmalig in den entsprechenden Tabellen abgelegt werden.
Die Verbindung zwischen den Informationen wird ebenfalls über Tabellen gespeicherte
Relationen hergestellt (dabei kann es sich auch um eine weitere Spalte in einer der Datentabellen handeln).
Eine Aufgabe des Datenbankmanagementsystems ist es, den berechtigten Benutzern
den Zugriff auf die so verteilten Daten zu ermöglichen. Dazu ist es notwendig, für eine
Anfrage an die Datenbank die entsprechenden Einträge in den betroffenen Tabellen zu
lokalisieren und zur Ausgabe oder weiteren Verarbeitung zusammenzufügen.
In der Datenbank einer Firma können beispielsweise in einer Tabelle Departments
alle Informationen zu einer Abteilung und in der Tabelle Employees die Informationen
über die Angestellten gespeichert sein, insbesondere auch die Abteilungsnummer (deptnr ) der Abteilung, der ein Angestellter zugeteilt ist. Ein Personalbearbeiter kann dann
folgende SQL-Anfrage verwenden, um die Namen aller Angestellter mit den zugehörigen
Abteilungsnamen zu erhalten:
select EMPLOYEES.name, DEPARTMENTS.name from EMPLOYEES, DEPARTMENTS
where EMPLOYEES.deptnr = DEPARTMENTS.deptnr;
In der Relationenalgebra entspricht dies:
πe.name, d.name (EMPLOYEES e ./ DEPARTMENTS d)
e.deptnr=d.deptnr
Eine solche Anfrage stellt die Datenbank vor die Aufgabe, zu allen Datensätzen (Tupeln) in Employees diejenigen in Departments zu finden, deren Einträge in den jeweiligen
deptnr -Spalten gleich sind. Da bei dieser Anfrage die Inhalte mehrerer Tabellen verknüpft
werden müssen, spricht man von einem Verbund (engl.: join).
KAPITEL 2. VERBUNDE IN DATENBANKEN
4
Hier ist der Operator, der zum Vergleich beider der beiden Tabellen herangezogen
wird, der Gleichheitsoperator (EMPLOYEES.deptnr = DEPARTMENTS.deptnr ). Aus
diesem Grund kann man auch von einem Equiverbund sprechen. Es sind aber auch andere
Verbundoperatoren möglich.
Wie ein Datenbanksystem eine Verbundanfrage bearbeiten kann, möchte ich in den
folgenden Abschnitten kurz skizzieren, wobei sich die Beschreibung an [HR01] und [Li97]
orientiert.
2.1
Verbund ohne Index
Die Bearbeitung der an die Datenbank gestellten Anfrage ist möglich, indem zu jeder Spalte der Tabelle Employees die Departments-Tabelle jeweils komplett durchsucht (gescannt)
wird, wobei man diejenigen Paare, bei denen die Vergleichsoperation erfüllt ist, hier also
gleiche deptnr, der Ergebnismenge zufügt. Dieses Verfahren, alle Datensätze einer Tabelle
für jeden Datensatz einer anderen Tabelle zu überprüfen, nennt man direkter Verbund
(nested loops join), wobei in einer äußeren Schleife (loop) alle Tupel der einen Tabelle
gelesen werden, um sie dann in einer inneren, im Schleifenrumpf der äußeren Schleife
befindlichen (nested ) Schleife, mit den jede Runde neu zu lesenden Tupeln der anderen
Tabelle zu vergleichen. Handelt es sich um größere Tabellen, so ist dieses Verfahren ineffizient, denn die Anzahl der zu lesenden Einträge entspricht in etwa dem Produkt der
Tabellenmächtigkeit der beteiligten Tabellen.
Die Verarbeitungsgeschwindigkeit bei der Ausführung eines Gleichheits-Verbundes,
lässt sich steigern, wenn die Daten linear angeordnet werden können und entsprechend
der Ordnung sortiert gespeichert wurden. Gibt es in wenigstens einer Tabelle keine Duplikate in der betreffenden, sortierten Spalte, so ist ein Verbund durch Mischen (merge
sort) möglich. Dabei werden die beiden Tabellen sequenziell durchlaufen, wobei jeweils
die Sequenz mit dem kleineren Element weiterrückt. Die gefundenen Paare werden der
Ergebnismenge zugefügt. Beim Mischverbund braucht jedes Tupel nur einmalig gelesen
werden, weswegen die Anzahl der Lesevorgänge proportional der Summe der Tabellenmächtigkeiten ist. Für nicht duplikatfreie, sortierte Tabellen bildet man jeweils Gruppen
mit gleichen Attributen, die dann beim Verbund jeweils miteinander verglichen werden.
1
2
1
1
1
1
1
1
2
2
2
3
2
3
4
5
5
5
4
5
5
5
Abbildung 2.1: Verbund zweier sortierter Tabellen
KAPITEL 2. VERBUNDE IN DATENBANKEN
5
Zur Illustration der Vergleichspartner betrachte man Abbildung 2.1, wo jeweils das
erste Attribut der linken Tabelle auf Gleichheit mit dem zweiten Attribut der rechten
Tabelle geprüft werden soll. Der linke Verbund besitze dabei keine Duplikate beim ersten
Attribut, während beim rechten Verbund erst die grau unterlegten Gruppen gebildet
werden, um deren Elemente anschließend miteinander zu vergleichen.
Da eine Tabelle in der Regel nur nach einer Spalte sortiert werden kann, ist ein vorheriges Umsortieren nötig, bevor ein Mischverbund erstellt werden kann. Das Umsortieren
nimmt dann aber bei größeren Tabellen, im Vergleich zum eigentlichen Verbund, die meiste Zeit in Anspruch. (Bei einer Tabellenmächtigkeit von N ist der Zeitbedarf O(N log N ).)
Um Tabellen nicht vor jedem Verbund neu sortieren zu müssen, sind weitere Strukturen
nötig, um einen Verbund effizient zu unterstützen. Dies ist mit einem Index möglich, einer Struktur die bereits zur Unterstützung der Suche in einzelnen Tabellen Anwendung
findet.
2.2
Indexe in Datenbanken
Sollen in einer Tabelle nicht alle, sondern nur einzelne Datensätze lokalisiert werden,
so kann die Datenbank durch Erstellung von Indexstrukturen unterstützt werden. Diese
werden für die Spalten erstellt, afür die eine Selektion stattfinden soll, ein Index erstellt
wird. Um bestimmte Einträge zu finden, ist dadurch kein kompletter Scan einer Tabelle
mehr notwendig. Statt dessen kann die Datenbank bei der Auswahl eines Zugriffspfades
entscheiden, einen Index zu verwenden.
In einem Index sind zu Schlüssel, die aus den Werten in der indexierten Spalte generiert werden, zumeist Verweise auf die Speicherorte der entsprechenden Daten abgelegt
(z.B. als Tupel-ID oder RowID). Bei einer Selektion wird daher in einem deutlich weniger Speicherseiten belegendem Index nachgesehen, welche Tupel als Ergebnis in Frage
kommen. Speicherzugriffe auf externen Speicher sind vergleichsweise zeitraubend. Somit
lohnt sich der zusätzliche Verwaltungsaufwand mit einer solchen Zugriffsstruktur besonders, wenn häufig Anfragen gestellt werden, die nur einen kleinen Teil des Datenbestandes
einer Tabelle selektieren. Dadurch braucht zusätzlich zu den Indexdaten möglicherweise
nur ein Bruchteil des eigentlichen Datenbestandes geladen werden.
Da Datenbanken die Tupel üblicherweise zu Seiten gruppiert im externen Speicher ablegen, sollte die Ergebnismenge des Index-Scans vor dem eigentlichen Datenzugriff nach
den Speicherseiten der referenzierten Speicherseiten sortiert werden. Sonst kann ein kompletter Scan der gesamten Tabelle schon bei mittlerer Selektivität einer Anfrage effizienter
sein als ein Indexscan, da der mögliche Geschwindigkeitsvorteil des indexgestützten Zugriffes durch die zur Auffindung der entsprechenden Seiten auf externen Speichermedien
benötigte Zeit bei wahlfreiem Zugriff zunichte gemacht wird, wenn beim einzelnen Aufsuchen der Tupel die Speicherseiten mehrfach gelesen werden müssen.
Statt Referenzen auf die Datensätze im Index abzulegen, ist es auch denkbar, weitere Kopien der Attribute abzuspeichern. Solche Indexe haben Vorteile bei aggregierenden
Funktionen (wie sum(GEHALT)), da die betreffenden Daten nicht erst in den Speicherseiten der Datensätze gesucht werden müssen, sondern direkt mittels des Indexes ausge-
KAPITEL 2. VERBUNDE IN DATENBANKEN
6
wertet werden können.
Bei den Zugriffstrukturen unterscheidet man, ob sie für die Indexierung von Primärschlüsseln oder Sekundärschlüsseln ausgelegt sind. Bei Primärschlüsseln ist der Suchschlüssel einmalig, so dass eine Suche auf höchstens einen Satz führt. Zugriffspfade für
Sekundärschlüssel, auch sekundäre Zugriffspfade genannt, können dagegen mehrere Datensätze als Ergebnis liefern. Die bei sekundären Zugriffspfaden verwendeten Strukturen
kann man in Einstiegsstruktur und Verknüpfungsstruktur aufteilen. Als Einstiegsstruktur sind alle Strukturen möglich, die auch bei Primärschlüsseln verwendet werden. Statt
zum Schlüssel eine RowID abzuspeichern, wird dann aber die Verknüpfungsstruktur oder
eine Referenz darauf im Index abgelegt. Typischerweise verwendet man als Verknüpfungsstruktur eine Liste von RowIDs. Statt einer einzelnen Referenz auf einen Tupel erhält man
bei einer erfolgreichen Suche diese Liste und damit gleich alle Referenzen auf Tupel, die
im Index den gleichen Schlüssel besitzen. Ein Schlüssel wird dabei nur einmal im Index
abgelegt, kommen ein weiterer Eintrag mit gleichem Schlüssel hinzu, wird lediglich die
Verknüpfungsstruktur um ein Element erweitert.
Alternativ wäre es auch möglich, die Einträge mehrfach mit gleichem Schlüssel im
Index abzulegen, wenn man dies bei der Suche entsprechend berücksichtigt. Dadurch
werden die Schlüssel zwar redundant gespeichert, man spart sich aber ein aufwendiges
Listenmanagement für die Verknüpfungsstruktur, weswegen dieses Verfahren in der folgenden Vorstellung einiger Indexstrukturen gelegentlich verwendet wird.
Als Indexstrukturen kommen vor allem die sogenannten Mehrwegbäume (und dabei
besonders B*-Bäume) in Frage, die in einem Knoten mehrere Verzweigungsmöglichkeiten besitzen. Binäre Suchbäume werden in Datenbanken selten eingesetzt, da sie überlicherweise auf Anwendungen, die im Hauptspeicher ablaufen, ausgerichtet, aber nicht
auf seitenbasierte Verarbeitung ausgelegt sind. Für einige Anwendungsfälle können HashVerfahren und Bitmap-Listen eingesetzt werden.
2.2.1
B*-Bäume
B*-Bäume gehören zu den hohlen“ oder blattorientierten Bäumen, denn die eigentlichen
”
Informationen befinden sich ausschließlich in den Blattknoten. Die Knoten haben feste
Größen, die sich üblicherweise an den Blockgrößen eines Speichertransfers zu einem externen Speichermedium orientieren (also z.B. 4kB). Die inneren Knoten beinhalten lediglich
Referenzschlüssel, die als Wegweiser bei der Suche nach den richtigen Blättern verwendet
werden. Wesentliche Kriterien eines B*-Baumes sind:
• Alle Blätter sind gleich weit von der Wurzel entfernt
• Interne Knoten beinhalten zwischen k und 2k Schlüssel
• Blätter haben zwischen j und 2j Einträge
• Die Wurzel nimmt eine Sonderstellung ein: Sie hat mindestens 2 Nachfolger oder
ist ein Blatt. Als Blatt kann sie auch weniger als j Einträge besitzen.
KAPITEL 2. VERBUNDE IN DATENBANKEN
7
Die Parameter k und j richten sich nach dem Speicherbedarf der in den Knoten gespeicherten Informationen und sagen im Wesentlichen aus, dass ein Knoten höchstens voll
und mindestens halbvoll sein muss.
Es ergeben sich zwei verschiedene Knotenformate: Interne Knoten enthalten Schlüssel
und Zeiger auf Nachfolgeseiten, Blätter Schlüssel Si und die dazugehörenden Datenelemente Di (Zeiger oder Zeigerlisten auf Datensätze). Die Informationen aller Knoten werden nach den Schlüsseln sortiert abgespeichert. Zur Suche nach Schlüsselbereichen (und
zur Restrukturierung des Baumes) ist es hilfreich, die Knoten nicht nur von der Wurzel
zu den Blättern zu verlinken, sonden zusätzliche Zeiger auf den linken und rechten Nachbarknoten vorzusehen. In Abbildung 2.2 sind die beiden Knotentypen dargestellt. Dabei
muss die aktuelle Anzahl der in internen Blättern gespeicherten Schlüssel x zwischen k
und 2k liegen (für Blätter: j ≤ y ≤ 2j). Man beachte auch, dass die internen Knoten auf
einen Nachfolgeknoten mehr verweisen, als Schlüssel darin gespeichert sind.
interne Knoten
Blattknoten
S1
S2
..........
Sx−1
Sx
freier Platz
S1 D1 S2 D2 .......... Sy−1 Dy−1 Sy Dy freier Platz
Abbildung 2.2: Möglicher Knotenaufbau eines B*-Baumes
Aus den eingeschränkten Verzweigungsgraden (j, k) der beiden Knotentypen des Baumes resultiert, dass sich die Baumhöhe nur in gewissen Grenzen bewegen kann. Für N
N
Datenelement bewegt sich die Baumhöhe h für h ≥ 2 zwischen 1 + log2k+1 ( 2j
) (alle KnoN
ten maximal gefüllt) und 2 + logk+1 ( 2j ) (minimal gefüllte Knoten, die Wurzel hat nur 2
Nachfolger).
Die Baumhöhe entspricht aber gerade der Anzahl der zu lesenden Seiten bei einer
Suche nach einem einzelnen Schlüssel. Damit lässt sich der Zeitaufwand zur Suche eines Schlüssels abschätzen. Zur Optimierung der Suche ist die Reduzierung der Höhe am
geeignetsten, denn dies ist der dominierende Faktor bei der Zugriffszeit. Grundgedanke
ist dabei die Verbreiterung des Baumes, was durch Erhöhung der Anzahl der in internen
Knoten gespeicherten Zeiger möglich ist. Dies kann bei gleicher Seitengröße geschehen,
indem die Schlüssellänge verkürzt wird. Bei gewissen Datentypen (z.B. Zeichenketten)
kann dies sehr leicht durch Kompressionsverfahren geschehen, da die internen Schlüssel
nur Wegweiserfunktion haben. (Bei Strings beispielsweise: Speicherung nur des Präfixs
oder Speicherung nur der Zeichen, in denen sich der Schlüssel vom Vorgänger und Nachfolger unterscheidet.)
Zur Suche im Baum vergleicht man den Suchschlüssel mit den in internen Knoten gespeicherten Referenzschlüsseln, bis man den ersten größeren oder gleichen gefunden hat.
Die Suche wird dann mit dem Teilbaum, der durch den links neben dem Referenzschlüssel liegenden Zeiger verlinkt ist, fortgesetzt, bzw. mit dem Zeiger ganz rechts, falls alle
Referenzschlüssel kleiner sind. Erreicht man die Blattebene, so sucht man, bis man den
passenden Schlüssel gefunden hat.
KAPITEL 2. VERBUNDE IN DATENBANKEN
8
Da die Einträge auf Blattebene ebenfalls sortiert vorliegen, hat man alle Einträge
gefunden, sobald ein größerer Schlüssel als der Suchschlüssel beim sequentiellen Durchsuchen der Schlüssel auftritt. Passiert dies nicht, so wird die Suche in den rechten Nachbarknoten fortgesetzt. Zeiger auf die Nachbarknoten erleichtern dies. Im B*-Baum ist nicht
nur eine Suche auf Gleichheit möglich, sondern auch auf einen Wertebereich sehr einfach.
Die Suche beginnt mit der linken Intervallgrenze als Suchschlüssel. Auf der Blattebene
werden dann alle Einträge bis zur rechten Grenze geliefert. Soll die Sortierreihenfolge
umgedreht werden, so nimmt man die obere Intervallgrenze, wobei man evtl. den rechts
neben den Referenzschlüsseln liegenden Zeiger verfolgt. Auf der Blattebene wird dann der
Verweis auf den linken Nachbarknoten ausgenutzt, und ab dort jeweils von rechts nach
links bis zur unteren Intervallgrenze gesucht.
Die Einfüge- oder Löschoperationen funktionieren sehr einfach, solange dadurch die
Einschränkungen der Knotenbelegung nicht verletzt werden. (An der richtigen Position einfügen, bzw. löschen und die sonstigen Einträge entsprechend aufrücken.) Im Falle
einer Unter- oder Überfüllung werden Algorithmen zur dynamischen Restrukturierung
eingesetzt. Dazu wird ein voller Knoten entweder auf die Nachbarknoten verteilt (die mit
Zeigern auf die Nachbarknoten der gleichen Ebene leicht zu finden sind) oder man spaltet
einen Knoten in zwei neue, halb so große Knoten auf. Der linke Knoten wird anstelle des
ursprünglichen Knotens im Vorgängerknoten verlinkt (der Referenzschlüssel links daneben muss evtl. geändert werden), der rechte muss rechts daneben mit einem neuen Zeiger
und entsprechendem Referenzschlüssel in den Vorgängerknoten eingesetzt werden. Die
Referenzschlüssel, die immer zwischen zwei Zeigern und damit Teilbäumen stehen, müssen immer genau so gewählt werden, dass alle Blätter im linken Teilbaum kleiner-gleich
und im rechten größer-gleich sind. Sie können damit aus dem linkesten Blatt des rechten
Teilbaumes generiert werden. Das Aufsplitten kann sich bis zur Wurzel fortsetzen, falls
keine Einträge auf Nachbarknoten verteilt werden können. Wird die Wurzel aufgeteilt, so
erhält der Baum eine neue Wurzel mit den beiden beim Aufteilen enstandenen Knoten
als Nachfolger.
Bei einer Unterfüllung kann man versuchen, sich zunächst Einträge aus Nachbarknoten
zu leihen, ansonsten müssen zwei Knoten zusammengefasst werden. Dies löst wiederum
ein Entfernen von Referenzschlüssel und Zeigern aus den Vorgängerknoten aus, was sich
bis zur Wurzel forsetzen kann. Wurzeln mit nur noch einem Nachfolger werden gelöscht,
und der alte Nachfolger als neue Wurzel verwendet.
Abschließend betrachte man noch als Beispiel den B*-Baum in Abbildung 2.3, wo ein
Element mit Schlüssel p“ eingesetzt werden soll. Zur Vereinfachung ist in den Blättern
”
nur die Schlüsselkomponente dargestellt und die Blätter fassen lediglich einen Eintrag.
Die internen Knoten haben bei zwei Referenzschlüsseln Platz für 3 Nachfolgeknoten.
Zuerst wird der Knoten gesucht, in den p eingesetzt werden muss. Dies ist das Blatt
1 (auf der vorletzten Ebene ist im Knoten 2 der Schlüssel q“ der erste größer-gleich p“,
”
”
der linke Zeiger führt zu 1). Das Blatt fasst (hier) nur einen Eintrag, deswegen wird es
geteilt und das Blatt mit p“ erzeugt. Der linke Knoten ist Blatt 1a, er bleibt in 2 verlinkt.
”
Das neue Blatt 1b muss rechts von o“ eingefügt werden, mit Schlüssel p“. In 2 ist nicht
”
”
genug Platz für einen neuen Eintrag, deswegen muss wieder geteilt werden. Die Knoten
KAPITEL 2. VERBUNDE IN DATENBANKEN
9
j t
3
e
4
n r
x
2
c c
f
a c c
l m
o q
r s
j l m 1
r r s
e f
n o q
v
y z
x y z
t v
j r
n p
e
t x
2a
c c
f
a c c
l m
2b
q
o
j l m
e f
r s
v
r r s
n o
p q
y z
x y z
t v
1a 1b
Abbildung 2.3: Aufteilung von Knoten beim Einsetzen in einen B*-Baum
2a und 2b entstehen und müssen in 3 eingetragen werden. Eigentlich müsste Knoten 3
geteilt werden, da aber in Knoten 4 noch Platz ist, kann ein Eintrag verschoben werden.
Dies bewirkt auch, dass der Referenzschlüssel r“ in die Wurzel wandert, und t“ als neuer
”
”
Schlüssel in Knoten 4, denn r“ ist jetzt das linkeste Blatt des ganz rechten Teilbaumes
”
in der Wurzel. In 3 ist jetzt Platz, um den Knoten 2b einzutragen, dessen linkestes Blatt
p“ ist.
”
2.2.2
Hash-Index
Hash-Indexe verwenden Schlüsseltransformations-Verfahren zur Auffindung der Indexinformationen. Die Speicheradresse des Indexeintrages eines Datensatzes wird durch eine
geeignete Hash-Funktion direkt aus dem Suchschlüssel berechnet. Im Idealfall wird keine weitere Hilfsstruktur benötigt, um dadurch den Datensatz aufzufinden. Die Aufgabe
der Hashfunktion besteht darin, allen möglichen Werten des Schlüsselraumes einen Wert
des Adressraumes zuzuweisen. Für die bei Datenbankanwendungen interessanten HashVerfahren, die auf Externspeicher auf Seitenbasis ausgelegt sind, entspricht der Adressraum einer Seitennummer, in der möglichst alle Indexeinträge mit demselben Hash-Wert
untergebracht werden.
Da üblicherweise nicht alle der möglichen Schlüssel gerade verwendet werden, ist es
in der Regel nicht möglich, eine Hashfunktion zu finden, die eine kollisionsfreie Satzzuordnung ermöglicht, ohne extreme Speicherplatzverschwendung in Kauf zu nehmen.
Deswegen nimmt man hin, dass zu verschiedenen Schlüsseln die gleiche Adresse berech-
KAPITEL 2. VERBUNDE IN DATENBANKEN
10
net wird. Diese Speicher- oder Segment-Adresse bezeichnet eine Seite fester Größe des
externen Speichers, auch Bucket (Eimer, Korb) genannt, in dem mehrere Datenelemente
Platz finden können, ohne eine Kollision auszulösen.
Zahlreiche Hash-Verfahren wurden im Laufe der Zeit entwickelt, die eine gleichmäßige
Ausnutzung des zur Verfügung stehenden Adressbereiches erstreben. Einige davon sind:
Restklassenbildung: (Divisionsrestverfahren) Die Speicherdarstellung des Schlüssels
wird als ganze Zahl s interpretiert. Mit einer Hash-Funktion h(s) = s mod n
wird daraus eine Restklasse berechnet, die eine Speicheradresse des Adressraumes
der Mächtigkeit n ergibt. Mit der Wahl der Bucket-Anzahl wird dabei wesentlich
die Gleichverteilung der Schlüssel beeinflusst. Empfehlenswert sind Primzahlen, die
nicht benachbart zu Potenzen des vom Rechner verwendeten Zahlensystems liegen
sollten, da sich sonst bei gleichen Endziffern ähnliche Adressmengen in verschiedenen Zahlenbereichen wiederholen.
Dieses Verfahren wird insbesondere bei nicht bekannter Schlüsselverteilung empfohlen.
Faltung: Der in einzelne Bestandteile zerlegte Schlüssel wird durch Spiegelungen oder
Verschiebeoperationen überlappend übereinandergelegt. Durch Addition, Multiplikation oder Boolsche Verknüpfung erhält man einen Hash-Wert, der nur noch an
den verfügbaren Adressraum angepasst werden muss.
Dieses Verfahren kann gut in Kombination mit der Restklassenbildung eingesetzt
werden, um eine noch bessere Gleichverteilung zu erreichen.
Multiplikationsverfahren: Der Schlüssel wird mit einer Konstanten multipliziert oder
mit potenziert. Aus der sich ergebenden Zahl wählt man eine von der Größe des
Adressraumes abhängige Anzahl von Bits aus, die dann den Hash-Wert, also die
Adresse bilden.
Zufallsgenerator: Ein an den zur Verfügung stehenden Adressraum angepasster PseudoZufallszahlengenerator wird mit dem Schlüssel als Saat“ aufgerufen. Sollte eine
”
Kollisionsbehandlung notwendig sein, so kann mit einem weiteren Aufruf des Generators (reproduzierbar) eine ganze Folge von weiteren Adressen berechnet werden.
Ziffernanalyse: Ist die Menge der zu speichernden Schlüssel vorab bekannt, so kann
diese Menge untersucht werden, um eine Verteilungsschiefe bei ungünstiger Parameterwahl (z.B. der zu verwendenden Bits) rechtzeitig zu erkennen und zu beheben.
Dieses Verfahren ist sehr aufwendig, gewährleistet aber eine gute Gleichverteilung.
Leider sind in produktiven Umgebungen die Schlüssel üblicherweise nicht a priori bekannt, weswegen das Verfahren praktisch nur für statische Daten angewendet
werden kann.
Da es bei festgelegter Anzahl von Adressen im Laufe des Datenbankbetriebes zu Kollisionen, also vollen Index-Speicherseiten, kommen kann, ist eine Kollisions- oder Überlaufbehandlung notwendig, wenn weitere Schlüssel indexiert werden sollen. Bei der Suche
im Index sind evtl. vorhandene Überlaufbereiche entsprechend mit einzubeziehen.
KAPITEL 2. VERBUNDE IN DATENBANKEN
11
Tritt eine Kollision auf, so kann diese aufgelöst werden, indem ein Speicherplatz innerhalb einer anderen Seite des ursprünglich Adressraumes (des Primärbereiches) gesucht
wird oder es wird eine weitere Speicherseite im sogenannten Überlaufbereich erstellt.
Möchte man nur den Primärbereich verwenden, so ist die Aufnahmefähigkeit des Indexes
durch die bei der Indexerstellung vorgegebene Seitenzahl begrenzt. Gibt es ein Kollision,
so verwendet man den Korb mit der nächsten relativen Seitennummer, erstellt eine Verkettung der Seiten oder benutzt eine weitere Hash-Funktion (oder einen weiteren Aufruf
der Hash-Funktion) zur Berechnung des Ablageplatzes. Durch Clusterbildung kann es bei
hohem Belegungsfaktor (≥ 80%) verstärkt zu Mehrfachkollisionen kommen. Zusätzlich
ist das Löschen erschwert, da die Kollisionen dann wieder aufgelöst werden müssen, bzw.
man Löschvermerke und spätere Reorganisation benötigt. Daher empfiehlt es sich einen
separaten Überlaufbereich einzurichten. Dort können neue Speicherseiten angelegt werden, die durch Verkettung mit dem Primärbereich erreichbar sind. Dadurch erhöht sich die
Anzahl der zu einem Hash-Wert speicherbaren Einträge dynamisch, jeweils um die Größe
eines Korbes. Auch wenn die doppelte Kapazität des Primärbereiches gespeichert werden
muss, kann die Anzahl der durchschnittlich benötigten Seitenzugriffe üblicherweise < 2
gehalten werden.
Als Beispiel für einen Hash-Index mit verkettetem Überlaufbereich betrachte man
Abbildung 2.4, wo die Hashfunktion aus einer Faltung mit anschließendem Divisionsrestverfahren besteht.
Ein Datenelement, dessen zugehöriger Schlüssel binär die Folge 1001011010101100
besitzt, soll neu in den Index aufgenommen werden. Dazu wird der Schlüssel in ein höherwertiges und niederwertiges Byte aufgeteilt, und die beiden Teile mit einer bitweisen
XOR-Operation (⊕) verknüpft. Die sich ergebende Zahl hat dezimal den Wert 58. Da
fünf Buckets vorgesehen sind, wird mit dem Divisionsrestverfahren eine Korb-Nummer
berechnet, wobei als Modul 5 verwendet wird. Der berechnete Bucket (3) ist schon voll,
so dass in den Überlaufbereich ausgewichen werden muss. Dieser ist über Verkettung
der zusätzlich benötigten Seiten an den Primärbereich angeschlossen. Dort ist noch für
zwei Elemente Platz, so dass ein Eintrag ohne weitere Kollision an der dritten Position
vorgenommen werden kann.
Ist die Indexgröße bekannt, so kann mit einem Verfahren, das nach dem Prinzip des
Open Adressing arbeitet, ein Index erstellt werden, der mit genau einem Externspeichzugriff einen gesuchten Indexeintrag findet. Die Belegung sollte dabei aber dennoch nicht
über etwa 80% steigen, denn sonst kann die Zugriffseigenschaft nicht mehr garantiert
werden. Als zusätzliche Datenstruktur wird eine sogenannte Separatortabelle ständig im
Hauptspeicher gehalten, die etwa ein Byte pro Bucket benötigt.
Zunächst erfolgt die Beschreibung der Suche: Zu jedem Schlüssel wird nicht nur ein
Hash-Wert, sondern gleich ein ganzer Vektor von möglichen Adressen berechnet, z.B. mit
einem Pseudozufallsgenerator, der den Schlüssel als Saat verwendet. Zusätzlich wird mit
einer weiteren Funktion eine Signatur für jeden Schlüssel berechnet. Die Separatortabelle
enthält für jeden Bucket einen Eintrag, der bestimmt, bis zu welcher Signatur Einträge auf
der entsprechenden Speicherseite gemacht werden. Zur Bestimmung der Speicherseite auf
dem externen Speicher werden die Werte des Hash-Vektors nacheinander durchgegangen
KAPITEL 2. VERBUNDE IN DATENBANKEN
Primärbereich
0 S1
D1
D2
D3
1 S1
D1
D2
D3
D4
2 S1
D1
D2
D3
D4
S2
S3
S2
S3
S4
S2
S3
S4
3 S1
S2
S3
S4
4 S1
S2
D1
D2
D3
D4
12
Überlaufbereich
S1
S2
S3
S4
D1
D2
D3
D4
S1
D1
Schlüssel: 1001011010101100
10010110
00111010
S1
S2
S3
D1
D2
D3
001110102 = 5810 mod 5
3
D1
D2
Abbildung 2.4: Hash-Index mit separatem Überlauf und verketteten Buckets
(sondiert), wobei jeweils der Eintrag in der Separatortabelle mit der Signatur verglichen
wird. Ist die Signatur kleiner als der Eintrag in der Tabelle, so befindet sich der Datensatz
in dem entsprechenden Bucket (oder existiert nicht), ansonsten muss der Eintrag des
nächsten Vektor-Elements überprüft werden.
Soll ein neuer Eintrag eingefügt werden, so wird ebenfalls ein Hash-Vektor und eine
Signatur für den zugehörigen Schlüssel berechnet. Wie bei der Suche wird die Signatur
entsprechend dem Hash-Vektor mit den in Frage kommenden Buckets bzw. ihren Einträgen in der Separatortabelle verglichen. Der erste Bucket, wo die Signatur kleiner als der
Eintrag in der Separatortabelle ist, kommt damit in Frage. Ist dort noch genügend Platz,
so kann der Eintrag vorgenommen werden. Ist dies nicht der Fall, so wird der Eintrag des
Buckets in der Separatortabelle verkleinert, bis dadurch wenigstens ein Platz im Bucket
frei wird. (Bei leerer Tabelle und damit bis zum ersten Überlauf, steht der Eintrag in der
Separatortabelle auf dem maximalen Wert.) Dies bedeutet, dass diejenigen Einträge, die
durch den jetzt niedrigeren Separator-Eintrag nicht mehr in diesem Bucket bleiben dürfen
und entsprechend ihres Hash-Vektors in andere Körbe eingetragen werden müssen. Damit
sich dies nicht unendlich fortsetzt, ist es sehr wichtig, dass der Füllgrad des Indexes klein
genug bleibt (als Richtwert: < 80%), sonst explodieren“ die Einfügekosten. Da es beim
”
Löschen von Einträgen äußerst schwierig ist, die Einträge zu finden, die bereits den Korb
wechseln mussten, ist ein Erhöhen der Einträge in der Separatortabelle nicht vorgese-
KAPITEL 2. VERBUNDE IN DATENBANKEN
13
hen, denn ohne die Schlüssel ist auch der Hash-Vektor nicht bekannt Stattdessen müsste
der Index komplett neu aufgebaut werden, wenn die Einträge in der Separatortabelle zu
niedrig werden.
Die bisher beschriebenen Verfahren verwenden eine feste Anzahl Buckets, weswegen sie
zu den statischen Hash-Verfahren zählen. Daneben gibt es die dynamische Verfahren, bei
denen man versucht, die Bucket-Anzahl an den Speicherbedarf anzupassen. Dabei wird bei
einem Überlauf“ die Anzahl der Buckets so erhöht, dass weitere Einträge beim Einfügen
”
bereits durch die Hash-Funktion richtig aufgeteilt werden können. Ein Verfahren, dass
einen Index für die Buckets verwendet, möchte ich dazu kurz skizzieren:
Statt den Wert der Hash-Funktion, zum Beispiel durch das Restklassenverfahren,
an die vorgegebene Anzahl an Körben anzupassen, wird der dabei berechnete PseudoSchlüssel zunächst nicht weiter transformiert. Stattdessen wird damit ein Blatt eines
Digitalbaum gesucht, welches den entsprechenden Korb darstellt. Die Kanten des Digitaloder Radix-Baumes sind jeweils mit einem Teil (ein oder bei größerem Verzweigungsgrad
entsprechend mehrere Bits) der Schlüssel des Bit-Strings markiert, die im Blatt am Ende
des Weges durch den Baum zu finden sind. Droht beim Einsetzen ein Überlauf, so wird
der Baum an der entsprechenden Stelle weiter aufgeteilt. In Abbildung 2.5 kann man
beispielsweise sehen, wie dies beim Einsetzen des Schlüssels mit Hash-Wert 11010 gleich
zum mehrfachen Aufteilen des Buckets mit dem Präfix 1 führt. Es entstehen zunächst
Buckets, die die Pseudoschlüssel, die mit 10 und 11 beginnen, aufnehmen würden. Da es
aber keinen zu speichernden Hash-Wert gibt, der mit 10 beginnt, landen alle Einträge im
rechten Teilbaum. Dieser wird abermals aufgeteilt in zwei Körbe für die Pseudo-Schlüssel,
die mit 110 respektive 111 beginnen.
0
0
1
0
1
00010
00101
00110
00110
01000
01101
01100
01110
00*
01*
0
11001
11101
11111
11110
1*
1
1
0
1
0
00010
00101
00110
00110
01000
01101
01100
01110
00*
01*
1
10*
11001
11010
11101
11111
11110
110*
111*
Abbildung 2.5: Hash-Index mit Digitalbaum als Index
Zur leichteren Veranschaulichung sind in der Abbildung die Hash-Werte, d.h. Pseudoschlüssel, in den Blättern eingetragen. Tatsächlich würde man dort wie immer die
eigentlichen Schlüssel und zugehörigen Datenelemente abspeichern.
Das Löschen von Einträgen ist problemlos möglich. Gemeinsam an einem Knoten angehängte Blätter können dabei problemlos wieder zusammengefügt werden, falls dabei
KAPITEL 2. VERBUNDE IN DATENBANKEN
14
genügend freie Positionen entstehen. In Abbildung 2.5 reicht beispielsweise das Löschen
eines Eintrages in den Blättern mit Präfix 110 oder 111, um die Blätter wieder zusammenfügen zu können (dann auch gleich mit dem leeren Knoten). Problematisch ist, dass
der Baum nicht ausbalanciert wird. Wird er durch zu viele gleiche Pseudoschlüssel bis
zur vollen Länge der Bit-Strings erweitert, so muss eine andere Kollisionsbehandlung
verwendet werden.
Anzumerken ist noch, dass mit Hash-Indexen zwar Indexstrukturen realisierbar sind,
die mit nur einem externen Speicherzugriff auskommen können, um einen Indexeintrag
zu lokalisieren, aber im Allgemeinen geht dabei die Ordnung der Einträge verloren. Bereichsanfragen sind deshalb nicht ohne weiteres zu realisieren.
2.2.3
Bitmap-Listen
Bitmap-Listen oder Bitmap-Indexe sind besonders gut zur Indexierung von Attributen geeignet, die sehr viele gleiche Schlüssel besitzen. Die Idee besteht darin, zu jedem Schlüssel
einen Bit-String zu speichern. Die einzelnen Bitpositionen dieses Bit-Strings korrespondieren wiederum mit einzelnen Datensätzen (oder Speicherseiten, die qualifizierte Datensätze
enthalten). Problematisch bei der Indexierung einzelner Sätze in den Seiten ist, dass in
modernen Datenbanken die Satzlänge meist variabel ist (z.B. bei Zeichenketten). In den
Bitlisten müssen dann evtl. sehr viele ungenutzte Bits bereitgehalten werden, wenn lange
Einträge eine Seite füllen, also nicht die maximale Anzahl von Einträgen in der Seite ausgenutzt wird, oder die maximale Anzahl in einer Seite speicherbarer Einträge wird auf die
vorgesehene Anzahl Bitpositionen in der Bitliste begrenzt, was bei sehr vielen kleinen Einträge zu ungenutzten Speicherseiten führt. Ein Ausweg bietet die zusätzliche Einrichtung
einer Zuordnungstabelle, die auch bei bestimmten Satzaddressierungstechniken eingesetzt
wird. Eine Bitposition entspricht dann einem Eintrag in der Zuordnungstabelle, die zur
Bestimmung der Seite eines Datensatzes herangezogen wird.
Die Vorteile von Bitmap-Listen bestehen in geringerem Speicherplatzverbrauch, falls
nur wenige verschiedene Schlüssel vorhanden sind. Statt Zeiger auf jeden Datensatz (oder
jede Seite) in einem Mehrwegbaum zu speichern, die meist jeweils mehrere Byte beanspruchen, reicht hier ein einzelnes Bit, um einen Eintrag anzuzeigen.
Beispielsweise betrachte man Abbildung 2.6, wo die einzelnen Bit-Listen für die untereinander angeordneten Schlüssel eine Matrix ergeben. Im Beispiel soll der Index auf
Speicherseiten verweisen, daher können mehrere Schlüssel in einer Seite vorkommen (mehrere 1en in einer Spalte). In einer Spalte steht genau dann eine 1, wenn der zu der Zeile
gehörende Schlüssel in der entsprechenden Seite vorhanden ist. Schlüssel wie S2 , die auf
fast allen Seiten vorhanden sind, benötigen nur einen relativ kurzen Bit-String, statt über
20 Einträge in einem Mehrwegbaum bzw. der Verknüpfungsstruktur eines sekundären Zugriffspfades.
Sucht man Tupel, die im indexierten Attribut die Schlüssel S1 oder S2 besitzen, so ist
dies mit einer OR-Verknüpfung der Bit-Strings zu ermitteln, Sind mehrere Attribute einer
Relation mit einem Bitmap-Index indexiert, können auch Bit-Strings der verschiedenen
Indexe bei Anfragen wie
KAPITEL 2. VERBUNDE IN DATENBANKEN
15
111 111 1 1 1 12 2 22 222 222
01 23 456 789 012 345 678 901 23 456 789 Seiten−Nr.
S 1 00 00 100 000 000 000 110 0 01 00 001 000
S 2 11 11 100 111 010 101 110 111 11 101 010
S 3 00 11 011 000 101 000 000 011 11 000 101
S 4 01 10 000 100 000 010 000 000 00 010 100
S 5 10 01 101 000 000 101 011 010 11 100 001
S 6 00 00 000 010 000 000 000 000 00 000 000
Schlüssel
Abbildung 2.6: Bitmap-Index: Schlüssel mit Bit-Strings als Bitmatrix dargestellt
select * from JEANS
where COLOR = ’RED’ and SIZE = ’L’;
miteinander verknüpft werden. Eine effiziente Hardwareunterstützung ist dabei möglich,
denn mengenalgebraische Operationen (AND, OR, NOT) lassen sich in modernen Prozessoren
oft schon mit einer Wortlänge von 64 Bit oder mehr parallel verarbeiten: Die Bitlisten
können dadurch schnell stückweise miteinander verknüpft werden.
Negativ auf den Speicherplatzbedarf und damit auch auf die Performance wirken sich
viele Einträge aus, die nur auf wenigen (oder sogar nur einer) Seite vorhanden sind. Denn
im Prinzip speichert man in der Bit-Liste nicht nur die Anwesenheit eines Attributes
in einem Datensatz (Bit gesetzt), sondern mit einem ungesetzten Bit auch dessen Abwesenheit ab. Sollten in Abbildung 2.6 viele Schlüssel wie S6 vorkommen, so wäre ein
Mehrwegbaum möglicherweise die geeignetere Indexstruktur. Für Primärschlüssel ist ein
Bitmap-Index ungeeignet, da jeder Schlüssel nur genau einmal vorkommt.
Dem Speicherplatzbedarf bei zunehmender Anzahl von Schlüsseln kann man in gewissen Grenzen entgegentreten, wenn man die Bit-Strings durch den Einsatz von Komprimierungstechniken effizienter speichert. So bietet es sich an, lange Null- oder Eins-Folgen,
oder häufig vorkommende Bitmuster durch bestimmte Codes zu kodieren. In Frage kommen praktisch alle verlustlos komprimierenden Verfahren, wie Bitfolgen-Komprimierung
(run length compression), besondere Codes für Nullfolgen und Bitmuster, Huffman-Codes,
Block-Kompression mit einem zusätzlichen Dicrectory, etc.
2.2.4
Verallgemeinerte Zugriffspfadstruktur
Bisher wurden Verfahren beschrieben, wo sich die Indexierung auf genau ein Attribut
genau einer Tabelle bezog. Daher ist es naheliegend, für jeden Index eine eigene Datenstruktur (z.B. einen B*-Baum) aufzubauen, denn dabei werden die Daten üblicherweise
auch nahe beieinander auf dem externen Speicher abgelegt.
Einen etwas anderen Ansatz verfolgt man bei verallgemeinerten Zugriffspfadstrukturen. Die Idee besteht dabei darin, Schlüssel mit dem gleichen Wertebereich in einer
gemeinsamen Struktur zu verwalten, auch wenn sie von verschiedenen Attributen oder
KAPITEL 2. VERBUNDE IN DATENBANKEN
16
Tabellen stammen. So könnte man beispielsweise Längenangaben, Zeiten, und Personalnummern, möglicherweise nach einer geeigneten Konvertierung, als Ganzzahlen in einem
gemeinsamen Mehrwegbaum unterbringen. Verwendet man B*-Bäume, so bleiben die internen Knoten davon im Aufbau unberührt. Dort dienen die Referenz-Schlüssel schließlich
nur als Wegweiser zu den richtigen Blättern. Allerdings ändert sich der Aufbau der Blätter, wo zu jedem Schlüssel Informationen zu allen in diesem Baum indexierten Attributen gespeichert werden. Statt eines Datenelementes speichert man Längen-Informationen
und Zeigerlisten für die jeweiligen Teil-Indexe. In Abbildung 2.7 ist dies für ein SchlüsselDatenelement-Paar in einem B*-Baum skizziert.
L1 Zeiger
L2 Zeiger
Ln Zeiger
Schlüssel L1 L2 ... Ln Z11 ... Z1j Z21 ... Z2k ... Zn1
Längen−Informationen
Znl
n Zeigerlisten
Abbildung 2.7: mehrere Zeigerlisten bei verallgemeinerten Zugriffspfaden
In der Abbildung werden n Attribute in einem gemeinsamen Index verwaltet. Das
Datenelement zum Schlüssel besteht daher aus n Längenangaben und n Zeigerlisten. Die
Längenangaben Li ermöglichen das Auffinden der zugehörigen Zeigerlisten Zih . Für Primärschlüssel haben die Zeigerlisten entweder 0 oder 1 Element (Primärschlüssel existiert
nicht, oder existiert), andere Attribute können beliebig häufig vorkommen. Da sich durch
die Zeigerlisten variabler Länge nicht immer gleich viele Schlüssel und Datenelemente
in einem Knoten unterbringen lassen, ist eine geeignete Überlaufbehandlung vorzusehen. Dies kann über eine Verkettung weiterer zu einem Knoten gehörenden Seiten in
einem separaten Überlaufbereich (wie bei den Hash-Indexen) oder durch die Aufteilung
in weitere Blätter des Baumes, z.B. mit einem weiteren (gleichen) Schlüssel, erfolgen. Die
Baumoperationen müssen dies entsprechend berücksichtigen. So muss beispielsweise die
Suchfunktion fortfahren, bis alle dem Suchkriterium entsprechenden Schlüssel gefunden
wurden, nicht nur bis zum ersten.
Die Höhe eines B*-Baumes als verallgemeinerte Zugriffsstruktur ändert sich auf Grund
des hohen Verzweigungsgrades nur unwesentlich, denn der Verzweigungsgrad der internen
Knoten ist von den zusätzlich abzulegenden Informationen nicht betroffen. Es können
aber, im Vergleich zu einem einfachen Index, zusätzliche Schlüssel hinzukommen, da auch
mehr Blätter benötigt werden. Dies sollte den Baum aber lediglich um einige wenige
Ebenen vergrößern. Daher sind die direkten Zugriffe nur unwesentlich langsamer als bei
einfachen Indexstrukturen.
Durch die gemeinsame Nutzung der Schlüssel brauchen diese nicht mehrfach in internen Knoten verschiedener einfacher Indexe abgespeichert zu werden, zudem ist es sogar
wahrscheinlicher, dass sich (zumindest die oberen) Knoten bereits in datenbankinternen Puffern befinden, wenn ein neuer Indexzugriff erfolgt. Aufgrund der zusätzlichen
Informationen in den Blättern (evtl. mit Verkettung) ist ein sequentieller Zugriff auf die
Datensätze unter Zuhilfenahme des Indexes nur geringfügig langsamer.
KAPITEL 2. VERBUNDE IN DATENBANKEN
17
Besonders vorteilhaft ist dagegen, dass bestimmte Verbunde auf geradezu natürliche
Weise unterstützt werden: Beim eingangs erwähnten Beispiel
select EMPLOYEES.name, DEPARTMENTS.name from EMPLOYEES, DEPARTMENTS
where EMPLOYEES.deptnr = DEPARTMENTS.deptnr;
befinden sich bei gemeinsamer Indexierung die Informationen zu EMPLOYEES.deptnr
und DEPARTMENTS.deptnr beim gleichen, gemeinsamen Schlüssel, und können damit
direkt ausgegeben werden.
2.3
Indexgestützter Verbund
Existiert ein Index auf eine oder mehrere der bei einem Verbund beteiligten Spalten, so
kann dieser auch bei der Bearbeitung einer Verbundanfrage verwendet werden. Nicht mehr
alle Datensätze müssen bei der Suche nach Verbundpaaren (evtl. mehrfach) eingelesen
werden, sondern dies kann mit dem deutlich kleineren Index geschehen. Der Index passt
möglicherweise sogar komplett in den Hauptspeicher des Datenbankrechners, so dass dort
der komplette Verbund berechnet werden kann, bevor die zugehörigen Datensätze gelesen
werden müssen.
Lassen sich die Schlüssel des Indexes ordnen, so werden die Indexeinträge in der Regel
auch geordnet gespeichert, so dass sie im Index schnell gefunden werden. Dies kommt
wiederum den Verbundalgorithmen zugute, die geordnete Wertemengen benötigen. Zur
Anwendung des bereits erwähnten Mischverbundes reicht es bereits, wenn über den Index
die Tupel auf logischer Ebene geordnet sind. Statt der eigentlichen Tupel kann der Verbund anhand der Schlüssel der Indexeinträge stattfinden. Die Verarbeitung der Verbünde
kann ausschließlich mit Schlüssel- und RowID-Listen erfolgen, wenn für die betroffenen
Attribute geeignete Indexe zur Verfügung stehen. Ist kein Verbund kompletter Tabellen gefordert, so kann diese Liste noch mit weiteren Selektionen verknüpft werden (oder
bereits sein), bevor der eigentliche Zugriff auf die Datentupel erfolgen muss.
2.3.1
Verbundindexe
Als spezieller Zugriffspfad für Zwei-Weg-Verbunde ist es möglich, einen Verbundindex
(join index ) bereitzustellen. Prinzipiell handelt es sich dabei um einen vorberechneten
Verbund: Die Tupel-IDs (TIDs) der Verbundpaare bilden dabei einen Eintrag in einer
zweistelligen Relation V , die alle Paare eines Verbundes enthält, die zu einer gegebenen
Operation auf zwei Verbundattributen gehören. Also etwa
V = {(a.T ID, b.T ID)|f (a.α, b.β) = true, a ∈ A, b ∈ B}
wobei α und β die Verbund-Attribute der Zeilen a und b in den Relationen A, B sind.
Die Relationen müssen dabei nicht verschieden sein. Die boolsche Funktion f kann auch
deutlich komplexer als einfache Vergleiche (=, 6=, <, ≤, etc.) sein und bereits Selektionen
auf α und β berücksichtigen.
Da ein solcher Verbundindex nicht notwendigerweise im Hauptspeicher des Datenbanksystems Platz findet, sollte er geeignet gespeichert werden. Zum schnellen Zugriff
KAPITEL 2. VERBUNDE IN DATENBANKEN
18
empfiehlt sich eine geordnete Speicherung jeweils einer Kopie in zwei B*-Bäumen, eine
nach dem ersten, die andere nach dem zweiten Attribut sortiert. Sollte die Verbundrichtung vorgegeben sein, reicht möglicherweise auch die Speichung einer nach dem relevanten
Attribut sortierten Kopie.
A
T−ID1
B
T−ID7
T−ID3
T−ID2
T−ID1
T−ID4
T−ID2
T−ID1
T−ID2
T−ID6
T−ID3
T−ID3
T−ID5
T−ID1
3
1 7
1 4
2 1
2 6
3 2
3 3
5 1
1 4
2 6
1 7
3
2 1
5 1
3 2
3 3
Abbildung 2.8: Verbundindex als Tabelle und als B*-Bäume, jeweils nach den Tupel-IDs
sortiert
Im Falle eines kompletten Verbundes kann der B*-Baum sequentiell traversiert werden.
Kommen Selektionen hinzu, ergibt sich bei der vorherigen Selektion eine Liste der auszuwählenden Tupel-IDs. Diese wird sortiert, um aus dem entsprechend sortierten B*-Baum
die entsprechenden Verbundpaare in einem Durchlauf auszuwählen. Die referenzierten
Datensätze werden dann vom Externspeicher geholt und unter Beachtung möglicher Projektionsklauseln ausgegeben.
Eine andere Form von Verbundindex lässt sich mit Bitmap-Listen erstellen. Statt
Datensätze oder Speicherseiten der eigenen Relation repräsentieren die einzelnen Stellen
eines Bit-Strings dabei Primärschlüssel oder Listen davon (Tupel-IDs/RowIDs), einer
anderen Tabelle. Werden die Bit-Strings komprimiert, so ergibt sich ein Verbund, der
im Vergleich zu einer materialisierten Sicht eines Verbundes speichereffizienter ist, denn
bei einer materialisierten Sicht werden die RowIDs in der Regel nicht komprimiert (siehe
auch [O9i-DWG] und [O9i-DPTG]).
Änderungsoperationen auf den beteiligten Relationen können sehr umfangreiche Aktualisierungen des Verbundindexes nach sich ziehen. Möglicherweise ist es bei größeren
Änderungen der Relationen günstiger, den Verbundindex komplett neu aufzubauen, statt
viele Änderungen einzelner Zeilen nachzuvollziehen. Daher empfiehlt sich der Einsatz
eines Verbund-Indexes besonders für Anwendungen, die viele Verbund-Abfragen, aber
wenige Datenänderungen nach sich ziehen, wie dies bei Data-Warehouse Anwendungen
oft der Fall ist.
Die Idee einen vorberechneten Verbund zu speichern, lässt sich auf einfache Art und
Weise von einem Zwei-Wege-Verbund auf einen n-Wege-Verbund verallgemeinern. Statt
mehrere zweistellige Verbunde bei mehrstelligen Verbunden zu verknüpfen, kann auch
ein mehrstelliger Verbund vorab berechnet werden. Dazu werden für weitere Attribute
und damit verbundene Verknüpfungen weitere Spalten in der Verknüpfungstabelle be-
KAPITEL 2. VERBUNDE IN DATENBANKEN
19
reitgestellt, was wiederum mehrere B*-Bäume für die entsprechenden Sortierungen nach
sich ziehen kann. Dabei sollte man beachten: Je mehr Verbundoperationen in einem gemeinsamen Index vorberechnet sind, desto spezieller ist sein Einsatzbereich und damit
umso geringer der praktische Nutzen für beliebige Verknüpfungen. Zudem wird der Index
zunehmend wartungsaufwendiger bei Aktualisierungsoperationen.
2.3.2
Verbunde in objektrelationalen Datenbanken
In objektrelationalen Datenbanken gibt es für den Benutzer die Möglichkeit eigene Datentypen (und dazugehörende Methoden) aus den bestehenden Datentypen zu konstruieren.
Diese Flexibilität bringt allerdings mit sich, dass die auf Standarddatentypen basierenden
Indexierungsalgorithmen nicht oder zumindest nicht ohne weiteres, auf die neuen Objekte
angewand werden können.
Die Indexierung zusammengesetzter (oder mehrdimensionaler) Datentypen kann erreicht werden, indem die einzelnen Komponenten dieses Datentyps jeweils einzeln indexiert werden. Bei einer Suche werden dann die Ergebnisse der einzelnen Indexabfragen über mengentheoretische Operationen miteinander verbunden. Dieses Verfahren kann
aber nicht alle Anwendungen abdecken, so ist eine Indexunterstützung für nearest neighbour -Abfragen bei räumlichen Daten nicht ohne weiteres zu bewerkstelligen. Auch ist es
meist nicht sehr effektiv, denn die Eigenschaften der Objekte werden dabei nicht weiter ausgenutzt. Die Methode einzelne Suchschlüsselkomponenten zu einem gemeinsamen
Suchschlüssel zu konkatenieren, verspricht keine Verbesserung, denn bei Ablage dieses
Schlüssels in einem B*-Baum wird er nur nach der ersten Komponente sortiert gespeichert. Ordnungen auf anderen Teilschlüsseln gehen verloren und können bei einer Suche
nicht mehr ausgenutzt werden.
Kapitel 3
Spatial Join Algorithmen
Eine auf den Datentyp angepasste Indexstruktur kann deren Eigenheiten ausnutzen, um
effektiv zu arbeiten. In diesem Kapitel werden einige indexgestützte Verfahren vorgestellen, die dem Verbund räumlicher Objekten dienen. Dabei sind die verwendeten Verbundalgorithmen auf die jeweiligen Indexstrukturen für räumliche Datensätze zugeschnitten,
die daher ebenfalls in diesem Kapitel skizziert werden.
Der indexgestütze Join-Prozess wird normalerweise in zwei Stufen aufgeteilt: In einer
ersten Filter-Phase werden mit Hilfe des Indexes möglichst viele Verbundpaare aussortiert, die nicht der entsprechenden Verbundoperation entsprechen. Da in den Indexen
nicht die tatsächlichen Objektgeometrien hinterlegt sind, sondern meist leichter zu handhabende Approximationen (beispielsweise ein Minimum Bounding Rectangle MBR) kann
der Index nur zur Berechnung einer Kandidatenliste beitragen. In einer zweiten Filterung werden die tatsächlichen Objekt-Geometrien betrachtet, um aus diesen Paaren die
Ergebnis-Menge zu generieren. Da die zweite Phase bei allen Algorithmen gleich ist, sind
dort keine Laufzeitunterschiede zu erwarten. Daher wird die zweite Filter-Phase bei der
Beschreibung der Algorithmen nicht weiter betrachtet.
Auch sind keine prinzipiellen Unterschiede bei der Verwendung verschiedener Operatoren (disjoint, containment, intersection(=overlap), equality, touching) zu erwarten:
Bei vielen Index-Strukturen kann in weiten Teilen nur auf Überlappung oder Disjunktheit geprüft werden. Bei den vorgestellten Baum-Strukturen ist beispielsweise erst auf
der Blatt-Ebene die Unterscheidung möglich.
3.1
indexed nested loops join
Dies ist einer der einfachsten Join-Algorithmus. Dabei werden alle Elemente der einen
Tabelle nacheinander abgearbeitet. Zu jedem Eintrag oa der äußeren Tabelle (oder des
zugehörigen Indexes) werden mittels des Indexes der zweiten Tabelle alle Objekte ob (der
2. Tabelle) gefunden, die dem Vergleichsoperator Θ entsprechen, also etwa ob sich deren
bounding box mit oa schneiden. Existiert auch für die äußere Tabelle ein Index, so kann
dabei sehr einfach eine zusätzliche Selektion stattfinden.
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
21
indexed-join1 (A, B: Index, qa : Query, Θ: Operator);
FOR (oa ∈ IndexScan(A,qa )) DO
construct query qb for Θ and oa
FOR (ob ∈ IndexScan(B,qb )) DO
output(oa , ob )
END
END
END
Diese Strategie hat den Vorteil , dass sie praktisch bei allen Indexstrukturen angewand
werden kann.
3.2
R*-tree join
Dieser Algorithmus ist von Brinkhoff, Kriegel, Seeger in [BKS93] vorgeschlagen und dort
noch bezüglich der CPU- und I/O-Performance optimiert. Er ist deutlich leistungsfähiger als der indexed nested loops join, wenn dieser auch auf einer Baumstruktur basiert.
Denn während beim indexed nested loops join zumindest die oberen Knoten sehr häufig durchlaufen werden müssen, werden sie beim R*-Baum-Verbund nur selten besucht,
denn der Algorithmus arbeitet rekursiv und muss damit nicht jedesmal bei der Wurzel
neu beginnen.
3.2.1
Aufbau eines R-Baumes
Ein R-Baum [Gut84] besitzt eine Datenstruktur, die mit denen von B+ -Bäumen vergleichbar ist, aber mehrdimensionale Rechtecke als Objekte beinhaltet. Bei einem R-Baum
handelt es sich um einen ausgeglichenen Baum, dessen innere Knoten (mbr,child)-Paare
beinhalten, wobei mbr ein Minimum Bounding Rectangle und child einen Zeiger auf einen
Nachfolge-Knoten beinhaltet. Die mbr -Einträge werden nach einer Änderung des Baumes
so aktualisiert, dass sie danach wieder das kleinste (achsenparallele) Rechteck enthalten,
das die Rechtecke aller Nachfolger-Knoten umschließt. Ein Blatt beinhaltet (mrb,obj)Paare, die wiederum ein MBR und einen Verweis auf das entsprechende Objekt in der
Datenbank enthalten.
Im R-Baum werden in den Knoten, die üblicherweise die Größe (oder ein Vielfaches)
einer Datenseite des externen Speichers haben, zwischen m und M solcher Paare gespeichert (2 ≤ m ≤ dM/2e) Die Wurzel besitzt wenigstens 2 Nachfolger oder ist selber
ein Blatt ist, in der dann auch weniger als m Knoten gespeichert sein können. Da kein
prinzipieller Unterschied zwischen einem Zeiger auf einen weiteren Nachfolgeknoten und
einem Zeiger auf ein konkretes Datenobjekt besteht, kann man die Knoteneinträge auch
zu (mbr,ref )-Paaren verallgemeinern.
Wie in [BKSS90] gezeigt wird, ist der R∗ -Baum eine besonders effiziente Spezialisierung der R-Bäume. Er unterscheidet sich nur in den Einsetz- und Teilungsmethoden, aber
nicht im Aufbau der Datenstruktur von den ursprünglichen R-Bäumen.
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
a
b
3
1
22
a
b
c
4
2
1
c
5
6
7
2
3
4
5
6
7
zu den Datensätzen der Objekte
Abbildung 3.1: Beispiel eines R-Baumes
Der Algorithmus, der dem Einfügen von Knoten in einen Baum implementiert, betrachtet bei R*-Bäumen nicht nur die Vergrößerung der Fläche bei der Auswahl des
Einfügepfades, sondern minimiert ebenfalls die Überschneidungen (bei den Knoten der
vorletzten Ebene), die durch das Einfügen eines neuen Knotens entstehen. Muss ein Knoten geteilt werden, so werden die Rechtecke zunächst in Bezug auf ihre Lage auf den
Achsen des Raumes sortiert. Diejenige Achse wird ausgewählt, die verspricht, dass der
Rand um die zu bestimmenden zwei Gruppen am kleinsten wird. Dann wird eine Verteilung der Einträge in zwei Gruppen entsprechend der gewählten Achse ausgewählt, welche
die Überschneidungen zwischen den beiden Gruppen minimiert.
3.2.2
Spatial Join
Der Basis-Algorithmus für einen räumlichen Verbund mit zwei R-Bäumen mit Wurzeln
A und B funktioniert folgendermaßen:
rtree-join1 (A, B: R-Node);
FOR (EA ∈ A) DO
FOR (EB ∈ B with EB .mbr ∩ EA .mbr 6= ∅) DO
IF ((A is a leaf page) OR (B is a leaf page)) THEN
output(EA , EB )
ELSE
ReadPage(EA .ref); ReadPage(EB .ref);
rtree-join1(EA .ref, EB .ref)
END
END
END
END
Der rekursiv arbeitende Algorithmus wird zunächst mit den Wurzeln der zu verbindenden Bäume aufgerufen. Es folgt ein Tiefendurchlauf durch die beiden Bäume, wobei
diese parallel durchsucht werden.
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
23
Zu jedem Eintrag EA des Knotens A wird geprüft, ob die Elemente EB aus B eine
nichtleere Schnittmenge mit EA besitzten. Man beachte hierbei, dass A und B jeweils
auf der gleichen Stufe der entsprechenden Bäume stehen (daher: paralleler Durchlauf).
Werden sich schneidende Einträge gefunden, so wird geprüft, ob es sich dabei bereits um
Blätter einer der Bäume handelt. Dann ist keine weitere Rekursionsstufe mehr nötig und
möglich, denn in wenigstens einem Baum beinhaltet das Objekt bereits den Verweis auf
die Datenbank. Die Funktion output verfolgt denjenigen Baum bis zu den Blättern, der
noch nicht die Blattebene erreicht hat , wobei die MBR des Datenobjektes des Baumes auf
Blattebene berücksichtigt werden sollte. Alle Paare (EAi , EBj ), die sich am Ende dieses
Baumabstieges ergeben, werden als Ergebnis der ersten Filterstufe ausgegeben. Handelt
es sich bei den Knoten beider Bäume bereits um Blätter, so ist das Ergebnis das Paar
(EA , EB ).
Sollte es sich weder bei A noch bei B um ein Blatt handeln, so wird der Algorithmus mit den sich schneidenden Knoten rekursiv aufgerufen, nachdem die entsprechenden
Seiten in den Speicher geladen wurden (falls sie sich noch nicht dort befinden).
Der Algorithmus überprüft alle Elemente in EA .ref und EB .ref. Da die Schnittmenge
der MBRs aber im Allgemeinen kleiner als die einzelnen MBR sind, uns aber nur Elemente aus diesem Schnitt interessieren, ergibt sich eine erste Optimierung, indem man
als weiteren Parameter ein Rechteck angibt, in dem gesucht werden soll.
Vor jeder Rekursion werden die Seiten der referenzierten Knoten in den Speicher geladen. Um die I/O-Zugriffe zu senken, werden üblicherweise Puffer (im Programm oder
vom Betriebssystem/DBMS) verwendet, die den Zugriff auf externen Speicher minimieren
sollen. Ein bereits im Speicher/Puffer befindlicher Knoten soll möglichst zuerst verwendet werden, statt ihn später wieder neu laden zu müssen. Mittels eines Plane-SweepAlgorithmus können die Rechtecke eines Knotens in gewissen Maßen geordnet werden.
Dadurch lassen sich die Vergleiche koordinieren, so dass es (in gewissen Grenzen) möglich ist, sie zu optimieren. Dabei findet dieser Sortiervorgang beim Laden der Seiten in
den Speicher statt, denn die Struktur des R-Baumes soll nicht verändert werden. Das
Sortierung im Speicher beschränkt die Auswahl der anzuwendenden Sortieralgorithmen
auf einfache Typen, da sonst der Performance-Vorteil zunichte gemacht wird, wenn eine
Seite dennoch öfter geladen werden muss.
Eine etwas anderen Weg zur Optimierung dieses Algorithmus wird in [HLR97] vorgeschlagen: Dabei werden die beiden Bäume jeweils ebenenweise betrachtet. Statt aber in
einer Rekursion und damit einer Tiefensuche mögliche Verbundpartner zu suchen, werden
erst alle möglichen Paare auf einer Baumebene bestimmt.
Die Zwischenergebnisse sind dabei nötigenfalls auf externem Speicher zwischenzuspeichern, wenn sie nicht mehr im Hauptspeicher verwaltet werden können. Die Verbundpaare
eines Zwischenergebnisses werden dann geordnet, um untergeordnete Knoten möglichst
selten lesen zu müssen.
Ist dies geschehen, so wird der Verbund für die nächste Ebene berechnet, wobei die
Knoten in der berechneten Reihenfolge untersucht werden.
Statt die Prozedur des normalen R-Baum Verbundes rekursiv zu gestalten, ist es
natürlich auch möglich, die zu untersuchenden Knoten auf einen Stack abzulegen. Beim
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
24
Eintritt in die Prozedur werden jeweils die beiden Elemente entfernt, die beim rekursiven
Aufruf die Wurzeln darstellen. Statt einer neuen Rekursion mit EA .ref und EB .ref werden
diese beiden Elemente auf den Stack abgelegt. Die Prozedur wird in einer Schleife dann
so oft aufgerufen, bis alle Elemente des Stacks entfernt sind.
3.3
Seeded Trees
Ming-Ling Lo und Chinya V. Ravishankar schlagen in [LR94] eine Methode vor, die die
Anzahl der Vergleiche bei den R-Baum-indexgestützten Joins reduzieren soll. Des weiteren bietet sie sich an, wenn nur für einen der beteiligten Datensätze ein Index existiert.
Als Beispiel betrachte man Abbildung 3.2. Dort sind die Bounding-Boxen der Knoten der
Ausgangssituation
Gruppierung bei R−Baum−Erzeugung
für den 2. Datensatz
Datenobjekt im 2. Datensatz
Bounding Box von Baum 1
Günstigere Gruppierung
Bounding Box von Baum 2
Abbildung 3.2: Ungünstige und günstige bounding box -Verteilung in einem Seeded Tree
obersten Ebene eines bereits bestehenden Indexes zu sehen und einzelne Datenobjekte
eines 2. Datensatzes, auf denen ebenfalls ein Index erstellt werden soll. Der übliche Algorithmus würde, wie im mittleren Teil der Abbildung zu sehen ist, möglicherweise kleinere
MBRs erzeugt, die aber jeweils mehrere bounding boxes des 1. Datensatzes schneiden.
Dies würde jeweils mehrere Vergleiche hervorrufen. Ein günstigeren Verteilung ergibt
sich, wenn die Objekte wie im rechten Teil der Abbildung zu sehen gruppiert werden,
denn dadurch reduziert sich die Anzahl der Vergleiche der Knoten des 1. Datensatzes mit
den Knoten des 2. Datensatzes auf der obersten Ebene auf jeweils 1 Paar.
Die Idee von Seeded Trees besteht also darin, eine gewisse Anzahl von Ebenen eines
existierenden R-Baumes für die Konstruktion eines Baumes für den zweiten Datensatz zu
übernehmen. Dadurch kann man die spätere Form des Seeded Trees so beeinflussen, dass
ein Verbund der beiden Datenstrukturen weniger Knoten-Vergleiche und damit weniger
zu lesende Knoten, also Seiten vom externen Speicher benötigt. Die Join-Phase ist mit
der des R-Baumes identisch, denn der Seeded Tree hat eine sehr ähnliche Struktur.
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
3.3.1
25
Seeded Tree Konstruktion
Um einen Seeded Tree aufzubauen, wird in der seeding phase eine gewisse Anzahl an
Ebenen eines existierenden R-Baumes herangezogen, um das Aussehen des Trees zu beeinflussen. Dabei können sowohl die bounding boxes als auch die Mittelpunkte der MBRs
des R-Baumes kopiert werden. Lo und Ravishankar ermittelten einen Vorteil, wenn nur
die Mittelpunkte der MBRs kopiert werden (zumindest auf der untersten Ebene, darüber
liegen dann entweder auch die Mittelpunkte der MBRs des kopierten Baumes oder die
MBRs werden wie im R-Baum aus den Nachfolge-Knoten errechnet).
Die kopierten Ebenen nennt man seed levels, die Ebenen darunter grown levels, wobei
die unterste Ebene der seed levels, also die Vorgänger der grown levels, slot-level genannt
werden. Die ref-Komponente der (mbr, ref )-Paare dieser Ebene werden auch slot-pointer
genannt. Dort werden im Laufe der Baumerstellung Unterbäume wachsen, bei denen es
sich um normale R-Bäume handelt.
Initial ist nur die mbr-Komponente auf der slot-level-Ebene besetzt, um zu bestimmen,
in welchen Unterbaum ein neuer Eintrag einzufügen ist. Die ref-Komponente verweist
anfangs auf einen reservierten Wert, z.B. NULL. In der ersten Ebene der grown levels
liegen also Wurzeln von R-Bäumen, oder die slot pointer verweisen auf den initialen
Wert, NULL-Zeiger.
a
d
k
l
e
m
f
n
o
p
q
b
c
g h
i
r s
t
u
v
j
w
x
y
kopieren
der
oberen
Ebenen
zu den Datensätzen in der Datenbank
grown
levels
seed levels
a
d
e
f
b
c
g h
i
j
slot level
slot pointer (initial: NULL)
Abbildung 3.3: Seeded Tree Konstruktion
Während der growing phase werden die Daten-Objekte in den Baum eingefügt. Dazu
wird der Baum von der Wurzel zu den slot-Ebenen durchlaufen, wobei in jeder Ebene
eine passende Richtung gewählt wird. Handelt es sich um das erste Objekt für einen slot
während der Wachstumsphase, so wird ein neuer Knoten alloziiert, das Daten-Objekt in
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
26
diesen Knoten eingefügt, und der slot pointer auf diesen Knoten gesetzt. Ansonsten zeigt
der slot pointer bereits auf die Wurzel eines R-Baumes, in den das Objekt ganz normal
eingefügt wird. Ein Aufsplitten der Wurzel des Teilbaumes wird allerdings nicht weiter in
die seed level weitergeleitet, sondern führt zu einer Aktualisierung des slot pointers auf
die nunmehr neue Wurzel.
Während die MBRs in den Unterbäumen in der Wachstumsphase ständig aktualisiert
werden, muss man dies in den seed levels nicht unbedingt tun, denn sonst riskiert man,
dass sie sich zu sehr von der gewünschten (nämlich der vorgegebenen) Form entfernen.
Man kann sich verschiedene Varianten vorstellen, die MBRs nach einer Einfüge-Operation
z.B. nur bis zu der slot-Ebene zu aktualisieren oder nur die eingefügten Daten, nicht aber
die initialen MBRs auf bestimmten Ebenen zu aktualisieren.
Da zur Konstruktion des Seeded Trees in der Regel nur eine bestimmte Anzahl an
Speicherplätzen für die Knoten im Hauptspeicher zur Verfügung steht, der üblicherweise
von den Knoten benötigte Platz aber weit größer ist, schlagen Lo und Ravishankar vor,
nicht sofort mit der Konstruktion der R-Bäume zu beginnen, sondern die Daten-Objekte
erst in verketteten Listen zu speichern, wobei jedem slot pointer eine Liste zugeordnet
wird. Sollte der Pufferspeicher nicht mehr ausreichen, so werden in einem batch alle Listen,
die einen bestimmten Füllungsgrad überschreiten, auf den externen Speicher geschrieben
(gibt es nicht genug Listen, die den Füllungsgrad überschreiten, so wird der Füllungsgrad
so lange erniedrigt, bis ausreichend viele Listen und damit Daten ausgelagert sind). Dieser
lineare I/O-Zugriff geht viel schneller, als ein zufälliger Zugriff, der beim simultanen Erzeugen der R-Bäume entstehen würde. Sind alle Daten-Objekte den Listen zugeordnet, so
werden aus den einzelnen Listen die R-Bäume generiert. Aufgrund der vielen slots sollten
die einzelnen Teilbäume klein genug sein, um einzeln komplett im Pufferspeicher Platz
zu finden, bevor sie wiederum in einem Rutsch auf den externen Speicher geschrieben
werden können.
Sind alle R-Bäume erzeugt, so werden in der clean-up phase die MBRs auch auf den
seeded levels an die tatsächlichen Gegebenheiten angepasst, d.h. die MBRs werden aus den
jeweiligen Einträgen der untergeordneten Knoten berechnet. Es werden nicht benötigte
slots entfernt und die Datenstruktur konsistent gemacht. (NULL-Zeiger, Unterteilung nach
grown- und seed-Level entfernen, etc.)
Man beachte, dass die gewachsenen Bäume alle unterschiedliche Höhen haben können,
dies aber vom Join-Algorithmus bereits berücksichtigt wird, wenn unterschiedlich hohe
R-Bäume miteinander verbunden werden sollen.
In [LR98] geben Lo und Ravishankar gute“ Grenzen für die Anzahl der slots und
”
die Knoten in der slot-Ebene an, da eine zu große Anzahl davon schnell zu einem großen
Speicherplatz-Verbrauch bei den zur Verfügung stehenden Puffern führt und eine zu geringe Zahl den Join-Algorithmus negativ beeinflusst. Man sollte dabei aber beachten, dass
nur die Effizienz, aber nicht die Funktionsfähigkeit beeinflusst wird. In [LR98] wird ebenfalls vorgeschlagen, die Wurzeln der gewachsenen Teilbäume zusammen in gemeinsamen
Knoten zu lagern (durch geeignete reservierte Einträge getrennt). Bei vielen Teilbäumen
kann es sein, dass die Wurzeln nicht ganz gefüllt sind (und auch nicht mit mindestens m
Einträgen, wie dies bei einem R-Baum für den kompletten Datensatz auf dieser Ebene
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
27
der Fall wäre). Das Zusammenlegen führt zu einer gewissen Speicherplatzersparnis, aber
vor allem zu einem geringeren Puffer-Bedarf während der Join-Phase.
3.4
Spatial Hash-Join
In [LR96] beschreiben Lo und Ravishankar einen Join-Algorithmus, der nicht auf einer
Baum-Struktur beruht, sondern ad hoc eine Datenstruktur für den Join-Prozess aufbaut.
Er soll dennoch mit den Baum-basierten Algorithmen konkurrieren können, auch wenn
diese auf einen bereits vorhandenen R-Baum Index zurückgreifen.
3.4.1
Funktionsweise des Hash-Joins
Hash-Algorithmen benutzen eine Hash-Funktion, die die Tupelmenge in eine gewisse Anzahl von Equivalenzklassen unterteilt. Die Tupel in derselben Klasse werden dann demselben Container (bucket) zugeordnet (partition phase). Sucht man also z.B. in zwei Tabellen
diejenigen Einträge mit der gleichen Produktnummer, so könnten beim Hash-Join die Tupel mit den gleichen Endziffern (bei der Produktnummer) in einem Container landen. Da
die Hash-Funktion für beide Tabellen gleich gewählt wird, braucht man für den eigentlichen Vergleich nur noch die Tupel in den entsprechenden Containern heranzuziehen,
wenn die Join-Attribute verglichen werden (join phase).
Bei geographischen Daten ist diese Strategie allerdings nicht ausreichend. Dazu betrachte man in einem Datensatz die Objekte A und B, in einem anderen Y und Z, wobei
Y sowohl A als auch B überschneidet. (Siehe Abbildung 3.4) Ordnet man nun A und
Y einem Container zu, da sich deren Mittelpunkt auf der linken Halbebene befindet, B
und Z dem Container für die rechte Halbebene, so findet man zwar Überschneidungen
zwischen A und Y, sowie B und Z, aber keine zwischen B +Y.
Y
Container−Grenzen
B
Objekt des 1. Datensatzes
Z
A
Objekt des 2. Datensatzes
Abbildung 3.4: Einfache Container-Zuodnung für Spatial Join
Die einzige Möglichkeit sicherzustellen, dass alle Vergleichspartner in einem Container
landen, besteht in der Regel darin, nur einen einzigen Bucket zu verwenden. Dann entspricht die Performance aber auch nur der des nested loops join. Der Ausweg aus diesem
Dilemma besteht darin, auch verschiedene“ Buckets miteinander zu vergleichen (multiple
”
matching), was aber beim Konzept des relationalen Hash-Joins gerade vermieden werden
soll, oder man teilt einem Objekt mehrere Container zu (multiple assignment), muss also
redundante Daten und eine höhere I/O-Last erzeugen.
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
28
Beim spatial Hash-Join bestehen die Partitions-Funktionen aus zwei Komponenten:
Eine Zuweisungsfunktion (assignment function) und einer Menge von Container-Ausdehnungen (bucket extents), jeweils ein bucket extent pro bucket. Eine bucket extent korrespondiert mit einer Region im Gebiet, in dem der Spatial Join berechnet werden soll,
muss aber nicht zwangsläufig zusammenhängend sein. Er wird von der Zuweisungsfunktion benutzt, um einem Datenobjekt ein Bucket zuzuordnen, in der Join Phase dient
er der Identifizierung der Container, die miteinander verglichen werden müssen. Die Zuweisungsfunktion weist die Datenobjekte den Containern zu, je nachdem, wie sie zu den
Container-Ausdehnungen in Beziehung stehen. Je nach Design kommen dafür zum Beispiel die Container in Frage, deren Ausdehnung ein Objekt am meisten überlappt, oder
alle Container, deren Ausdehnung das Objekt schneiden.
In der Join Phase müssen die Container des 1. Datensatzes (innen) mit allen Containern des 2. Datensatzes (außen) verglichen werden, in denen sich überlappende Objekte
befinden können.
Der Hash-Join Algorithmus kann von existierenden Indexen profitieren, da er in der
untersten Ebene des R-Baumes die MBRs der Daten-Objekte findet, und diese somit
nicht selbs berechnen muss.
3.4.2
Beispiele für Zuweisungs- und Ausdehnungsfunktionen
Beispiel 1
Bucket extents
Zuweisungsfunktion
Join-Bucket Paare
multiple assignment
Aufteilung der Kartenfläche in gleich große, nicht überlappende Regionen, z.B. einem regelmäßigem Gitter mit
n × n Zellen. Der innere und äußere Datensatz besitzen
die gleichen Container-Ausdehnungen, die sich während
der Zuweisungsphase nicht ändern.
Ein Datenobjekt wird jedem Container zugewiesen, dessen Ausdehnung das Objekt überlappt. Ein Objekt kann
also in mehreren Buckets vertreten sein.
Vergleiche jeden inneren mit dem entsprechenden äußerem Container, der also die gleiche Ausdehnung besitzt.
Dabei werden Objektpaare, die sich in mehreren Containern überschneiden mehrfach gefunden: Duplikate sollten am Ende entfernt werden.
Nachteilig bei dieser Methode ist, dass die Containergrößen nicht auf die Verteilung
der Objekte eingehen. Einige Buckets können sehr viele Objekte enthalten, während andere nahezu leer sind. Auf Grenzen endende Objekte sollten in beiden Containern abgelegt
werden, dadurch wird die Datenmenge weiter vergrößert. Am Ende der Join Phase ist
noch zusätzlicher Aufwand nötig, um doppelt gefundene Objektpaare zu eleminieren.
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
Beispiel 2
Bucket extents
Zuweisungsfunktion
Join-Bucket Paare
29
multiple matching
Begonnen wird wird wiederum mit n × n Zellen, die
nicht überlappend die gesamte Karte bedecken. Innerer und äußerer Datensatz beginnen mit den gleichen
initialen bucket extents, doch werden diese beim Zuweisen der Objekte vergrößert, so dass sie das ganze
Objekt umschließen. Daher werden sich die ContainerAusdehnungen am Ende der Zuweisungsphase in der Regel unterscheiden.
Jedes Objekt wird dem Container zugewiesen, dessen
Fläche am wenigsten vergrößert wird. Bei mehreren
Kandidaten kommt das Objekt in den Bucket mit dem
nächsten Schwerpunkt.
Jeder innere Container wird mit jedem äußeren Container verglichen, dessen Ausdehnung sich überschneidet.
In der Join-Phase muss bei dieser Methode die Reihenfolge der Container sorgfältig
ausgewählt werden, wenn man einen Puffer ausnutzen möchte und es vermeiden will, die
Buckets mehrfach zu laden.
Beispiel 3
Bucket extents
Zuweisungsfunktion
Join-Bucket Paare
Lo und Ravishankars Vorschlag - multiple assignment
Die inneren Bucket Extents werden mittels eines Bootstrap-Verfahrens (beschieben in [LR95]) mit eine Anzahl Punkte initialisiert. Während der Zuweisung wachsen die Bucket Extents, um die Objekte zu umgeben.
Die äußeren Buckets werden mit den Ausdehnungen der
inneren initialisiert und verändern sich nicht weiter.
Beim inneren Datensatz werden die Objekte den Bucket
Extents so zugewiesen, wie dies auch bei den R-Bäumen
(und Seeded Trees) geschieht. Die MBRs werden nur
minimal vergrößert und Überlappungen möglichst vermieden. Im äußeren Datensatz muss jedes Objekt allen Containern zugewiesen werden, deren Ausdehnung
überlappt wird. Ist dies nicht der Fall, wird kein Eintrag
benötigt, da das Objekt dann für den Join irrelevant ist.
Da die Ausdehnungen der Container gleich sind, wird für
jeden inneren nur ein äußerer Container herangezogen.
Andere Überschneidungen brauchen nicht berücksichtigt
werden, da die Datenobjekte des äußeren Datensatzes
sonst schon dupliziert worden wären.
Um externe Speicherungen möglichst effizient zu gestalten, kann wie schon bei den
Seeded Trees eine Batch-Write Strategie angewendet werden, wenn die Puffer in der
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
30
Zuweisungs-Phase einen bestimmten Füllungsgrad erreicht haben. Der Vergleich der Objekte in den einzelnen Containern in der Join-Phase kann im einfachsten Fall über einen
nested loops join, oder besser noch ähnlich dem indexed nested loops Join (siehe 3.1)
stattfinden. Dazu baut man zu einem der Datensätze einen kleinen R-Baum, der als
Index genutzt wird, wenn die anderen Objekte der Reihe nach abgearbeitet werden.
3.5
Z-Codes und Standard-Indexe
Eine andere Möglichkeit einen räumlichen Verbund herzustellen, ergibt sich aus einer
anderen Indexierungsmöglichkeit. In [OM84] wird der zu indexierende Raum in gleichmäßige, sich nicht überlappende Regionen aufgeteilt. Im 2-dimensionalen Fall erzeugt man
eine Einteilung in 2d × 2d Kacheln. Wenn man die Aufteilung des Raumes in jede Achse
binär nummeriert, so werden die Kacheln eindeutig über einen Bit-String identifiziert, der
durch Verschachtelung der Bit-Folgen der Achsen entsteht (siehe dazu Abbildung 3.5).
Dies entspricht einer (rekursiven) N-förmigen Nummerierung der Kacheln, genannt ZOrdnung. (Ändert man die Reihenfolge der Verschachtelung so ergeben sich die Z-Form.
Die Idee, den Raum in disjunkte Rechtecke zu unterteilen, die mittels eine space-filling
curve geordnet und nummeriert werden ist schon sehr alt und wurde zuerst 1890 von
Peano veröffentlicht [Pea90].) Mittels dieser Nummerierung werden die Kacheln linear
geordnet, wobei räumliche Nähe in dem Sinne erhalten bleibt, dass die Hamming-Distanz
der Bit-Strings benachbarter Kacheln klein ist.
2.+4. bit
2. bit
01
1
11
11 0101 0111 1101 1111
10 0100 0110 1100 1110
01 0001 0011 1001 1011
0
00
0
10
1
1. bit
00 0000 0010 1000 1010
00
01
10
11
1.+3. bit
Abbildung 3.5: Aufteilung des Raumes: Z-Code
Objekte werden nun indexiert, indem man zunächst feststellt, welche Kacheln von
einem Objekt geschnitten werden. Anschließend bestimmt man eine Menge von Kacheln
(sogenannten Z-Elemente oder Z-Codes), die das Objekt repräsentieren. Der Prozess, die
Z-Element-Menge zu bestimmen, wird Dekomposition genannt. Die Z-Elemente bestehen
aus den Präfixen von Bit-Strings. Schneidet ein Objekt beispielsweise die Kacheln 0010,
0011, 0100 und 0110, so ergibt sich die Z-Element-Menge: {001, 0100, 0110} (man beachte,
dass 0010 und 0011 zu 001 zusammengefasst werden (vergleiche dazu das Polygon in
Abbildung 3.6).
Es gibt einige Alternativen zur Dekomposition eines Objektes. Statt genau die Elemente zu bestimmen, die das Objekt schneidet, kann auch auch ein einziges Z-Element
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
11
11
10
10 0100 0110
01
01
00
00
00
01
10
11
11
31
Polygon:{001,0100,0110}
Kreis:{11}
001
00
01
10
11
Abbildung 3.6: Z-Elemente von Objekten
verwendet werden, welches das ganze Objekt umschließt. Liegt ein Objekt allerdings
ungünstig, so kann dieses Z-Element allerdings sehr groß sein und den Index damit unbrauchbar machen (genau ein Z-Code für B in Abbildung 3.7) Berechnet man zunächst
die bounding box eines Objektes, und daraus die Z-Element-Menge, so ist die Bestimmung der geschnittenen Kacheln wesentlich einfacher. Dies hat allerdings den Nachteil,
unregelmäßig geformte Objekte schlechter zu repräsentieren (3. v.l. in Abbildung 3.7, die
bounding box zu A ist rot dargestellt) als die Z-Elemente, die man erhält, wenn man die
exakte Geometrie eines Objektes verwendet (2. v.l. in Abbildung 3.7).
A
011
01
0100
1
B
exaktes Objekt A
MBR von A
genau ein Z−Code für B
Abbildung 3.7: Verschiedene Dekompositionsmöglichkeiten
Die bei der Dekomposition gefundenen bit-strings lassen sich lexikographisch ordnen,
damit qualifizieren sie sich als Schlüssel in einem B*-Baum, wie ihn die meisten Datenbanken als Index-Struktur unterstützen. Das Polygon aus Abbildung 3.6 würde unter den
Schlüsseln 001, 0100 und 0110 in einem B-Baum indexiert.
Ist ein Element B in einem anderen (A) enthalten, so müssen die z-Elemente von A
jeweils die Präfixe der Elemente von B sein. Schneiden sich zwei Elemente, so besitzen
sie jeweils ein gemeinsames Element bzw. wenigstens ein Z-Element ist das Präfix eines
Elementes des anderen Objektes. Man beachte, dass sich die einzelnen Elemente zweier
Objekte nur entweder enthalten können (das eine ist Präfix eines anderen), bereits gleich,
oder nichtüberschneidend sind. Es gibt keine Überschneidungen in dem Sinne, dass es bei
Elementen A und B eine nichtleere Schnittmenge gibt, die wiederum sowohl von A als
auch B verschieden ist.
Für einen räumlichen Verbund (beschrieben in [OM88]) nutzt man diese Eigenschaft
aus, um Schnitte und Überdeckungen zu finden. Dazu muss auf den zu verbindenden
Tabellen jeweils die gleiche“ Einteilung der Kacheln existieren. Die bit-strings können
”
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
32
theoretisch unterschiedlich lang sein, da bei einer Erhöhung der Auflösung die Präfixe
erhalten bleiben. Es kann dann natürlich vorkommen, dass ein Objekt feiner repräsentiert
wird, also statt einer Zeichenkette 01 die Elemente 0101, 0110 und 0111 erhält (aber
nicht 0100). Daraus resultieren mehr zu speichernde Einträge, es ergeben sich aber keine
Vorteile, falls die andere Tabelle nur mit 2 Bit arbeitet, also ohnehin nur auf das Präfix
01 getestet wird.
Zur Berechnung eines joins müssen alle Objekte der beiden Tabellen indexiert sein.
Dazu werden alle Objekte decomposed, also in Elemente aufgeteilt, die wiederum mit
einer Kennzeichnung auf das zugehörige Objekt in der jeweiligen Indexstruktur (z.B.
B+-Baum) abgespeichert werden. Während des Join-Vorganges werden die B+Bäume in
der lexikographischen Ordnung der gespeicherten bit-strings ausgelesen, d.h. man liest
001 vor 01 vor 010 etc. Man kann sich die Bäume also wie Sequenzen vorstellen, die von
vorne nach hinten gelesen werden.
Man betrachte als Beispiel Abbildung 3.8, mit den folgenden Sequenzen:
obere Tabelle: 001(B), 0011(A), 1001(B)
untere Tabelle: 001(C), 100(D)
: A={0011}
11
11
: B={001,1001}
10
01
00
01
00
10
A
B
00
01
10
11
00
01
11
11
11
10
!
C
10
10
:C={001}
!
!
01
00
00
01
00
10
D
:D={100}
01
11
00
01
10
11
Abbildung 3.8: Verschachtelung von Objekten
Man verwendet für jede Sequenz einen Cursor. Desweiteren benötigt man jeweils einen
Stapelspeicher, auf dem man sich die gerade bearbeiteten Elemente merkt. Problematisch
sind die ineinander verschachtelten Objekte, wo ein z-element von einem größeren zelement eines anderen Objektes überdeckt wird (im Beispiel ist dies bei A und B der
Fall). Folgender Algorithmus findet alle sich überschneidenden Paare:
z-tree-join (L,R: Sequenz);
L-Stack, R-Stack: Stack;
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
33
result: List;
event: Element;
L.start(); R.start();
WHILE ( NOT L.eof() && R.eof() && L-Stack.empty() && R-Stack.empty() ) DO
event=min(lowest(L.current()), lowest(R.current()),
highest(L-Stack.top()), highest (R-Stack.top());
SWITCH (event){
CASE lowest(L.current()):
enter-element(L, L-Stack); BREAK;
CASE lowest(R.current()):
enter-element(R, R-Stack); BREAK;
CASE highest(L-Stack.top()):
exit-element(L-Stack, R-Stack, result); BREAK;
CASE highest(R-Stack.top()):
exit-element(R-Stack, L-Stack, result); BREAK;
}
END;
return result;
END
Dazu kommen die Prozeduren enter-element und exit-element, die die Elemente auf
den Stack schreiben und entfernen, sowie für das Füllen der Ergebnisliste zuständig sind.
Die Funktionen lowest und highest liefern zu einem z-element den Wert der niederwertigsten bzw. höchstwertigsten Kachel des Elementes. Verwendet man vier Bit, so ergibt
dies lowest(10)=1000 und highest(10)=1011. enter-element und exit-element sind
folgendermaßen definiert:
enter-element (A: Sequenz, A-Stack: Stack);
A-Stack.push(A.current);
A.next();
END
exit-element (A-Stack,B-Stack: Stack, result: List);
report-pairs(A-Stack.top(), B-Stack, result);
A-Stack.pop();
END
Dabei sucht report-pairs(A,B-Stack,result) die Elemente des B-Stack nach einem gemeinsamen Präfix mit dem Element A durch und fügt die zugehörigen Objekte in die
Ergebnisliste result ein, falls sie sich dort noch nicht befinden.
Zur Funktionsweise des Algorithmus: Die Haupt-Prozedur läuft durch beide Sequenzen
und schiebt die Elemente im Laufe der Zeit auf die zugehörigen Stapelspeicher. Sind
die Stapel leer und die Sequenzen durchlaufen, so ist man fertig. Dies geschieht in einer
Schleife, an deren Anfang jeweils das weitere Vorgehen entschieden wird. Dazu betrachtet
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
34
man die Kacheln der Elemente, die sich an den aktuellen Positionen von Stapel und
Sequenz befinden. Besitzen die Elemente einer Sequenzen eine Kachel, die numerisch
kleiner als die größte Kachel der oberen Elemente auf einem der Stacks ist, so kann es
eine Überschneidung geben. In enter-element wird die entsprechende Sequenz weiter
ausgelesen und das Element auf den zugehörigen Stack verschoben. Dies kann nur für
Elemente geschehen, die in den Elementen des Stapelspeichers enthalten sind oder das
gleiche Z-Element besitzen, sowie bei leerem Stapel. Größere“ Elemente kommen in der
”
Sequenz zuerst, wurden also schon auf den Stapel befördert, oder das oberste Element
des Stapels hat einen kleineren highest-Wert, dann würde es aber bei exit-element
zuerst aus dem Stapel entfernt. Sind keine Elemente mit kleinerem lowest-Wert mehr
in der Sequenz, so werden die Elemente der Stapelspeicher miteinander verglichen und
entfernt. Dazu wird in exit-element das oberste Element des Stapels mit dem niedrigsten
highest-Wert mit den Elementen des anderen Stapels auf gemeinsame Präfixe verglichen
und die zugehörigen Objekte der Ergebnisliste zugefügt. Als Beispiel zur Entwicklung des
Stapels und dem Auslesen der Sequenzen betrachte man noch Abbildung 3.9.
Die Laufzeit des Verbundes ist in etwa linear zur Anzahl der Elemente in den BBäumen. Die Element-Zahl hängt wiederum mit dem Feinheitsgrad der Kachelung zusammen. Erhöht man den Feinheitsgrad und damit die Anzahl der Elemente je Objekt,
so werden weniger Paare gefunden, die sich tatsächlich nicht überschneiden, sondern nur
aufgrund großer Kacheln eine gemeinsame Kacheln belegten. Dafür erhöht sich die Laufzeit des Verbundes entsprechend. In [OM88] wird statt eines einfachen A.next() eine etwas
aufwendigere Funktion zum Fortschreiten in den Sequenzen verwendet, die offensicht”
lich“ überflüssige Einträge überspringt, so dass sich eine sublineare Laufzeit ergeben kann.
KAPITEL 3. SPATIAL JOIN ALGORITHMEN
Stack:
35
L−Stack:
−
Sequenz:
L: 001(B), 0011(A), 1001(B)
R−Stack:
−
R: 001(C), 100(D)
L−Stack:
001(B)
L: 0011(A), 1001(B)
R−Stack:
−
R: 001(C), 100(D)
L−Stack:
001(B)
L: 0011(A), 1001(B)
R−Stack:
001(C)
R: 100(D)
L−Stack:
001(B) 0011(A)
L: 1001(B)
R−Stack:
001(C)
R: 100(D)
L−Stack:
001(B)
L: 1001(B)
R−Stack:
001(C)
R: 100(D)
L−Stack:
−
L: 1001(B)
R−Stack:
001(C)
R: 100(D)
L−Stack:
−
L: 1001(B)
R−Stack:
−
R: 100(D)
L−Stack:
−
L: 1001(B)
R−Stack:
100(D)
R: −
L−Stack:
1001(B)
L: −
R−Stack:
100(D)
R: −
L−Stack:
−
L: −
R−Stack:
100(D)
R: −
−
R−Stack: −
L−Stack:
L: −
R: −
gefundene Paare:
(A,C)
(A,C), (B,C)
(A,C), (B,C)
(A,C), (B,C)
(A,C), (B,C)
(A,C), (B,C), (B,D)
(A,C), (B,C), (B,D)
Abbildung 3.9: Beipiel zum z-tree-join
Kapitel 4
GiST-Indexe
Um den gesteigerten Anforderungen an die in einer Datenbank zu speichernden Daten
gerecht zu werden (Geographische Daten, CAD-Datensätze, Dokument-Bibliotheken, Fingerabdrücke), wurden im Laufe der Zeit zwei wesentliche Ziele bei der Erforschung von
Suchbaum-Technologien verfolgt:
1. Spezialisierte Suchbäume: Es wurde eine sehr große Anzahl von Suchbäumen für
spezifische Probleme entwickelt. Darunter so bekannte, wie die R-Baum-Varianten
zur Suche in räumlichen Datensätzen. Obwohl diese Suchbäume einen Durchbruch
im jeweiligen Fachgebiet darstellen können, sind sie doch sehr aufwendig in der
Implementierung und Wartung. Neue Anwendungen müssen in der Regel von Grund
auf neu entwickelt werden. Das betrifft selbst grundlegenden Baumoperationen,
wie Suche, Einsetzen, Struktur-Wartung und Synchronisation bei konkurrierendem
Zugriff.
2. Suchbäume für erweiterbare Datentypen: Alternativ zur Entwicklung neuer Strukturen können bestehende Datenstrukturen, wie B*-Bäume und R-Bäume, bezüglich
der Datentypen, die sie unterstützen, erweiterbar gemacht werden. So können B*Bäume zur Indexierung von Daten benutzt werden, auf denen sich eine lineare Ordnung definieren lässt. Die Menge der Datentypen, die damit erfasst werden kann,
ist damit zwar deutlich größer, aber die Anfragen beschränken sich weiterhin auf
die Möglichkeiten des zugrunde liegenden Baumes. Bei B*-Bäumen sind dies also
Anfragen auf Gleichheit oder Wertebereiche, bei R-Bäumen bleibt es bei Gleichheit,
Überlappung und Überdeckung.
Dieser Mangel an Flexibilität bei den Anfragen führt zu Problemen bei der Entwicklung neuer Anwendungen. Es ist eher unwahrscheinlich, dass Anfragen auf lineare
Ordnungen oder räumliche Gegebenheiten auch für neue Datentypen angemessen
sind.
Um die Beschränkungen der beiden Strategien zu überwinden, stellen J.M. Hellerstein,
J.F. Naughton und A. Pfeffer in [HNP95] eine dritte Möglichkeit vor, um SuchbaumTechnologien zu erweitern.
KAPITEL 4. GIST-INDEXE
37
Dort wird eine neue Datenstruktur namens Generalized Search Tree (verallgemeinerter
Suchbaum, kurz: GiST) eingeführt, die sowohl bezüglich der unterstützten Datentypen,
als auch der möglichen Anfragen leicht erweiterbar ist. Die Konfiguration eines GiST zur
Darstellung eines bestimmten Baumtyps geschieht über die Anpassung von lediglich sechs
Methoden, die in der GiST-Basisklasse nur abstrakt deklariert sind.
4.1
Überblick über GiST
Bei einem verallgemeinerten Suchbaum handelt es sich um einen ausgeglichenen Baum
mit variablem Verzweigungsgrad. Die Wurzel besitzt einen Verzweigungsgrad zwischen 2
und M , alle anderen Knoten zwischen kM und M , wobei für den minimalen Füllfaktor
k gilt: M2 ≤ k ≤ 12 .
Die Knoten enthalten Einträge der Form E = (p, ptr), bestehend aus einem Prädikat
p und einem Zeiger ptr, der bei internen Konten auf einen Nachfolgeknoten und bei
Blättern des Baumes auf Tupel (oder Objekte) in einer Datenbank verweist. Die im GiST
zu speichernden Daten werden also nur in den Blättern abgelegt, die alle den gleichen
Abstand zur Wurzel haben. Das Prädikat klassifiziert die über ptr erreichbaren Knoten
oder Datensätze: Sie dürfen der entsprechenden Eigenschaft nicht widersprechen, bzw.
alle darüber erreichbaren Knoten besitzen diese Eigenschaft. Im Gegensatz zu B*-Bäumen
oder R-Bäumen muss es sich bei den Prädikaten nicht zwangsläufig um kontinuierliche
Bereiche handeln, es sind beliebige Sub-Kategorien möglich, wie etwa ist sternförmig“,
”
konvex“ oder ungerade“, aber natürlich gehen auch (mehrdimensionale) Intervalle, in
”
”
denen sich dann die über ptr erreichbaren Daten befinden, je nach Anforderung an die
zu speichernden Daten.
p1 ptr1
p11 ptr11 p12 ptr12
p2 ptr2
p21 ptr21 p22 ptr22 p23 ptr23
Zeiger auf
DB-Tupel oder -Objekte
Abbildung 4.1: Prädikate und Pointer in den GiST-Knoten
Möchte man einen neuen Baumtyp erstellen, erstellt man eine neue Unterklasse der
Basisklasse GiST. Die möglichen Ausprägungen dieses Baumtyps sind dann die Objekte
vom Typ der Unterklasse. Die Spezialisierung geschieht, indem man sechs Methoden neu
definiert, die in der GiST-Klasse nur abstrakt definiert sind. Die Methoden des GiST
rufen dann während der Suche, dem Einsetzen und Löschen im Baum die Methoden der
Unterklasse auf und können somit auf die Bedürftnisse des zu speichernden Datentyps
angepasst werden. Folgende Methoden werden für eine neue Klasse benötigt:
KAPITEL 4. GIST-INDEXE
38
Consistent(E,q): Zu einem Knoten E = (p, ptr) und einem Anfrageprädikat q wird
false zurückgegeben, wenn sicher ist, dass sich die Prädikate widersprechen, ansonsten gibt man true zurück.
Es ist nicht zwingend notwendig, hier akkurat vorzugehen, da der Algorithmus auch
bei fälschlicherweise zurückgegebenem wahr richtig funktioniert. Dann wird aber die
Performance beeinträchtigt, da Teilbäume unnötigerweise untersucht werden.
Union(P ): Zu einer Menge P = {E1 , E2 , . . . , En } von Knoten Ei = (pi , ptri ) gibt die
Funktion ein Prädikat r zurück, welches für alle Tupel gültig ist, die unter ptr1
bis ptrn abgelegt sind. Dazu bestimmt man ein r mit gemeinsamen Eigenschaften
aller pi . Dies kann z.B. ein alle pi umschließendes Intervall (B-Baum) oder Rechteck
(R-Baum) sein.
Compress(E): Zu einem Knoten E = (p, ptr) gibt man einen Knoten (π, ptr) zurück,
wobei π einen komprimierten Repräsentanten von p darstellt. In einem R-Baum
wäre das beispielsweise die bounding box eines Polygons.
Decompress(E): Zu einem komprimierten Knoten E = (π, ptr) wird ein Eintrag (r, ptr)
zurückgegeben, so dass r ein gültiges Prädikat für das p ist, aus dem π bei Compress
hervorgegangen ist. Für einen R-Baum wäre Decompress die Identität: Die bounding
box ist selbst bereits ein Polygon, welches das ursprüngliche Objekt umschließt.
Penalty(E1 ,E2 ): Zu zwei gegebenen Knoten E1 = (p1 , ptr1 ), E2 = (p2 , ptr2 ) wird ein
Strafmaß berechnet, das entsteht, wenn E2 in den Teilbaum, der bei E1 beginnt,
eingefügt wird. Damit soll beim Einsetzen oder Teilen eines Knotens entschieden
werden, wo ein Knoten zu platzieren ist. Typischerweise wird der Größenanstieg des
Prädikates genommen, etwa size(Union({E1 , E2 }))−size(p2 ), mit einer geeigneten
Funktion size, die bei einem R-Baum beispielsweise die Fläche eines Prädikats (also
eines MBRs) berechnet.
PickSplit(P ): Gibt zu einer Menge P mit M + 1 Einträgen eine Partition in zwei
Teilmengen P1 , P2 zurück, die jeweils mindestens kM Einträge besitzen (hier wird
also der minimale Füllfaktor kontrolliert). Hier sollte man versuchen, die Menge
möglichst gut“ zu teilen. Für R-Bäume bedeutet dies zum Beispiel: Geringe Über”
lappung, kleine Flächen und niedrige Umfänge der MBRs um die sich ergebenden
Teilmengen.
Wenn die aufrufende Anwendung die Datensätze - insbesondere die Schlüssel - bereits
in einer für den Baum geeigneten Form bereitstellt, so ist es möglich, für Compress und
Decompress jeweils die Identitätsfunktion zu wählen. Eine interessante Möglichkeit ergibt
sich für Bäume, deren Wertebereich mit einer Ordnung versehen ist. Später wird man
sehen, dass dafür aus Effizienzgründen eine etwas andere Speicherung im GiST gewählt
werden kann. Für den Fall der Speicherung von Intervallen als Prädikat, kann Compress
für den linkesten Eintrag eines internen Knotens ein Prädikat der Größe Null liefern, für
die anderen Einträge im internen Knoten nur die linke Grenze des Intervalles. Decompress
KAPITEL 4. GIST-INDEXE
39
liefert dann wieder ein Intervall zurück, welches aus der vorhandenen linken Grenze und
dem Eintrag des nächsten Knotens generiert wird. Beim linkesten Eintrag verwendet man
−∞ und die linke Grenze des nächsten Eintrages, der sich auch in einem anderen Knoten
befinden kann, oder ∞, falls es keinen größeren“ Eintrag mehr gibt. Daraus ergibt sich
”
eine Speicherung von n − 1 Schlüsseln für n Einträge im Knoten, wie dies auch in B*Bäumen üblich ist.
Hat man diese Methoden für den gewünschten Datentyp implementiert, so können die
vom GiST bereitgestellten Suchbaummethoden, die im Folgenden kurz skizziert werden,
diese nach Bedarf aufrufen. Die Methoden Compress und Decompress werden jeweils
aufgerufen, wenn ein neuer Schlüssel in einen Knoten eingefügt bzw. ausgelesen wird.
Dies geschieht implizit und wird im Folgenden nicht weiter beschrieben.
Suche: Zu einem Prädikat q wird ausgehend von der Wurzel R eines GiST eine Tiefensuche durchgeführt. Dazu wird zu jedem Eintrag E des Knotens mittels der Methode
Consistent(E, q) herausgefunden, ob der Teilbaum oder Datensatz als Ergebnis in
Frage kommen kann. Bei internen Knoten wird die Suche mit dem entsprechenden
Teilbaum rekursiv weitergeführt, Blätter können anhand der tatsächlichen Datenbankeinträge genauer überprüft werden (oder man überlässt dies der aufrufenden
Anwendung, indem das Tupel einfach der Ergebnismenge zugefügt wird).
Suche in linearen Ordnungen: Ist der zu indexierende Wertebereich mit einer linearen Ordnung versehen, und bestehen die zu unterstützenden Anfragen aus Gleichheit und Wertebereichen, so kann die Suche noch effektiver gemacht werden, indem
man bei der Anlage der Spezialisierung eine zusätzliche Methode Compare(E1 , E2 )
implementiert, und ein Flag isOrdered setzt. Compare liefert zurück, welches der
zu den Knoten gehörenden Prädikate bezüglich der Ordnung kleiner“ ist. Die Ein”
träge werden in den Knoten geordnet abgelegt. Bei der Suche kann dann erst der
kleinste das Suchprädikat erfüllende Eintrag, und dann alle weiteren berücksichtigt werden, bis das Prädikat nicht mehr erfüllt ist. Die restlichen Einträge dieses
Knotens brauchen nicht weiter untersucht werden. Die Methoden eines geordneten
Baumes müssen sicherstellen, dass sich zwei verschiedene Einträge eines Knotens
nicht überlappen. Consistent(E1 , E2 ) muss dazu für jedes Paar von Einträgen E1 , E2
eines Knotens false liefern.
Einsetzen: Anhand des Prädikats des einzusetzenden Eintrags E wird der Knoten im
GiST gesucht, wo der neue Eintrag eingefügt werden soll. Dazu wird, beginnend bei
der Wurzel, jeweils Penalty(F ,E) für alle Einträge F eines Knotens aufgerufen, und
zu dem Eintrag mit der geringsten Strafe der zugehörige ptr verfolgt, bis man einen
Knoten einer vorher anzugebenden Stufe erreicht hat. Für das Einsetzen neuer Knoten in den GiST ist dies die Blattebene. Ist dort noch ausreichend Platz, so wird
der Eintrag in den Knoten eingefügt. Handelt es sich um einen geordneteten Baum,
so verwendet man Compare, um die richtige Position zum Einfügen im Knoten zu
finden. Sollte nicht mehr ausreichend Platz vorhanden sein, so muss der Knoten
geteilt werden. Dazu wird mittels PickSplit die beste Partition der Einträge des
KAPITEL 4. GIST-INDEXE
40
Knotens (und des neuen Eintrages) in zwei Teile gesucht. In geordneten Bäumen
muss PickSplit die Ordnung erhalten, also alle Einträge im linken Teil der Partition
müssen bezüglich der durch Compare festgelegten Ordnung kleiner sein als im rechten Teil. Steht die Partition fest, so verbleibt der eine Teil im aktuellen Knoten, der
andere wird in einem neuen Knoten gespeichert. Dieser neue Knoten wird in den
Vorgänger des ursprünglichen Knotens eingefügt, wobei sich bei Platzproblemen im
Vorgänger diese Methode des Aufteilens und Einsetzens bis zur Wurzel fortsetzen
kann. Ist in der Wurzel nicht ausreichend Platz, so wird sie ebenfalls aufgeteilt und
eine neue Wurzel des Baumes installiert, welche die beiden aus der alten Wurzel
entstandenen Knoten aufnimmt.
Die Prädikate der Knoten, die von Änderungen betroffen sind, müssen angepasst
werden. Dazu wird, beginnend auf der Ebene, die auf die Blattebene zeigt, für einen
Eintrag E = (p, ptr), dessen Teilbaum unter ptr geändert wurde, Union mit den
Einträgen des Teilbaumes aufgerufen, und p entsprechend korrigiert. Dies setzt sich
gegebenenfalls bis zu den Einträgen der Wurzel fort.
Löschen: Um einen Eintrag E = (p, ptr) aus einem GiST zu löschen, sucht man zuerst
mittels der Such-Methode über das Prädikat p das Blatt, in dem sich E befindet.
E wird aus dem Blatt entfernt. Hierbei kann es passieren, dass nun weniger als
kM Einträge im Knoten verbleiben. Je nachdem, ob man sich in einem geordneten
Baum befindet, oder isOrdered nicht gesetzt ist, kann jetzt unterschiedlich verfahren
werden.
In geordneten Bäumen wird die von B*-Bäumen bekannte borrow or coalesce- (borgen oder verschmelzen-) Technik verwendet: Sind in einem (gemäß der Ordnung)
Nachbarknoten mehr als 2kM Einträge, so werden diese gleichmäßig auf den aktuellen und den Nachbarknoten aufgeteilt. Ansonsten wandern alle Einträge des
zu kleinen Knoten in den Nachbarknoten. Der leere Knoten, sowie der Eintrag im
Vorgängerknoten auf diesen Knoten, werden entfernt. Sollten dadurch wiederum zu
wenige Einträge im Vorgängerknoten entstehen, setzt sich der borrow or coalesceProzess nötigenfalls bis zur Wurzel fort. (Siehe dazu auch [Com79].)
Ist der Baum nicht geordnet, so wird eine Technik, die auch bei R-Bäumen verwendet wird, herangezogen (vgl. [Gut84]). Sind in einem Knoten zu wenige Einträge,
so merkt man sich die Einträge und deren Stufe im Baum in einer Menge Q. Der
Eintrag im Vorgängerknoten auf den leeren“ Knoten wird entfernt (und sollte sich
”
dadurch wieder ein unterfüllter Knoten ergeben, so setzt sich das Merken der Einträge in Q und Entfernen aus dem Vorgängerknoten notfalls bis zur Wurzel fort).
Anschließend werden die Elemente in Q einzeln wieder in den Baum eingesetzt,
wobei die Stufe, die man sich gemerkt hat, beim Einsetzen berücksichtigt wird.
Bei beiden Vorgehensweisen werden die Prädikate für die Teilbäume jeweils an die
aktuelle Situation angepasst, um sie so spezifisch wie möglich zu halten. Hat die
Wurzel nur noch einen Unterknoten, so wird dieser zur neuen Wurzel des GiST,
und die alte Wurzel gelöscht.
KAPITEL 4. GIST-INDEXE
41
In manchen Anwendungen, in denen nach einem Löschvorgang ein baldiges Einsetzen von neuen Knoten erwartet wird, kann es nützlich sein, dass unterfüllte Knoten
nicht sofort entfernt werden (siehe dazu [JS93]). Dann reicht es aus, die Prädikate
der Knoten anzupassen. Die unterfüllten (oder leeren) Knoten verbleiben im Baum.
Derzeit sind drei GiST-Implementierungen im Internet zu finden:
• Die Universität Berkeley entwickelte eine in C++ verfasste Implementierung libgist,
die im April 2000 in der (bisher abschließenden) Version 2.0 freigegeben wurde.
• An der Universität Kapstadt entwickelte man 1997 aus einer frühen Version (0.9)
der libgist eine Portierung nach JAVA.
• Desweiteren existiert eine Implementierung, die in die Datenbank PostgreSQL eingebunden ist und dort derzeit (Juni 2003) auch verwendet und weiter gepflegt wird
([GiST-PSQL]). Insbesondere verwendet sie ein Paket namens PostGIS, welches
von Refractions Research Inc. unter der GPL ([GPL]) veröffentlicht wurde. PostGIS ([GiST-PGIS]) soll in der Version 1 (derzeit ist 0.7.5 aktuell) PostgreSQL zu
einer OpenGIS konformen räumlichen Datenbank erweitern (bezüglich der Simple
Features Specification for SQL).
Da diese Arbeit eine bereits bestehende Anbindung der libgist an eine Oracle-Datenbank
um räumliche Verbunde erweitern soll, ist hier insbesondere die Implementierung aus Berkeley interessant:
4.2
libgist der University of California at Berkeley
An der Universität Berkeley wurde von 1997 bis 2000 eine GiST-Implementierung namens
libgist in C++ erstellt. Diese wird im Folgenden kurz vorstellen möchte, da sie im Rahmen
dieser Diplomarbeit verwendet wird.
4.2.1
GiST-Erweiterungen
Die libgist enthält einige Erweiterungen des GiST-Konzeptes, die in [Aok98] vorgeschlagen
werden. Dies sind u.a.: Eine Cursorerweiterung, um statt einer Tiefensuche mittels eines
Stacks auch andere Reihenfolgen zu ermöglichen, in denen die Knoten bei der Suche durchlaufen werden. Außerdem kann ein Status zum aktuellen Suchlauf gespeichert werden. So
kann in R-Bäumen mit einer Prioritätswarteschlange auch eine nearest-neighbour-Suche
realisiert werden, die zu einem Objekt das nächstgelegene ermittelt. Statt consistent wird
eine allgemeinere Methode search angeboten, die, je nach Baumtyp, auf vorhandene Ordnungen der Einträge Rücksicht nehmen kann (beispielsweise binäre Suche bei geordneten
Einträgen). Das Knotenlayout ist dazu so zu ändern, dass GiST-Spezialisierungen die
Ordnung der gespeicherten Werte bestimmen können. Statt mit penalty alle Teilbäume
auf Prädikatänderungen beim Einfügen überprüfen zu müssen, kann findMinPen eine
KAPITEL 4. GIST-INDEXE
42
Ordnung ausnutzen, um nur wenige Teilbäume zu überprüfen. insert sorgt für ein der
Ordnung entsprechendes Einfügen neuer Einträge in die Knoten.
Es handelt sich dabei um eine echte Erweiterung in dem Sinne, dass die ursprüngliche
Funktionalität nicht beeinträchtigt wird. So kann man sich für ungeordnete Knoteneinträge auch entscheiden, in search weiterhin alle Einträge auf Konsistenz zu prüfen, die
Einträge in den Knoten beliebig vorzunehmen und weiterhin einen Stack bei der Suche
zu verwenden.
4.2.2
Funktionsübersicht
Die libgist stellt dem Anwender eine einheitliche Schnittstelle zur Verwendung verallgemeinerter Suchbäume zur Verfügung. Durch die Erzeugung eines Objektes der Basisklasse
gist wird ein Sitzungsobjekt erstellt, welches einen generischen verallgemeinerten Suchbaum darstellt. Erst beim Öffnen oder Erzeugen eines Suchbaumes wird die SuchbaumSpezialisierung festgelegt. Dann können Anfragen oder Änderungen ausgeführt werden,
bis der Baum geschlossen wird, und das gist-Objekt damit seine spezielle Ausprägung
wieder verliert. Vor der Zerstörung des Objektes können mehrere Sitzungen stattfinden,
also auch noch weitere, andere Suchbäume geöffnet und verwendet werden. Damit ist es
nicht nötig, Objekte einer festgelegten Spezialisierung zu verwenden, sondern zur Laufzeit
kann eine der implementierten Spezialisierungen ausgewählt werden.
Die typische Verwendung eines GiST-Objektes für einen Anwender läuft folgendermaßen ab: Zunächst wird ein Objekt der Klasse gist initialisiert und dabei der zugehörige
Konstruktor aufgerufen. Mit den Methoden open(Dateiname) oder create(Dateiname,
Spezialisierung) wird ein existierender Suchbaum geladen oder ein neuer, mit der entsprechenden Spezialisierung, erstellt.
Mittels insert(. . .) können jetzt Paare bestehend aus Schlüssel- und Datenelement
in den Baum eingefügt werden (also beispielsweise ein MBR und eine RowID).
Die Suche im Baum verläuft mehrstufig. Bevor der Suchvorgang beginnt, werden noch
zwei weitere Objekte benötigt: Eines vom Typ gist_query_t, das zur Spezifizierung der
Anfrage dient, und ein Iterator, im Datenbank-Kontext auch Cursor genannt, vom Typ
gist_cursor_t, der Informationen zum Baumdurchlauf und der aktuellen Suche speichert. Die Anfragen sind vom verwendeten Baumtyp abhängig, in R*-Bäumen etwa Anfragen auf Überlappung, Schnitt oder Gleichheit mit dem angegebenem Suchprädikat. Die
Methode fetch_init(. . .) initialisiert den Cursor entsprechend der Anfrage. Dabei wird
an Hand der Anfrage die zu verwendende Cursor-Erweiterung ausgewählt. Die Abfrage
des Suchergebnisses wird mit mehrmaligem Aufruf der Methode fetch(. . .) ausgeführt.
fetch() liefert dazu Schlüssel und Datenelement in vorher zu reservierende Speicherbereiche.
Um Einträge aus dem Baum zu entfernen, ist eine Anfrage zu konstruieren, die genau
die zu löschenden Einträge findet. Die Methode remove(Query) löscht alle Einträge des
Baumes, die der Anfrage entsprechen.
Der aktuelle Baumzustand kann jederzeit durch einen Aufruf von flush() mit der zugehörigen Datei synchronisiert werden. Ansonsten geschieht dies spätestens beim Beenden
der Verbindung des GiST-Objektes mit einer Index-Datei durch die Methode close().
KAPITEL 4. GIST-INDEXE
4.2.3
43
Klassenübersicht
Die Kernklasse der libgist-Bibliothek ist die Basisklasse gist, die die Methoden für den
Anwender bereitstellt. Die Bibliothek arbeitet dateibasiert, die verallgemeinerten Suchbäume werden jeweils in einzelnen Dateien abgelegt. Als Schnittstelle zum Dateisystem
existiert eine Klasse gist_file. Sie stellt eine seitenbasierte Verwaltung zur Verfügung
und ermöglicht die Speicherung einiger Meta-Daten in einem Datei-Header (z.B. magicstring, Baum-Spezialisierung, ungenutze Seiten). Damit die Seiten bei mehrfacher Verwendung nicht ständig neu geladen oder geschrieben werden müssen, kann eine festgelegte
Zahl davon im Hauptspeicher verwaltet werden. Der Zugriff darauf erfolgt über eine HashTabelle (gist_htab). Die Größe eines Baumknotens entspricht der einer Speicher-Seite.
Zugriff auf die Seitenstruktur (Seiten-Header, Einträge (slots), Verzeichnis der Einträge)
und zugehörige Management-Funktionen erfolgt über die Klasse gist_p.
Zur Erweiterung des Baumes sind mehrere Klassen als Ausgangspunkte vorgesehen.
gist_ext_t spezifiziert die grundlegende Erweiterungsklasse. Die abstrakten Methoden
(insert, findMinPen, serach, union,. . .) müssen in Unterklassen spezifiziert werden. Diese
Klasse ermöglicht weitgehende Kontrolle über das Knoten-Layout des Baumes.
Sind die Knoten eines Baumes nicht geordnet, so kann die Klasse gist_unordered_t
als Ausgangspunkt gewählt werden. Darin wird ein Interface, wie im original GiSTArtikel ([HNP95]) beschrieben, bereitgestellt. Das Knotenlayout wird dabei von der Klasse gist_unorderedn_t (man beachte das zusätzliche n“), einer Unterklasse von gist_
”
ext_t, bereitgestellt. Zur Erstellung einer neuen (ungeordneten) Baumspezialisierung
werden in einer Unterklasse die abstrakten Methoden aus gist_unordered_t implementiert. Ein Objekt davon wird dann dem Konstruktor des gist_unorderedn_t-Objektes
übergeben, um das komplette Objekt mit den geforderten Knoten-Methoden (nämlich
ungeordnete Speicherung) und GiST-Methoden (consistent, penalty,. . .) zu erhalten. Eine zusätzliche abstrakte Methode in gist_unordered_t ist queryCursor, die zu einer
Suchanfrage (der Klasse gist_query_t) eine passende Cursor-Erweiterung (gist_cursor_
ext_t) angeben muss.
Die Cursor, vom Typ gist_cursor_t, werden genau wie ein gist-Objekt generisch
erzeugt. Erst bei der Verbindung mit einer Suchanfrage wird ein spezieller Cursor, der
vom Typ gist_cursorext_t oder einer Unterklasse dieses Typs ist, ausgewählt und zum
Anfragen von Datensätzen verwendet.
4.2.4
Suchbaumerweiterungen
Für die libgist sind zahlreiche Suchbaum-Spezialisierungen verfügbar und auch in der
Distribution enthalten. Dies umfasst unter anderem B-Bäume für Ganzzahlen und Zeichenketten, sowie R-Bäume, R*-Bäume, SS-Bäume und SR-Bäume für Punkte und/oder
Rechtecke. Als Cursor-Erweiterungen sind eine Prioritäts-Warteschlange und ein lookup
stack enthalten, der als Prioritätswarteschlange mit der Einfügezeit als Priorisierung realisiert ist.
Möchte man einen neuen Suchbaumtyp implementieren, so werden zwei Vorgehensweisen vorgeschlagen:
KAPITEL 4. GIST-INDEXE
44
1. Die Spezialisierung wird aufgeteilt in die Teilbereiche
• Knotenlayout
• abstrakte Basisklasse mit für dieses Knotenlayout spezifischen Methoden
• spezielle Erweiterungsklasse zur Implementierung der Suchbaum-Methoden
Dabei kann für ungeordnete Einträge in den Knoten die Klasse gist_unorderedn_t
als Ausgangsbasis für das Knotenlayout verwendet werden. Die zugehörigen Methoden sind in gist_unordered_t abstrakt deklariert und müssen in einer Unterklasse
definiert werden.
2. Die Spezialisierung wird direkt als Unterklasse der Erweiterungsklasse gist_ext_t
erstellt. Knoten- und datenrelevante Methoden werden in einer gemeinsamen Klasse
beschrieben.
Ein Beispiel für die erste Vorgehensweise sind die R-Bäume und einige verwandte Spezialisierungen (Sphere-Tree, SS-Tree) der libgist-Distribution. Diese verwenden eine gemeinsame Klasse rtbase_ext_t, die in weiteren Unterklassen für den jeweiligen Baumtyp
vervollständigt werden.
Die zweite Vorgehensweise eignet sich besonders für weniger aufwendige Methoden,
wo nicht viele Variationen erwartet werden. Die B-Baum-Spezialisierungen der libgist sind
Beispiele für dieses Verfahren.
Werden Varianten der bereits vorgegebenen Bäume erstellt, so kann dies geschehen,
indem die Methoden einzelner Teil-Klassen eines Baumes in weiteren Unterklassen überschrieben werden.
Die Implementierung des R*-Baumes in der libgist ist in Abbildung 4.2 skizziert.
Dieser Ausschnitt der libgist-Klassenstruktur zeigt, wie eine R*-Baum-Erweiterung als
Erweiterungsobjekt an die Klasse gist angebunden ist. Die abstrakten Methoden sind in
der Abbildung kursiv dargestellt.
Die R*-Baum-Erweiterung überschreibt die Methoden pickSplit() und findMinPen()
der Knotenlayout-Klasse gist_unorderedn_ext_t, die wiederum die abstrakte Erweiterungsklasse gist_ext_t implementiert. In den Methoden der Knotenlayout-Klasse werden Methoden eines Objektes der Klasse gist_unordered_ext_t verwendet. Ein solches Objekt muss daher dem Konstruktor von rstar_ext_t übergeben werden. Konkret
wird bei R*-Bäumen ein Objekt der Klasse rt_ext_t verwendet, einer Unterklasse von
rt_base_ext. Diese Basisklasse definiert die abstrakten Methoden aus gist_unordered_
ext_t und vereint einige Methoden, die in verschiedenen darauf aufbauenden Unterbäumen verwendet werden (R-Bäume, SR-Bäume, SS-Bäume, etc.).
Neue Baumspezialisierungen sind der libgist noch bekannt zu machen (gist_ext.h,
gist_extensions.h). Erst dadurch kann das generische GiST-Objekt die richtigen Unterklassen der Erweiterungsklasse für die gewählte Spezialisierung auswählen. Weitere
Details zur Erstellung neuer Suchbaum-Spezialisierungen oder Cursor-Erweiterungen findet man unter [AK01].
KAPITEL 4. GIST-INDEXE
45
gist_ext_t
+gist_ext_list: gist_ext_t *[]
+myID: enum gist_ext_ids
+myName: char *
+insert()
+remove()
+updateKey()
+findMinPen()
+search()
+getKey()
+pickSplit()
+unionBp()
+queryCursor(): gist_cursorext_t*
gist_unorderedn_ext_t
gist
#_ext: gist_ext_t *
#_file: gist_file
+create()
+open()
+close()
+insert()
+remove()
+fetch_init()
+fetch()
+flush()
+ext: gist_unordered_ext_t &
+insert()
+remove()
+updateKey()
+findMinPen()
+search()
+getKey()
+pickSplit()
+unionBp()
+queryCursor(): gist_cursorext_t *
rstar_ext_t
+ext: rt_ext_t &
+findMinPen()
+pickSplit()
gist_unordered_ext_t
+consistent()
+penalty()
+union_bp()
+pickSplit()
+queryCursor(): gist_cursorext_t *
rtbase_ext_t
+consistent()
+union_bp()
+queryCursor(): gist_cursorext_t *
rt_ext_t
+penalty()
+pickSplit()
Abbildung 4.2: Ausschnitt aus der libgist-Klassenstruktur: R*-Baum-Spezialisierung
4.2.5
Einschränkungen der libgist
Einige Einschränkungen bestehen bei der verwendeten libgist-Version, insbesondere im
Vergleich zu den Vorschlägen der original GiST-Publikation [HNP95]:
• Der Verzweigungsgrad eines Knotens hängt von der Seitengröße (festgelegt durch
die Konstante SM_PAGESIZE) und dem tatsächlichen Platzbedarf der Einträge ab.
Insbesondere bei variabler Schlüssellänge kann die Anzahl der Einträge je Knoten
stark variieren.
• Löschvorgänge werden nicht an Hand des zu löschenden Eintrages, sondern an Hand
einer Suchanfrage durchgeführt. Der Benutzer muss also zum Löschen genau eines
Eintrages eine Such-Anfrage formulieren, die genau diesen Eintrag findet. Ansonsten
werden alle der Anfrage entsprechenden Einträge gelöscht. Dabei darf die Anfrage
nur Operatoren verwenden, die unabhängig von anderen Einträgen auswertbar sind,
also insbesondere keine nearest neighbour -Anfragen, wo erst am Ende der Suche klar
ist, welcher Eintrag die Suchbedingung erfüllt.
Diese Einschränkung hat zur Folge, dass Einträge mit identischen Schlüsseln nicht
einzeln aus dem Baum entfernt werden können. Verschärft wird dieses Problem
durch die Tatsache, dass durch Komprimierung der Schlüssel Duplikate noch wahr-
KAPITEL 4. GIST-INDEXE
46
scheinlicher werden, zumal in einer Datenbank-Relation Duplikate generell erlaubt
sind.
• Werden Einträge aus Blättern gelöscht, so werden die Suchprädikate des Vorgängers
nicht neu berechnet und bleiben dadurch gegebenenfalls unnötig groß.
• Leere Knoten werden nicht gelöscht, sondern verbleiben im Baum. Auch ein Zusammenfassen unterfüllter Knoten wird nicht untersützt. Dies kann nach häufigen
Löschoperationen zu schlechten Anwortzeiten führen, da aufgrund unzureichend gefüllter Knoten unnötig viele Teilbäume expandiert werden müssen.
Kapitel 5
GiST Indexe für Oracle
Oracle 9i bietet als objektrelationale Datenbank die Möglichkeit, benutzerdefinierte Typen und zugehörige Objekte aus bereits existierenden Typen zu erstellen. Die Typen
und Methoden können zu einem Data Cartridge zusammengefasst werden, so dass sie
als Einheit in die Datenbank eingebunden werden können. Einige Methoden können Vergleichsoperationen implementieren, die dem Datenbanksystem als Operatoren bekannt
gemacht werden können.
Ein Beispiel für ein Data Cartridge ist das Spatial Cartridge 1 der Oracle Corporation,
welches die Datenbank um Datentypen für räumliche Daten ergänzt (Punkte, Linienzüge, Polygone, zusammengesetzte Polygone, etc. ) Weiterhin stellt Spatial eine Reihe
von Operatoren zur Selektion (Auswahl von Datensätzen in einem query window ) und
Aggregation (Vereinigung, MBR-Berechnung) dieser Datentypen zur Verfügung. Zur Indexunterstützung einiger Operationen werden R-Bäume und Quadtree-Indexe angeboten.
5.1
Oracle - Extensible Indexing Interface
Um effektiv auf Objekte zugreifen zu können, ist es sinnvoll, passende Indexstrukturen
bereitzustellen. Dies ermöglicht das Extensible Indexing Interface des Oracle Data Cartridge Interface (ODCI), welches dem Benutzer eine Schnittstelle zur Verfügung stellt,
um eigene Datentypen geeignet zu indexieren. Dazu muss der Benutzer eine Reihe vorgegebener Methoden implementieren, die bei einer Anfrage an die Datenbank verwendet
werden, wenn ein unterstützter Operator auf einer indexierten Spalte zur Selektion einzelner Zeilen verwendet wird. Die Schlüssel und RowIDs der Datensätze sollen dabei in
einer geeigneten Datenstruktur gespeichert und schnell wieder abrufbar sein.
Aus Gründen der Konsistenzerhaltung und Transaktionssicherheit wird eine Speicherung der Indexdaten in Tabellen der Datenbank empfohlen. Im Falle eines konkurrierenden Zugriffes zweier Benutzer kann das Datenbanksystem bei externen Speicherstrukturen
keinen kollisionsfreien Zugriff (z.B. während einer Änderung des Indexes) gewährleisten.
Werden Tabellen genutzt, kann die Transaktionsverwaltung der Datenbank genutzt werden, um gleichzeitige Lese- und Schreibversuche zu koordinieren.
1
Produktname: Oracle Spatial, auch einfach Spatial genannt
KAPITEL 5. GIST INDEXE FÜR ORACLE
48
Im Falle einer Rücknahme von Änderungen (rollback) muss auch der zugehörige
Index zurückgesetzt werden. Bei externen Speicherstrukturen sind dazu die Änderungen
zu protokollieren, oder Kopien der Datenstrukturen vor Änderung anzulegen, um wieder
einen konsistenten Zustand herstellen zu können. Einfacher geht dies bei der Nutzung
von Tabellen der Datenbank zur Ablage der Speicherstrukturen, da diese ebenfalls zurückgesetzt werden können.
Die Methoden des Extensible Indexing Interface können in jeder Programmiersprache implementiert werden, die durch die Datenbank unterstützt werden. Somit kommen
PL/SQL, Java und C in Frage, letztere sind als externe Sprachen über PL/SQL einzubinden. Aus Performance-Gründen ist C insbesondere zur Implementierung aufwendigerer
Algorithmen zu empfehlen. C-Programme werden dazu als shared library bereitgestellt,
deren Prozeduren durch einen Erweiterungsprozess (extproc) der Datenbank aufgerufen werden. Damit kommt auch C++ als Programmiersprache in Frage, denn C++Prozeduren können über eine Linker-Anweisung in C-Semantik bekannt gemacht werden.
Vor der Erstellung eines benutzerdefinierten Indexes muss ein Objekttyp erstellt werden, der die vom Indexing Interface geforderten Methoden beinhaltet und der Speicherung des Zustandes zwischen zwei externen Bibliotheks-Aufrufen dient (scan context). Zu
diesem Objekttyp kann ein Indextyp erstellt werden, der spezifiziert, welche Operatoren
durch diesen benutzerdefinierten Index unterstützt werden.
Die Operatoren müssen auch als funktionale Implementierung vorliegen, d.h. auch ohne Indexunterstützung angeboten werden. Ist nur eine indexgestützte Verwendung sinnvoll, so kann die funktionale Implementierung beispielsweise mit einer Fehlermeldung
abbrechen, die den Benutzer zur Erstellung eines Indexes auffordert. Unter gewissen Voraussetzungen ist es allerdings auch möglich, in der funktionalen Implementierung auf den
Index zuzugreifen2 .
Sind diese Vorbereitungen getroffen, so kann ein Index zu einem passenden Attribut
erstellt werden:
create index INDEXNAME on TABELLE(SPALTE)
indextype is INDEXTYP parameters(’PARAMETER’);
5.1.1
Methoden des Extensible Indexing Interface
Bei der Erstellung eines Indexes wird die Methode ODCIIndexCreate(. . .) aufgerufen.
Der Methode wird eine Struktur mit zahlreichen Informationen zu der zu indexierenden
Tabelle, etwaige Parameter, die der Benutzer im SQL-Kommando angegeben hat und
eine Datenbank-Verbindung übergeben. ODCIIndexCreate() muss nun für den Aufbau
der nötigen Indexstrukturen sorgen, wobei es der Implementierung überlassen bleibt, wo
die dabei anfallenden Daten abgelegt werden.
Über die Datenbankverbindung sind Rückrufe (callbacks) an die Datenbank möglich.
Darüber ist eine Speicherung der Indexdaten in einer Tabelle der Datenbank möglich,
ohne sich als externes Programm separat (mit entsprechender Authentifizierung) mit
2
Siehe dazu Seite 7-35 in [O9i-DCDG]: Creating Index-based Functional Implementation“
”
KAPITEL 5. GIST INDEXE FÜR ORACLE
49
der Datenbank verbinden zu müssen. Sollte die zu indexierende Tabelle bereits Einträge
enthalten, so ist es Aufgabe von ODCIIndexCreate() diese über die Datenbankverbindung
abzurufen und in die Indexstruktur einzufügen.
Eine Änderung eines Indexes (alter index . . .) führt zu einem Aufruf der Methode
ODCIIndexAlter(). Hiermit ist es u.a. möglich, einem Index neue Parameter zu übergeben (z.B. einen anderen Speicherort), den Indexnamen zu ändern, oder einen Neuaufbau
des Indexes anzufordern, falls die Qualität des Indexes nach einer längeren Verwendung
nachlässt. Die Methode sollte den Index entsprechend der neuen übergebenden Parameter
ändern, soweit dies unterstützt wird.
Soll ein Index nicht mehr weiter verwendet werden, so wird er mittels drop index . . .
gelöscht. ODCIIndexDrop() ist dafür zuständig, die vom Index genutzten Ressourcen wieder freizugeben.
ODCIIndexCreate
Indexoperationen
ODCIIndexDrop
Abbildung 5.1: Lebenszyklus eines benutzerdefinierten Indexes
Während des laufenden Betriebes können sich die Daten der indexierten Tabelle ändern. Betrifft dies auch die indexierte Spalte, so führt ein Aufruf von insert, update
und delete jeweils zu einem Aufruf von ODCIIndexInsert(), ODCIIndexUpdate() und
ODCIIndexDelete(). Zusätzlich zur RowID werden bei Einfüge- und Änderungsoperationen die neu einzutragenden Werte, bei Änderungs- und Löschoperationen die alten Werte
übergeben.
bis alle Einträge gefunden
ODCIIndexStart
ODCIIndexFetch
ODCIIndexClose
Abbildung 5.2: Methodenaufruf beim Indexzugriff
Die Anfragebearbeitung geschieht, wie in Abbildung 5.2 zu sehen ist, in drei Stufen:
ODCIIndexStart() übergibt man den Operator, der durch den Index unterstützt werden soll, und möglicherweise ein Vergleichsobjekt. Zusätzlich wird ein Vergleichsintervall
(Start- und Stopp-Wert) angegeben, welches beispielsweise eine Zielgröße angeben kann.
Wird ein Index beispielsweise bei einer Anfrage verwendet, um Objekte einer bestimmten Größe (z.B. die Fläche eines Polygones) zu selektieren, so kann diese hier übergeben
werden.
Die Ergebnisse der Indexabfrage bestehen aus den RowIDs der entsprechenden Zeilen
der indexierten Tabelle. Diese werden bei einem Aufruf der Methode ODCIIndexFetch()
jeweils als Menge von Zeigern zurückgeliefert (deren Anzahl ODCIIndexFetch als Parameter angegeben wird). Evtl. muss die Methode mehrmals ausgeführt werden, bis alle
Ergebnisse abgerufen sind.
KAPITEL 5. GIST INDEXE FÜR ORACLE
50
Da zwischen den einzelnen Funktionsaufrufen der Programmfluss zur Datenbank zurückkehrt, ist es notwendig, Informationen zum aktuellen Status einer Anfrage zwischenzuspeichern (sämtliche Informationen zur Operation werden nur ODCIIndexStart() übergeben!). Im Falle einer externen C-Implementierung geschieht dies, indem von der Datenbank (bzw. dem extproc-Prozess) Speicher angefordert wird (OCIMemoryAlloc()),
der zur Ablage des sogenannten scan context dient. ODCIIndexStart() richtet den scan
context ein, indem Speicher für eine Struktur mit allen zu speichernden Informationen
angefordert wird. Diese Speicheradresse wird dann im Objekt gespeichert, welches auch
diese Methoden beinhaltet. ODCIIndexFetch kann damit wieder die Speicherstruktur erhalten und den eigenen Status darin vermerken. Dabei ist es durch Einsatz von Zeigern
in der Speicherstruktur und wiederholtem Aufruf von OCIMemoryAlloc auch möglich,
weiteren Speicher zu alloziieren.
Werden keine weiteren Ergebnisse mehr zurückgeliefert, so soll ODCIIndexClose() für
den Abschluss der Operation sorgen, indem alle verwendeten Ressourcen wieder freigegeben werden.
5.2
OraGIST-Bibliothek
Im Rahmen einer Diplomarbeit ([Lö01]) wurde von Ulf Löckmann eine Bibliothek entwickelt, die es ermöglicht, eine Verbindung der Schnittstellen der libgist und der Datenbank
Oracle (damals in der Version 8i) über das Extensible Indexing Interface zu erstellen.
Damit ist eine Unterstützung verallgemeinerter Suchbäume als Indexierungsstruktur
für benutzerdefinierte Indexe möglich.
5.2.1
Änderungen an der libgist
Um einige der Beschränkungen der libgist zu überwinden, wurden Teile der Bibliothek
geändert, um sie als Erweiterung der Oracle-Datenbank nutzbarer zu machen.
Da es sinnvoll ist, die Speicherstruktur der indexierten Daten in der Datenbank abzulegen, die libgist aber dateibasiert arbeitet, wurde die Klasse gist_file so geändert,
dass nicht Seiten einer Datei verwendet, sondern die Datenbankverbindung zur Ablage
von Speicherseiten genutzt werden kann.
Die in der libgist verwendeten Seiten haben eine feste Länge SM_PAGESIZE. Bei dateibasierter Operation wird aus einer angeforderten Seitennummer eine Position in einer Datei berechnet und ein entsprechend großer Speicherbereich gelesen oder geschrieben. Zur
Verwendung der Datenbank zur Seitenspeicherung wird im Datenbanksystem eine zweistellige Relation erstellt, die als Attribute eine Seitennummer (pageno vom Typ NUMBER)
und einen Speicherbereich zur Aufnahme der Daten besitzt (page: RAW(SM_PAGESIZE)).
Anstelle eines Dateizugriffes wird bei Ablage in der Datenbank in der Klasse gist_file
ein entsprechendes SQL-Kommando zum Lesen oder Schreiben einer Datenseite generiert
und ausgeführt.
Beim Löschen von Einträgen fordert die libgist, dass eine Anfrage formuliert wird,
die die zu löschenden Einträge spezifiziert. Dabei werden alle Einträge gelöscht, deren
KAPITEL 5. GIST INDEXE FÜR ORACLE
51
Schlüssel der Anfrage enstprechen. Um auch einzelne Einträge aus einem Suchbaum entfernen zu können, deren Schlüssel mehrfach vorhanden sind, wurde die libgist dahingehend
geändert, dass beim Löschen auch die RowID, also das zu einem Eintrag gehörende Datenelement, übergeben wird. Damit ist eine eindeutige Identifizierung des zu löschenden
Eintrages möglich, die beim Löschen berücksichtigt wird.
Bei der Suche eines zu löschenden Eintrages wird die formulierte Anfrage verwendet,
um den richtigen Knoten ausfindig zu machen. Zur Suche wird wiederum die zugehörige Cursor-Erweiterung verwendet. Dabei wird aber nicht der Pfad von der Wurzel zu
dem Knoten mit dem zu löschenden Eintrag gespeichert, weswegen die libgist auch den
umgekehrten Weg nicht kennt, um die Prädikate der Vorgängerknoten zu aktualisieren.
Um unnötig große Prädikate zu vermeiden, wurde das Knotenlayout der libgist dahingehend geändert, dass auch Verweise auf die Vorgängerknoten in den Knoten abgelegt
werden. Die Vorgänger können nach einem Löschvorgang gefunden, und die Prädikate
der Nachfolger aktualisiert werden. Dies geschieht in einer neuen Methode _shrink()
der Hauptklasse gist, bis sich im Laufe der Verarbeitung, von den Blättern Richtung
Wurzel fortschreitend, die Prädikate nicht mehr ändern.
Da leere Knoten in der original Implementierung der libgist im Baum verbleiben,
können Löcher“ im verwendeten Speicherbereich entstehen und unnötigen Speicherplatz
”
belegen. Um dies zu vermeiden, wird in _shrink() der Eintrag einer leeren Seite zunächst
aus dem Vorgänger entfernt. Anschließend wird die letzte Speicherseite des Baumes an die
nun freie Seitenposition bewegt und die Datei entsprechend gekürzt (oder die letzte“ Seite
”
in der Datenbank gelöscht). Mit den gespeicherten Verweisen auf den Vorgängerknoten
kann dieser gefunden, und der neue Speicherplatz der ehemals letzten Seite aktualisiert
werden. Dieser Vorgang kann sich, sollten dadurch wiederholt freie Knoten entstehen,
gegebenenfalls bis zur Wurzel fortsetzen.
Des weiteren wurden noch Anpassungen vorgenommen, um die libgist auf dem installierten Linux-System fehlerfrei kompilieren zu können. Gefundene Fehler in der ProgrammLogik wurden behoben. (Nähere Informationen finden sich in Kapitel 7 und Anhang A in
[Lö01].)
5.2.2
Struktur der Implementierung
Die OraGiST-Architektur besteht aus zwei Teilen: Der eine Teil ist unabhängig vom Datentyp und verwendeten Suchbaum. Die OraGiST-Library stellt damit das Bindeglied
zwischen der Schnittstelle der libgist und dem Extensible Indexing Interface dar. Dieser Teil implementiert die Methoden des Indexing Interfaces, in denen die generischen
Indexfunktionen der zugrunde liegenden libgist aufgerufen werden. Auch die Anpassung
der libgist an die Datenhaltung in Tabellen der Datenbank ist der Library-Komponente
zuzurechnen.
Die OraGiST-Toolbox ist dagegen vom Typ der zu indexierenden Objekte abhängig.
Dazu gehört insbesondere auch eine Anpassung der verwendeten Suchbäume an die darin
zu speichernden Schlüssel. Bei der Erstellung einer neuen OraGiST-Spezialisierung wird
der Benutzer durch ein Tool oragist unterstützt. Es handelt sich dabei um ein ShellSkript zur Automatisierung benötigter Vorarbeiten.
KAPITEL 5. GIST INDEXE FÜR ORACLE
52
Abbildung 5.3 gibt einen Überblick über die Struktur der OraGiST und des Zusammenspiels der verwendeten Schnittstellen.
Oracle Datenbank
Methoden−Aufrufe
GiST
Oracle Indexing Interface
1
1
1
GiST−Index−Datei
GiST−Erweiterung
1
gespeichert in
DB−Tabelle
n
1
OraGiST−Bibliothek
OraGiST
Benutzerdef. Indexstruktur
1
1
OraGiST−Erweiterung
+getExtension()
+getQuery()
+needFilter()
+approximate()
n
OraGiST−Toolbox
libgist−library
n
Benutzerdef. Datenbankobjekt
GiST−Eintrag
Abbildung 5.3: Architektur der OraGiST
5.2.3
Die Klasse OraGiST
Die abstrakte Klasse OraGiST stellt die Basisklasse aller mit dieser Bibliothek zu erstellenden Suchbäume dar. OraGiST ist selber eine Unterklasse der gist-Klasse der libgist, stellt
aber zusätzliche Funktionalitäten bereit. Diese bieten eine Schnittstelle zum Extensible
Indexing Interface und dienen dem Aufbau eines Suchbaumes zu einer Datenbankrelation. Dabei benötigte Funktionen, etwa das Auslesen der Tupel einer nichtleeren Tabelle
bei der Indexerstellung, werden bereits teilweise bereitgestellt.
Die wichtigsten implementationsunabhängigen öffentlichen Methoden sind:
ogcreate(): Für den Indexnamen wird ein Tabellenname oder Dateiname von der Spezialisierung erfragt, der bei der Anlage des Suchbaumes mit create() verwendet
wird. Das zu indexierenden Attribute und die zugehörigen RowIDs werden (im Fall
einer nichtleeren Tabelle) ausgelesen, die Schlüssel komprimiert, und die jeweiligen
Paare im Suchbaum abgelegt.
ogdrop(): Löschen des Suchbaumes und der zugehörigen Speicherstruktur.
oginsert(): Speicherung und Komprimierung des Attributes mit der zugehörigen RowID.
KAPITEL 5. GIST INDEXE FÜR ORACLE
53
ogdelete(): Eine Anfrage, die mindestens das zu löschende Element liefert, wird erstellt
und remove() mit der RowID des Elementes übergeben.
ogfetch init(): Initialisierung einer Suche mit einem Aufruf von fetch_init().
ogfetch(): Bis zu der von der Datenbank geforderten Höchstzahl an Antwortsätzen werden Einträge aus dem Suchbaum gelesen (fetch()). Die dabei erhaltenen RowIDs
werden in einer Collection gesammelt. Diese wird dem DB-System zur weiteren
Verarbeitung zurückgegeben.
ogclose(): Der Suchbaum wird über die gist-Methode close() geschlossen.
Zur Verwendung eines GiST-Indexes zur Indexierung eines Datentyps der OracleDatenbank ist eine Unterklasse von OraGiST zu erstellen, die die abstrakten Methoden
dieser Klasse implementiert. Die Unterklasse hängt dabei wesentlich von den folgenden
drei Parametern ab:
1. Von der zu verwendenden Suchbaum-Spezialisierung (R*-Baum, B-Baum,. . .)
2. Vom Datentyp, der dem Suchbaum als Schlüssel dient (Intervalle, Rechtecke, Quader, Kreise,. . .)
3. Vom Datentyp der Datenbank, der durch den Schlüssel dargestellt oder angenähert
werden soll (Polygon, Kreis, Intervall,. . .)
Um auf die Datentypen einzugehen, müssen folglich die datentypabhängigen abstrakten Methoden in OraGiST spezifiziert werden. Dies geschieht durch Definition der folgenden öffentlichen, indextypspezifischen Methoden in einer Unterklasse von OraGiST:
getExt(): Diese Methode soll eine Referenz auf die zu erstellende Suchbaum-Spezialisierung zurückliefern. Dies kann durch Erzeugung eines Objektes des Erweiterungstyps
gist_ext_t oder durch einen Zeiger auf einen entsprechenden Suchbaum geschehen.
(Zur Auswahl eines R-Baumes für Rechtecke beispielsweise: rt_rect_ext)
getMaxKeySize(): Die maximale Größe eines Schlüssels im Suchbaum wird benötigt,
um für einen Aufruf von fetch() ausreichenden Speicherplatz bereitzuhalten. Die
von fetch() ebenfalls benötigte Größe eines Datenelementes ist durch die feste
Länge der RowIDs vorgegeben.
getIdxFileName(): Zum angegebenen Indexnamen soll eine Zeichenkette für einen gültigen Dateinamen erzeugt werden. Üblicherweise gibt man die Konkatenation eines
Verzeichnisses im Dateisystem mit dem Indexnamen zurück.
approximate(): Diese Methode dient der Komprimierung der abzuspeichernden Schlüssel und wird in den Methoden ogcreate() und oginsert() verwendet. Dazu erhält
approximate() das Objekt und eine Datenbankverbindung, die zum vollständigen Abruf komplexer Objekte genutzt werden kann. Zu dem Objekt soll damit ein
für den Suchbaum geeigneter Schlüssel erstellt werden. Im Falle eines GeometrieObjektes kann dies beispielsweise die bounding box sein.
KAPITEL 5. GIST INDEXE FÜR ORACLE
54
getQuery(): Die Suche im Baum muss entsprechend dem verwendeten Operator bei der
Datenbank-Anfrage spezifiziert werden. Dazu ist für die mit dem Index verbundenen
Operatoren eine entsprechende Anfrage an den Suchbaum zu liefern. getQuery()
erhält die anfragespezifischen Parameter, also Suchoperator, Vergleichobjekt und
Ergebnisbereich, und soll dazu ein Objekt vom Typ gist_query_t zurückliefern.
Für R-Bäume kann dies beispielsweise rt_query_t::rt_equal, rt_query_t::rt_
overlap oder rt_query_t::rt_nearest sein.
needVerify(): Wenn die in dem Suchbaum gespeicherten Schlüssel nur Annäherungen
der indexierten Objekte sind (also beispielsweise ein MBR in R-Bäumen), der Baumzugriff daher nur mögliche Kandidaten einer Operation liefert, so soll hier true
zurückgeliefert werden. In einer privaten Methode verify() der Klasse OraGiST
wird dann eine Abfrage an die Datenbank gestellt, die mittels der funktionalen
Implementierung des Operators das Ergebnis für das tatsächliche Objekt in der
Datenbank überprüft. Nur falls verify() ebenfalls true, also Übereinstimmung
bezüglich der Operation liefert, wird die RowID der Ergebnismenge zugefügt.
Ist die Operation allein durch die Anfrage an den Suchbaum auswertbar, so kann
needVerify() false zurückliefern. Die Ergebnisse der Suchbaum-Anfrage finden
sich dann alle in der Ergebnis-Menge wieder.
5.3
Oracle - Pipelined Table Functions
In Oracle 9i ist es nicht möglich, einen Verbund-Index als benutzerdefinierte Indexstruktur
bereitzustellen. Dehalb kann das Oracle Extensible Indexing Interface nicht verwendet
werden, um einen Verbund zu berechnen, der Indexstrukturen für beide Verbundpartner
verwendet. Es wäre zwar möglich, einen zweiten Index bei der Berechnung heranzuziehen,
aber das Indexing Interface erlaubt es nur, eine RowID zurückzugeben.
Es ist also notwendig, eine Methode zu verwenden, die es erlaubt, die RowIDs der Ergebnismenge paarweise zurückzuliefern. Eine offensichtliche Möglichkeit ist die Erstellung
einer Funktion, die die Ergebnistupel in einer zweistelligen Relation ablegt. Dies erfordert
jedoch einen relativ hohen Verwaltungsaufwand, da die RowID-Paare einzeln mit einem
Callback in die Ergebnisrelation der Datenbank eingefügt werden müssen.
Aus Effizienzgründen wurde daher in dieser Arbeit eine andere Vorgehensweise gewählt: Die Verwendung einer Funktion, die eine Kollektion zurückliefert, auf die wie
auf eine Tabelle zugegriffen werden kann. Dies ist durch die Definition einer sogenannten
pipelined table function möglich. (Beschrieben in Kapitel 12 in [O9i-DCDG]). Diese Funktionen stehen im from-Teil einer select-Anweisung. Die zurückgelieferten Zeilen können
wie die Zeilen einer Tabelle weiterverarbeitet werden, so dass weitere Verknüpfungen
damit möglich sind.
Eine Funktion, die eine Kollektion als Rückgabewert besitzt (also ein varray oder
eine nested table) kann mit dem Schlüsselwort PIPELINED deklariert werden. Damit wird
angezeigt, dass die Funktion ihr Funktionsergebnis iterativ zurückliefern kann, statt alle
Zeilen nach der kompletten Berechnung zu übergeben.
KAPITEL 5. GIST INDEXE FÜR ORACLE
55
Pipeline-Funktionen können selber auch Kollektionen als Parameter übergeben werden. Dies geschieht entweder über einen Parameter eines Kollektions-Typs oder mittels
eines REF CURSORs. Dadurch ist es möglich, mehrere Pipeline-Funtionen hintereinanderzuschalten:
select * from TABLE(f(CURSOR(SELECT * FROM TABLE(g()))));
Die Ausgabe der Funktion g() wird dabei nicht erst komplett in einer temporären Tabelle oder einem Cache zwischengespeichert, sondern kann direkt von f() weiterverarbeitet werden. Im Rahmen dieser Arbeit ist dies allerdings nicht vorgesehen. Die PipelineFunktion dient nur als Datenquelle, sie liefert die Row-IDs der Verbundpartner. Zudem
ist die Übergabe eines Cursors an eine extern implementierte Funktion in der aktuellen
Version 9i noch nicht möglich.
Die Implementierung einer Pipeline-Funktion kann auf zwei Arten geschehen: Entweder als native PL/SQL-Funktion, wo das Laufzeitsystem die einzelnen Rückgabewerte
(evtl. zu Blöcken zusammengefasst) sofort nach ihrer Fertigstellung (mit PIPE ROW(Rückgabewert)) weiterleitet, oder man verwendet die Interface-Vorgehensweise, die bei einer Implementierung in einer externen Programmiersprache (C, Java) verwendet werden
muss. Da nur die Interface-Vorgehensweise für diese Arbeit interessant ist, wird nur diese
weiter betrachtet.
Um eine Pipeline-Funktion in der Interface-Variante zu erstellen, muss zuerst ein
Implementierungs-Typ erstellt werden. Dieser objektwertige Typ stellt Speicherplatz zur
Ablage eines Scan-Kontextes zur Verfügung und beinhaltet die Methoden des ODCITableInterfaces:
ODCITableStart(): Diese Methode initialisiert den Scan-Kontext und führt Aktionen
zur Vorbereitung der anschließenden Operationen aus, wie beispielsweise das Parsen
von Parametern oder Öffnen von Dateien.
ODCITableFetch(): Hier werden evtl. eingehende Datensätze verarbeitet und die Ausgaben zu Teil-Kollektionen zusammengestellt. Um nicht zu viele Kontextwechsel zu
haben, wird nicht jede einzelne Ausgabezeile zurückgeliefert, sondern mehrere Zeilen ergeben eine Teilmenge der Ausgabe-Kollektion. Deren maximale Mächtigkeit
wird der Methode zusätzlich zum Scan-Kontext als Parameter übergeben.
Der Scan-Kontext wird benötigt, um die in ODCITableStart angelegten Datenstrukturen verwenden zu können. Im Falle externer Prozeduren ist es damit möglich, den Speicher, der vom extproc-Prozess angefordert wurde oder wird, auch in
nachfolgenden Aufrufen dieser Methode wiederzuverwenden, ohne dass darin gespeicherte Daten zwischen den Kontextwechseln verloren gehen.
ODCITableClose(): Wurden von der Fetch-Methode keine weiteren Datensätze, sondern ein NULL-Wert zurückgeliefert, so wird diese Methode gestartet, um die verwendeten Ressourcen wieder freizugeben.
KAPITEL 5. GIST INDEXE FÜR ORACLE
56
Werden Pipeline-Funktionen im from-Teil einer Selektion angegeben, so wird für jede
dieser Funktionen ein Objekt des zugehörigen Implementierungs-Typs erstellt. In den jeweiligen Objekten können die zu den einzelnen Funktionen gehörenden Scan-Kontexte
abgelegt werden (also beispielsweise Strukturen mit von einem Erweiterungsprozess angeforderten Speicherbereichen), worauf die Methoden des ODCITable-Interfaces dann
zugreifen können. Ist der Programmfluß nach ODCITableClose wieder zur Datenbank
zurückgekehrt, so ist die Pipeline-Funktion beendet, und das zugehörige Objekt kann
zerstört werden.
bis alle Einträge geliefert
ODCITableStart
ODCITableFetch
ODCITableClose
Abbildung 5.4: Methoden des ODCITable-Interfaces
Wie man in Abbildung 5.4 sieht, ähnelt dieses Interface stark dem Methodenaufruf
des Extensible Indexing Interface bei der Abfrage eines Indexes. Doch im Gegensatz zum
Indexing Interface werden hier nicht einzelne RowIDs sondern Kollektionstypen zurückgeliefert, also beispielsweise Mengen von Objekten.
Der Rückgabewert einer Pipeline-Funktion muss dabei aber nicht zur Compile-Zeit
feststehen, sondern kann bei Funktionen, die verschiedene Datentypen zurückliefern sollen, auch erst zur Laufzeit bestimmt werden. Zur Compile-Zeit einer SQL-Anfrage wird
dann die Methode ODCITableDescribe() mit den Parametern der Funktion aufgerufen. Diese sollte dann den Rückgabe-Typ spezifizieren (oder einen undurchsichtigen Typ
(opaque type) liefern, der der Datenbank nicht bekannt ist, aber in anderen Methoden,
die typischerweise auch in externen Sprachen formuliert sind, weiterverarbeitet werden
kann).
In der Regel werden die Rückgabewerte einer Funktionen aber einen vorgegebenen
Typ besitzen, der dann vor der Deklaration einer Pipeline-Funktion definiert wird. Standardtypen und benutzerdefinierte Typen können dann von zugehörigen Operatoren bei
Vergleichen mit den Rückgabewerten herangezogen werden.
Kapitel 6
Entwurf des Verbund-Algorithmus
6.1
Vorüberlegungen
In Kapitel 3 wurden mehrere Algorithmen zur Erstellung eines räumlichen Verbundes
vorgestellt. Die zugrunde liegenden Indexstrukturen und Techniken kann man dabei unterscheiden in die Algorithmen, die den Raum in sich nicht überschneidende Bereiche
teilen (Z-Codes, teilweise Spatial Hash-Join) und eine Zuordnung zwischen den Objekten
und diesen Bereichen herstellen, und in Algorithmen, die die räumliche Lage der Objekte
in einer Baumstruktur speichern (R-Baum, Seeded-Tree), wobei die Koordinaten von Bereichen des Raumes (die sich auch überlappen dürfen) in den Baumknoten als Wegweiser
zu den Knoten mit den Objekten verwendet werden.
Da im Verlauf dieser Diplomarbeit keine komplett neue Indexstruktur zur Indexierung
von benutzerdefinierten Objekten in Oracle erstellt werden sollte sondern eine Erweiterung der bestehenden Index-Implementierung um räumliche Verbunde, wird ein Verbundalgorithmus benötigt, der auf OraGiST aufgesetzt werden kann.
Z-Codes lassen sich zwar in B*-Bäumen abspeichern, die auch von verallgemeinerten
Suchbäumen unterstützt werden, aber sie erlauben kaum Anpassungen. Insbesondere ist
eine Verallgemeinerung auf mehr als zwei Dimensionen schwierig, denn die den Raum
aufteilenden Z-Elemente würden zunehmend ihre Eigenschaft verlieren, einen ähnlichen
Z-Code bei räumlicher Nähe zu haben. Hinzu kommt, dass die Bit-Strings, die die Objekte
beschreiben, sehr viel länger und zahlreicher werden würden. Dies wirkt sich negativ auf
den dazugehörigen Verbundalgorithmus aus, so dass dieser nicht weiter in Frage kommt.
Der Spatial Hash-Join wird besonders für Verbundstrukturen empfohlen, denen kein
Index zugrunde liegt. Doch gerade das Erstellen der Schlüssel (MBRs) zu den Objekten
ist einer der Punkte, der mit am zeitaufwendigsten ist, da bisher keine geeignete Schnittstelle zur Datenbank bereit steht, um große Objektmengen an externe Programme (oder
Bibliotheken) zu liefern. Auch in der OraGiST-Bibliothek müssen die Objekte bei der
Indexerstellung einzeln aus der Datenbank abgerufen werden, was den wesentlichen Zeitfaktor bei der Indexerstellung ausmacht.
Die Verwendung eines vorhandenen Indexes zur Erfassung der Objektbeschreibungen (MBRs) ist eine Möglichkeit, diesen Kostenfaktor zu begrenzen. Doch muss weiter-
KAPITEL 6. ENTWURF DES VERBUND-ALGORITHMUS
58
hin für beide Indexe anschließend eine ähnliche Baumstruktur aufgebaut werden, deren
Objekte dann miteinander verglichen werden. Insbesondere dieses aufwendige Einrichten nur temporär genutzter Speicherstrukturen erscheint im Vergleich zum potentiellen
Performance-Gewinn des Algorithmus zu aufwendig. Für den Fall eines konkurrierenden
Zugriffes (zwei gleichzeitig ablaufende Verbundberechnungen) sollten diese Speicherstrukturen der Verwaltung der Datenbank unterliegen. Doch gerade das Ablegen und Anfordern von Datenseiten in der Datenbank ist ein entscheidender Flaschenhals bezüglich der
Zugriffsgeschwindigkeit.
Seeded Trees scheinen aufgrund ihrer Baumstruktur bestens zur Speicherung in einem
verallgemeinerten Suchbaum geeignet zu sein. Tatsächlich wäre es auch ohne weiteres
möglich, die oberen Ebenen eines Baumes zu kopieren. In die erste Ebene der grown level
könnte man leere Blätter in diesen kopierten Baum einfügen.
Wird jetzt der Baum gefüllt, so würden zuerst diese Blätter entsprechend der Schlüsselinformation der einzufügenden Elemente gesucht und gefüllt. Doch sollte es notwendig
sein, dass ein Knoten geteilt wird, so würde in einem verallgemeinerten Suchbaum das
normale Teilen stattfinden, so dass nicht die für einen Seeded Tree typischen, unterschiedlich hohe R-Teilbäume an die slot-pointer angefügt werden. Statt dessen würde der Baum
normal wachsen, d.h. dass er zur Wurzel hin“ wächst: Knoten werden geteilt, was sich
”
bis zur Wurzel fortsetzen kann und nötigenfalls zu einer neuen Wurzel des Baumes führt.
Dies zerstört aber gerade die gewünschte Eigenschaft, nämlich identische oder zumindest
ähnliche obere Ebenen.
Möchte man trotzdem ein Verhalten wie bei den Seeded Trees erhalten, z.B. kleine GiSTs an die slot-pointer anhängen, so wären sehr starke Änderungen an der doch
recht komplexen GiST-Bibliothek nötig. Insbesondere erwartet bzw. erstellt diese einen
ausgeglichenen Baum, denn die Art des Knotens (interner Knoten mit Prädikaten und
Verweisen auf weitere Seiten oder Blatt mit Prädikaten und Datenelementen) wird anhand der Höhe (level ) im Baum entschieden.
Das parallele Durchsuchen zweier R-Bäume ist dagegen ein relativ einfacher Algorithmus, den man leicht auf andere Bäume übertragen kann.
6.2
Verbundalgorithmen für GiST
Aufgrund der Baumstruktur eines verallgemeinerten Suchbaumes liegt es nahe, für einen
Verbund einen auf Bäumen arbeitenden Algorithmus an die Besonderheiten eines GiST
anzupassen. Am aussichtsreichsten erscheint dabei die für R-Bäume verwendete Methode,
zwei Baumstrukturen parallel zu durchsuchen.
Hierbei wird bei der Auswahl der zu besuchenden Teilbäume beim R-Baum-Verbund
eine nichtleere Schnittmenge der MBRs verwendet. Da in einem GiST auch andere Datentypen indexiert sein können, es sich bei den Schlüsseln damit nicht zwangsläufig um
Rechtecke handeln muss, ist eine andere geeignete Operation bereitzustellen, um herauszufinden, ob sich ein Verbundpaar in den zugehörigen Teilbäumen befinden könnte.
KAPITEL 6. ENTWURF DES VERBUND-ALGORITHMUS
6.2.1
59
Einfacher GiST-Verbund
Eine einfache Variante eines Verbundes zweier GiST-Strukturen leistet folgender Algorithmus:
gist-join1 (A: GiST1-Node, B: GiST2-Node);
FOR (EA ∈ A) DO
FOR (EB ∈ B) DO
IF meta_intersect(EB .pred, EA .pred) THEN
IF ((A is a leaf page) OR (B is a leaf page)) THEN
output(EA , EB )
ELSE
gist-join1(EA .ptr, EB .ptr)
END
END
END
END
END
Wie beim Verbund zweier R-Bäume bewegt sich der Algorithmus parallel durch zwei
Datenstrukturen. Dazu werden alle Einträge EA des einen Baumes A mit den Elementen EB des zweiten Baumes B verglichen. Statt auf eine nichtleere Schnittmenge zweier
Bounding-Boxen zu prüfen, wird hier eine vom Benutzer anzugebende boolsche Funktion meta intersect aufgerufen, die an Hand der Prädikate der Einträge prüft, ob ein
Verbundpaar gefunden werden kann. Ist dies möglich, so werden die möglichen Paare in
output ausgegeben, falls es sich bei einem der Knoten bereits um ein Blatt handelt. Dabei muss ein Teilbaum möglicherweise bis zu den Blättern weiterverfolgt werden. Handelt
es sich nicht um Blätter, so wird der Algorithmus mit den beiden Teilbäumen rekursiv
aufgerufen, die durch die zu den Einträgen gehörenden ptr referenziert werden.
meta intersect kann so gestaltet werden, dass nicht nur Prädikate gleichartiger GiST-Spezialisierungen überprüft werden können, sondern auch verschiedene Datentypen erlaubt sind. Als Beispiel könnte man sich eine Anwendung vorstellen, die zu Fahrzeugen
in regelmäßigen Zeitabständen eine Position abspeichert. Wird diese Kombination aus
Zeitpunkt und Position in einem gemeinsamen Datentyp in der Datenbank gespeichert
und mit einem verallgemeinerten Suchbaum indexiert, so möchte man vielleicht einen
Verbund mit einer ebenfalls gespeichert Landkarte erzeugen. Eine Anfrage wäre dann
beispielsweise: Welche Fahrzeuge sind (auch) auf Autobahnen unterwegs“. Die Schlüs”
sel der Karte bestehen aber vermutlich nur aus Koordinaten, während die Prädikate der
Fahrzeuginformationen Kombinationen aus Koordinate und Zeitkomponente sein könnten. meta intersect braucht aber nur überprüfen, ob sich die Koordinaten-Komponente
eines Fahrzeuges mit denen einer Autobahn schneidet, die Zeit-Information braucht hier
nicht berücksichtigt werden.
KAPITEL 6. ENTWURF DES VERBUND-ALGORITHMUS
6.2.2
60
Zusätzliche Selektionen
Die Beispiel-Anfrage wirft aber gleich eine weitere Frage auf: Wie erhält man die Autobahnen? Angenommen in der Straßenkarte sind alle Straßen und zusätzlich deren Typ
gespeichert. Diese Information gehöre zum Datentyp der Karte und ist auch als Information im GiST der Landkarte gespeichert. Dann wäre es sinnvoll, statt aller Elemente der
Knoten nur diejenigen zu überprüfen, die einer vorherigen Selektion entsprechen. Eine
etwas erweiterte Version eines GiST-Verbundes stellt daher folgender Algorithmus dar:
gist-join2 (A: GiST1-Node, α:Predicate1, B: GiST2-Node, β: Predicate2);
FOR (EA ∈ A) DO
IF consistent1(EA , α) THEN
FOR (EB ∈ B) DO
IF consistent2(EB , β) THEN
IF meta_intersect(EB .pred, EA .pred) THEN
IF ((A is a leaf page) OR (B is a leaf page)) THEN
output(EA , EB )
ELSE
gist-join2(EA .ptr, α, EB .ptr, β)
END
END
···
END
Mit den jeweils zu den GiST-Spezialisierungen gehörenden consistent-Methoden
können dabei Elemente vorselektiert werden, die für den Verbund nicht in Frage kommen.
Wie bei der Suche im GiST besagt ein positiver consistent-Aufruf für ein Element aber
noch nicht zwangsläufig, dass wirklich ein der Anfrage entsprechender Eintrag gefunden
werden muss. Dies ist in output zu berücksichtigen, wo die entsprechende consistentMethode weiter verwendet werden muss, falls man einen Teilbaum weiterverfolgen muss,
und nicht bereits zwei Blätter gefunden wurden.
Während meta intersect für die Beispielanfrage weiterhin nur die Koordinaten-Komponente zu vergleichen braucht, sollte es bei einer entsprechenden Anfrage damit möglich
sein, nur bestimmte Elemente der Bäume zu selektieren. Für die Karte etwa nur bestimmte Straßen, oder bei den Fahrzeug-Informationen nur bestimmte Zeitintervalle.
6.2.3
Verbundalgorithmus für die libgist
Die beiden vorigen Abschnitte zeigen, wie ein Verbund für eine GiST-Struktur rekursiv berechnet werden kann. Im Zusammenhang dieser Arbeit ist es allerdings notwendig,
dass die dabei gefundenen Verbundpaare nicht als Ganzes am Ende des Algorithmus zur
Verfügung stehen, sondern nach und nach abgerufen werden können. Denn nur so kann
das ODCITable Interface ausgenutzt werden, dass bereits Daten an die Datenbank zurückliefert, auch wenn der Verbund noch nicht komplett berechnet ist. Es macht daher
KAPITEL 6. ENTWURF DES VERBUND-ALGORITHMUS
61
Sinn, statt rekursiver Aufrufe und damit der impliziten Verwendung eines Stapelspeichers
(der Rücksprungadressen und Daten der verschachtelten Prozeduraufrufe), eigene Stacks
einzusetzen, auf denen der aktuelle Fortschritt der Suche festgehalten wird. Mittels eigener Stacks ist es auch möglich, die Verbundberechnung zu unterbrechen und später
wieder fortzusetzen, ohne komplizierte Aktionen auszuführen, um eine tiefe Rekursion zu
unterbrechen und dann wieder herzustellen.
Gesucht ist also ein Algorithmus, der einzelne Verbundpaare zurückliefert. Dazu ist
der bisherige Algorithmus aufzuteilen in eine Initialisierung des Baumdurchlaufes und
eine anschließend (gegebenenfalls mehrfach) auszuführenden Prozedur, die einzelne Verbundpaare berechnet und zurückliefert.
join-init (A: GiST1, α:Predicate1, B: GiST2, β: Predicate2);
queryA =construct a query for α and A
queryB =construct a query for beta and B
initialize stackA with A.root()
initialize stackB with B.root()
END
Während der Initialisierungsphase werden für jeden Baum Anfragen erstellt, die später
zum Auffinden der zu verbindenden Elemente des Baumes verwendet werden. Desweiteren
wird jeweils ein Stapelspeicher erstellt und mit dem Wurzelknoten der jeweiligen Bäume
gefüllt. Die Anfragen und Stapelspeicher, sowie Zugriffsstrukturen auf die GiST-Objekte
sollten dabei geeignet gespeichert werden (beispielsweise in einem Objekt, welches diese Prozedur als Methode besitzt), damit sie anschließenden Aufrufen von join-fetch
(ebenfalls als Methode dieses Objektes) zur Verfügung stehen.
join-fetch();
WHILE (NOT (stackA .empty() OR stackB .empty())) DO
IF (stackA .top().isItem() AND stackB .top().isItem()) THEN
RETURN (stackA .pop(), stackB .pop())
ELSE
A=stackA .pop()
B=stackB .pop()
FOR (EA ∈ search(A, queryA )) DO
FOR (EB ∈ search(B, queryB )) DO
IF meta_intersect(EA .pred, EB .pred) THEN
stackA .push(EA )
stackB .push(EB )
END
END
END
END
{ELSE}
KAPITEL 6. ENTWURF DES VERBUND-ALGORITHMUS
62
END
{WHILE}
RETURN EOF
END
Zur Vereinfachung berücksichtigt obiger Algorithmus zunächst wieder nur Bäume mit
der gleichen Baumhöhe, auf unterschiedlich hohe Suchbäume wird am Ende der Beschreibung nochmal kurz gesondert ein.
Am Anfang des Verbundalgorithmus wurde auf beide Stapelspeicher die Wurzel der
jeweiligen Suchbäume geschoben. Zu beachten ist, dass sich auch im Verlauf der Verbundberechnung auf den beiden Stacks (am Ende eines Schleifendurchlauf) immer jeweils
gleich viele Elemente auf den Stapeln befinden, denn eine Pop- oder Push-Operation wird
immer sowohl auf dem einen, als auch auf dem anderen Stapel durchgeführt.
Dieser Teil des Algorithmus arbeitet nun folgendermaßen: join-fetch liefert entweder
ein Paar (EA , EB ) von Verbundelementen oder EOF zurück (4. oder vorletzte Zeile).
Dazu wird zunächst untersucht, ob die Stapel bereits leer sind (eigentlich reicht es hier,
zu überprüfen, ob einer der Stapel leer ist, denn wie bereits erwähnt befinden sich am
Anfang einer Schleife immer gleich viele Elemente auf den Stacks). Ist dies der Fall, so
können keine weiteren Elemente gefunden werden und es wird EOF zurückgegeben.
Ansonsten wird der Typ der obersten Elemente der Stapelspeicher überprüft. Handelt es sich dabei um ein item, also einen Datensatz, oder einen Knoten1 ? Im Falle von
Datensätzen werden die Elemente von den jeweiligen Stapeln entfernt und können zurückgegeben werden; die Prozedur wird damit vorerst beendet. Im anderen Fall werden
sie ebenfalls entfernt, um darauf die Suchbaummethode search(node, query) anzuwenden, die die Menge alle Einträge des Knotens node liefert, die dem Suchprädikat query
entsprechen.
Die Ergebnisse der search-Aufrufe müssen nun jeder-mit-jedem“ mittels meta_
”
intersect verglichen werden. Passen die Einträge zusammen, so werden sie auf die
zugehörigen Stacks geschoben. Innerhalb eines Knotenpaares werden also erst alle Verbundpaare der Nachfolger oder enthaltenen Datenelemente ermittelt und auf den Stacks
abgelegt, statt sofort nach dem Finden eines Eintrages eine Rekursionsstufe weiter zu
gehen. Gefundene Verbundpaare (zwei items) landen daher auch erst auf den Stapeln:
Sie werden erst beim nächsten Durchlauf der WHILE-Schleife ausgegeben.
Handelt es sich um verschieden hohe Bäume, so ist die Suche im Baum nur bei dem
höheren Baum weiterzuführen. Die Einträge müssen aber dennoch mit den items des Baumes mit weniger Ebenen verglichen werden, bis auch beim höheren Baum die Blatt-Ebene
erreicht ist und ebenfalls items bei der Suche gefunden werden. Dazu ist es notwendig, die
item-Einträge bei jedem passenden Paar (wo meta_intersect also true liefert), wieder
auf den Stack zu pushen. Damit wird auch erreicht, dass die Anzahl der auf den beiden
Stapeln gespeicherten Einträge am Ende einer Schleife wieder gleich ist.
1
bei gleich hohen Bäumen ist der Typ des obersten Elementes jeweils gleich
Kapitel 7
JoinGiST
Ziel dieser Arbeit ist es, dem Benutzer eine Klassenstrukur bereitzustellen, die er erweitern
und verwenden kann, um einen Verbund zwischen zwei GiST-Strukturen zu berechnen, die
mit der libgist, bzw. speziell OraGiST erstellt wurden. Dazu muss er in einer Unterklasse
der Erweiterungsklasse einige Methoden implementieren, um auf die eingesetzten GiSTSpezialisierungen einzugehen.
7.1
Struktur
Der Zugriff auf die internen Knoten eines libgist-Suchbaumes kann nicht über die öffentlichen Methoden der Bibliothek geschehen, denn dies ist damit nicht vorgesehen. Um
dennoch auch auf private oder geschützte Methoden und Datenstrukturen zugreifen zu
können, gibt es zwei Alternativen bei der Implementierung des Verbundalgorithmus: Die
Klasse gist wird verändert, oder man erlaubt einer anderen Klasse den Zugriff auf die
Interna der Klasse gist.
Bei der ersten Variante wären umfangreiche Änderungen oder Ergänzungen der gistMethoden nötig. Um auf zwei Bäumen arbeiten zu können, wäre es zudem notwendig, zahlreiche Datenstrukturen für dieses doppelte Verwendung hinzuzufügen. Zusätzlich
würde eine starke Verwebung der Komponenten ein späteres Aktualisieren auf neue Versionen der libgist erschweren.
Daher wurde im Rahmen dieser Diplomarbeit der zweite Weg eingeschlagen, nämlich
eine eigenständige Klasse erstellt, die auf internen Strukturen der gist-Objekte zugreifen
darf. C++ ermöglicht dies, indem zwei Klassen als befreundet deklariert werden. Dazu
sind nur zwei weitere Zeilen im Quellcode der GiST-Bibliothek notwendig: In der HeaderDatei gist.h macht class JoinGiST den Namen der anderen Klasse bekannt, friend
class JoinGiST im Rumpf der gist-Klassen-Deklaration erlaubt der Klasse JoinGiST
den Zugriff auf interne Komponenten dieser Klasse.
Da die Eigenschaft eine befreundete Klasse zu sein nicht vererbbar ist, ist es nicht
möglich, weitere Unterklassen der Klasse JoinGiST zu erstellen, ohne die FreundschaftsEigenschaft und damit den uneingeschränkten Zugriff auf gist-Komponenten zu verlieren. Die Anpassung an die zu verbindenden Suchbäume muss daher in einer Erweite-
KAPITEL 7. JOINGIST
64
rungsklasse JoinGiST_ext_t vorgenommen werden. Die virtuellen Methoden der Erweiterungsklasse können in Unterklassen spezifiziert werden. Ein Objekt dieser Unterklassen
ist dann dem JoinGiST-Objekt zu übergeben, so dass dieses dessen spezialisierte Methoden verwenden kann.
Das JoinGiST-Objekt benötigt weiterhin Informationen über die zu verwendenden Indexdatensätze, Tabellen und Attribute. Diese werden in der Klasse JoinGiST_parameters
gesammelt und teilweise verarbeitet.
1
JoinGiST
−joinext: JoinGiST_ext_t *
−parameters: JoinGiST_parameters *
−dbh: DBConnectionHandle
−gist1: gist *
−gist2: gist *
−query1: gist_query_t *
−query2: gist_query_t *
+join_start(): int
+join_fetch(rows:int): int
−double_fetch(...): rc_t
1
1
2
gist
#_ext: gist_ext_t *
#_isOpen: bool
#_file: gist_file
+create()
+open()
+close()
+flush()
+insert()
+remove()
+fetch_init()
+fetch()
+isEmpty()
+extension(): gist_extension_t *
JoinGiST_ext_t
+meta_intersect(key1,isLeaf1:bool,...): bool
+getLeftIdxFileName(idxname:char *): char *
+getRightIdxFileName(idxname:char *): char *
+getLeftQuery(selection:string): gist_query_t *
+getRightQuery(selection:string): gist_query_t *
1
JoinGiST_parameters
#op: string *
#tablename1: string *
#tablename2: string *
#selection1: string *
#selection2: string *
#indexname1: string *
#indexname2: string *
#dbh: DBConnectionHandle
+getSpecialization(out spec1:string *&,
out spec2:string *&): bool
+getOperator(): string *
+getLeftIndexname(): string *
+getRightIndexname(): string *
+getLeftSelection(): string *
+getRightSelection(): string *
+getLeftTablename(): string *
+getRightTablename(): string *
+getLeftColumnname(): string *
+getRightColumnname(): string *
Abbildung 7.1: Ausschnitt der JoinGiST-Klassenstruktur
Bei der verwendeten gist-Klasse handelt es sich um die im Rahmen von OraGiST
erweiterte Variante, die ihre Suchbäume in der Oracle-Datenbank abspeichern kann.
7.2
7.2.1
Methoden der JoinGiST-Klassen
JoinGiST parameters
Die Klasse JoinGiST_parameters speichert die Optionen, die der Start-Funktion des
ODCITable-Interface übergeben werden. Außerdem werden in dieser Klasse die in der
Datenbank gespeicherten Parameter zu den Indexdatensätzen abgefragt. Die öffentlichen
Methoden dieser Klasse sind:
JoinGiST parameters(): Der Konstruktor der Parameter-Klasse erhält erhält sämtliche Parameter, die der Benutzer beim Aufruf der Verbund-Funktion angibt. Dieses
werden der externen Prozedur als C-Zeichenketten (char *) übergeben. Im einzelnen handelt sich sich dabei um:
KAPITEL 7. JOINGIST
65
• operator: Die Verbundoperation, die vom Benutzer gewünscht wird. Diese wird
lediglich in einer C++ Zeichenkette (string *) abgelegt
• table1, table2: Die Tabellen- und Attributbezeichner werden zusammen in einer Zeichenkette übergeben. Die geschützte Prozedur set_SchemaTableColumn
wird verwendet, um diese in die einzelnen Bestandteile zu zerlegen und separat
abzuspeichern.
• selection1, selection2: Die Selektionen werden, falls sie angegeben sind, unverarbeitet in der Klasse abgelegt.
• additional: Dieser Parameter kann zusätzliche Informationen beinhalten und
wird ebenfalls unverarbeitet abgelegt (z.B. wäre die Angabe einer Log-Datei
denkbar).
Zusätlich wird noch ein Datenbankverbindung und jeweils ein Bezeichner für den
Besitzer der Indexdateien gespeichert.
getSpecialization(): Diese Methode prüft zuerst, ob die Parameter der im Konstruktor angegebenen Tabellen schon abgefragt wurden. Ist dies der Fall, so wurde
dabei auch die Bezeichnung der Indexspezialisierung abgerufen, andernfalls wird
check_dba_data() aufgerufen. Die Rückgabe dieser Bezeichner geschieht über zwei
String-Zeiger-Referenzen die als Parameter erwartet werden. Es wird keine Kopie
erstellt, der Speicherplatz der Strings darf daher nicht freigegeben werden (dies
geschieht im Destruktor).
getLeftIndexname(): Hier wird ebenfalls erst überprüft, ob die Datenbank schon abgefragt wurde (ansonsten wird dies getan). Der Indexname, der zur linken Tabelle
gehört, wird dann als C-Zeichenkette zurückgegeben.
getRightIndexname(): Macht dasselbe - für die rechte Tabelle.
isLeftOratab(), isRightOratab(): geben den boolschen Wert zurück, ob die entsprechende Indexdatei in der Datenbank gespeichert ist. Dies wird (wie in OraGiST)
erkannt, indem der PARAMETER-Eintrag des Indexes auf die Zeichenkette " loc=f"
überprüft wird.
getLeftSelection(), getRightSelection(), getLeftTablename(), getRightTable
name(), getLeftColumnname() und getRightColumnname() geben jeweils
Zeiger auf die zugehörigen Strings zurück.
˜JoinGiST parameters(): Der Destruktor gibt die verwendeten Speicherbereiche wieder frei. (Da dies die übliche Aufgabe eines Destruktors ist, wird dies bei den anderen
Komponenten der JoinGiST-Klassenstruktur nicht weiter beschrieben.)
Zusätzlich sind noch einige nichtöffentliche Methoden enthalten. Diese dienen der Abfrage der Tabellen ALL_INDEXES und ALL_IND_COLUMNS der Datenbank, wo insbesondere
KAPITEL 7. JOINGIST
66
die Spalte PARAMETERS interessiert. Denn darin stecken die Informationen, welche Spezialisierung der Index verwendet, und ob er in der Datenbank oder einer Datei gespeichert
ist.
check one dba data(): Die Methode ruft die Index-Daten zu einer der beiden Tabellen
ab, indem eine Anfrage an die Datenbank formuliert und ausgeführt wird. Die erhaltenen Werte werden im Objekt gespeichert, so dass sie nicht nochmals abgerufen
werden müssen.
check dba data(): Durch zweifachen Aufruf von check_one_dba_data werden die Daten zu beiden Indexen abgefragt.
set SchemaTableColumn(): Die table-Parameter enthalten sowohl Angaben zu den
Tabellen, als auch den Spalten, für die ein Index existieren soll. Diese Daten werden
hier getrennt, um sie anschließend in den entsprechenden Variablen abzulegen.
7.2.2
JoinGiST ext t
Die Erweiterungs-Klasse JoinGiST_ext_t dient als Basisklasse für Erweiterungsklassen,
die der Anpassung an die zu verbindenden Bäume dienen. Zu einer Kombination zweier
OraGiST-Suchbäume muss jeweils eine Erweiterungsklasse erstellt werden, um auf die
in den Bäumen gespeicherten Datenstrukturen (und die zugrunde liegenden Suchbäume) einzugehen. Sämtliche Methoden dieser Klasse sind lediglich abstrakt deklariert und
müssen noch in einer Unterklasse definiert werden. Die öffentlichen Methoden sind:
setOperator: Dieser boolschen Methode wird der Verbundoperator (als String) übergeben. Diese sollte hier nicht einfach abgelegt werden, sondern schon verarbeitet
und in einer internen Darstellung gespeichert werden. Bei erfolgreicher Verarbeitung wird true als Rückgabewert erwartet, wird keine Operator im String erkannt,
liefert man false, um einen Fehler zu signalisieren.
getLeftIdxFileName(): Zu dem Parameter idxname vom Typ char * wird ein gültiger
Name im Dateisystem generiert, unter dem der Index abgespeichert ist. Diese Funktion muss auch (ohne Left“) in den Unterklassen von OraGiST angegeben werden,
”
der Rumpf dieser Prozedur kann daher einfach von der zu unterstützenden Spezialisierung übernommen werden. Der Dateiname wird ebenfalls als C-Zeichenkette
(char *) zurückgegeben.
getRightIdxFileName(): Für einen anderen Suchbaum könnte der Pfad zu einer Indexdatei unterschiedlich sein, daher muss er ebenfalls abgefragt werden.
getLeftMaxKeySize(), getRightMaxKeySize(): Die maximale Schlüssellänge, wie
sie ebenfalls in den OraGiST-Spezialisierungen angegeben sind.
getLeftQuery(), getRightQuery(): Diesen Methoden wird jeweils ein Zeiger auf eine
C++ -Zeichenkette übergeben, die eine Selektion beschreibt. Dabei ist das Format,
KAPITEL 7. JOINGIST
67
dass zur Spezifizierung dieser Selektion verwendet wird, demjenigen überlassen, der
diese Methoden implementiert - es wird lediglich eine Vorgabe gemacht: Handelt es
sich um einen NULL-Zeiger, so soll eine Abfrage erstellt werden, die den ganzen Baum
findet. (Bei den R-Baum-Derivaten der libgist erreicht man dies, indem man als
Anfrage-Operator rt_nooper angibt.) Aufgabe ist es nun, den String zu verarbeiten
und eine Anfrage vom Typ gist_query_t * zurückzuliefern.
meta intersect(): Diese Methode implementiert den Vergleich der Schlüssel der beiden
Bäume. Dazu wird der boolschen Methode jeweils ein Zeiger auf einen Schlüssel
und jeweils die Information, ob es sich um einen Schlüssel eines Blattes handelt
übergeben, denn in einigen Bäumen unterscheiden sich die Schlüssel der internen
Knoten von denen in den Blättern. Anhand dieser Informationen und des im Objekt gespeicherten Verbundoperators wird ein Vergleich der Schlüssel vorgenommen.
Kommen die Datenelemente, die zu den Schlüsseln gehören, als Verbundpartner in
Frage, bzw. könnten sich unter zugehörigen Teilbäumen Verbundpartner finden, so
wird true zurückgegeben. Entsprechen zwei Datenelemente in Blätter nicht dem
Verbundoperator oder sind Verbundpartner in zwei Teilbäumen ausgeschlossen, so
soll die Methode false liefern.
Die Zeiger auf die Schlüssel sind typlos (bzw. vom Typ void *), der Benutzer sollte
anhand der verwendeten Spezialisierung wissen, wie die Schlüssel in den Knoten des
Suchbaumes abgelegt sind. Nach einem entsprechenden Cast (auf eine entsprechenden Speicherstrukturen) kann dann auf die Komponenten der Schlüssel zugegriffen
werden.
7.2.3
JoinGiST
Die Klasse JoinGiST ist die Hauptklasse der JoinGiST-Strukur. In ihr wird der eigentliche
Verbund der Indexdatensätze berechnet. Daher ist es auch notwendig, dort die erforderlichen Datenstrukturen zusammenzufassen. Diese Klasse ist in der Klasse gist als friend
eingetragen, damit auf deren internen Datenstrukturen zugegriffen werden kann. Da durch
diese Klasse für den Endanwender aber kein unkontrollierter Zugriff auf private Elemente
der Klasse gist möglich ist, sondern lediglich einige zusätzliche Methoden bereitgestellt
werden, ist JoinGiST als Erweiterung der Zugriffsmöglichkeiten auf GiST-Objekte anzusehen, nicht als Bruch in der Kapselung der Datenstrukturen.
JoinGiST(): Dem Konstruktor wird neben der Datenbankverbindung und dem Parameter-Objekt ein Objekt vom Typ JoinGiST_ext_t * übergeben und im Objekt
abgelegt.
join start(): Diese Methode sorgt für die Vorbereitung des Verbundes. Dazu sind die
Indexnamen und ihre Speicherorte abzufragen. Zwei neue gist-Objekte werden
erstellt und die Indexstrukturen geöffnet. Von der Erweiterungsklasse werden zwei
Abfragen (gist_query_t) an die Suchbäume angefordert, wobei evtl. angegebene
KAPITEL 7. JOINGIST
68
Selektions-Parameter berücksichtigt werden müssen. Rückgabewert ist ein FehlerCode, der angibt, ob alles bei den Vorbereitungen geklappt hat oder ein Fehler
vorliegt.
join fetch(): Diese Methode liefert eine Anzahl an Verbundpaaren in einem Kollektionstyp zurück. Dazu erhält sie neben dem Zeiger auf den anzulegenden Kollektionstyp (und einem Indikator, der angibt, ob es sich dabei um einen NULL-Wert handelt,
die Kollektion also keine Elemente enthält) einen Wert, der angibt, wie viele Paare
angefordert werden.
In dieser Methode wird die private Funktion double_fetch() verwendet, um die
beiden Indexstrukturen parallel zu durchsuchen. Die beiden RowIDs die dabei zurückgeliefert werden, werden mittels der Methode return_results() der Ausgabekollektion zugefügt. Diese geschieht mehrfach, bis die erforderliche Anzahl an
Verbundpaaren gefunden und zur Ausgabe vorbereitet wurde, oder bis keine weiteren Verbundpaare mehr geliefert werden, die Suche also beendet ist.
Neben einigen kleineren privaten Methoden, die jeweils Teilaufgaben übernehmen (z.B.
die Erstellung der gist-Objekte oder das Laden der Datensätze), sind noch die folgenden
privaten Methoden interessant:
double fetch() Diese Methode implementiert den eigentlichen Verbund-Algorithmus.
Dazu wurden die Methoden fetch (und _fetch) der Klasse gist so angepasst,
dass sie nicht nur auf einem gist-Objekt arbeiteten, sondern auf zweien. In dieser Methode werden daher die einzelnen Knoten der beiden Bäume besucht und
die Einträge bestimmt, die der Anfrage (gist_query_t) entsprechen. Anstatt die
qualifizierten Einträge der einzelnen Indexe einfach auf die zugehörigen Stapelspeicher zu verschieben, wird hier aber erst ein Vergleich der Schlüssel aller gefundenen
Einträge des einen Baumes mit den Schlüsseln der Einträgen des anderen Baumes
vorgenommen (mittels meta_intersect der Erweiterungs-Klasse). Die dabei gefundenen Paare werden dann parallel auf die Stapelspeichern geschrieben. Da die
Operationen auf den Stacks immer parallel stattfinden, werden sie dort auch wieder
zusammen abgerufen. Sollten sich im Laufe dieses Prozesses zwei Dateneinträge auf
den Stapeln befinden, so ist ein Paar gefunden. Die obersten Einträge werden daher
von den beiden Stacks entfernt und die zugehörigen Schlüssel und RowIDs werden
zurückgegeben. Der aktuelle Fortschritt des Baumdurchlaufes bleibt dabei erhalten,
da er sich aus den auf den Stacks gespeicherten Werten ergibt. Die Stapel gehören
zu den gist-Objekten und werden daher nicht gelöscht, sondern bleiben bis zum
nächsten Aufruf von double_fetch() unverändert.
Der Methode werden zahlreiche Parameter übergeben (jeweils für jeden Baum:
gist-Objekt, Cursor, Schlüssel-Zeiger, Schlüssel-Länge, Daten-Zeiger, Daten-Länge, eof-Indikator), die größtenteils der Ausgabe der gefundenen Datensätze dienen.
return results(): Dieser Methode werden zwei Zeiger auf die zurückzuliefernden RowIDs (die als char * gespeichert sind) übergeben. Diese werden in einen String-Typ
KAPITEL 7. JOINGIST
69
der Datenbank umgewandelt und dem jeweiligen Teilobjekt (join_result_type.
left oder join_result_type.right) der Ergebnis-Kollektion zugefügt.
7.2.4
odci2join - Schnittstelle zwischen JoinGiST und Oracle
JoinGiST ist als objektorientierte Bibliothek konzipiert und in C++ implementiert. Um
die bereitgestellte Funktionalität mit der Datenbank verwenden zu können, ist es notwendig eine shared library zu erstellen, deren vom extproc-Prozess aufzurufenden Prozeduren
als extern "C" deklariert sind. Diese Prozeduren werden vom Linker entsprechend den
C Funktionsaufruf-Konventionen in der Bibliothek erstellt.
Die C-Prozeduren und auch ein Teil der eigentlichen Programmlogik befinden sich
in der Datei odci2join.cc. Hier werden die externen Prozeduren definiert, die vom
ODCITable-Interface aufgerufen werden. Die C-Prozeduren ODCITableFetch und ODCI
TableClose werden in den gleichnamigen Methoden des Table-Interface aufgerufen.
Um eine unterschiedliche Anzahl an Parametern zu unterstützen, ist in der InterfaceImplementierung die Methode ODCITableStart mehrfach überladen. Es wurden drei
PL/SQL-Funktionen (join3, join5 und join6) erstellt, die diese Interface-Implementierung verwenden. Je nach Parameterzahl wird eine der ODCITableStart-Methoden
aufgerufen, die wiederum eine der als extern deklarierten Prozeduren ODCITableStart_3,
ODCITableStart_5 oder ODCITableStart_6 der JoinGiST-Bibliothek aufrufen (diese können nicht überladen werden, da dies nicht der C-Konvention eines Funktionsaufrufes entspricht). Die C-Funktionen rufen wiederum die C++ Prozedur ODCITableStartCPP auf.
Diese wird nur einmal benötigt, da sie als polymorphe Prozedur implementiert ist. (Einsetzen von NULL-Werten für nicht angegebene Parameter.)
Folgendes leisten die Funktionen in odci2join.cc
ODCITableStartCPP: Die Datenbankverbindung wird initialisiert und der ScanKontext eingerichtet. Anschließend wird ein neues Objekt der Klasse JoinGiST_
parameters erstellt und dessen Methode getSpecialization verwendet, um die
Indextypen der zu verbindenden Bäume festzustellen. Anhand dieser Informationen wird das zugehörige Objekt vom Typ JoinGiST_ext_t ausgewählt, erstellt
und dem Konstruktor des zu erstellenden JoinGiST-Objektes zusammen mit dem
Parameter-Objekt übergeben. Die Methode start() des JoinGiST-Objektes bereitet dieses schließlich auf den Verbund vor.
ODCITableFetch: Die Datenbankverbindung wird initialisiert und der Scan-Kontext
ausgelesen, um die damit gespeicherten Informationen wiederherzugestellen. Anschließend kann die angeforderte Anzahl an Datensätzen über die Methode join_
fetch des JoinGiST-Objektes angefordert werden.
ODCITableClose: Die Datenbankverbindung wird initialisiert und der Scan-Kontext
ausgelesen. Erst dann hat man Zugriff auf das JoinGiST-Objekt - um es zu zerstören
und damit die belegten Ressourcen wieder freizugeben.
KAPITEL 7. JOINGIST
7.3
Einschränkungen
7.3.1
Cursorerweiterung
70
Bezüglich der verwendbaren Suchbaumerweiterungen gibt es eine Einschränkung: Es werden nur Suchbaum-Erweiterungen (bzw. zugehörige Anfragen) unterstützt, die die lookup
stack -Cursorerweiterung verwenden. Die Verwendung der priority queue-Erweiterung ist
damit ausgeschlossen. Dies hat zur Folge, dass keine nearest neighbour -Verbünde möglich
sind.
Da die exakte Berechnung des nächsten Nachbarn eines Objektes aber mit Indexen, die
die MBR des Objektes als Schlüssel verwenden, nicht sicher möglich ist, sollte dies keine
wesentliche Einschränkung darstellen. Üblicherweise werden bei der indexunterstützten
nearest neighbour -Suche die besten n Objekte zurückgeliefert. Als Beispiel, wo dies nicht
B
C
A
D
F
E
Abbildung 7.2: Nearest neighbour Suche
zum gewünschten Erfolg führt, betrachte man Abbildung 7.2. Werden nur die 4 nächsten
Objekte zum Punkt A abgefragt, so liefert der Index lediglich die Objekte B, C, D und E,
denn die MBRs (rot) der Objekte, die dem Index als Schlüssel dienen, liegen am inneren
Kreis und sind A damit am nächsten. Die tatsächlichen Geometrien dieser Polygone sind
dem Index nicht bekannt. Daher weiß er auch nicht, dass das Rechteck F dem Punkt A
eigentlich am nächsten ist.
Eine exakte Berechnung der vier aussichtsreichsten Kandidaten wäre daher nur möglich, wenn die tatsächlichen Geometrien aller Objekte, die sich im äußeren Kreis (also
dem die vier Geometrien umschließenden Kreis) befinden, von der Datenbank abgefragt
und bei der nearest neighbour -Suche verwendet werden.
7.3.2
Compiler
Derzeit ist die libgist-Bibliothek noch nicht an den aktuellen GNU-C-Compiler angepasst.
Die Compiler der 3.x-Reihe verlangen eine Anpassung an die neuen C++-Header, sowie
eine damit einhergehende Nutzung des std::-Namespaces (oder der Angabe von using
namespace std;) in allen Programmteilen. Daher muss das Paket mit dem alten Compiler
der Version 2.95.3 kompiliert werden.
KAPITEL 7. JOINGIST
71
Der C++ -Compiler g++ dieser alten Version hat allerdings Probleme bei der Ausnahmebehandlung im Zusammenhang mit großen Projekten. Löst man in einem try-Block
eine Ausnahme aus (mit throw), so landet man nicht im zugehörigen catch-Block, sondern dass Programm springt an eine zufällige“ Stelle (und stürzt ab).
”
Daher sollte die ansonsten zu empfehlende Verwendung der C++ -Ausnahmebehandlung
mit dieser Compilerversion auch nicht in Unterklasser der JoinGiST-Erweiterungsklasse
verwendet werden.
7.3.3
Speicherort der Indexdaten
JoinGiST verwendet die Index-Daten von OraGiST und ist damit bei konkurrierendem
Zugriff auf diese Datensätzen mit den gleichen Problemen konfrontiert. Insbesondere bei
einer Ablage im Dateisystem besteht kein Schutz, um eine Änderung des Datenbestandes
durch einen anderen Prozess zu unterbinden.
Daher ist es dringend zu empfehlen, die Daten in einer Tabelle der Datenbank abzulegen, indem bei der Indexerstellung der Parameter "loc=db" angegeben wird. Dann
garantiert die Datenbank, dass sich die Sicht auf die Daten für die Dauer des Statements
nicht verändert, auch wenn andere Operationen schreibend auf die Datensätze zugreifen.
Sollte dies in gleichen Statement geschehen, so kann es aber zu ein
Kapitel 8
Anwendung und Performance
8.1
8.1.1
Anwendung
Formulierung von Anfragen
Um eine Verbundanfrage zu initiieren, wird die Verbundfunktion mit den dazu erforderlichen Parametern (die vom Typ varchar2 sind) aufgerufen. Möchte man keine Selektionen
verwenden, so verwendet man join3. Diese Funktion erwartet nur drei Parameter: Der
Verbundoperator und die beiden Tabelle (mit dem zugehörigen, im Index erfassten Attribut). Ein Aufruf geschieht dann nach folgendem Muster:
select * from TABLE(join3(’operator’,
’tabelle1(attribut1)’, ’tabelle2(attribut2)’));
Dabei dürfen zwischen den ’-Zeichen der Tabellenbezeichner keine unnötigen Leerzeichen
vorkommen. Optional kann vor dem Tabellennamen ein Schema angegeben werden, gefolgt von einem Punkt ".". Bis zur öffnenden Klammer kommt der Tabellenname und
zwischen den Klammern steht die bei der Indexerstellung angegebene Spalte. Der Operator kann eine beliebige Zeichenkette sein, die allerdings von der verwendeten Unterklasse
von JoinGiST_ext_t erkannt werden muss. Es steht also dem Benutzer bei der Implementierung seiner Erweiterung frei, welche Operationen unterstützt werden und wie diese
ausgewählt werden. JoinGiST selber verwendet den Operator nicht.
Soll beim Indexzugriff gleich eine Selektion berücksichtigt werden, so verwendet man
join5, mit fünf Parametern:
select * from TABLE(join5(’operator’,
’tabelle1(attribut1)’, ’tabelle2(attribut2)’,
’selection1’, ’selection2’));
Die selection“-Einträge müssen dabei ebenfalls für das Erweiterungs-Objekt Sinn erge”
ben, so dass daraus eine Suchbaum-Anfrage erstellt werden kann.
KAPITEL 8. ANWENDUNG UND PERFORMANCE
8.1.2
73
Weitere Verbunde
Die join-Funktionen liefern über das ODCITable-Interface jeweils Paare von RowIDs
zurück, die das Ergebnis der ersten Filterstufe repräsentieren. Die Rückgabe besteht aus
einem eigenständigen Typ JOIN_RESULT_TYPE mit den Komponenten LEFT und RIGHT,
vom Typ VARCHAR2, in denen die RowIDs als String stehen. Diese können wie ganz
normale Attribute einer Tabelle für weitere Verknüpfungen herangezogen werden.
select L.attribut3, R.attribut5 from tabelle1 L, tabelle2 R,
TABLE(join5(’operator’,
’tabelle1(attribut1), tabelle2(attribut2),
’selection1’ ,’selection2’)) J
where L.ROWID=J.LEFT and R.ROWID=J.RIGHT
and L.attribut3 > 100
and plsqlOperator(L.attribut1, R.attribut2) = ’TRUE’;
Dies macht für die zurückgegebenen RowIDs natürlich nur Sinn, wenn sie mit der
Pseudo-Spalte ROWID der zugehörigen Tabellen verglichen werden.
Im letzen Beispiel könnte plsqlOperator beispielsweise der Durchführung der zweiten Filterstufe eines Verbundes dienen. Sind nämlich noch weitere zusätzliche Selektionen
vorhanden (wie hier mit L.attribut3 > 100, so macht es keinen Sinn, die zweite Filterstufe schon in der JoinGiST-Bibliothek auszuführen. Denn der Index wird ja gerade
verwendet, um teure“ Operationen mit den tatsächlichen Objekten zu vermeiden. Daher
”
ist es sinnvoll, diese erst zu anzuwenden, wenn möglichst viele Objekte aussortiert wurden. Vorraussetzung ist dabei natürlich, dass der Optimierer der Datenbank die Kosten
eines Operatoraufrufes richtig einschätzt und den Verbund in der richtigen Reihenfolge
durchführt.
Zusätzlich gibt es noch eine Funktion join6, mit einem weiteren Parameter additional,
der derzeit nicht genutzt wird, aber beispielsweise zur Angabe zusätzlicher Optionen
verwendet werden könnte. Denkbar wäre beispielsweise die Angabe einer Log-Datei oder
weiterer Optionen an JoinGiST oder die Erweiterungsklasse.
8.2
Praktisches Beispiel mit Performance
Um das Abschneiden des Verbundalgorithmus zu testen wurden zunächst zwei Tabellen
erstellt, zu denen mit OraGiST jeweils eine Indexstruktur aufgebaut wurde.
Der eine Datensatz, NENNDORF enthält 2655 Objekte, zumeist Linienzüge oder Polygone, die einen Ausschnitt aus einer Karte repräsentieren. Der andere Datensatz RECHTECKE
besteht aus 25000 Rechtecken, die dieses Kartenmaterial teilweise überlagern. Die Rechtecke sind in regelmäßigen Abständen über dem gleichen Koordinaten-Raum angelegt,
wobei aber einige mehrfach vorhanden sind.
Die Indexdatensätze werden in der Datenbank abgespeichert, wie dies allgemein empfohlen wird.
KAPITEL 8. ANWENDUNG UND PERFORMANCE
OraGiST
Oracle Spatial
rstar rect ext
R-Baum
74
Anzahl gefundener
Überschneidungen
Anlegen des Indexes
zu NENNDORF
8.3s
2.2s
(Anzahl Elemente)
2655
Anlegen des Indexes
zu RECHTECKE
72.9s
11.0s
(Anzahl Elemente)
25000
1. spatial join
(index overlap)
18.6s
33.0s
-299s
72652
2. spatial join
(overlap)
352.2s
159.3s
-513.7s
38802
self-join
NENNDORF
13.2s
33.1s
48029
self-join
RECHTECKE
24.1
277s
121000
Die ersten beiden Zeilen geben die Zeiten wieder, die benötigt wurden, um die Indexstrukturen zu erstellen und (bei OraGiST) diese in die Datenbank zu verschieben.
Beim ersten spatial join wird eine Anfrage an den Index über folgende Selektionen
berechnet:
select count(*) from
TABLE(join3(’overlap’, ’RECHTECKE(GEOMETRY)’,’NENNDORF(GEOMETRY)’));
bzw. mit der Oracle Spatial -Funktion sdo_filter:
select count(*) from NENNDORF N, RECHTECKE R,
where sdo_filter(r.geometry, n.geometry, ’querytype=join’)=’TRUE’;
Dabei stellte sich heraus, dass sdo_filter auf ein Vertauschen der Reihenfolge der zu
verbindenden Tabellen mit starken Performance-Verlusten reagiert. Die Verbundberechnung dauert bei einer Anfrage mit sdo_filter(n.geometry, r.geometry,. . . ) nicht
mehr etwa 33 Sekunden, sondern 299 Sekunden. Der symmetrisch arbeitende Verbundalgorithmus, der in JoinGiST verwendet wird, zeigt diese Anfälligkeit nicht.
Beim zweiten spatial join soll zusätzlich die tatsächliche Geometrie überprüft werden.
Dies geschieht mit der Methode sdo_geom.relate:
select count(*) from NENNDORF n, RECHTECKE r,
TABLE(join3(’overlap’,’NENNDORF(GEOMETRY)’,
’RECHTECKE(GEOMETRY)’)) j
where sdo_geom.relate(r.geometry, ’ANYINTERACT’, n.geometry,0.5)=’TRUE’
and r.rowid=right and n.rowid=left;
KAPITEL 8. ANWENDUNG UND PERFORMANCE
75
Oracle Spatial stellt eine Funktion zur Verfügung, die die zweite Filterstufe gleich mit
implementiert. Diese ist hier vermutlich auf Grund der besseren Anbindung an die Datenbank im Vorteil:
select count(*) from RECHTECKE r, NENNDORF n
where sdo_relate(r.geometry, n.geometry,
’mask=ANYINTERACT querytype=JOIN’)=’TRUE’;
Ändert man hier die Reihenfolge der Tabellen in sdo_relate, so ergibt sich wieder eine
deutlich schlechtere Zeit zur Berechnung des Verbundes.
Zu letzt wurde noch jeweils ein self-join über den Index berechnet (wie beim 1. spatial
join, mit dem Unterschied, dass jeweils das gleiche Objekt als Verbundpartner angegeben
wurde).
8.3
Performance-Analyse
Da der eigentliche Verbund mit JoinGiST bereits nach wenigen Sekunden berechnet
ist, und damit auch eine Anfrage ohne weitere Verknüpfungen, lohnt es sich vermutlich
nicht, hier weitere Optimierungen des Algorithmus vorzunehmen Dabei ist die VerbundBerechnung durchwegs schneller als mit den R-Bäumen des Oracle Spatial Cartridge,
obwohl die externe Implementierung vermutlich mit einem größeren Overhead beim Funktionsaufruf und einer schlechteren Anbindung an die Daten (bei Speicherung in der Datenbank) besitzt.
Soll aber nicht nur eine Verbundberechnung aufgrund der Indexdaten stattfindet, also
ohne das die tatsächlichen Objektgeometrien herangezogen werden, sondern auch ein weitere Verknüpfung mit den ursprünglichen Tabellen stattfinden, um weitere Operationen
auf den original Datensätzen ausführen zu können, so bricht die Verarbeitungsgeschwindigkeit zunehmend ein. Sie bleibt aber auf einem gleichmäßigen Niveau, im Gegensatz
zu den indexgestützten Operatoren des Spatial Cartride, die vermutlich den Index aber
einer gewissen Verbundkomplexität nicht mehr richtig verwenden könnem.
Die größte Schwachstelle bei JoinGiST ist vermutlich das Auffinden der zugehörigen
Datensätze durch die Datenbank. Diese kann nicht auf eine Zugehörigkeit der PipelineFunktion zu den anderen Tabellen schließen, so dass die üblichen Algorithmen zur Berechnung eines Verbundes zweier verschiedener Tabellen angewandt werden.
8.4
Ausblick und Verbesserungsmöglichkeiten
Es wäre möglich und erstrebenswert, eine engere Verzahnung mit OraGiST zu erreichen.
Würden die Spezialisierungen dort ebenfalls mit Erweiterungsobjekten stattfinden, statt
Unterklassen der OraGiST-Klasse zu erstellen, so könnte man diese einfach in die Erweiterungen von Join-GiST einsetzten. In diesen müsste dann praktisch nur noch die
meta_intersect-Methode und die Selektionserstellung implementiert werden. Indexnamen und Schlüssellängen könnten direkt von den OraGiST-Erweiterungen übernommen
werden.
KAPITEL 8. ANWENDUNG UND PERFORMANCE
76
Die Verbindung der Datenbank mit externen Funktionen ist nicht so effizient und flexibel, wie man es sich wünschen würde. So wäre es wünschenswert, das Indexing Interface
so zu erweitern, dass auch auch Verbundindexe möglich sind. Gut wäre auch die Möglichkeit, Indexe zu erstellen, die mehrere Attribute einer Tabelle in einer gemeinsamen
Datenstruktur halten kann, so dass gemeinsam zu indexierende Attribute nicht erst zu einem Objekttyp zusammengefasst werden müssen, um darauf eine Indexstruktur erstellen
zu können.
Anhang A
Beispiel einer OraGiST-Erweiterung
Möchte man einen benutzerdefinierten Index mit OraGiST erstellen, so sollte man zunächst den Datentyp und zugehörige Operatoren in Oracle erstellen. Zur Erstellung eines
OraGiST-Spezialisierung, verwendet man das Shell-Skript oragist. Ruft man es mit
oragist define
auf, so wird nach Angabe des Schemas und Oracle-Datentyps der indexierte werden soll,
einem Namen für die neue Spezialisierung sowie eines Oracle-Accounts mit Passwort
der Oracle Type Translator aufgerufen, um eine C-Header-Datei für diesen Datentyp
anzulegen. Zusätzlich erhält man drei weitere Dateien: Eine C++ -Header-Datei definiert
eine neue Unterklasse von OraGIST, die zu spezialisierenden Methoden brauchen nur
noch in der .cc-Datei implementiert werden. Ein SQL-Skript muss schließlich noch um
die Operatoren und den Indextyp ergänzt werden, die von der Erweiterung unterstützt
werden sollen. Der Implementationstyp des Oracle Index Implementation Types ist bereits
vorgegeben.
Sind die fehlenden Angaben in diesen Dateien implementiert, so kann mit
oragist register
OraGiST um den neuen Indextyp erweitert werden. Dabei wird odci2oragist.cc um
den neuen Indextyp erweitert, die Bibliothek wird neu kompiliert und der Indextyp in der
Datenbank eingerichtet (d.h. das SQL-Skript ausgeführt). Die Implementierungsdateien
befinden sich nun im Unterverzeichnissen des Verzeichnisses ogext, Änderungen müssen also dort vorgenommen werden. Sollte das Kompilieren fehlgeschlagen sein, so sollte
oragist register kein zweites Mal aufgerufen werden (denn dies würde zu mehrfachen
Eintragungen in odci2oragist.cc führen), sondern lediglich make ausgeführt werden.
Es folgt das Beispiel aus [Lö01] zur Indexierung des Oracle-Typs MDSYS.SDO_GEOMETRY
in der Erweiterung sdoggist (teilweise verändert, insbesondere ohne nearest neighbour Operationen, da diese nicht von JoinGiST unterstützt werden.):
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
Implementierung der typspezifischen OraGiST -Methoden:
sdoggist.cc
#include
#include
#include
#include
#include
”sdoggist.h”
”gist.h”
”gist rtpred rect.h” // FOR PREDICATES
”gist rtree.h” // FOR R−TREE EXTENSION
<stdio.h>
gist ext t ∗sdoggist :: getExt()
{
return &rt rect ext; // R−TREE EXTENSION
}
int sdoggist :: getMaxKeySize()
{
return 4 ∗ sizeof(double);
}
char ∗sdoggist::getIdxFileName(char ∗idxname)
{
char ∗tmp = new char[100];
sprintf (tmp, ”/opt/oracle/oragist−indices/%s”, idxname);
return tmp;
}
void sdoggist :: printKey(void ∗key, smsize t keysize)
{
double ∗rect = (double ∗)key;
fprintf (stderr , ”[%f, %f, %f, %f] (%d)\n”,
rect [0], rect [1], rect [2], rect [3], keysize );
}
void ∗sdoggist::approximate(DBConnectionHandle dbh,
void ∗value, void ∗value ind,
smsize t &size)
{
og MDSYS SDO GEOMETRY ∗geom
= (og MDSYS SDO GEOMETRY ∗)value;
og MDSYS SDO GEOMETRY ind ∗geom ind
= (og MDSYS SDO GEOMETRY ind ∗)value ind;
double ∗rect = new double[4];
78
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
double x, y;
int isPoint ;
if (OCINumberToInt(dbh−>errhp,
&(geom−>SDO GTYPE),
sizeof(isPoint ),
OCI NUMBER SIGNED,
(dvoid ∗)&isPoint)
!= OCI SUCCESS) return NULL;
if ( isPoint%10 == 1) // POINT TYPE
{
if (OCINumberToReal(dbh−>errhp,
&(geom−>SDO POINT.X),
sizeof(x),
(dvoid ∗)&x)
!= OCI SUCCESS) return NULL;
if (OCINumberToReal(dbh−>errhp,
&(geom−>SDO POINT.Y),
sizeof(y),
(dvoid ∗)&y)
!= OCI SUCCESS) return NULL;
rect [0] = x ; rect [1] = x;
rect [2] = y ; rect [3] = y;
}
else
// COMPLEX OBJECT
{
OCIIter
∗ iterator ;
boolean
eoc;
dvoid
∗elem;
OCIInd
∗elemind;
if (OCIIterCreate(dbh−>envhp,
dbh−>errhp,
geom−>SDO ORDINATES,
&iterator)
!= OCI SUCCESS) return NULL;
// GET FIRST X COORDINATE
if (OCIIterNext(dbh−>envhp,
dbh−>errhp,
iterator ,
&elem,
(dvoid ∗∗)&elemind,
&eoc)
!= OCI SUCCESS) return NULL;
79
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
if (OCINumberToReal(dbh−>errhp,
(OCINumber ∗)elem,
sizeof(x),
(dvoid ∗)&x)
!= OCI SUCCESS) return NULL;
// GET FIRST Y COORDINATE
if (OCIIterNext(dbh−>envhp,
dbh−>errhp,
iterator ,
&elem,
(dvoid ∗∗)&elemind,
&eoc)
!= OCI SUCCESS) return NULL;
if (OCINumberToReal(dbh−>errhp,
(OCINumber ∗)elem,
sizeof(y),
(dvoid ∗)&y)
!= OCI SUCCESS) return NULL;
rect [0] = x ; rect [1] = x;
rect [2] = y ; rect [3] = y;
sword status = OCIIterNext(dbh−>envhp,
dbh−>errhp,
iterator ,
&elem,
(dvoid ∗∗)&elemind,
&eoc);
while (!eoc && (status == OCI SUCCESS))
{
// GET REMAINING COORDINATES
if (OCINumberToReal(dbh−>errhp,
(OCINumber ∗)elem,
sizeof(x),
(dvoid ∗)&x)
!= OCI SUCCESS) return NULL;
if (OCIIterNext(dbh−>envhp,
dbh−>errhp,
iterator ,
&elem,
(dvoid ∗∗)&elemind,
&eoc)
!= OCI SUCCESS)
return NULL; // Y COORDINATE MISSING
80
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
if (OCINumberToReal(dbh−>errhp,
(OCINumber ∗)elem,
sizeof(y),
(dvoid ∗)&y)
!= OCI SUCCESS) return NULL;
if (x<rect [0]) rect [0] = x ; // SET UP
if (x>rect [1]) rect [1] = x ; // BOUNDING BOX
if (y<rect [2]) rect [2] = y ; //
if (y>rect [3]) rect [3] = y ; //
status = OCIIterNext(dbh−>envhp,
dbh−>errhp,
iterator ,
&elem,
(dvoid ∗∗)&elemind,
&eoc);
}
OCIIterDelete(dbh−>envhp, dbh−>errhp, &iterator);
}
size = 4 ∗ sizeof(double);
return rect;
}
gist query t ∗sdoggist :: getQuery(DBConnectionHandle dbh)
{
rt rect ∗r1
= new rt rect(2, (double ∗)qi−>appr cmpval);
int pred;
pred = ((qi−>start && ∗(qi−>start))
? rt query t :: rt overlap
: rt query t :: rt nooper);
return new rt query t((rt query t::rt oper)pred,
rt query t :: rt rectarg ,
(rt pred ∗)r1 );
}
bool sdoggist::needVerify()
{
: true;
// ... WHEREAS ”sdorel” NEEDS VERIFY
}
81
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
gist query t ∗sdoggist :: getEqualsQuery(DBConnectionHandle dbh,
void ∗value,
void ∗value ind)
{
smsize t size ;
rt rect ∗r1
= new rt rect(2, (double ∗)approximate(dbh,
value , value ind,
size ));
return new rt query t
((rt query t :: rt oper)rt query t :: rt equal ,
rt query t :: rt rectarg ,
(rt pred ∗)r1 );
}
82
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
Headerdatei der Klasse sdoggist:
sdoggist.h
#ifndef sdoggist ULO
# define sdoggist ULO
#include <oragist.h>
#include <oci.h>
#include <sdoggist oratype.h>
class sdoggist :public OraGiST
{
public:
sdoggist(DBConnectionHandle dbh = NULL)
: OraGiST(dbh) {}
// BASE−CLASS CONSTRUCTOR
gist ext t ∗getExt();
int getMaxKeySize();
char ∗getIdxFileName(char ∗idxname);
void printKey(void ∗key, smsize t keysize);
void ∗approximate(DBConnectionHandle dbh,
void ∗value,
void ∗value ind,
smsize t &size );
gist query t ∗getQuery(DBConnectionHandle dbh);
bool needVerify();
gist query t ∗getEqualsQuery(DBConnectionHandle dbh,
void ∗value,
void ∗value ind);
};
#endif
83
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
Headerdatei für den Datentyp (Pendant zu MDSYS.SDO_GEOMETRY):
sdoggist_oratype.h
#ifndef SDOGGIST ORATYPE ORACLE
# define SDOGGIST ORATYPE ORACLE
#ifndef OCI ORACLE
# include <oci.h>
#endif
typedef
typedef
typedef
typedef
OCIRef og MDSYS SDO GEOMETRY ref;
OCIRef SDO POINT TYPE ref;
OCIArray SDO ELEM INFO ARRAY;
OCIArray SDO ORDINATE ARRAY;
struct SDO POINT TYPE
{
OCINumber X;
OCINumber Y;
OCINumber Z;
};
typedef struct SDO POINT TYPE SDO POINT TYPE;
struct SDO POINT TYPE ind
{
OCIInd atomic;
OCIInd X;
OCIInd Y;
OCIInd Z;
};
typedef struct SDO POINT TYPE ind SDO POINT TYPE ind;
struct og MDSYS SDO GEOMETRY
{
OCINumber SDO GTYPE;
OCINumber SDO SRID;
struct SDO POINT TYPE SDO POINT;
SDO ELEM INFO ARRAY ∗ SDO ELEM INFO;
SDO ORDINATE ARRAY ∗ SDO ORDINATES;
};
typedef struct og MDSYS SDO GEOMETRY
og MDSYS SDO GEOMETRY;
struct og MDSYS SDO GEOMETRY ind
84
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
{
OCIInd atomic;
OCIInd SDO GTYPE;
OCIInd SDO SRID;
struct SDO POINT TYPE ind SDO POINT;
OCIInd SDO ELEM INFO;
OCIInd SDO ORDINATES;
};
typedef struct og MDSYS SDO GEOMETRY ind
og MDSYS SDO GEOMETRY ind;
#endif
85
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
Erzeugung des Indextyps in SQL:
sdoggist.sql
−− GiST extension name
−− sdoggist
−− SQL Type of indexed column :
−− MDSYS.SDO GEOMETRY
−− CREATE INDEXTYPE IMPLEMENTATION TYPE −−
create or replace type gist MDSYS SDO GEOMETRY im
as object
(
scanctx RAW(4),
static function
ODCIGetInterfaces(ifclist OUT SYS.ODCIObjectList)
return NUMBER,
static function
ODCIIndexCreate(ia SYS.ODCIIndexInfo,
parms VARCHAR2)
return NUMBER,
static function
ODCIIndexAlter(ia SYS.ODCIIndexInfo,
parms VARCHAR2,
alter option NUMBER)
return NUMBER,
static function
ODCIIndexDrop(ia SYS.ODCIIndexInfo)
return NUMBER,
static function
ODCIIndexInsert(ia SYS.ODCIIndexInfo,
rid VARCHAR2,
newval MDSYS.SDO GEOMETRY)
return NUMBER,
static function
ODCIIndexDelete(ia SYS.ODCIIndexInfo,
rid VARCHAR2,
oldval MDSYS.SDO GEOMETRY)
return NUMBER,
static function
ODCIIndexUpdate(ia SYS.ODCIIndexInfo,
rid VARCHAR2,
oldval MDSYS.SDO GEOMETRY,
newval MDSYS.SDO GEOMETRY)
86
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
return NUMBER,
static function
ODCIIndexStart(sctx IN OUT
gist MDSYS SDO GEOMETRY im,
ia SYS.ODCIIndexInfo,
op SYS.ODCIPredInfo,
qi SYS.ODCIQueryInfo,
strt NUMBER,
stop NUMBER,
cmpval MDSYS.SDO GEOMETRY)
return NUMBER,
member function
ODCIIndexFetch(nrows NUMBER,
rids OUT SYS.ODCIRidList)
return NUMBER,
member function
ODCIIndexClose return NUMBER
);
/
−− CREATE IMPLEMENTATION UNIT −−
create or replace type body gist MDSYS SDO GEOMETRY im
is
static function
ODCIGetInterfaces(ifclist OUT SYS.ODCIObjectList)
return NUMBER is
begin
ifclist
:= SYS.ODCIObjectList
(sys .ODCIObject(’SYS’,’ODCIINDEX1’));
return ODCIConst.Success;
end ODCIGetInterfaces;
static function
ODCIIndexCreate(ia SYS.ODCIIndexInfo,
parms VARCHAR2)
return NUMBER
as external
name ”ODCIIndexCreate C”
library oragist
with context parameters (
context,
ia , ia indicator struct ,
87
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
parms,
return OCINUMBER);
static function
ODCIIndexAlter(ia SYS.ODCIIndexInfo,
parms VARCHAR2,
alter option NUMBER)
return NUMBER
as external
name ”ODCIIndexAlter C”
library oragist
with context parameters (
context,
ia , ia indicator struct ,
alter option ,
alter option indicator ,
parms,
return OCINUMBER);
static function
ODCIIndexDrop(ia SYS.ODCIIndexInfo)
return NUMBER
as external
name ”ODCIIndexDrop C”
library oragist
with context parameters (
context,
ia , ia indicator struct ,
return OCINUMBER);
static function
ODCIIndexInsert(ia SYS.ODCIIndexInfo,
rid VARCHAR2,
newval MDSYS.SDO GEOMETRY)
return NUMBER
as external
name ”ODCIIndexInsert C”
library oragist
with context parameters (
context,
ia , ia indicator struct ,
rid , rid indicator ,
88
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
newval, newval indicator struct ,
return OCINUMBER);
static function
ODCIIndexDelete(ia SYS.ODCIIndexInfo,
rid VARCHAR2,
oldval MDSYS.SDO GEOMETRY)
return NUMBER
as external
name ”ODCIIndexDelete C”
library oragist
with context parameters (
context,
ia , ia indicator struct ,
rid , rid indicator ,
oldval , oldval indicator struct ,
return OCINUMBER);
static function
ODCIIndexUpdate(ia SYS.ODCIIndexInfo,
rid VARCHAR2,
oldval MDSYS.SDO GEOMETRY,
newval MDSYS.SDO GEOMETRY)
return NUMBER
as external
name ”ODCIIndexUpdate C”
library oragist
with context parameters (
context,
ia , ia indicator struct ,
rid , rid indicator ,
oldval , oldval indicator struct ,
newval, newval indicator struct ,
return OCINUMBER);
static function
ODCIIndexStart(sctx IN OUT
gist MDSYS SDO GEOMETRY im,
ia SYS.ODCIIndexInfo,
op SYS.ODCIPredInfo,
qi SYS.ODCIQueryInfo,
strt NUMBER,
stop NUMBER,
89
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
cmpval MDSYS.SDO GEOMETRY)
return NUMBER
as external
name ”ODCIIndexStart C”
library oragist
with context parameters (
context,
sctx , sctx indicator struct ,
ia , ia indicator struct ,
op, op indicator struct ,
qi , qi indicator struct ,
strt , strt indicator ,
stop , stop indicator ,
cmpval, cmpval indicator struct ,
return OCINUMBER);
member function
ODCIIndexFetch(nrows NUMBER,
rids OUT SYS.ODCIRidList)
return NUMBER
as external
name ”ODCIIndexFetch C”
library oragist
with context parameters (
context,
self , self indicator struct ,
nrows, nrows indicator,
rids , rids indicator ,
return OCINUMBER);
member function
ODCIIndexClose return NUMBER
as external
name ”ODCIIndexClose C”
library oragist
with context parameters (
context,
self , self indicator struct ,
return OCINUMBER);
end;
/
−−−−−−−−− CREATE OPERATOR RELATE −−−−−−−−−
90
ANHANG A. BEISPIEL EINER ORAGIST-ERWEITERUNG
create function mysdorelate(m1 MDSYS.SDO GEOMETRY,
m2 MDSYS.SDO GEOMETRY)
return number is
val number;
begin
if sdo geom.relate(
m1,
MDSYS.SDO DIM ARRAY(
MDSYS.SDO DIM ELEMENT(’X’, 354100000,
355700000, 1),
MDSYS.SDO DIM ELEMENT(’Y’, 579800000,
581200000, 1)),
’ANYINTERACT’, m2,
MDSYS.SDO DIM ARRAY(
MDSYS.SDO DIM ELEMENT(’X’, 354100000,
355700000, 1),
MDSYS.SDO DIM ELEMENT(’Y’, 579800000,
581200000, 1)))
= ’TRUE’
then val := 1;
else val := 0;
end if ;
return val ;
end;
/
create operator sdorel binding (MDSYS.SDO GEOMETRY,
MDSYS.SDO GEOMETRY)
return number
using mysdorelate;
−−− CREATE INDEXTYPE FOR SDOREL −−−
create indextype sdoggist 01 for
sdorel(MDSYS.SDO GEOMETRY, MDSYS.SDO GEOMETRY),
using gist MDSYS SDO GEOMETRY im;
91
Anhang B
Beispiel einer JoinGiST-Erweiterung
Um eine neue JoinGiST-Erweiterung zu implementieren, wird ebenfalls ein kleines ShellSkript verwendet, um den Anwender bei dieser Aufgabe zu unterstützen.
Existiert noch kein Objekt für den Rückgabewert, so wird dieses mit
joingist init
in der Datenbank angelegt. Dabei wird das SQL-Skript joingist_resultimpl.sql ausgeführt, das zusätzlich gleich den Implementierungs-Typ des ODCITable Interfaces anlegt. Um diese Skript ausführen zu können, ist die Angabe von Username und Passwort
notwendig. Da diese als Parameter zu sqlplus auf der Eingabezeile erscheinen, wird sicherheitsbewussten Anwendern empfohlen den Typ direkt aus sqlplus erstellen (es wird
die Berechtigung Objekte in Oracle erstellen zu dürfen benötigt):
@’/pfad/zu/joingist_resultimpl.sql’
Damit die Implementierung die Bibliothek mit dem Alias joingist findet, muss zusätzlich
mit
create library joingist as ’/pfad/zu/joingist.so’
in sqlplus der Pfadname der Bibliothek bekannt gemacht werden. (Dazu benötigt man
Rechte, um in Oracle Bibliotheken erstellen zu dürfen)
Um eine neue Unterklasse der JoinGiST-Erweiterungsklasse JoinGiST_ext_t zu erstellen, ruft verwendet man:
joingist create
Es werden die Namen der beiden OraGiST-Erweiterungsklassen abgefragt, für die der Verbund implementiert werden soll. Daraus ergibt sich der Basisname der neuen JoinGiSTErweiterungsklasse und es werden zwei Dateien dazu erstellt: Eine Header-Datei, die
diese Klasse als Unterklasse von JoinGiST_ext_t deklariert und ein Datei, in der die
C++ -Methoden implementiert werden müssen. Zusätzlich wird die Datei odci2join.cc
um eine Abfrage für diese Spezialisierungs-Kombination erweitert, so dass das neue Objekt bei einem Verbundaufruf der Klasse JoinGiST übergeben werden kann.
ANHANG B. BEISPIEL EINER JOINGIST-ERWEITERUNG
Definition des Rückgabeobjektes und der ODCITable-Implementation:
joingist_resultimpl.sql
CREATE TYPE join result type AS OBJECT (
left VARCHAR2(18),
right VARCHAR2(18)
);
CREATE TYPE join result set IS TABLE OF join result type;
CREATE OR REPLACE TYPE join result impl AS OBJECT (
key RAW(4),
STATIC FUNCTION ODCITableStart(sctx OUT join result impl,
op IN STRING, t1 IN STRING, t2 IN STRING,
s1 IN STRING, s2 IN STRING, add IN STRING)
RETURN BINARY INTEGER
AS LANGUAGE C
LIBRARY joingist
NAME ”ODCITableStart 6”
WITH CONTEXT
PARAMETERS (
context,
sctx , sctx INDICATOR STRUCT,
op, t1 , t2 , s1 , s2 , a,
RETURN),
STATIC FUNCTION ODCITableStart(sctx OUT join result impl,
op IN STRING, t1 IN STRING, t2 IN STRING,
s1 IN STRING, s2 IN STRING)
RETURN BINARY INTEGER
AS LANGUAGE C
LIBRARY joingist
NAME ”ODCITableStart 5”
WITH CONTEXT
PARAMETERS (
context,
sctx , sctx INDICATOR STRUCT,
op, t1 , t2 , s1 , s2,
RETURN),
STATIC FUNCTION ODCITableStart(sctx OUT join result impl,
op1 IN STRING, t1 IN STRING, t2 IN STRING)
RETURN BINARY INTEGER
AS LANGUAGE C
93
ANHANG B. BEISPIEL EINER JOINGIST-ERWEITERUNG
LIBRARY joingist
NAME ”ODCITableStart 3”
WITH CONTEXT
PARAMETERS (
context,
sctx , sctx INDICATOR STRUCT,
op1, t1 , t2,
RETURN),
MEMBER FUNCTION ODCITableFetch(self IN OUT join result impl,
nrows IN NUMBER,
rslt OUT join result set)
RETURN BINARY INTEGER
AS LANGUAGE C
LIBRARY joingist
NAME ”ODCITableFetch”
WITH CONTEXT
PARAMETERS (
context,
self , self INDICATOR STRUCT,
nrows,
rslt , rslt INDICATOR,
RETURN),
MEMBER FUNCTION ODCITableClose(self IN join result impl)
RETURN BINARY INTEGER
AS LANGUAGE C
LIBRARY joingist
NAME ”ODCITableClose”
WITH CONTEXT
PARAMETERS (
context,
self , self INDICATOR STRUCT,
RETURN)
);
/
CREATE FUNCTION join3(op IN STRING, t1 IN STRING, t3 IN STRING)
RETURN JOIN RESULT SET
PIPELINED USING join result impl;
/
CREATE FUNCTION join5(op IN STRING, t1 IN STRING, t3 IN STRING,
s1 IN STRING, s2 IN STRING)
RETURN JOIN RESULT SET
94
ANHANG B. BEISPIEL EINER JOINGIST-ERWEITERUNG
PIPELINED USING join result impl;
/
CREATE FUNCTION join6(op IN STRING, t1 IN STRING, t3 IN STRING,
s1 IN STRING, s2 IN STRING, a IN STRING)
RETURN JOIN RESULT SET
PIPELINED USING join result impl;
/
95
ANHANG B. BEISPIEL EINER JOINGIST-ERWEITERUNG
Headerdatei der Erweiterungs-Klasse sdoggist_sdoggist:
join_sdoggist_sdoggist.h
#include ”gist rtpred rect.h” //for query predicates
#include ”joingist.h”
class sdoggist sdoggist : public JoinGiST ext t{
public:
bool setOperator(string ∗&op);
gist query t ∗getLeftQuery(string ∗&selection);
gist query t ∗getRightQuery(string ∗&selection);
char ∗getLeftIdxFileName(char ∗idxname);
char ∗getRightIdxFileName(char ∗idxname);
int getLeftMaxKeySize();
int getRightMaxKeySize();
bool meta intersect(const void ∗key1, const bool &leaf1,
const void ∗key2, const bool &leaf2);
protected:
bool equalOp;
};
96
ANHANG B. BEISPIEL EINER JOINGIST-ERWEITERUNG
Implementierung der typspezifischen JoinGiST -Methoden:
join_sdoggist_sdoggist.cc
#include ”join sdoggist sdoggist.h”
//join with two indexes of type sdoggist
#include <string>
bool sdoggist sdoggist :: setOperator(string ∗&op){
// if keyword equal is present : equality−join
if (op==NULL) return false; //no real string
if (op−>find(”equal”)==op−>npos){
//not found
equalOp=false;
} else {
//find () returned something smaller than npos:
//”equal”−>found
equalOp=true;
}
return true;
}
gist query t ∗ sdoggist sdoggist :: getLeftQuery(string ∗&selection){
//parse a string of the form ”x1, y1, x2, y2”
if (! selection ) return NULL; //selection−pointer is NULL
double ∗rect coordinates=new double[4];
string :: size type index1=0;
//rect coordinate order is
//x low, x high,y low ,y high
rect coordinates[0]=atof( selection −>c str());
// x low set
index1=selection−>find(’,’, index1+1);
if (index1 != selection −>npos){
rect coordinates[2]=atof(( selection −>c str()+index1+1));
} else {
return NULL; //something went wrong
} // y low set
index1=selection−>find(’,’, index1+1);
if (index1 != selection −>npos){
rect coordinates[1]=atof(( selection −>c str()+index1+1));
} else {
97
ANHANG B. BEISPIEL EINER JOINGIST-ERWEITERUNG
return NULL; //something went wrong
} // x high set
index1=selection−>find(’,’, index1+1);
if (index1 != selection −>npos){
rect coordinates[3]=atof(( selection −>c str()+index1+1));
} else {
return NULL; //something went wrong
} // y high set
rt rect ∗rect=new rt rect(2,rect coordinates);
gist query t ∗query=new rt query t(rt query t::rt overlap,
rt query t :: rt rectarg , rect );
return query;
}
gist query t ∗ sdoggist sdoggist :: getRightQuery(string ∗&selection){
//same as left query ...
return getLeftQuery(selection);
}
//this is from sdoggist.cc : ”getIdxFileName()”
char ∗sdoggist sdoggist :: getLeftIdxFileName(char ∗idxname){
char ∗tmp = new char[100];
sprintf (tmp, ”/opt/oracle/oragist−indices/%s”, idxname);
return tmp;
}
//it might be, that you store your different Indexes in different directories
char ∗sdoggist sdoggist :: getRightIdxFileName(char ∗idxname){
char ∗tmp = new char[100];
sprintf (tmp, ”/opt/oracle/oragist−indices/%s”, idxname);
return tmp;
}
//this is from sdoggist.cc : ”getLeftKeySize()”
int sdoggist sdoggist :: getLeftMaxKeySize()
{
return 4 ∗ sizeof(double);
}
int sdoggist sdoggist :: getRightMaxKeySize()
{
98
ANHANG B. BEISPIEL EINER JOINGIST-ERWEITERUNG
return 4 ∗ sizeof(double);
}
bool sdoggist sdoggist :: meta intersect(const void ∗key1, const bool &leaf1,
const void ∗key2, const bool &leaf2){
//we support two Operators: intersection and equality
//for equality equalOp will be set , otherwise test for intersection
double ∗rect1=(double ∗)key1;
double ∗rect2=(double ∗)key2;
//equality : leaf entrys must be equal,
//
other nodes just have to overlap
if ( leaf1 && leaf2)
if (equalOp==true)
if (( rect1[0]==rect2[0]) &&
(rect1[1]==rect2[1]) &&
(rect1[2]==rect2[2]) &&
(rect1[3]==rect2[3]))
return (true);
else return (false);
//test for intersection : return false if they do not overlap
//(here we count ”touching” towards ”overlapping”)
// if a.high < b.low
or
a.low > b.high −−> no overlap
if (( rect1 [1] < rect2 [0]) || ( rect1 [0] > rect2 [1]))
return (false);
if (( rect1 [3] < rect2 [2]) || ( rect1 [2] > rect2 [3]))
return (false);
return (true);
}
99
Literaturverzeichnis
[Aok98]
Paul M. Aoki: Generalizing Search in Generalized Search Trees Proceedings
of the 14th International Conference on Data Engineering, Orlando, Februar
1998, S. 380-389
[AK01]
Paul M. Aoki, Marcel Kornacker: Guide to Writing Extensions
http://gist.cs.berkeley.edu/libgist-2.0/ext_guide.html, März 2001
[BKS93]
Thomas Brinkhoff, Hans-Peter Kriegel, Bernhard Seeger: Efficient Processing
of Spatial Joins Using R-trees Proceedings of ACM SIGMOD International
Conference on Management of Data, Mai 1993, S. 237-246
[BKSS90] Norbert Beckmann, Hans-Peter Kriegel, Ralf Schneider, Bernhard Seeger: The
R∗ -tree: An Efficient and Robust Access Method For Points and Rectangles
Proceedings of the ACM SIGMOD International Conference on Management
of Data, Atlantic City, Mai 1990, S. 322-331.
[Com79]
Douglas Comer: The Ubiquitous B-Tree Computing Surveys, 11(2): S. 121-137,
Juni 1979
[GiST-PSQL] Teodor
Sigaev,
Oleg
Bartunov:
GiST
for
PostgreSQL
http://www.sai.msu.su/~megera/postgres/gist/, Mai 2003
[GiST-PGIS] Dave Blasby (Principal Developer) et al.: PostGIS: Geographic Objects for
PostgreSQL http://postgis.refractions.net/
[GPL]
GNU General Public License
[Gut84]
A. Guttman: R-trees: A Dynamic Index Structure for Spatial Searching Proceedings of ACM SIGMOD International Conference on Management of Data,
Boston, 1984, S. 47-57
http://www.gnu.org/copyleft/gpl.html
[HLR97] Yum-Wu Huang, Ning Jing, Elke A. Rundensteiner: Spatial Joins Using Rtrees: Breadth-First Traversal with Global Optimizations Proceedings of the
23rd VLDB Conference, Athen, 1997
[HNP95] Joseph M. Hellerstein, Jeffrey F. Naughton, Avi Pfeffer: Generalized Search
Trees for Database Systems Technical Report #1274, University of Wisconsin
at Madison, July 1995
LITERATURVERZEICHNIS
101
siehe auch: Generalized Search Trees for Database Systems (Extended Abstract)
Proceedings of the 21st VLDB Conference, Zürich, Schweiz, 1995, S. 562-573
[HR01]
Theo Härder, Erhard Rahm: Datenbanksysteme: Konzepte und Techniken der
Implementierung 2. Auflage, Berlin, Heidelberg, New York, Barcelona, Hongkong, London, Mailand, Paris, Tokio, 2001, Springer-Verlag
[JS93]
Theodore Johnson, Dennis Shasha: Why Free-at-Empty Is Better Than Mergeat-Half Journal of Computer Sciences and Systems, 47(1), August 1993, S.
45-76
[Kle03]
Carsten Kleiner: Modelling Spatial, Temporal and Spatio-Temporal Data in
Object-Relational Database Systems Dissertation, Fachbereich Informatik, Universität Hannover, 2003
[KL03]
Carsten Kleiner, Udo W. Lipeck: OraGiST - How to Make User-Defined Indexing Become Usable and Useful G. Weikum, H. Schöning, E. Rahm (eds.),
BTW 2003 - Datenbanksysteme für Business, Technologie und Web - Tagungsband der 10. BTW-Konferenz 26.-28.2.2003, Leipzig, LNI P-26, GI, Bonn, 2003,
S. 324-334.
[Li97]
Udo Lipeck: Datenbanksysteme Vorlesung am Institut für Informatik, Fachgebiet Datenbanken und Informationssysteme im Wintersemester 1997/98, Universität Hannover
[Lö01]
Ulf Löckmann: Erweiterung eines objektrelationalen Datenbankmanagementsystems um verallgemeinerte Suchbäume als Indexstrukturen Diplomarbeit am
Institut für Informatik, Fachgebiet Datenbanken und Informationssysteme,
Universität Hannover, Mai 2001
[LR95]
Ming-Ling Lo, Chinya V. Ravishankar: Generating seeded trees from data sets
The Fourth International Symposium on Large Spatial Databases (Advances
in Spatial Databases: SSD ’95), Portland, Maine, August 1995, Springer-Verlag
[LR96]
Ming-Ling Lo, Chinya V. Ravishankar: Spatial Hash-Joins Proceedings of
ACM SIGMOD International Conference on Management of Data, Montreal, Juni 1996, S. 247-258
[LR94]
Ming-Ling Lo, Chinya V. Ravishankar: Spatial Joins Using Seeded Trees Proceedings of ACM SIGMOD International Conference on Management of Data,
Minneapolis, Mai 1994, S. 209-220
[LR98]
Ming-Ling Lo, Chinya V. Ravishankar: The Desing and Implementation of
Seeded Trees: An Efficient Method for Spatial Joins IEEE Transactions on
Knowledgment and Data Engineering, Vol. 10, Nr. 1, Januar/Februar 1998, S.
136-152
LITERATURVERZEICHNIS
102
[OM84]
Jack A. Orenstein, T. H. Merrett: A Class of Data Structures for Associative
Searching Proceedings of the 4th ACM SIGACT-SIGMOD-SIGARTS Symposium on Principles of Database Systems, ACM Press, New York, 1984, S.
181-190
[OM88]
Jack A. Orenstein, Frank A. Manola: PROBE Spatial Data Modeling and Query Processing in an Image Database Application IEEE Transactions on Software Engineering, Vol. 14, Nr. 5 Mai 1988, S. 611-629
[O9i-CIPG] Jack Melnick: Oracle9i Call Interface Programmer’s Guide (Release 2)
Oracle Corporation, März 2002
[O9i-DCDG] William Gietz: Oracle9i Data Cartridge Developer’s Guide (Release 2)
Oracle Corporation, März 2002
[O9i-DPTG] Connie Dialeris Green: Oracle9i Database Performance Tuning Guide and
Reference (Release 2) Oracle Corporation, Oktober 2002
[O9i-DWG] Paul Lane: Oracle9i Data Warehousing Guide (Release 2) Oracle Corporation, März 2002
[O9i-PCP] Syed Mujeeb Ahmed, Jack Melnick, Neelam Singh, Tim Smith: Pro*C/C++
Precompiler Programmer’s Guide (Release 2) Oracle Corporation, März 2002
[O9i-SUG] Chuck Murray: Oracle9i Spatial User’s Guide and Reference Oracle Corporation, Juni 2001
[Pea90]
G. Peano: Sur une courbe qui remplit toute une aire plane Mathematische
Annalen 36, 1890, S. 157-160
Erklärung
Hiermit versichere ich, dass ich die vorliegende Arbeit und die zugehörige Implementierung selbstständig verfasst und dabei nur die angegebenen Quellen und Hilfsmittel
verwendet habe.
Hannover, 27. Juni 2003
Oliver Schweer
Herunterladen