18.3 Parallele Joins
Dieses optimale Leistungsverhalten ist jedoch nicht mehr erreichbar, wenn die innere Relation zu groß für eine vollständige Hauptspeicherallokation ist. In diesem Fall ist eine Überlaufbehandlung erforderlich, wofür es zahlreiche Alternativen gibt. Wir beschränken unsere Betrachtungen auf Ansätze, welche auf einer Partitionierung der Relationen basieren. Diese Idee wurde zuerst für die GRACE-Datenbankmaschine realisiert [Ki83]. Beim GRACE-Hash-Join wird vor der eigentlichen Join-Bearbeitung eine Partitionierungs-Phase durchgeführt (Abb. 18-4a). In dieser Phase wird zunächst die innere Relation S gelesen und mittels einer auf dem Join-Attribut angewendeten Partitionierungsfunktion g (Hash- oder Bereichspartitionierung) in q Partitionen S1 bis Sq unterteilt, so daß jede S-Partition im Hauptspeicher gehalten werden kann. Zur Partitionierung werden im Hauptspeicher q Ausgabepuffer von mindestens 1 Seite reserviert, welche auf Externspeicher ausgeschrieben werden, sobald sie keine weiteren S-Tupel mehr aufnehmen können. Das Ausschreiben der erzeugten Partitionen auf temporäre Dateien erfolgt somit weitgehend asynchron. Nach Partitionierung der S-Relation erfolgt eine analoge Zerlegung von R in q Partitionen R1 bis Rq unter Anwendung derselben Partitionierungsfunktion. Die Partitionierung garantiert, daß die Verbundpartner von Partition Si nur in der Partition Ri vorliegen können (für alle i, 1 i q). Daher genügt es zur eigentlichen Join-Berechnung, wie in Abb. 18-4b,c illustriert, das Hash-Join-Basisverfahren auf jede der q zusammengehörigen S- und R-Partitionen anzuwenden (Einlesen von Si in eine Hauptspeicher-Hash-Tabelle; Lesen von Ri mit Bestimmung der Verbundpartner).
Der Hybrid-Hash-Join verlangt jedoch i.a. eine ungleichmäßige Partitionierung, um den Hauptspeicher für die erste Partition weitgehend nutzen zu können. Zudem ist die maximale Nutzung des Hauptspeichers im Einbenutzerbetrieb zwar sinnvoll, kann allerdings im Mehrbenutzerbetrieb gleichzeitig laufende Transaktionen höherer Priorität stark benachteiligen. Diese Nachteile können durch einen adaptiven Hash-Join umgangen werden, der auf der gleichförmigen Zerlegung in q möglichst gleichgroße Partitionen aufbaut. Ein solcher Ansatz ist der in [PCL93] vorgestellte "Partially Preemptible Hash Join" (PPHJ). Dabei werden für die ersten k der q S-Partitionen die Hash-Tabellen im Hauptspeicher gehalten, für die q-k anderen lediglich ein Ausgabepuffer von einer Seite. Somit läßt sich für die k ersten Partitionen die Join-Berechnung unmittelbar ausführen, und nur für die restlichen Partitionen werden E/A-Vorgänge auf temporäre Dateien nötig. Weiterhin kann die Anzahl der im Hauptspeicher resident gehaltenen S-Partitionen dynamisch variiert werden. So wird deren Anzahl verringert, wenn die Hash-Tabellen wegen ungenauer Schätzungen doch nicht vollständig Platz im Hauptspeicher finden oder wenn aufgrund von Speicheranforderungen durch andere Transaktionen der für die Join-Bearbeitung verfügbare Platz sinkt. Für die betroffenen Partitionen werden dann die Tupel der Hash-Tabelle auf eine temporäre Datrei ausgeschrieben und der Speicherplatz bis auf eine Seite für den Ausgabepuffer freigegeben. Umgekehrt können bei wachsender Hauptspeicherverfügbarkeit während der Partitionierungs-Phase einige bereits auf Platte geschriebene S-Partitionen wieder in den Hauptspeicher gebracht werden, um die weitere Join-Bearbeitung direkt auf ihnen vorzunehmen.
Eine allgemeine Optimierungsmöglichkeit zur Reduzierung von E/A-Vorgängen auf temporären Dateien besteht in der Nutzung von Bitvektoren. Der Bitvektor basiert dabei auf einer auf dem Join-Attribut anzuwendenden Hash-Funktion (z.B. die zur Join-Verarbeitung genutzte Funktion h) und enthält für jede Hash-Klasse ein Bit. Während des Einlesens der S-Relation in der Partitionierungs-Phase wird die Hash-Funktion auf jedes S-Tupel angewendet und das zugehörige Bit gesetzt. Dies kann dann zur Reduzierung der größeren Relation R genutzt werden, da nur noch solche R-Tupel in der weiteren Verarbeitung zu berücksichtigen sind, für die das zugehörige Bit gesetzt ist. Durch diese Verkleinerung der R-Partitionen können oft zahlreiche E/A-Vorgänge eingespart und die Join-Bearbeitung beschleunigt werden. Ein hoher Filtereffekt kann bereits erwartet werden, wenn die Anzahl der Bits doppelt so hoch liegt wie die Tupelanzahl der inneren Relation S [Gra93].
Die dynamische Partitionierung der Eingaberelationen zur Parallelisierung der Join-Bearbeitung ähnelt offenbar der Partitionierung zur Überlaufbehandlung im zentralen Fall. Denn in beiden Fällen wird über eine Hash- oder Bereichspartitionierung eine Zerlegung der beteiligten Relationen in mehrere Partitionen vorgenommen, wobei die Join-Bearbeitung für eine Partition der inneren Relation auf genau eine Partition der äußeren Relation beschränkt wird. Dies gestattet nun eine unmittelbare Parallelisierung. Zudem kann im verteilten Fall aufgrund des (im Mittel um den Faktor p) verringerten Join-Umfangs pro Knoten mit einer höheren Wahrscheinlichkeit das Basisverfahren angewendet werden, bei dem keine Überlaufbehandlung notwendig ist[84].
Wenn die S-Partitionen in den Join-Rechnern jedoch nicht vollständig im Hauptspeicher Platz finden, wird eine zusätzliche dynamische Partitionierung zur Überlaufbehandlung erforderlich. Diese kann prinzipiell vor der Umverteilung an den Datenknoten oder nach der Umverteilung an den Join-Rechnern erfolgen. Wird die Überlaufbehandlung an den Join-Rechnern vorgenommen, ergibt sich folgende Vorgehensweise:
1. Paralleles Lesen und Umverteilen von S
an jedem S-Knoten j (j=1, ..., m) führe parallel durch:
lies lokale S-Partition Sj und sende jedes S-Tupel an zuständigen Join-Rechner (Anwendung der Verteilungsfunktion f auf dem Join-Attribut)
2. Paralleles Partitionieren der S-Partitionen
in jedem Join-Knoten k führe parallel durch (k=1...p)
- Sk' := Sjk (j=1..m) (Menge der von Join-Knoten k empfangenen S-Tupel)
- Partitionierung in q Sub-Partitionen zur Überlaufbehandlung Sk1' bis Skq'
durch Anwendung einer Partitionierungsfunktion g auf dem Join-Attribut
3. Paralles Lesen und Umverteilen von R
an jedem R-Knoten i (i=1..n) führe parallel durch:
lies lokale R-Partition Ri und sende jedes R-Tupel an zuständigen Join-Rechner (Verteilungsfunktion f)
4. Paralleles Partitionieren der R-Partitionen
in jedem Join-Knoten k führe parallel durch (k=1...p)
- Rk' := Rik (i=1..n) (Menge der von Join-Knoten k empfangenen R-Tupel)
- Partitionierung in q Sub-Partitionen zur Überlaufbehandlung Rk1' bis Rkq'
durch Anwendung einer Partitionierungsfunktion g auf dem Join-Attribut
Nach diesem Schritt ergibt sich die in Abb. 18-6 gezeigte Situation.
5. Parallele Join-Berechnung
in jedem Join-Knoten k führe parallel durch (k=1...p):
für l = 1, 2 bis q führe nacheinander aus:
berechne Skl' Rkl'
schicke Ergebnis an Koordinator
Die Überlaufbehandlung an den Join-Rechnern bringt den Vorteil mit sich, daß dort praktisch jeder lokale Ansatz gewählt werden kann. So entspricht der gezeigte Algorithmus der parallelen Version eines GRACE-Hash-Joins. Die Realisierung eines parallelen Hybrid-Hash-Joins ist analog möglich. Hierzu wird in jedem Join-Rechner k bei der Überlaufbehandlung die jeweils erste S-Partition Sk1' im Hauptspeicher gehalten, um E/A-Vorgänge einzusparen und mit den zugehörigen R-Tupeln die Join-Berechnung sofort vornehmen zu können (Voraussetzung hierfür ist jedoch wieder die Sequentialisierung von S- und R-Bearbeitung). In ähnlicher Weise können auch adaptive Hash-Verfahren in den Join-Rechnern angewendet werden.
Alternativ zu der beschriebenen Vorgehensweise kann eine Überlaufbehandlung bereits an den Datenknoten erfolgen (s. Übungsaufgaben). Ein Vorteil dabei ist die Vermeidung von E/A-Vorgängen an den Join-Rechnern, so daß selbst Rechner ohne Platten (diskless nodes) zur Join-Berechnung nutzbar werden.
Auch für die parallele Hash-Join-Berechnung kann die Anzahl der Join-Prozessoren sowie ihre Auswahl wieder dynamisch - in Abhängigkeit der aktuellen Systemauslastung - festgelegt werden. Die Untersuchung [RM95] zeigte, daß hierfür vor allem die aktuelle Hauptspeicherverfügbarkeit der einzelnen Rechner berücksichtigt werden sollte, um eine Zuordnung zu finden, die mit möglichst wenigen E/A-Vorgängen zur Überlaufbehandlung auskommt. Dazu ist es bei hoher Hauptspeicherauslastung empfehlenswert, eine höhere Anzahl von Join-Prozessoren zu wählen, um den Speicherbedarf pro Knoten zu reduzieren. Dies ist die entgegengesetzte Vorgehensweise wie zur Behandlung von CPU-Engpässen, bei denen sich zur Reduzierung des Kommunikations-Overheads eine Reduzierung des Paralleltätsgrades empfiehlt.