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