Ausarbeitung Hauptseminar Data Stream Management Systems
Transcription
Ausarbeitung Hauptseminar Data Stream Management Systems
Ausarbeitung Hauptseminar Data Stream Management Systems Thema: Optimierungsmodelle für DSMS Bearbeiter: Andreas Unseld Betreuer: Steffen Rost 1 1. Einleitung 1.1 Beispiel 1: Beispiel zur Optimierung von Programmen 2. Herkömmliche Optimierungsmodelle 2.1 Ein Überblick über die Verarbeitung von SQL – Statements in System R 2.2 Die Kostenanalyse im Detail 2.3 Beispiel 2 3. Optimierungsmodelle für Kontinuierliche Querys 3.1 Problemstellung 3.2 Beispiel 3: Beispiel zur Problemstellung 4. Moving Window (time-unit based optimization) 4.1 Einführung 4.2 Beispiel 4: Beispiel zu Moving Windows 4.3 Optimierung von Moving Windows 4.4 Funktionsweise von NLJ und HJ 4.5 Allgemeine Kostenabschätzung für Joins 4.6 Kostenabschätzung für Nested Loop Joins 4.7 Kostenabschätzung für Hash Joins 4.8 Beispiel 5 5. Grundlagen der rate-based Optimierung 5.1 Ein Überblick 5.2 Die Ausgaberate als Funktion von der Eingaberate 5.3 Kostenanalyse für Joins 5.4 Beispiel 6 5.5 Genereller Rahmen für die rate-based Optimierung 5.6 Beispiel 7 6. Fazit 7. Anhang 2 1. Einleitung Optimierungen sind aus den verschiedensten Bereichen bekannt. Nicht nur von der Informatik, sondern auch aus der Industrie. Beispiele hierfür sind vor allem Prozessoptimierungen in Fertigungsanlagen oder Optimierungen im Bezug auf Transaktionen und Budget. In der Informatik assoziieren wir Optimierung mit der Reduktion von CPU-, Speicher- und I/O Last. Eine solche Ressourcenschonung lässt sich z..B. durch Verbesserungen im Programmcode erreichen, indem „Überflüssige“ Befehle wegrationalisiert werden. Hierbei können, - insbesondere bei oft wiederholten Programmschleifen – deutliche Performancesteigerungen erzielt werden. Diese sogenannte Code – Optimierung übernimmt mittlerweile der Compiler. Man kann die Code –Optimierung also als letzen Schritt in der Erstellung eines Programms verstehen. Die Basis für ein optimiertes Programm ist allerdings ein möglichst optimales Programmmodell. Unter einem Programmmodell kann man eine Projektion des Programmablaufs auf eine vereinfachte modellhafte Struktur verstehen. Analog zum Programmmodell ist ein Optimierungsmodell eine vereinfachte Struktur eines meist deutlich komplexeren Vorganges. 1.1 Ein Beispiel zur Optimierung von Programmen (Beispiel 1): Es soll ein Programm erstellt werden, dass die Summe X = ∑x (i = 1..100) berechnet. Eine Lösung wäre: X = 0; For ( i = 1; i <=100; i++) X = X + i; Die Direkte Übersetzung in Assembler ist: Mov AX, 0 ;X Mov BX, 1 ;i Mov CX, 100 Mark: Add AX, BX Inc BX Loop Mark Der Codeoptimierer des Compilers könnte daraus machen: Mov AX, 0 ;X Mov CX, 100 Rep Add AX,CX Das verkleinert den Rechenaufwand auf weniger als die Hälfte im Vergleich zum ersten Programm, behält aber dennoch den Aufwand O(x). Ein findiger Programmierer könnte allerdings das Programm: X = n(n+1) / 2 entwickeln, welches für n = 100 das gewünschte Ergebnis liefert und den konstanten Aufwand O(1) hat. 3 2. Herkömmliche Optimierungsmodelle: In dieser Ausarbeitung sollen Optimierungsmodelle für Data Stream Management Systeme (DSMS) behandelt werden. Um einen Überblick zu bekommen, was ein Optimierungsmodell für DSMS ist, betrachten wir zunächst herkömmliche Optimierungsmodelle wie sie für Datenbank Management System (DBMS) verwendet werden. Dazu sehen wir uns die Verarbeitung von SQL – Statements im DBMS System – R an. Der Optimierer von System R bietet, obwohl schon sehr alt, einen guten Einblick in das Thema. System R war ein experimentelles Datenbanksystem von IBM, welches in den siebziger Jahren entwickelt wurde. Es gilt als eines der ersten SQL – Datenbanksysteme. Aus dem System R Projekt stammt das „Standart – Paper“ [1] zur Optimierung von DBMS. Die Autorin Selinger beschreibt darin ein Kosten – basiertes Optimierungsmodell. 2.1 Ein Überblick über die Verarbeitung von SQL – Statements in System R System R erhält SQL – Statements entweder als Benutzereingabe von einem Terminal, oder als Eingabe aus einem anderen Programm. Sobald System R das Statement vollständig erhalten hat, werden vier Schritte zur Verarbeitung und Beantwortung der Eingabe durchgeführt: 1. Parsen des Statements: Der Parser untersucht die Query – Blocks der Eingabe auf syntaktische Korrektheit. Ein Query – Block besteht aus einer SELECT Liste, einer FROM Liste und aus einem WHERE – Baum. Die SELECT Liste gibt an, welche Attribute gesucht sind. Die FROM Liste beinhaltet die Tabellen, in denen gesucht werden soll und der WHERE – Baum enthält die abzurufenden Items, Referenzen auf die benötigten Tabellen und boolesche Verknüpfungen von Bedingungen die durch den Benutzer (oder ein Programm) definierten wurden. Man sollte beachten dass ein Statement aus mehreren Query – Blocks bestehen kann, da die im WHERE – Baum gestellten Bedingungen wiederum Querys enthalten können. Ein Beispiel für einen Query – Block ist: SELECT Name FROM Mitarbeiter WHERE (Beruf != `Geschäftsführer` AND Alter > 50) Wenn die syntaktische Analyse beendet wurde, und keine Fehler im Statement gefunden wurden, so wird das Statement an das Optimierungsmodul weitergereicht. 2. Optimierung: Das Optimierungsmodul in System R hat den größten Aufgabenbereich. Es leistet die Vorarbeit, welche für die Optimierung notwendig ist, optimiert die Queries in Bezug auf die benötigte Verarbeitungszeit und übergibt den resultierenden Ausführungsplan in ASR (Access Specification Language) an den Code – Generator. Dieser Vorgang soll im Folgenden präziser erläutert werden. 4 Im Vorfeld der Optimierung berechnet das Optimierungsmodul die Tabellen und Spalten, die in den Query – Blöcken referenziert werden und verifiziert deren Existenz. Zudem sammelt das Modul Informationen und Statistiken über die einzelnen Referenzen. Das sind insbesondere die Physikalische Verteilung der Daten einer Tabelle auf dem Datenträger, die Kardinalität der Tabellen (Kardinalität = Anzahl von Reihen in einer Tabelle) und die so genannten Access – Paths. Access – Paths sind Wege über die auf die gewünschte Information zugegriffen werden kann. Das soll ein Beispiel verdeutlichen: Gegeben sind 4 Tabellen die sich wie in der Grafik dargestellt referenzieren: Tabelle D Tabelle A Tabelle B Tabelle C Grafik: Beispiel für Access – Paths Gesucht sind Elemente aus der Tabelle C. Das Optimierungsmodul hat aber nur Referenzen auf die Tabelle A und die Tabelle D. Also muss es sich, um auf die Tabelle C zugreifen zu können zwischen den Access – Paths A Æ B Æ C oder D Æ C entscheiden. Da der Path D Æ C der Kürze ist, würde die Entscheidung auf ihn fallen. Sind die wesentlichen Daten gesammelt, so wird die Ausführungsreihenfolge der Query – Blocks bestimmt. Ist dies geschehen, so werden die Relationen in den Blocks betrachtet. Befinden sich in einem Block mehrere Relationen, (dies ist der Fall bei Joins) so wird die Join – order (die zeitliche Ausführungsreihenfolge der Joins) permutiert. Aus diesen Permutationen entstehen die Query – Plans, welche dann dem eigentlichen Optimierungsprozess übergeben werden. Ein Query – Plan ist eine Möglichkeit eine Query zu beantworten. Haben wir z.B. eine Query welche die Joins A B und A C beinhaltet, so wäre ein Query – Plan: (A B) C und (A C) B ein Anderer. Der in System R verwendete Optimierer (Kernkomponente des Optimierungsmoduls) optimiert Querys, indem er die Kosten der möglichen Query - Pläne abschätzt, und den günstigsten Plan zur Ausführung bringt. Die Kosten eines Plans entstehen aus der Summe von der geschätzten Anzahl zu ladenden Seiten (IO) und der geschätzten Anzahl von auszuführender Instruktionen (CPU). Diese Schätzungen werden mit Hilfe der im Vorfeld gesammelten Daten durchgeführt. Im Folgenden soll nun die Kostenanalyse etwas genauer betrachtet werden. 5 2.2 Die Kostenanalyse im Detail Eines der Schlüsselkonzepte von System R war die Klassifikation von Operatoren in Bezug auf ihre Trennschärfe (engl.: selectivity). Die Trennschärfe gibt an, wie viele Tupel einer Tabelle durch den Operator affektiert werden. Das kann an dem oben gezeigten Beispiel erläutert werden: Plan SELECT Name FROM Mitarbeiter WHERE ( Beruf != `Geschäftsführer` AND Alter > 50) Hier könnte man annehmen, dass das Alter der Mitarbeiter zwischen 20 und 60 gleich verteilt ist. Dementsprechend sind 25 % der Mitarbeiter vom Operator > in Alter > 50 betroffen. Der Trennschärfe – Faktor liegt hier also bei 0.25. Selinger präsentiert in ihrem Paper eine Tabelle von Operatoren, und Strategien um deren Trennschärfen zu schätzen. Diese Strategien sind von der physischen Verteilung der Daten in der Datenbank abhängig. Sind keine oder nur unzureichende Statistiken bekannt, so wird der Trennschärfe-Faktor einfach auf F=1/10 (magic number) geschätzt. DBKatalog: Kardinalität en von einzelnen Relationen Statistik über Verteilung von Relationen Trennschärf en von Prädikaten Kosten für den Plan Wenn man die Verteilung von Daten, und den Trennschärfe – Faktor von jedem Operatortyp kennt, lassen sich die Kardinalitäten von jeder durch Datenbankoperatoren entstehenden Tabelle und der Aufwand die Daten von einem Festspeicher zu lesen, abschätzen. Hierzu wird zuerst die Kardinalität von jeder gespeicherten Relation bestimmt. Diese Information erhält der Optimierer aus dem Datenbankkatalog. Wenn nun ein Operator auf eine Tabelle in System R angewendet wird, multipliziert der Optimierer die ursprüngliche Kardinalität mit dem Trennschärfe – Faktor des Operators. Daraus resultiert die Kardinalität der entsehenden Tabelle. Aus den Kardinalitäten lassen sich nun die Kosten für die einzelnen Operatoren eines Plans abschätzen. Die Gesamtkosten für den Plan sind die Summe der Einzelkosten. 6 2.3 Beispiel 2 Das zuvor gezeigte soll nun anhand eines Beispiels verdeutlicht werden: Wir betrachten das SQL – Statement: SELECT x FROM A,B,C WHERE ( A.x = B.y AND A.x = C.z) Die möglichen Query – Plans sind: (A B) C und (A C) B. Der Einfachheit halber nehmen wir an, das die Tabellen A, B und C alle die gleiche Kardinalität |A| = |B| = |C| = 100 haben. Nehmen wir in Bezug auf die Trennschärfe an, dass 10 % der Werte A.x gleich der Werten B.y sind und das der Operator A.x = B.y eine Übereinstimmung von 50 % erzielt. Auf Basis der gegebenen Daten soll nun der bessere Query – Plan bestimmt werden. Zunächst stellen wir die Pläne als Graphen dar: Plan A |A A Plan B B| = 5 B A |B = 100| |A A |A C| = 50 B C A |C| = 100 |A| = 100 |A| = 100 A |A C A C| = 5 C B| = 10 |C| = 100 C B |B| = 100 B Grafische Darstellung von Query - Plans Stellen wir nun die Kosten für die beiden Pläne auf: Dazu nehmen wir an, die Kosten für einen Joins seinen das Produkt der Kardinalitäten von den verwendeten Tabellen. Das ist selbstverständlich eine stark vereinfachte Form der Kostenanalyse, ist aber zu Beispielzwecken geeignet. Die Kosten für Plan A betragen: K ( PlanA) = K ( A × C ) + K ( A × B) = A * C + A * B = 100 *100 + 50 *100 = 15000 Die Kosten für Plan B getragen: K ( PlanB) = K ( A × B) + K ( A × C ) = A * B + A * C = 100 *100 + 10 *100 = 11000 Man sieht das Plan B günstiger ist als Plan A 7 Hat der Optimierer nun den günstigsten Plan gewählt, so übersetzt er in ASR. Diese Übersetzung erhält der Codegenerator. 3. Code – Generator: Der Code – Generator übersetzt den erhaltenen Plan in Maschinensprache. Dies macht er, indem er für die einzelnen Operatoren des Plans in einer internen, vordefinierten Tabelle den zugehörigen Maschinen – Code erfragt, und den Operator damit ersetzt. 4. Code – Ausführung: Die Code – Ausführung ist der letzte Schritt in der Bearbeitung des SQL – Statements, und liefert das gewünschte Ergebnis. Das Kostenbasierte Optimierungsmodell ist ein häufig verwendetes Konzept. Es hat sich schon früh gezeigt, das dieses Modell gute Ergebnisse erreicht und dabei effizient arbeitet. Neben dem kostenbasierten Ansatz existieren allerdings noch weitere Ansätze. Schon in System R wurden sogenannte Interesting Orders verwendet. Bei diesem Ansatz werden Teilabschnitte von Plänen und deren Ergebnisse auf ihr Wiederverwertbarweit getestet. Existieren z.B. in einem Plan zwei oder mehrere Abschnitte, welche gleiche Ergebnis liefern, so bekommt dieser Plan eine gute Bewertung, da das Ergebnis für die besagten Teilabschnitte nur einmal berechnet werden muss, und damit die Teilabschnitte einfach ersetzt werden können. 8 3. Optimierungsmodelle für kontinuierliche Querys Im vorherigen Kapitel haben wir Optimierungsverfahren kennen gelernt, wie sie in traditionellen DBMS verwendet werden. Im Folgenden sollen Optimierungsmodelle für DSMS gezeigt werden. 3.1 Problemstellung: Gegeben ist ein DBMS, welches n Eingabeströme und m Ausgabeströme hat. An den Eingabeströmen kommen Daten in unterschiedlicher Geschwindigkeit an. Dies können z.B. Datenströme von entfernten Internet-Hosts sein, welche über verschieden schnelle Anbindungen verfügen. . . . . 1 1 2 2 Data Stream Management System n . . . . m Die Problemstellung, mit der sich die später gezeigten Optimierungsmodelle beschäftigen ist: Wie kann der Optimierer eines Data Stream Management System einen Queryplan wählen, der für die verschieden schnelle Eingabeströme und darauf ausgeführten Operatoren einen möglichst hohen Datendurchsatz erzielt. Das Prinzip eines Optimierers wurde im einführenden Kapitel schon erwähnt, soll hier aber noch mal explizit aufgeführt werden: Der Optimierer erhält eine Menge von Plänen als Eingabe. Jedem Plan im Suchraum des Optimierers (Menge der Pläne, aus denen der Optimierer einen Plan selektieren soll) wird anhand von geeigneten Daten Kosten zugeordnet. Der Plan mit den geringsten Kosten wird zur Ausführung gebracht. 9 3.2 Beispiel 3: Wir verwenden wieder die Query aus Beispiel 2. Nur das die Query diesmal nicht auf einer Datenbank sondern auf Datenströmen ausgeführt wird. Gegeben sind die Eingabeströme A, B und C. Die Datenraten von A,B und C betragen je 100 Tupel/Sekunde. Auf diese Ströme soll die Query -Plans (A B) C und (A C) B ausgeführt werden. Plan A |A A Plan B B| = 5 B A |B| = 100 |A A |A C| = 50 B C A |C| = 100 |A| = 100 |A| = 100 A |A C A C| = 5 C B| = 10 |C| = 100 C B |B| = 100 B Welchen Plan soll der Optimierer nun wählen? Und nach welcher Strategie? Theoretisch könnte man ein kardinalitätsbasiertes Optimierungsmodell von einem herkömmlichen Data Base Management System verwenden. Man berechnet einfach die Entstehenden Kardinalitäten von A B und A C und entscheidet anhand des Ergebnisses welcher Join zuerst ausgeführt werden soll. Um die Kardinalitäten von A B und A C zu berechnen, benötigt man den Trennschärfe – Faktor und vor allem die Kardinalitäten der Ströme A, B und C. Der Trennschärfe – Faktor der Joins könnte eventuell noch geschätzt werden. Aber was sind die Kardinalitäten der Ströme A, B und C? Diese sind bei kontinuierlichen Querys meistens nicht bekannt, und theoretisch nicht mal wohl definiert. (Da die Ströme theoretisch unendlich lang sein können.) Aus diesem Grund ist das kardinalitätsbasierte Optimierungsmodell im Falle von DSMS nicht praktikabel. Daher müssen neue Ansätze gefunden werden. Ein weiterer Ansatzpunkt zur Optimierung stellen die verwendeten Operatoren dar. So gibt es verschiedene Methoden einen Join auszuführen. Welche die effizienteste ist hängt von der gegebenen Situation ab. Deswegen sollen in dieser Ausarbeitung zwei Optimierungsansätze vorgestellt werden. Die erste Optimierungsmethode (Optimierung von Moving Windows) zeigt Möglichkeiten, wie man in bestimmten Situationen eine möglichst effiziente Join – Methode wählt. Der Zweite Optimierungsansatz (rate –based Optimization) versucht eine möglichst optimale Join – order zu wählen. Die gezeigten Ansätze wurden von der University of Wisconsin-Madison für das Niagara – Projekt [4] entwickelt. 10 4. Moving Window 4.1 Einführung: Wenn man sich überlegt, wie man einen Join über zwei Datenströme ausführt, sieht man schnell folgendes Problem: Wie schon erwähnt können Datenströme als theoretisch unendlich lang angenommen werden. Um nun, wie bei Joins notwendig, auf herkömmliche Art und Weise jedes Tupel des einen Stroms mit jedem Tupel des anderen Stroms zu vergleichen, benötigt man unendlich viel Speicherplatz. Selbstverständlich ist das nicht praktikabel. Aus diesem Grund soll nun bevor wir uns mit der Optimierung von Queries auf Datenströme geschäftigen, eine Technik gezeigt werden, welche die Verarbeitung von Joins über unendlich langen Datenströmen ermöglicht: Der Ansatz dabei ist, die Eingabeströme nicht in voller Länge zu betrachten, sondern ausschnittsweiße. 4.2 Beispiel 4: Gegeben sei einen Join mit den unendlich langen Eingabeströmen L und R welche die Eingaberaten rl und rr haben. Man betrachtet nun nicht die Gesamteingaben sondern nur die letzten tl Sekunden von L und die letzten tr Sekunden von R. Tl und Tr werden als Zeitfenster der Ströme L und R bezeichnet. Es ist offensichtlich, dass sich der Inhalt der Fenster mit der Zeit verändert sofern die Eingaberaten größer als Null sind. Man könnte also sagen, die Fenster bewegten sich mit der Zeit über die Ströme. Daher wird die Technik moving Window genannt. Tl Strom L Tr Strom R Zeit t Bildliche Darstellung von moving Windows 11 4.3 Optimierung von moving Windows (time – unit based optimization) Ziel dieses Kapitels wird sein, eine Optimierungsmöglichkeit für moving Windows zu finden. Es geht nicht darum, wie im vorherigen Kapitel, die Kosten eines gesamten Query – Plans zu schätzen. Es werden hier nur die Kosten für verschiedene Join – Methoden unter bestimmten Gegebenheiten geschätzt um die jeweilig beste Methode zu bestimmen. Dabei betrachten wir hier nur die Join – Methoden Nested Loop Join (NLJ) und Hash Join (HJ). Gegebenheiten definieren sich insbesondere durch das Verhältnis Ankunftsrate rl zur Ankunftsrate rr. Gefragt wird also z.B: Im Strom A kommen fünfmal soviel Tupel wie im Strom B an, welcher Join – Algorithmus ist der effizienteste? 4.4 Funktionsweise von Nested Loop Joins: Um die Funktionsweise von NLJ zu erklären, bedienen wir uns wieder des Beispiels 2, bzw. speziell des Joins A.x = B.y. Der NLJ Algorithmus macht nichts anderes als jeden Eintrag A.x aus A mit jedem Eintrag von B.y aus B zu vergleichen. Das wir durch zwei for – Schleifen realisiert: Algorithmus: FOR EACH A.x FROM A { FOR EACH B.y FROM B { IF [A.x = B.y] { Füge A.x zur Ausgabe hinzu; } } } Der Aufwand des NLJ – Algorithmus ist O( A * B ) . Funktionsweise von Hash Joins: Wir betrachten den gleichen Join wie bei NLJ. Der HJ – Algorithmus erstellt zuerst eine Hash – Tabelle von A.x und vergleicht dann jedes Element von B.y mit der Hash – Tabelle. Algorithmus: BAUE HASH TABELLE HASH(A.x) VON A.x FOR EACH B.y FROM B { IF [HASH(A.x) CONTAINS B.y] { Füge A.x zur Ausgabe hinzu; } } Der Aufwand des HJ – Algorithmus ist O( B ) . Allerdings ist zu beachten, dass das erstellen der Hash – Tabelle auch Kosten verursacht. Eine Hash – Tabelle besteht aus hash – buckets. Man kann buckets als Untergliederung der Tabelle verstehen. Z. B. werden bekanntlich die Einträge in einem Telefonbuch anhand ihrer Anfangsbuchstaben in Kapitel untergliedert. Die Kapitel in einem Telefonbuch entsprechen den hash – buckets in einer Hash – Tabelle. 12 4.5 Kostenabschätzung für moving Window - Joins (ohne spezifische Join – Technik) Um entscheiden zu können, welcher Join – Algorithmus für eine Situation am besten geeignet ist, benötigen wir ein Kostenmodell, welches die Kosten von Join – Algorithmen in Bezug auf Moving Windows innerhalb einer Zeiteinheit schätzt. Betrachten wir die Zeitfenster Tl und Tr von Beispiel 4. Solange keine neuen Tupel in den Zeitfenstern erscheinen, wenn also die Eingaberaten null sind, können auch keine neuen Ausgabetupel entstehen. Folglich müssen auch keine Aktionen für den Join – Operator ausgeführt werden. Daher können wir sagen, dass Aktionen genau dann ausgeführt werden müssen, wenn in einem der Fenster neue Tupel eintreffen. Nehmen wir an, es trifft ein neues Tupel am Fenster Tl ein (am Fenster deswegen, weil wir das Einfügen des Tupels in das Fenster mit in unsere Kostenrechnung aufnehmen). Nun müssen folgende Aktionen ausgeführt werden: 1. Das Fenster Tr muss auf Tupel untersucht werden, welche vom Join – Operator affektiert werden. Die daraus resultierenden Kosten bezeichnen wir als probe(Tr). 2. Einfügen des ankommenden Tupels in das Fenster Tl. Die Kosten werden als insert(Tl) bezeichnet 3. Überprüfen des Fensters Tl auf Tupel, die nicht mehr im Zeitbereich des Fensters liegen. Dies Kosten bezeichnen wir mit invalidate(Tl). Analog gehen wir beim Fenster Tr vor. Es ist darauf zu achten, dass die Kosten probe, insert und invalidate in dieser allgemeinen Darstellung noch nicht definiert sind, da sie von dem verwendeten Join – Algorithmus abhängen. Bislang haben wir die nur die Kosten für ein ankommendes Tupel betrachtet. Um die Kosten des kontinuierlichen Joins während einer Zeiteinheit zu berechnen, müssen wir die geschätzten mit Ankunftsraten pro Zeiteinheit rl und rr berücksichtigen. Betrachtet man die Gesamtkosten C für den Join L R so erhalten wir: C = rl [ probe(Tr ) + insert (Tl ) + invalidate(Tl ) ] + rr [ probe(Tl ) + insert (Tr ) + invalidate(Tr ) ] (Formel 1) Zur Wiederholung: Der erste Teil der Formel beschreibt die Kosten welche durch ankommende Tupel am Fenster Tl pro Zeiteinheit verursacht werden, der zweite Teil macht das gleiche für Fenster Tr. Die Kosten für die Überprüfung (invalidate) fallen an, weil die Fenster auf Tupel überprüft werden müssen, welche schon älter als die vorgegebene Zeitspanne tl bzw. tr sind. Man kann die Komponenten in Formel 1 aber auch umsortieren, dass man sieht welche Kosten beim Zugriff auf Tupel im Fenster Tr und welche beim Zugriff auf Tupel im Fenster Tl entstehen: C = rl [ probe(Tr ) + insert (Tl ) + invalidate(Tl ) ] + rr [ probe(Tl ) + insert (Tr ) + invalidate(Tr ) ] =rl * probe(Tr ) + rl * insert (Tl ) + rl * invalidate(Tl ) + rr * probe(Tl ) + rr * insert (Tr ) + rr * invalidate(Tr ) = rr * probe(Tl ) + rl * (insert (Tl ) + invalidate(Tl )) + rl ( probe(Tr ) + rr * (instert (Tr ) + invalidate(Tr )) (Kosten für Zugriff auf Tupel in Tl) (Kosten für Zugriff auf Tupel in Tr) 13 Wozu die Umformung? Sinn und Zweck der Kostenanalyse ist es, Kosten für Joins hinreichend genau zu schätzen. Am Ende der Analyse muss die Feststellung stehen, Join – Methode A ist um Faktor X schneller als Join – Methode B. Dabei ist es nicht sinnvoll, die möglichst exakten Kosten zu kalkulieren, da die Kalkulation an sich schon Kosten verursacht. Deswegen ist man bestrebt, Kostenmodelle so einfach wie möglich zu halten. Aus diesem Grund berechnen wir in den folgenden Abschätzungen für konkrete Join – Algorithmen nur Folgendes: Kommt ein neues Tupel im Fenster Tl an, so muss es mit den Tupeln des Fensters Tr „gejoint“ werden (Join Tl to Tr). Der Grossteil der Kosten fällt in diesem Fall beim Zugriff auf die Tupel des Fensters Tr an. Deswegen werden auch nur die Kosten, welche im Fenster Tr entstehen berechnet. Diese sind repräsentativ für die Gesamtkosten welche beim Vorgang Join Tl to Tr entstehen. Analog gehen wir für Join Tr to Tl vor. 4.6 Kostenabschätzung für Nested Loop Joins Im Folgenden sollen die Kosten für Join(NLJ) Tl to Tr für eine Zeiteinheit behandelt werden. (Wir erinnern uns an die Zeiteinheiten tl und tr der Fenster Tl und Tr) Bei der Nested Loop Join Technik, muss bei jedem an Tl ankommenden Tupel jedes Tupel von Fenster Tr überprüft werden, ob es für den Join notwendig ist. Die Anzahl der Tupel, welche im Fenster Tr enthalten sind ist das Produkt aus Ankunftsrate rr (ankommende Tupel pro Zeiteinheit) und der Zeitspanne tr (Anzahl Zeiteinheiten) des Fensters Tr. Also |Tr| = rr * tr. Die Anzahl ankommender Tupel pro Zeiteinheit am Fenster Tl ist definitionsgemäß rl. Dementsprechend beträgt der Aufwand das Fenster Tr auf benötigte Tupel zu überprüfen pro Zeiteinheit rl* | Tr |= rl * rr * tr . Fehlen noch die Kosten für insert() und invalidate(). Die Kosten um ein Tupel in das Fenster Tr einzufügen betragen 1. Da am Fenster Tr pro Zeiteinheit rr Tupel ankommen betragen die Kosten rr. Für die Kosten invalidate() können wir annehmen, dass im Durchschnitt pro ankommenden Tupel auch Eins abläuft (veraltet). Deswegen betragen diese Kosten pro ankommenden Tupel ebenfalls 1. Dementsprechend betragen die Kosten für eine Zeiteinheit rr. Aus den drei Kostenteilen probe(), insert() und invalidate() könnten nun die Gesamtkosten für NLJ für eine Zeiteinheit berechnen. Was noch fehlt, sind die Kosten, welche anfallen um auf ein Tupel zuzugreifen. Diese Kosten nennen wir Cn. (Bislang entsprechen unsere Kosten der Anzahl der Zugriffe auf einzelne Tupel, welche bei der Verarbeitung des Joins anfallen). C ( NLJ ) =(rl* | Tr | + rr + rr ) * Cn = (rl * rr * tr + 2 * rr ) * Cn (Formel 2) 14 4.7 Kostenabschätzung für Hash Joins Diese Kostenabschätzung verläuft analog zur Kostenabschätzung von NLJ. 1. Kosten für probe(): Wenn ein Tupel am Fenster Tl ankommt, so muss die hash – Tabelle von Fenster Tr auf benötigte Tupel überprüft werden. Der Aufwand hierfür hängt von der Anzahl hash – buckets ab. Diese Anzahl bezeichnen wir für die Tabelle von Tr als |Br|. Bei guten hash – Algorithmen liegt die Anzahl der hash – buckets nahe der Anzahl der in der Tabelle enthaltenen Tupel. Der Aufwand für einen Look-up in der Tabelle ist die Anzahl enthaltener Tupel dividiert durch die Anzahl der hash – buckets. Zur Wiederholung: Die Anzahl der Elemente im Fenster Tr ist rr*tr. für einen Look – up. Die Dementsprechend sind die Kosten rr * tr | Br | resultierenden Kosten für probe() während einer Zeiteinheit betragen folglich: rl * rr * tr | Br | 2. Analog können wir für die Kosten invalidate() bestimmen. Hier ist nur darauf zu achten, dass der Vorgang pro Zeiteinheit nicht rl sondern rr mal ausgeführt wird. (Da im Durchschnitt rr Tupel im Fenster veralten). Daher sind die Kosten für invalidate: rr * rr * tr | Br | 3. Die Kosten um ein Tupel in die hash- Tabelle von Tr einzutragen (insert()) betragen 1. Für die eine Zeiteinheit also rr. Aus den drei Kostenteilen können wir, unter Verwendung des Faktors Ch, welcher für die Kosten um auf ein Tupel innerhalb einer hash – Tabelle zuzugreifen repräsentiert, wieder die Gesamtkosten herleiten: C ( HJ ) = (rl * rr * tr rr * tr + rr * ( + 1)) * Ch (Formel 3) | Br | | Br | 15 4.8 Beispiel 5: Erinnern wir uns an Beispiel 3. Aus diesem Beispiel verwenden wir nun Plan B, und versuchen mit den oben gelernten Formeln herauszufinden, welches jeweils der bessere Join – Algorithmus für die beiden im Plan vorkommenden Joins ist. Plan B |A A |A A |A| = 100 A C| = 5 C B| = 10 |C| = 100 C B |B| = 100 B Welche Daten sind Gegeben? 1. Die Eingaberaten: ra = rb = rc=100 T/s rab = 10 T/s (Ausgaberate von A B) 2. Die Zeitfenstergrößen legen wir auf ta=tb=tc=2s fest. 3. Cn sei 0.5 und Ch sei 0.65. (Der Zugriff auf einzelne Tupel ist bei HJ teurer, diese zwei Werte sind aber dennoch fiktiv) 4. Die bucket – size |B| der hash – Tabellen sei immer 10. Dementsprechend enthält eine hash – Tabelle Anzahl − Tupel buckets. 10 Betrachten wir Zuerst die Kosten von Join A to B: NLJ – Algorithmus (Formel 2): C ( A, b) = (ra * rb * tb + 2 * rb) * Cn = (100 * 100 * 2 + 2 * 100) * 0.5 = 10100 HJ – Algorithmus (Formel 3): C ( A, B) = (ra * rb * tb 100 * 2 rb * tb 100 * 2 + rb * ( + 1)) * Ch = (100 * + 100 * ( + 1)) * 0.65 = 2665 B |B| 10 10 In diesem Fall ist also der HJ – Algorithmus um Faktor 3.8 schneller. 16 Nun betrachten wir die Kosten Für Join A to C: NLJ – Algorithmus: C ( A, C ) = (ra * rc * tc + 2 * rc) * Cn = (10 *100 * 2 + 2 *100) * 0.5 = 1100 HJ – Algorithmus: C ( A, B ) = (ra * rc * tc rc * tc 100 * 2 100 * 2 + rc * ( + 1)) * Ch = (10 * + 100 * ( + 1)) * 0.65 = 1495 B |B| 10 10 In diesem Fall ist der NLJ – Algorithmus günstiger. 17 5. Grundlagen der rate – based Optimierung 5.1 Ein Überblick Im vorigen Kapitel haben Methoden kennen gelernt, um den besten Join – Algorithmus zu wählen. In diesem Kapitel sollen Vorgehensweißen gezeigt werden, mit denen man die beste Join – order für eine Situation schätzt. Wie der Name rate – based Optimierung schon vermuten lässt, sind die Grundlagen dieses Optimierungsmodells die Durchsatzraten der Eingabeströme. Der Ansatz des Verfahrens ist, mit Hilfe der Eingaberaten von Joins ihre Ausgaberate zu bestimmen. Mit Hilfe der Abschätzungen für die einzelnen Joins kann dann die Ausgaberate des gesamten Query – Plans geschätzt werden. Der Plan mit der höchsten Ausgaberate ist natürlich der Beste. Es sei darauf hingewiesen, dass dieses Optimierungsmodell unabhängig von den vorher gezeigten Moving Windows arbeitet. 5.2 Die Ausgaberate als Funktion von der Eingaberate Ziel dieses Kapitels ist es, eine Funktion herzuleiten, welche als Eingabe die Raten der Eingabeströme eines Joins erhält, und als Ausgabe die geschätzte Ausgaberate des Joins liefert. Es ist zu beachten, dass wir hier Joins im Allgemeinen betrachten, und nicht spezielle Join – Methoden wie wir sie im Kapitel Moving Windows kennen gelernt haben. Da bei den Berechnungen eine Reihe von Kostenvariablen verwendet wird sind diese Variablen mit Bedeutung hier aufgelistet: Kosten Variable Cl Cr T rr ro Bedeutung Kosten für die Bearbeitung der linken Eingabe für einen Join Kosten für die Bearbeitung der rechten Eingabe für einen Join Kosten um eine einzelne Übertragung zu machen Rechte Eingaberate (bei Joins) Ausgabe Rate 18 Fragestellung: Zu irgendeinem Zeitpunkt tx in der Join – Ausführung können Eingaben aus dem linken bzw. rechten Eingabestrom ankommen. Die Frage ist: Was ist die Rate der Ausgabe, welch durch die zum Zeitpunkt tx ankommenden Tupel generiert wird? Folgende Grafik soll die Fragestellung bildlich darstellen: t0 Eingaben zwischen t0 und tx |Zeitspanne zwischen t0 und tx | = tx tx Zeit t Durch Eingabe zum Zeitpunkt tx verursachte Ausgaben Vorgehensweiße: Um die Beantwortung dieser Frage zu vereinfachen, gehen wir vorerst nicht von einer kontinuierlichen Zeit aus, sondern betrachten zuerst diskrete Zeiteinheiten. Im Folgenden gehen wir in vier Schritten vor: 1. Berechnung der generierten Ausgabetupel für eine Eingabe zum Zeitpunkt tx innerhalb einer Zeiteinheit 2. Folgerung für mehrere Zeiteinheiten 3. Übergang von diskreten Zeiteinheiten zur kontinuierlichen Zeit 4. Berechnung der Ausgaberate ro Berechnung der Ausgaberate für eine Zeiteinheit: Zuerst überlegen wir uns, wie viele Ausgabe – Tupel durch die im Zeitpunkt tx ankommenden Tupel, während einer Zeiteinheit generiert werden. Wie in der oben stehenden Tabelle aufgeführt nehmen wir für den linken Eingabestrom eine Rate rl und für den Rechten eine Rate rr an. In der Zeitspanne zwischen t0 und tx kommen tx*rl Tupel aus dem linken Eingabestrom, und tx*rr aus dem Rechten in den Join. Wir wollen nun wissen, wie viele Ausgabetupel diese Eingabe produziert. Würden wir hier kartesische Produkte betrachten (in denen jedes Tupel vom linken Strom mit jedem Tupel vom Rechten Strom eine Ausgabe generiert), so wäre die Anzahl generierter Tupels während einer Zeiteinheit: # Tupel (kartesisches Pr odukt ) = rl * tx * rr * tx = rl * rr * tx ² 19 Wie viele Ausgaben bei einem Join produziert werden hängt vom Trennschärfe – Faktor f des verwendeten Operators (in Bezug auf die Eingabeströme) ab. Deswegen müssen wir diesen Faktor f mit in unsere Berechnung einfließen lassen, und erhalten für die Anzahl generierter Tupel: # Tupel ( Eine − Zeiteinheit ) = f * rl * tx * rr * tx = f * rl * rr * tx ² (Formel 4) 5.3 Beispiel 6: In diesem Beispiel verwenden wir den Join A B aus Beispiel 3. Die Eingaberaten ra und rb betragen jeweils 100 Tupel/Sekunde. Der Trennschärfe – Faktor fab beträgt 0.1. Den Zeitpunkt tx sei tx=3s: Die Anzahl der generierten Tupel beträgt: # Tupel = f * rl * rr * tx ² = 0.1 *100 *100 * 9 = 9000 Der Einfachheit wegen nehmen wir an, das der Begin unserer Zeiteinheit nicht zum Zeitpunkt tx=3 ist, sondern zum Zeitpunkt tx=t0. Das heißt, dass vor dem Zeitpunkt tx noch keine Tupel in den Join eingeflossen sind. Wir betrachten also die Anzahl der Tupel welche in der ersten Zeiteinheit der Join – Ausführung generiert werden: # Tupel (1steZeiteinheit ) = f * rl * rr = 0.1 *100 *100 = 1000 Nun betrachten wir die zweite Zeiteinheit nach Begin der Join - Ausführung: Am Ende der zweiten Zeiteinheit ist der Beitrag des linken Eingabestroms zur Gesamtausgabe des Joins: f * rl * 2 * rr . (Der Trennschärfe – Faktor multipliziert mit den Tupeln welche während der zweiten Zeiteinheit vom linken Strom gelesen wurden (rl) multipliziert mit der Anzahl Tupel welche vom rechten Strom währen der ersten und der zweiten Zeiteinheit gelesen wurden (2*rr)). Den Beitrag vom rechten Eingabestrom berechnen wir analog: f * rr * 2 * rl . Folglich ist die Gesamtausgabe, die durch die Eingaben vom linken bzw. rechten Eingabestrom während der zweiten Zeiteinheit generiert wurde: # Tupel (2te − Zeiteinheiten) = f * rl * 2 * rr + f * rr * 2 * rl − f * rr * rl = 2 * (2 * f * rr * rl ) − f * rr * rl (Die Subtraktion verhindert das einige Tupel zweimal mitgerechnet werden) (Formel 5) 20 Wenn man von Diskreten Zeiteinheiten zur kontinuierlichen Zeit übergeht, so muss man, um die Anzahl der generierten Ausgaben zu berechen folgendes Integral lösen: tx tx t0 t0 # Tupel (kontinuirlicheZeit ) = ∫ (2 * (t * f * rl * rr ) − f * rl * rr )dt = f * rl * rr * ∫ (2 * t − 1)dt mit t0 = 0 folgt: tx # Tupel (kontinuirlicheZeit ) = f * rl * rr * ∫ (2 * t − 1)dt = f * rl * rr * tx * (tx − 1) (Formel 6) 0 Berechnung der Ausgaberate: Bislang haben wir nur die durch Eingaben zum Zeitpunkt tx generierte Anzahl von Ausgaben betrachtet. Als letzen Schritt soll nun noch die Ausgaberate ro berechnet werden. Dies gestaltet sich einfach: Die Ausgaberate ist gleich der Anzahl der ausgegebenen Tupel dividiert durch die dazu benötigte Zeit. Für die Anzahl der ausgegebnen Tupel verwenden wir Formel 6. Die Kosten um ein im linken Strom ankommendes Tupel zu verarbeiten betragen laut Tabelle Cl. Die Kosten um ein Tupel aus dem Rechten Strom zu verarbeiten sind analog Cr. Wie schon bekannt erhalten wir aus dem linken Strom tx* rl Eingabetupel und aus dem Rechten tx*rr. Es ist zu berücksichtigen, dass die Kosten nicht konstant sind. Je mehr Tupel bereits in den Join eingeflossen sind, desto aufwendiger wird es auch, neue Tupel zu verarbeiten. (Da z.B. bei Nested Loop Joins jedes in einem Strom ankommende Tupel mit jedem bereits vorhandenen Tupel der anderen Seite verglichen werden muss.). Für die Kosten von Cl und Cr sei auf das vorige Kapitel verwiesen. Die Gesamtkosten für beide Eingaben belaufen sich folglich auf Kosten = Cl * tx * rl + Cr * tx * rr (Formel 7) Daraus resultiert die Ausgaberate ro: ro = Formel 6 f * rl * rr * tx * (tx − 1) f * rl * rr * (tx − 1) f * rl * rr * tx ≈ = = Formel 7 Cl * tx * rl + Cr * tx * rr Cl * rl + Cr * rr Cl * rl + Cr * rr (Formel 8) Mit Formel 8 können wir nun einen generellen Rahmen für die rate – based Optimierung erstellen. 21 5.4 Genereller Rahmen für die rate-based Optimierung Im vorherigen Kapitel wurde gezeigt, wie man die Ausgaberate eines Joins als eine Funktion der Eingaberaten darstellen kann. In diesem Kapitel soll gezeigt werden, wie mit Hilfe dieser Funktionen ein Plan bezüglich seiner Kosten evaluiert werden kann. Betrachten wir dazu wieder Plan B aus Beispiel 3: |A A |A A |A| = 100 A C| = 5 C B| = 10 |C| = 100 C B |B| = 100 B Wir stellen fest, dass der Ausgabestrom von A B als linker Eingabestrom von A C fungiert. Damit ist die linke Eingaberate des Joins A C gleich der Ausgaberate von A B. So kombiniert ließe sich die Gesamtausgaberate von Plan B als Funktion der Eingaberaten der Ströme A,B und C darstellen. Hat man einmal diese Funktion (nennen wir sie ro(Plan B)) könnte man daraus wieder die Anzahl Ausgaben in einem Zeitintervall berechnen. Der Plan mit den meisten Ausgaben in einem bestimmten Zeitintervall ist für dieses auch der beste Plan. Die Funktion für die von einem Plan ausgegebenen Tupel ist: ty # Gesammtausgabe( PlanB ) = ∫ ro( PlanB)dt (Formel 9) 0 Diese Funktion stellt den generellen Rahmen für die ratenbasierte Optimierung dar. 22 5.5 Beispiel 7: In diesem Beispiel soll nun endlich die Frage, welche schon zu Anfang aufgeworfen wurde, beantwortet werden: Welcher der beiden Pläne aus Beispiel 3 ist besser? (Man beachte, dass hier noch die maximalen Ausgaberaten angegeben sind. Sobald den Joins Kosten zugeordnet werden, sinken die Ausgaberaten unter das jeweilige Maximum! Wir können also den unten stehenden Graphen nur die Eingaberaten von A,B und C entnehmen.) Plan A |A A Plan B B| = 5 B A |B| = 100 |A A |A C| = 50 B C A |C| = 100 |A| = 100 |A| = 100 A |A C A C| = 5 C B| = 10 |C| = 100 C B |B| = 100 B Die Zeitvariable tx setzen wir auf tx=1 (Sekunden). Für die Kosten Cl und Cr nehmen wir der Einfachheit halber die konstanten Werte Cl = 0.8 und Cr=0.7 an. Für die folgenden Berechnungen wird die Formel 8 verwendet: Plan A: ro( AJoinC ) = f * rl * rr * tx 0.5 *100 *100 *1 = ≈ 33 Cl * rl + Cr * rr 0.8 *100 + 0.7 *100 Ausgaberate( PlanA) = ro( AJoinB) = f * ro( AJoinC ) * rr * tx 0.1 * 33 *100 *1 = ≈ 3,4 Cl * ro( AJoinC ) + Cr * rr 0.8 * 33 + 0.7 *100 Plan B: ro( AJoinB) = f * rl * rr * tx 0.1 *100 *100 *1 = ≈ 6,6 Cl * rl + Cr * rr 0.8 *100 + 0.7 *100 Ausgaberate( PlanB) = ro( AJoinC ) = f * ro( AJoinB) * rr * tx 0.5 * 6,6 *100 *1 = ≈ 4,3 Cl * ro( AJoinB) + Cr * rr 0.8 * 6,6 + 0.7 *100 23 Es ist ersichtlich, dass Plan B für die gegebenen Werte die bessere Lösung wäre. Da wir für tx = 1 angenommen haben, entsprechen die Ausgaberaten der Anzahl generierter Tupel. Die Anwendung von Formel 9 ist hier also unnötig. Achtung: Es sei nochmals darauf hingewiesen, dass die Kosten Cl und Cr hier nur der Einfachheit halber auf konstante Werte gesetzt wurden. Je mehr Tupel in einen Join eingeflossen sind, desto höher sind die Kosten um weitere Tupel zu verarbeiten. Aus diesem Grund wird auch der Zeitfaktor im Zähler der Formel 8 kompensiert. Wäre dies nicht der Fall, blieben die Kosten also wirklich konstant, so würde laut Formel 8 mit steigender Zeitkomponente tx auch die Ausgaberaten steigen, und die Formel wäre zweifelsohne falsch. Wollten wir z.B. Plan A für den Zeitpunkt tx = 2 berechnen, so müssten wir für die Kosten Cl=1,6 und Cr=1,4 annehmen. (Vorausgesetzt die Kosten steigen linear). ro( AJoinC ) = f * rl * rr * tx 0.5 *100 * 100 * 2 = ≈ 33 Cl * rl + Cr * rr 1.6 *100 + 1.4 * 100 Ausgaberate( PlanA) = ro( AJoinB) = f * ro( AJoinC ) * rr * tx 0.1 * 33 * 100 * 2 = ≈ 3,4 Cl * ro( AJoinC ) + Cr * rr 1.6 * 33 + 1.4 *100 Die Anzahl generierter Tupel kann hier mit Formel 9 berechnet werden: (Hier benötigen wir die Mittelwerte der Kosten Cl und Cr im Intervall [0,2]. Diese Mittelwert entsprechen der Kosten zum Zeitpunkt tx=1. Also Cl=0,8 und Cr=0,7) 2 1 f * ro( AJoinC ) * rr * tx ² f * ro( AJoinC ) * rr * tx = * ...[0,2] Cl * ro( AJoinC ) + Cr * rr 2 Cl * ro( AJoinC ) + Cr * rr 0 # Ausgabe = ∫ = 1 0.1 * 33 *100 * 4 * − 0 ≈ 6,8 2 0.8 * 33 + 0.7 *100 24 6. Fazit Die Anforderungen an Data Stream Management Systeme werden in naher Zukunft vermutlich stark ansteigen. Daher werden gute Optimierer unabdingbar sein. Das Forschungsgebiet „Optimierer für DSMS“ ist noch sehr jung. Die hier vorgestellten Konzepte befinden sich momentan in der Erprobungsfase. Es ist davon auszugehen, das zukünftig mächtigere und deutlich komplexere Optimierungsverfahren als die hier vorgestellten zur Anwendung kommen. 25 7. Anhang Quellenangaben: [1] Access path selection in an Relational Database Management System ( P. Griffits Selinger ) Zusätzlich http://www.cs.pdx.edu/~kgb/t/t.shtml [2] Evaluating Window Joins over Unbounded Streams (Jeffrey F. Naughton) [3] Rate-Based Query Optimization for Streaming Information Sources ACM SIGMOD'2002, June 4-6, Madison, Wisconsin, USA [4] Niagara Query Engine http://www.cs.wisc.edu/niagara/ 26