Bulkloading Indexes

Werbung
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
Algorithmen für Datenbanksysteme
Bulkloading Indexes
Eine Ausarbeitung von Fabrizio Steiner
1.
Einleitung
In den letzten Jahren kam ein grosses Interesse an “Spatial”- , Bilder- und
Text-Datenbanksystemen auf. “Spatial”-Datenbanksysteme sind entwickelt
worden, um räumliche Daten wie Punkte, Polygone oder Oberflächen zu
verwalten und zu speichern. In den erwähnten Systemen werden vielfach
grosse Mengen an Daten verwaltet. Die Datenobjekte für “Spatial”-Systeme
besitzen meistens 2 oder 3 Dimensionen (Euklidischer-Raum). Bei Text- oder
Bild-Datenbanksystemen besitzen die Datenobjekte meist noch mehr
Dimensionen und sind somit mehr-dimensional. Eine Dimension entspricht
jeweils einer Eigenschaft des Objektes in der realen Welt, zum Beispiel bei
Texten sind die Dimensionen Schlüsselwörter, die zuvor festgelegt wurden.
In den letzten 3 Jahrzehnten wurden eine Vielzahl mehr-dimensionaler
Indexstrukturen postuliert. Diese multi-dimensionalen Indexstrukturen
unterstützen Einfüge-, Lösch- und Update-Operationen, darüberhinaus
unterstützen sie Window-Abfragen oder Nearest-Neighbour-Search. Zu
diesen traditionellen Operationen kommt derzeit ein immer grösseres
Interesse an Bulk-Operationen auf. Bei Bulk-Operationen wird eine grosse
Anzahl an traditionellen Operationen nacheinander auf der Indexstruktur
durchgeführt, ohne von anderen Anfragen unterbrochen zu werden. Das
grosse Interesse an Bulk-Operationen ist zurückzuführen auf die immer
grösser werdende Datenmenge, die von Indexstrukturen verwaltet werden
muss. Bei grossen Mengen an Daten ist es zu ineffizient, eine Operation nach
der anderen auszuführen. Eine der wichtigsten Bulk-Operationen ist das
Erstellen einer mehr-dimensionalen Indexstruktur für eine gegebene Menge
von Datenobjekten, das Bulkloading.
In dieser Ausarbeitung steht das Bulkloading im Vordergrund. Zu Beginn
wird das I/O Modell, das für die Komplexitätsanalysen verwendet wird,
Seite 1/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
vorgestellt. Anschliessend wird erläutert, wie die meisten Bulkloading
Methoden arbeiten, sowie deren Probleme aufgezeigt. Anschliessend wird
der Algorithmus aus [1] vorgestellt, welcher diese Probleme vermindert.
2. Das I/O Modell
• Standard I/O Modell
von Aggarwal und
Vitter [4]
Das I/O Modell, das für die Komplexitätsanalysen der Algorithmen
verwendet wird, entspricht einem Disk-Modell. Es wird angenommen, dass
die Disk in mehrere Blöcke (Pages) einer fixen Grösse (B) aufgeteilt ist. Bei
jedem Diskzugriff wird ein Block transferiert, dies wird als einen I/O
definiert. Die Performance der Algorithmen wird in der Anzahl I/O
gemessen, die benötigt werden um eine Sequenz von N Objekten in die
Indexstruktur einzufügen. Bei der Ausführung der Algorithmen wird vom
gesamten Hauptspeicher gebrauch gemacht. Es werden die folgenden
Parameter im Modell verwendet.
N
Anzahl Datenobjekte, die in die Indexstruktur eingefügt werden
sollen.
M
Anzahl Datenobjekte, die in den Hauptspeicher passen.
B
Anzahl Datenobjekte pro Block (Page).
Die Anzahl Blöcke, die in die Indexstruktur eingefügt werden sollen, sowie
die Anzahl Blöcke, die in den Hauptspeicher passen, spielen in den I/O
Komplexitätsanalysen eine wichtige Rolle. Aus diesem Grunde definieren wir
die folgenden Werte, die bei den Analysen auftreten.
N/B =: n
Anzahl Diskblöcke, die in die Indexstruktur eingefügt
werden sollen.
M/B =: m
Anazhl Diskblöcke, die in den Hauptspeicher passen.
3. Bulkloading Methoden
• Bei mehrdimensionalen
Strukturen wird eine
Transformation von nDimensionen in eine
Dimension
Verschiedene Autoren haben Vorschläge gemacht für Bulkloading von RBäumen, z.B. [5], [6]. Alle diese Vorschläge arbeiten auf dieselbe Weise. Zuerst
werden die gesamten Daten nach einem globalen 1-dimensionalen
Kriterium sortiert. Nach dem Sortieren wird der folgende Ansatz verwendet,
um den R-Baum aufzubauen. Der R-Baum wird Bottom-Up erstellt, indem die
durchgeführt.
Seite 2/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
Daten nach der Linearen Ordnung geclustert werden. Danach wird von
unten nach oben jeweils die nächste Ebene von Knoten erstellt. Dies wird so
lange durchgeführt bis der gewünschte Root-Knoten erreicht ist, zu diesem
Zeitpunkt ist das Bulkloading beendet. Das Resultat ist eine R-BaumIndexstruktur der Datenobjekte.
• Die
Komplexitätsanalyse
kann im Abschnitt 6.3
nachgelesen werden.
Bei Verwendung des oben genannten I/O Modells kann gezeigt werden, dass
das externe Sortieren der Daten, mittels multiway Mergesort, im Worst-Case
Θ(n log m n) I/Os benötigt.
In [7] wurde herausgefunden, dass diese Art von Bulkloading im Normalfall
zu schlechter Abfrage-Performance führt, vor allem wenn die Dimension der
Datenobjekte gross ist. Für die Anwendungen, die in der Einleitung
beschrieben wurden, ist dies genau der Fall. Zudem wurden für andere als RBaum Indexstrukturen nur wenige bis keine Vorschläge gemacht, wie man
Bulkloading implementieren könnte.
Die I/O Kosten des Bulkloading einer 1-dimensionalen Indexstruktur, welche
die Sortierung beibehält ist somit asymptotisch optimal im Worst-Case,
wenn die untere Grenze der externen Sortierung erreicht wird. Somit ist das
Ziel, diese Grenze für das Bulkloading einer multi-dimensionalen
Indexstruktur, ohne Beeinflussung der Abfrage-Performance, zu erreichen.
Ebenfalls soll die Methode generisch sein für bestimmte baumbasierte Ziel
Indexstrukturen. Alle diese Indexstrukturen müssen die im folgenden
Abschnitt erläuterten Eigenschaften erfüllen.
4. Baumbasierte Indexstrukturen
Eine Indexstruktur wird in dieser Ausarbeitung als ein Baum angesehen.
Jeder Knoten des Baums entspricht einem Diskblock. Die RoutingInformationen sind in den Index-Knoten gespeichert und die Datenobjekte
werden in den Daten-Knoten gespeichert, welche die Blätter des Baums
sind. Der Teil des Baums, der aus den Index-Knoten besteht, wird als IndexBaum bezeichnet. Ein Index-Knoten entspricht einem Teilgebiet des dDimensionalen Raums der Datenobjekte. Die Index-Knoten besitzen
Referenzen auf die Unterbäume, zusätzlich zu jeder Referenz wird das Gebiet
Seite 3/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
des entsprechenden Unterbaums gespeichert, dies ist die Routing-Tabelle.
Somit kann für ein Datenobjekt, das bei einem Index-Knoten angekommen
ist, entschieden werden, an welchen Unterbaum es weitergereicht werden
soll. Bei einem R-Baum entspricht das Gebiet eines Index-Knotens dem
minimalen Rechteck, welches alle Kinder des Index-Knotens enthält.
Es wird ebenfalls angenommen, dass jede mulit-dimensionale Indexstruktur
die folgenden Operation unterstützt.
InsertIntoNode: Fügt ein Datenobjekt in einen Daten-Knoten ein oder
einen zusätzlichen Knoten in einen Index-Knoten.
Split:
Teilt einen Knoten, der zu viele Kinder besitzt, in zwei
Knoten auf.
ChooseSubTree: Wählt für ein Datenobjekt oder ein Gebiet den
entsprechenden Unterbaum, in welchen das
Datenobjekt eingefügt werden soll.
5. Einführung Buffer-Baum
• L. Arge hat im Jahre
1996 ein Proposal
veröffentlicht, in dem
er den Buffer-Baum
vorgestellt hat.
Eine wichtige Struktur um I/O effiziente Algorithmen zu erzielen, welche
Offline-Aufgaben erfüllen, ist der Buffer-Baum. Die Idee des Buffer-Baums ist
es, den Vorteil des grossen Hauptspeichers voll auszunützen. Beim R-Baum
besitzt ein Knoten jeweils B Unterbäume, somit müssen B Datenobjekte (= 1
Diskblock) geladen werden um auf ein bestimmtes Kind des Knotens
zuzugreifen. Folglich wird für jedes Rechteck, das eingefügt werden soll und
bei einem Knoten ankommt, ein I/O ausgeführt um die Kind-Informationen
zu laden. Was ineffizient ist, hier kommt der Buffer-Baum ins Spiel. Beim
Buffer-Baum werden Einfüge-Operationen gebuffert, damit der Overhead
vom Laden der Kind-Informationen auf mehrere Einfüge-Operationen
verteilt werden kann und somit minimiert wird.
Im Folgenden wird die Definition des Buffer-Baums von L. Arge ein wenig
angepasst und erläutert.
Definition:
Eine baumbasierte Indexstruktur ist ein Buffer-Baum der
Ordnung C mit maximaler Buffergrösse p, wenn die
folgenden Eigenschaften erfüllt sind.
Seite 4/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
• Jeder Index-Knoten enthält maximal C Einträge, welche auf
Unterbäume zeigen. C hängt von der Grösse des verfügbaren
Speichers ab.
• Jeder Index-Knoten besitzt einen Buffer mit maximal p
belegten Diskblöcken (Pages).
• Ausser für den letzten Buffer-Block ist es garantiert, dass die
belegten Blöcke des Buffers voll sind.
Es wird zwischen den folgenden Knotentypen unterschieden (siehe Grafik
2): Interne-Knoten, Leaf-Knoten und Daten-Knoten. Die Daten-Knoten
entsprechen einem Diskblock (Page) und enthalten die eigentlichen
Datenobjekte. Jeder Interne- und Leaf-Knoten enthält einen Buffer
Buffer
Routing Tabelle
der Grösse p und eine Tabelle (Routing-Tabelle) mit C Referenzen auf
Unterbäume. Die Struktur von Internen- und Leaf-Knoten ist in der
Grafik 1 dargestellt. Ein Buffer ist voll falls er p belegte Blöcke enthält,
also (B*p) Datenobjekte. Der Buffer ist leer, falls keine Datenobjekte
enthalten sind. Er kann auch temporär mehr als p Blöcke enthalten,
Grafik 1: Struktur eines Index-Knotens dies wird als ein Overflow definiert.
Interne-Knoten
IndexKnoten
Leaf-Knoten
Daten-Knoten
Grafik 2: Struktur eines Buffer-Baums
Soll ein Datenobjekt in einen Knoten eingefügt werden, wird dieses in den
Buffer des Index-Knoten eingefügt. Anschliessend wird der Einfüge-Prozess
unterbrochen und in den blocked Status gesetzt und ein neuer EinfügeProzess kann gestartet werden. Ist der Buffer eines Knotens voll (overflow),
wird dieser geleert in dem die gebufferten Datenobjekte und deren Prozesse
Seite 5/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
reaktiviert und zum nächsten Knoten weitergereicht werden. Anders als
beim R-Baum, werden nun die Routing-Informationen nur einmal für alle
Datenobjekte aus der Disk geladen (da C vom Speicher abhängt). Kommt ein
Datenobjekt in einem Daten-Knoten an, wird das Datenobjekt eingefügt und
der Prozess in den terminated Status gesetzt. Im nachfolgenden Beispiel
wird deutlich wie das Einfügen in einen Buffer-Baum ausgeführt wird.
Es ist einfach zu sehen, dass eine beliebige baumbasierte Indexstruktur als
ein Buffer-Baum implementiert werden kann.
6. Bulkloading mit Hilfe eines Buffer-Baums
• Im Beispiel werden die
Parameter wie folgt
Es wird nun gezeigt, wie mit Hilfe eines Buffer-Baums (aufgebaut auf einem
R-Baum) ein effizienter Bulkloading Algorithmus arbeiten kann, ohne die
Daten nach einem globalen 1-dimensionalen Kriterium zu sortieren und der
die Abfrage-Performance nicht beeinträchtigt.
festgelegt.
• B=3
• C=4
Bevor der Algorithmus detailliert vorgestellt wird, wird an einem einfachen
• p=2
Beispiel aufgezeigt wie der Algorithmus arbeitet.
6.1. Bulkloading an einem Beispiel
Als Beispiel nehmen wir, wie schon erwähnt, das Bulkloading eines R-Baums
unter zu Hilfenahme eines Bufferr19
r22
Baums. Es wird eine Datenmenge von
r20
r23
25 Rechtecken {r1, r2, ... ,r25}
r21
I1
e6
betrachtet. Diese sollen per
Bulkloading in einen R-Baum geladen
werden.
e7
r17
r13
r16
r14
r18
r15
I2
I3
e1
e2
e3
e4
r12
Operationen dargestellt. Der BufferBaum besteht aus 3 Index-Knoten
(I1,..., I3) und fünf Daten-Knoten (D1,...,
D5). Wie wir sehen können sind noch
11 Prozesse im blocked Status, da
D5
deren Rechtecke noch in den Buffern
e5
r1
r3
r5
r7
r9
r2
r4
r6
r8
r10
r11
D1
D2
D3
D4
Grafik 3: Buffer-Baum nach den ersten 23 Einfüge-Operationen.
Seite 6/20
In der Grafik 3 ist der Buffer-Baum
nach den ersten 23 Einfüge-
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
zu finden sind. Die anderen 12 Prozesse sind terminated, die Rechtecke sind
in den Daten-Knoten zu finden. Zum Beispiel wartet der Prozess des
Rechtecks r18 im Knoten I3 auf die Reaktivierung, um sein Rechteck weiter
nach unten zu propagieren und in einen Daten-Knoten zu schreiben.
Das Einfügen eines Rechtecks im Buffer-Baum verläuft ähnlich zum Einfügen
in einen R-Baum. Jedoch blockiert ein Index-Knoten immer den Prozess und
schreibt das Rechteck in seinen Buffer, später wird dann der Prozess
reaktiviert. Der Einfüge-Ablauf von r24 sieht folgendermassen aus. Da der
Buffer des Root-Knotens (I1) nicht voll ist, wird der Prozess blockiert und
dessen Rechteck in den Buffer von I1 geschrieben. Der letzte Datenblock des
Root-Knotens wird jeweils im Speicher gehalten, da in den meisten Fällen
der Prozess blockiert wird. Somit muss nicht bei jedem Einfügen eines
Rechtecks in den Buffer, zuerst mittels eines I/O der letzte Buffer-Block aus
der Disk geladen werden. Nun kann das Rechteck r25 eingefügt werden, nun
besteht aber das Problem, dass bereits der Buffer voll ist und somit r25 nicht
eingefügt werden kann. Das heisst, es muss zuerst der Buffer geleert
werden, dies geschieht indem alle blockierten Prozesse des Root-Knotens
reaktiviert (sequentiell) werden. Jeder Prozess nimmt sein Rechteck aus dem
Buffer des Root-Knotens, dabei wird das Rechteck aus dem Buffer gelöscht
und geht weiter zum nächsten Unterbaum. Welcher Unterbaum ausgewählt
wird, wird durch Aufrufen der Funktion ChooseSubTree festgestellt. Wenn
alle Prozesse reaktiviert wurden, ist der Buffer leer (cleared), die Situation ist
in Grafik 4 dargestellt. Die Rechtecke haben sich folgendermassen
aufgeteilt, {r19, r20, r22, r23} befinden sich nun im Buffer des Knotens I2 und
die Rechtecke {r21, r24} sind im Buffer von I3.
Seite 7/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
I1
e6
e7
r17
r22
r13
r16
r19
r23
r14
r18
r15
r21
r20
I2
r24
I3
e1
e2
e3
e4
e5
r1
r3
r5
r7
r9
r2
r4
r6
r8
r10
r11
D1
D2
r12
D3
D4
D5
Grafik 4: Buffer-Baum nach dem Leeren des Root-Buffers
r21
I3
e5
e8
e9
r7
r10
r9
r14
r8
r13
r12
r16
r18
D4
D6
D7
r24
I3
I4
e4
e5
e10
Knotens I3 der Fall.
D i e s e O ve r f l o ws
werden ebenfalls
durch das Leeren der
entsprechenden
verschoben haben, siehe Grafik 5
oberer Teil. Es kann auch ein
Overflow in einem Daten-Knoten
vorkommen, dies wird
gleichermassen behandelt wie in
r15
D5
Knoten der Buffer
mehr als p Blöcke
enthält. Dies ist z.B.
bei dem Buffer des
Buffer beseitigt. In
unserem Beispiel hat dies zur
Folge, dass sich die Rechtecke
{r13, ... , r16, r18, r21, r24} weiter
hinunter auf die Daten-Ebene
r24
e4
Nach dem Leeren
eines Buffers kann es
vorkommen, dass bei
manchen Kind-
e8
e9
r8
r10
r7
r9
r14
r18
r13
r21
r12
r16
r15
einem R-Baum. Es wird der DatenKnoten in zwei Knoten gespalten
(mittels Split), anschliessend
wird der neue Routing-Eintrag zum
Vater-Knoten propagiert. Es wird
nun angenommen, dass der
Prozess von r21 reaktiviert und
mittels ChooseSubTree ermittelt
wurde, dass das Rechteck weiter
zum Daten-Knoten D4 gereicht
Grafik 5: Das Splitten des Knotens I3 in zwei Knoten.
werden soll. Nun kommt es zum erwähnten Overflow im Daten-Knoten D4,
D4
D5
D8
Seite 8/20
D6
D7
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
dieser wird wie erwähnt durch ein Split gehandhabt und der neue RoutingEintrag des Knotens D8 (wurde durch Split erzeugt) wird nach oben an I3
gereicht. Dies führt aber erneut zu einem Problem. Der Knoten I3 besitzt
bereits C Einträge in seiner Routing-Tabelle und somit kann der neue Eintrag
nicht eingefügt werden. Es wird erneut ein Split ausgeführt, diesmal wird
der Index-Knoten I3 aufgeteilt und es wird ein neuer Daten-Knoten I4
erzeugt. Nun müssen noch die Buffer-Einträge aufgeteilt werden. Dies
geschieht durch Aufrufen von ChooseSubTree mit I3 und I4 und es wird
zurückgeben, auf welchen Knoten die Buffer-Einträge weitergegeben
werden sollen.
Anschliessend wird noch derjenige Buffer, welcher mehr Einträge enthält,
geleert. Dies ist in unserem Beispiel der Buffer von I4. Nach dem Abschluss
wird noch der neue Routing-Eintrag für I4 an den Vater-Knoten (hier I1)
propagiert. Nach Abschluss aller Restrukturierungen kann r25 eingefügt
werden.
Nachdem r25 eingefügt wurde und eventuelle Prozesse reaktiviert wurden,
kann es dennoch sein, dass bestimmte Prozesse immer noch blockiert sind.
Um das Bulkloading zu beenden wird noch in einer Tiefensuche ausgehend
vom Root-Knoten alle nicht leeren Buffer geleert. In der Grafik 6 ist nun der
fertige Buffer-Baum mit allen Einträgen dargestellt. Die Daten-Knoten
{D1, ..., D10} entsprechen nun den Daten-Knoten des gesuchten R-Baums.
I1
I2
e6
e7
e12
I3
e1
e2
e3
I4
e13
e4
e5
e10
e8
e9
r1
r3
r5
r6
r8
r10
r7
r9
r14
r12
r2
r4
r11
r20
r18
r13
r21
r24
r16
r15
r19
r17
r22
r23
D1
D2
D3
D10
r25
D4
D5
D8
D6
D7
Grafik 6: Buffer-Baum nach Leerung aller Buffer
Seite 9/20
e11
D9
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
Im Normalfall entsprechen die Index-Knoten des soeben erstellten BufferBaums nicht den Index-Knoten des gewünschten R-Baums. Um die IndexKnoten zu erhalten wird ein Bottom-Up Verfahren angewendet, dieses wird
in Abschnitt 6.2 erläutert.
6.2. Der Bulkloading-Algorithmus im Detail
In diesem Abschnitt wird der Algorithmus, der im vorhergehenden Abschnitt
an einem Beispiel erläutert wurde, im Detail besprochen. Im Nachfolgenden
wird der generische Algorithmus für Bulkloading erläutert. Die einzigen
Vorraussetzungen, welche an die zugrunde liegende Indexstruktur gestellt
werden, sind die 3 Operationen, die in Abschnitt 4 aufgezeigt wurden.
Eine einfache Methode den Buffer-Baum zu implementieren, wäre mittels
verschiedenen Prozessen oder mittels Multi-Threading. Dies würde direkt
vom Betriebssystem unterstützt werden, jedoch ist die Methode nicht
effizient, da es zu einem hohen Management-Overhead kommt, da ContextSwitches immer teuer sind.
Die Grundidee des Algorithmus unterscheidet sich nur gering von der
bekannten Einfüge-Operation, die eine Indexstruktur zur Verfügung stellt. Im
Normalfall wird bei einem Einfügen der Baum nach unten traversiert und an
einer bestimmten Stelle eingefügt, was dann zu einer Restrukturierung des
• Die mehrfache
Einfüge-Operation
wurde im Abschnitt
6.1 erläutert.
Baums führen kann, welche nach oben propagiert werden muss. Der BufferBaum führt nun mehrere Einfüge- bzw. Restrukturierungs-Operationen auf
einmal aus. Eine Restrukturierungs-Operation wird entweder durch einen
Overflow in einem Daten-Knoten oder der Routing-Tabelle in einem IndexKnoten ausgelöst. Dabei wird der Knoten geteilt und ein neuer RoutingEintrag im Vater-Knoten eingetragen. Die Idee ist, das Eintragen von neuen
Routing Informationen ebenfalls zu Buffern (in einer Liste). Der Vater-Knoten
schreibt den einzutragenden Routing-Eintrag in eine Liste und wartet, bis
alle Unterbäume ihre Restrukturierung beendet haben. Anschliessend
werden alle Einträge der Liste in die Routing-Tabelle eingetragen. Dies kann
wiederum einen Overflow produzieren, der sich weiter nach oben
propagiert.
Seite 10/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
Kommen wir nun zum detaillierten Algorithmus inkl. Pseudocode. Der
Bulkloading Algorithmus startet mit dem Aufruf der Funktion
BulkLoading, der einen neuen Einfüge-Prozess startet. Für jedes
Datenobjekt, das geladen werden soll wird diese Funktion ausgeführt. Zu
Beginn besitzt der Buffer-Baum einen Index-Knoten mit einer Referenz auf
einen leeren Daten-Knoten.
function BulkLoading(Root, Record){
if (Root.BufferLoad = B*p) {
new_childs = ClearBuffer(Root);
new_siblings = InsertChilds(Root, new_childs);
if (!new_siblings.isEmtpy())
}
}
InsertIntoBuffer(Root, R);
create a new root from new_siblings;
Wenn sich im Root-Knoten weniger als B*p Einträge im Buffer befinden, wird
der Rekord in den Buffer eingefügt, somit wird der Prozess geblockt und die
Funktion wird beendet. Der Buffer ist dynamisch und vergrössert sich (bis
• Die 2*B*p werden bei
der ClearLeafBuffer
Funktion erläutert.
maximal 2*B*p Einträge), somit wird ein neuer Rekord immer im den letzten
Datenblock des Buffers eingefügt. Nach B*p Aufrufen von BulkLoading ist
der Buffer voll und muss geleert werden, was der Reaktivierung der Prozesse
entspricht. Dies wird durch ClearBuffer erledigt, welches eine Liste von
neuen Kindern zurückgibt, die noch dem Root-Knoten eingefügt werden
müssen. Anschliessend werden diese Kinder mittels InsertChilds
eingefügt, wenn Overflows im Root-Knoten auftreten, werden diese neuen
Einträge vom InsertChilds zurückgegeben. Falls wir Siblings erhalten,
wird ausgehend von diesen ein neuer Root-Knoten erzeugt.
Betrachten wir nun die InsertChilds Funktion, welche neue Childs aus
einer Liste (new_childs) in einen Knoten (Node) einfügt, detailliert. Jeder
Eintrag in der Liste wird in den Knoten eingefügt, dazu wird mit
ChooseSubTree ermittelt, welches der Parent für den neuen Eintrag ist.
Falls new_siblings leer ist, ist Parent immer der Node. Wenn es im Parent zu
einem Overflow gekommen ist, wird dieser Parent geteilt und die neuen
Seite 11/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
Routing-Informationen werden in die new_siblings Liste aufgenommen.
Beim nächsten ChooseSubTree wird dieser neue Eintrag auch als
potentieller Parent in Betracht gezogen. Ist das Einfügen beendet und die
new_siblings Liste nicht leer, muss zudem der Buffer von Node aufgeteilt
werden.
function InsertChilds(Node, new_childs){
new_siblings = {};
foreach entry E in new_childs {
all_sibs = new_siblings
Parent = ChooseSubTree(all_sibs, E);
InsertIntoNode(Parent, E);
if (Parent.HasOverflow()) {
}
}
if (!new_siblings.isEmtpy()) {
foreach record R in Node.Buffer() {
Target = ChooseSubTree(all_sibs, R);
move R from Node.Buffer() to
Target.Buffer();
}
}
return new_siblings;
{entry of Node};
Split(Parent);
insert new entry into new_siblings;
}
Eine zentrale Funktion des Algorithmus ist die ClearBuffer Funktion,
welche den Buffer eines Knotens leert. Wir müssen hierbei zwischen
internen und leaf Index-Knoten unterscheiden, da diese getrennt behandelt
werden müssen.
Seite 12/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
function ClearBuffer(Node){
if (Node.isLeaf)
else
return ClearLeafBuffer(Node);
return ClearIndexBuffer(Node);
}
Für den Fall eines internen Knotens sieht die Funktion folgendermassen aus.
Für die ersten B*p Einträge aus dem Buffer wird mittels ChooseSubTree
ermittelt, in welchen Kind-Knoten der Eintrag eingefügt werden soll.
Anschliessend wird der Eintrag R in den Buffer des Kind-Knotens eingefügt.
Falls es in diesem Knoten zu einem Overflow gekommen ist, wird der Knoten
in der overflow_list vermerkt. Wurden alle B*p Einträge verarbeitet, wird für
jeder Knoten in der overflow_list ebenfalls noch ein ClearBuffer
ausgeführt und das Resultat in die new_childs Liste geschrieben. Die
new_childs Liste enthält nun Kinder, die durch ein splitten erzeugt wurden
und noch in den aktuellen Node eingefügt werden müssen. Anschliessend
werden alle diese Kinder-Knoten noch in den aktuellen Knoten eingefügt,
wobei die neuen Knoten, die eventuell durch ein Split erzeugt wurden,
zurückgegeben werden.
Eine wichtige Eigenschaft der Funktion ist es, dass der Buffer nur teilweise
geleert wird, also die ersten B*p Einträge. Dies hat zur Folge, dass ein Buffer
nur B*p Überlauf-Einträge besitzen kann, da maximal B*p Einträge vom
Vater erhalten werden können. Somit besitzt ein Buffer niemals mehr als
2*B*p Einträge und die maximale Grösse des Buffers ist bekannt.
Seite 13/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
function ClearIndexBuffer(Node){
overflow_list = {};
foreach R in first B*p records in Node.Buffer() {
Child = ChooseSubTree(Node,R);
InsertIntoBuffer(Child,R);
if (Child.BufferLoad > B*p)
}
new_childs = {};
foreach Child in overflow_list
return InsertChilds(Node, new_childs);
insert Child into overflow_list;
add ClearBuffer(Child) into new_childs;
}
Im zweiten Fall, falls wir den Buffer eines leaf Index-Knotens leeren
möchten, gehen wir wie folgt vor. Für jeden Eintrag im Buffer wird mittels
ChooseSubTree ermittelt wohin der Eintrag gehen soll, da wir uns in
einem leaf Index-Node befinden, gibt uns ChooseSubTree den DatenKnoten zurück. Durch das Einfügen des Eintrags in den Daten-Knote, kann es
zu einem Overflow gekommen sein, folglich muss ein Split ausgeführt
werden. Aufgrund des Einfügens der neuen Routing-Information, kann es
wiederum zu einem Overflow im leaf Index-Knoten kommen. Somit wird der
Knoten inklusive Buffer gesplittet und der neue Eintrag in der new_siblings
Liste vermerkt. Anschliessend wird auf dem Knoten ein ClearLeafBuffer
ausgeführt, welches den grösseren Buffer besitzt und die eventuellen
Rückgabewerte ebenfalls in der new_siblings Liste, welche anschliessend an
den Vater-Knoten weitergegeben wird, vermerkt.
Seite 14/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
function ClearLeafBuffer(Node){
foreach record R in Node.Buffer() {
DataNode = ChooseSubTree(Node,R);
InsertIntoNode(DataNode,R);
if (DataNode.HasOverflow()) {
Split(DataNode);
insert new entry into Node;
if (Node.HasOverflow()) {
SplitWithBuffer(Node);
new_siblings = {};
insert new entry into new_siblings;
//N is the new node
if (N.BufferLoad > Node.BufferLoad){
add ClearLearBuffer(N) to
new_siblings;
}else{
add ClearLearBuffer(Node) to
}
new_siblings;
return new_siblings;
}
}
}
return {};
}
Nachdem BulkLoading für alle Datenobjekte aufgerufen wurde, werden
noch alle nicht leeren Buffer mittels einer Tiefensuche geleert. Nun
entspricht die letzte Ebene das Baums, also die ebene der Daten-Knoten,
deren der gesuchten Indexstruktur. Die Index-Knoten können nicht
verwendet werden, da im Normalfall die Grösse der Knoten des BufferBaums nicht mit der Grösse der Indexstruktur übereinstimmt.
Die Index-Knoten der gesuchten Indexstruktur werden nun Ebene für Ebene
erstellt. Es werden alle Routing-Einträge der untersten Knoten-Ebene (Ebene
der leaf Index-Knoten) in einen neuen Buffer-Baum eingefügt, die RoutingEinträge werden wie Datenobjekte behandelt. Anschliessend wird von
Seite 15/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
diesem Buffer-Baum wiederum die Routing-Einträge der untersten leaf
Index-Ebene in einen neuen Buffer-Baum eingefügt. Dies geschieht solange
bis der resultierende Buffer-Baum aus nur noch einem einzelnen IndexKnoten mit einem Routing-Eintrag besteht.
Da für das Aufbauen der Indexstruktur Funktionen benutzt werden, die die
Indexstruktur anbietet, ist klar das die Abfrage-Performance nicht
beeinträchtigt wird.
6.3. Komplexitätsanalyse
Die untere Grenze für das Bulkloading ist gegeben durch das externe
Sortieren. Diese Grenze lässt sich mittels einem multiway Mergesort
erklären, welcher folgendermassen arbeitet. Es werden zu Beginn jeweils M
Datenobjekte in den Speicher geladen und sortiert, anschliessend werden
diese Datenobjekte wieder auf die Disk geschrieben und die nächsten M
Datenobjekte geladen. Hierzu benötigen wir Ο ( n ) I/Os. Nun müssen die
⎡⎢ N / M ⎤⎥ = ⎡⎢ n / m ⎤⎥ Listen, welche bereits sortiert sind, noch gemerged
werden. Es wird nun gebrauch gemacht vom grossen Hauptspeicher. Es
werden im Hauptspeicher m Buffer der Grösse B gehalten und m-1 Buffer
sortiert in den letzten Buffer gemerged. Ist ein Buffer komplett
abgearbeitet, wird der nächste Diskblock der entsprechenden Liste geladen.
Das heisst, es müssen alle n Diskblöcke einmal geladen und geschrieben
werden, dies führt zu Ο ( n ) I/Os. Im nächsten Durchlauf müssen um den
Faktor m-1 weniger Listen erneut gemerged werden. Somit wird die MergeRoutine log m −1 n / m mal durchlaufen, welche jeweils Ο ( n ) I/Os benötigt. Wir
erhalten für die totale Anzahl I/Os den folgenden Wert.
Ο ( n + n log m −1 n / m ) = Ο ( n + n log m n / m )
= Ο ( n + n log m n / m ) = Ο ( n + n log m n − n log m m )
= Ο ( n + n log m n − n ) = Ο ( n log m n )
Seite 16/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
Für die Komplexitätsanalyse des Bulkloading betrachten wir das Erstellen
eines R-Baums mit Hilfe eines Buffer-Baums. Das Ziel ist es, zu zeigen, dass
der Bulkoading-Algorithmus Ο ( n log m n ) I/Os benötigt und somit der unteren
Grenze des externen Suchens entspricht.
Der Parameter C spielt eine wichtige Rolle und beeinflusst die Kosten, die
zum Leeren eins Buffers benötigt werden. Damit diese Kosten klein bleiben,
sollen sämtliche Informationen, die benötigt werden, im Speicher gehalten
werden. Aus diesem Grunde soll die Routing-Tabelle eines Knotens sowie der
letzte Datenblock des Buffers und ein Datenblock für jeden Unterbaum im
Speicher gehalten werden. Dies führt zu der folgenden Constraint für den
⎡C ⎤
⎢ ⎥ + C +1≤ m
Parameter C. ⎢ B ⎥
Es wird die grösste Zahl C gewählt, welche die Constraint erfüllt, somit ist
C ≈ Ο ( m ) im Folgenden nehmen wir an, das p=1/2*C ist.
Theorem 1:
Die totalen I/O Kosten um N Rechtecke in einen leeren
R-Buffer-Baum einzufügen ist Ο ( n log m n ) .
Beweis:
Um dies zu beweisen, betrachten wir die Kosten für die
Leerung eines vollen Buffers (Internen-Knoten). Ein
Buffer besitzt maximal 2*B*p=B*C Datenobjekte. Es muss zu
Beginn die gesamten Routing-Informationen geladen werden,
dies benötigt Ο ( m ) I/Os. Zusätzlich muss für ein komplettes
Leeren eines Buffers, alle Datenobjekte des Buffers einmal
geladen werden was ebenfalls Ο ( m ) I/Os benötigt, da B*C
Datenobjekte C Diskblöcke entspricht und C ≈ Ο ( m ) ist.
Amortisiert betrachtet, benötigt folglich jedes Datenobjekt
Ο (1 / B ) I/Os. Ein Datenobjekt ist auf seinem Weg bis zum
Seite 17/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
Daten-Knoten in maximal Ο ( log m n ) vollen Buffern enthalten
und wird somit maximal Ο ( log m n ) aus einem Buffer geleert.
Dies macht für ein Datenobjekt totale I/O Kosten von
Ο (1 / B * log m n ) und für insgesamt N Datenobjekte folgern
sich Kosten von Ο ( N / B * log m n ) = Ο ( n log m n ) I/Os.
Es muss noch bewiesen werden, dass auch das Leeren von
Leaf-Knoten Ο ( m ) I/Os und die totalen Kosten für das Splitten
von Knoten Ο ( n ) I/Os benötigen. Die Beweise hierzu können
dem Dokument [1] entnommen werden.
Theorem 2:
Die I/O Kosten für das Leeren aller Buffer eines R-Buffer-Baums
ist Ο ( n ) .
Beweis:
Die totale Anzahl an Buffern ist Ο ( n / m ) . Wie zuvor erwähnt,
benötigt das Leeren eines Buffers Ο ( m ) I/Os. Dies kann zu
Splitoperationen führen, welche insgesamt
Ο (n)
I/Os
benötigen. Somit erhalten wir Ο ( n ) I/Os für das Leeren aller
Buffer eines R-Buffer-Baums.
Die Theoreme 1 und 2 zeigen, dass das Einfügen von N Rechtecken in einen R-
N⎞
⎛N
Buffer-Baum mit Ο ⎜ log m ⎟ I/Os ausgeführt werden kann. Somit
⎝B
B⎠
Seite 18/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
erhalten wir einen Buffer-Baum, dessen Daten-Knoten mit der gesuchten
Indexstruktur übereinstimmen. Die restlichen Ebenen werden rekursiv durch
erneutes Aufbauen eines R-Buffer-Baums erstellt. Beim Erstellen der ersten
Index-Ebene müssen Ο ( N / B ) Routing-Einträge in einen leeren R-BufferBaum eingefügt werden. Somit ergeben sich für diese Ebene totale I/O
N⎞
⎛ N
Kosten von Ο ⎜ 2 log m 2 ⎟ .
⎝B
B ⎠
Nehmen wir an, die gesuchte Indexstruktur besitze die Höhe h, dann
N ⎞
⎛ N
benötigt das Erstellen der Ebene (h-i) Ο ⎜ i +1 log m i +1 ⎟ I/Os.
⎝B
B ⎠
Die
totalen
Kosten
fürs
Bulkloading
sehen
wie
folgt
aus.
⎛ h N
N⎞
N⎞
⎛N
∑ Ο ⎜⎝ Bi log m Bi ⎟⎠ ≤ Ο ⎜⎝ ∑ Bi log m Bi ⎟⎠
i=0
0 ≤i
h
⎛
⎛ h N
⎞
⎛
N ⎞⎞
= Ο ⎜ ∑ i log m n ⎟ = Ο ⎜ n log m n ⎜ ∑ i ⎟ ⎟ =
⎝ 0 ≤i B
⎠
⎝ 0 ≤1 B ⎠ ⎠
⎝
Ο ( n log m n )
Somit ist der Algorithmus aus folgenden Überlegungen asymptotisch
optimal.
• Es wird die untere Grenze des externen Sortierens erreicht.
• Ein 1-dimensionaler R-Buffer-Baum kann für das Sortieren verwendet
werden.
7. Fazit
Bulkloading ist markant schneller um eine Indexstruktur aufzubauen, als
Eintrag für Eintrag einzufügen. Bisherige Ansätze waren meistens
beschränkt auf R-Bäume und hatten mässige Abfrage-Performance, da der
erzeugte R-Baum schlecht aufgebaut wurde.
Seite 19/20
Seminararbeit - Bulkloading Indexes - Fabrizio Steiner
Der in dieser Ausarbeitung vorgestellte Algorithmus ist generisch und somit
unabhängig von der zu Grunde liegenden Indexstruktur. Es müssen lediglich
die in Abschnitt 4 definierten Funktionen zur Verfügung stehen. Der
Algorithmus ist optimal in der I/O Kostenanalyse und benötigt im
wesentlichen dieselben Funktionen wie eine normale Einfüge-Operation,
dadurch wird die Abfrage-Performance nicht beeinträchtigt.
8. Referenzen und Literatur
[1]
Jochen van der Bercken, Bernhard Seeger and Peter Widmayer: “A
Generic Approach to Bulk Loading Multidimensional Index
Structures”, Proceedings of the 23rd VLDB Conference Athens,
Greece, 1997
[2]
L. Arge, K. H. Hinrichs, J. Vahrenhold and J. S. Vitter: “Efficient Bulk
Operations on Dynamic R-Trees”, Algorithmica 33, 104-128, 2002
[3]
L. Arge: “The Buffer Tree: A Technique for Designing Batched
External Data Structures”, Algorithmica 37, 1-24, 2003
[4]
A. Aggarwal and J. S. Vitter: “The input/output complexity of
sorting and related problems”, Communications of the ACM, 31
(9): 1116-1127, 1988
[5]
N. Rossopoulos, D. Leifker: “Direct Spatial Search on Pictorial
Databases Using Packed R-Trees”, Proc SIGMOD, 17-31, 1985
[6]
S.T. Leutenegger, J. Edgington, M. A. Lopez: “Efficient Bulk Loading of
R-Trees”, University of Denver, Technical Report 95-1
[7]
David J. DeWitt, Navin Kabra, Jun Luo, Jignesh M.Patel, Jie-Bing Yu:
“Client-Server Paradise”, Proc VLDB, 558-569, 1994
Seite 20/20
Herunterladen