Leopold-Franzens-Universität Innsbruck Institut für Informatik Datenbanken und Informationssysteme SQL-Rekursions-Tool Bachelor-Arbeit Lucas Ohlicher betreut von Michael Tschuggnall, PhD Univ.-Prof. Dr. Günther Specht Innsbruck, 20. Juni 2016 Zusammenfassung Rekursionen sind ein mächtiges und weitverbreitetes Werkzeug in der Informatik und sind teilweise unabdingbar, um gewisse Problemstellungen zu lösen. Genauso verhält es sich in Sprachen für relationale Datenbanken, wie zum Beispiel SQL. Da SQL als deklarative Sprache keine Schleifenkonstrukte unterstützt, muss für einige Anfragen auf eine Rekursion zurückgegriffen werden. Rekursionen für einfache Probleme fallen dabei meist schon relativ komplex aus und wirken für Studenten oftmals unverständlich. Auch gibt es kaum Möglichkeiten, Rekursionen gezielt zu üben oder verständlicher zu veranschaulichen. In dieser Arbeit wird ein webbasiertes Tool vorgestellt, mit dem Studenten SQL-artige Abfragen durchführen können. Dabei werden ein Großteil der wichtigsten Funktionen und Befehle, inklusive der Rekursion, aus SQL unterstützt. Diese wird zusätzlich in einer eigenen Darstellung interaktiv präsentiert, die es dem Benutzer ermöglicht, sich Schritt für Schritt durch die einzelnen Abschnitte einer rekursiven Abfrage zu klicken. Abstract Recursions are a powerful and commonly used method in computer science and can sometimes be necessary to solve certain problems. This also applies to languages that are used in relational databases, such as SQL. This is the case because it does not support loop constructs and therefore queries can sometimes only be solved by recursion. Even for simple problems the resulting query can be rather complex and furthermore for students hard to understand. Moreover, there hardly exist any means to explicitly practice recursions or display it in a more understandable way. In this thesis a web-based tool was developed, that allows students to make SQL-like queries. Most of the important functions and commands of SQL, including the recursion, are supported. Additionally, the recursion is presented interactively to allow the user to go through the different parts of a recursive query step by step. Inhaltsverzeichnis 1 Einleitung 1.1 Rekursion in der Informatik 1.2 Bedeutung der Rekursion in 1.3 Funktionsweise in SQL . . . 1.4 Sinn des Übungstools . . . . . . . . . . . . . . . . . . der relationalen Algebra . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Übungstool 2.1 Vorüberlegungen . . . . . . . . . 2.2 Graphische Benutzeroberfläche . 2.3 Parsen von SQL mit PEG.js . . . 2.4 Ausführung von SQL mit SQLike 2.5 Unterstützte Funktionen . . . . . 2.6 Browserkompatibilität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 4 4 9 . . . . . . . . . . . . . . . . . . 11 11 13 17 19 22 24 3 Rekursive Anfragen im Tool 27 3.1 Erkennung rekursiver Anfragen . . . . . . . . . . . . . . . 27 3.2 Ausführung rekursiver Anfragen . . . . . . . . . . . . . . 28 3.3 Interaktive Darstellung der Rekursion . . . . . . . . . . . 29 4 Zusammenfassung 34 Literaturverzeichnis 38 III Kapitel 1 Einleitung Dieses Kapitel dient als Einleitung zum Thema Rekursion in der Informatik. Dabei wird die Funktionalität der Rekursion allgemein erklärt und es wird auf ihre Einsatzgebiete hingewiesen. Danach wird speziell auf die Rekursion in SQL weiter eingegangen. Am Ende des Kapitels wird noch erläutert, warum Rekursionen für Neueinsteiger in diesem Gebiet oftmals schwer verständlich und auch, wie dieses Tool dabei helfen soll. 1.1 Rekursion in der Informatik In der Informatik gibt es grundsätzlich zwei Ansätze, um eine Aufgabe zu lösen, die es erfordert, gewisse Schritte beliebig zu wiederholen: Iteration oder Rekursion. Der iterative Ansatz nutzt verschiedene Schleifenkonstrukte der verwendeten Programmiersprache, um Codeteile so lange zu wiederholen, bis das gewünschte Ergebnis erreicht wird. Eine rekursive Lösung benötigt keine Schleifenkonstrukte, sondern besteht meistens aus einer Funktion, die sich wiederholt selbst aufruft, bis eine gewisse Abbruchbedingung erreicht wird. Das einfachste Beispiel, um Rekursion und Iteration zu vergleichen, ist die Berechnung der Fakultät. Diese ist definiert als n Y n! = 1 · 2 · 3 · . . . n = k k=1 und ist für negative oder nicht ganze Zahlen nicht definiert. Die Fakultät lässt sich auch rekursiv definieren: ( 1, n=0 n! = n · (n − 1)!, n > 0 Diese Definitionen lassen sich leicht als iterative beziehungsweise rekursive Funktionen implementieren. 1 KAPITEL 1. EINLEITUNG Listing 1.1: Fakultät iterativ und rekursiv f u n c t i o n f a c t o r i a l I t e r a t i v e (n){ var r e s u l t = 1 ; f o r ( var i = 1 ; i <= n ; i ++) r e s u l t ∗= i ; return r e s u l t ; } f u n c t i o n f a c t o r i a l R e c u r s i v e (n){ i f ( n == 0 ) return 1; else r e t u r n n∗ f a c t o r i a l R e c u r s i v e ( n −1); } Diese Beispiele beinhalten keine Fehlerbehandlung, die ungültige (nicht definierte) Werte abfangen soll, sondern nur die Berechnung gültiger Eingaben. Der Unterschied der beiden Ansätze wird gleich sichtbar: Die iterative Implementierung durchläuft eine Schleife von 1 bis n und berechnet damit das Ergebnis. Die Rekursion hingegen teilt sich in zwei Fälle auf, die Abbruchbedingung und die eigentliche Rekursion. Die Funktion ruft sich solange selbst mit dem Wert n-1 auf, bis die Abbruchbedingung n = 0 erreicht wird. Dann werden die Ergebnisse für alle Funktionen zurückgegeben und dabei das Endergebnis berechnet. Die Frage, ob ein Problem nun iterativ oder rekursiv gelöst werden soll, kann nicht universell beantwortet werden, sondern hängt von mehreren Faktoren ab: • Verwendete Programmiersprache Einige (Programmier-)Sprachen definieren keine Schleifenkonstrukte, sondern verlassen sich einzig auf die Rekursion, wenn wiederholt Code aufgerufen werden soll. Logische oder funktionale Programmierung beschränkt sich oftmals nur auf die Verwendung von Rekursionen. Die relationale Algebra, die theoretische Basis von SQL, unterstützt ebenfalls keine Schleifenkonstrukte. • Formulierung der Problemstellung Bereits anhand der Problemstellung kann oft festgestellt werden, ob eine Umsetzung in iterativer oder rekursiver Form sinnvoller ist. Falls ein Problem bereits in rekursiver Form formuliert ist, ist im Normalfall auch die Umsetzung als Rekursion einfacher. Des Weiteren kann auch die verwendete Datenstruktur zur Entscheidung herangezogen werden, da sich verschiedene Datenstrukturen unterschiedlich gut für die beiden Ansätze eignen. So lassen sich 2 Lucas Ohlicher KAPITEL 1. EINLEITUNG Baumstrukturen, zum Beispiel Binärbäume oder Dateisysteme, rekursiv um einiges einfacher und intuitiver durchlaufen. Weitere typische Beispiele für die Verwendung von Rekursionen sind das Parsen von Sprachen1 oder verschiedene Sortieralgorithmen basierend auf dem Divide-and-Conquer-Prinzip2 , wie zum Beispiel Quicksort oder Mergesort. • Performanz und Speicherverbrauch Performanz und Speicherverbrauch unterscheiden sich meistens kaum bei beiden Ansätzen, aber sollten dennoch beachtet werden. Eine Rekursion benötigt mehr Speicher und ist im Allgemeinen langsamer als eine Iteration, da die Funktionsaufrufe auf dem Stack gespeichert werden müssen, um das Ergebnis zurückgeben zu können. Dabei kann es bei zu vielen Funktionsaufrufen auch zu einem Stack Overflow kommen, der das Programm zum Absturz bringen kann. Allerdings können Rekursionen, sofern es die Programmiersprache unterstützt, optimiert werden. Eine Möglichkeit zur Optimierung ist es, die Rekursion als sogenannte Endrekursion zu schreiben. Dabei ist der letzte Befehl den die Funktion ausführt, das rekursive Aufrufen der Funktion. Dadurch kann die Rekursion vom Compiler als iterativ interpretiert werden. In Listing 1.2 wird die rekursive Berechnung der Fakultät (siehe Listing 1.1) in eine endrekursive Variante umgebaut. Durch das Einführen einer Hilfsfunktion wird die nachträgliche (beim Zurückgeben der Ergebnisse) Multiplikation vermieden. Listing 1.2: Fakultät endrekursiv func tion helper (n , f a c ){ i f ( n == 0 ) return fac ; else r e t u r n h e l p e r ( n−1, f a c ∗n ) ; } f u n c t i o n f a c t o r i a l E n d R e c u r s i v e (n){ return helper (n , 1 ) ; } Letztlich muss für jede Problemstellung abgewogen werden, ob diese 1 https://en.wikipedia.org/wiki/Recursive_descent_parser, zuletzt besucht am 08.06.2016 2 https://en.wikipedia.org/wiki/Divide_and_conquer_algorithms, zuletzt besucht am 08.06.2016 Lucas Ohlicher 3 KAPITEL 1. EINLEITUNG iterativ, falls Schleifenkonstrukte unterstützt werden, oder rekursiv umgesetzt werden soll. 1.2 Bedeutung der Rekursion in der relationalen Algebra Die relationale Algebra ist die theoretische Grundlage für Abfragesprachen in relationalen Datenbanken. SQL ist eine relational vollständige Sprache, das bedeutet, es werden alle Operationen der relationalen Algebra implementiert. Allerdings gibt es Abfragen, die mit der relationalen Algebra nicht durchgeführt werden können. Es ist zum Beispiel nicht möglich, die transitive Hülle einer Relation zu berechnen. Die Berechnung dieser Hülle ist für rückbezügliche Relationen notwendig. Diese finden in verschiedensten Formen ihre Anwendung, zum Beispiel zur Berechnung von Vorfahren basierend auf einer Eltern-Kind-Relation oder zur Wegberechnung. 1.3 Funktionsweise in SQL Mit SQL:19993 wurden Common Table Expressions (CTE) dem SQL Standard hinzugefügt. Eine CTE ist ein temporäres Set, welches das Ergebnis einer Query enthält. Auf dieses Set kann dann in einer Query referenziert werden beziehungsweise kann es auch auf sich selbst referenzieren. Mit Hilfe dieser Selbstreferenz ist es möglich, rekursive Abfragen durchzuführen. Eine Abfrage wird als rekursiv bezeichnet, wenn sie eine rekursive CTE referenziert. Die Funktionsweise von rekursiven Abfragen wird unter anderem auch in [KE13] oder [Vos08] behandelt. 1.3.1 Struktur einer rekursiven CTE Der folgende Abschnitt orientiert sich inhaltlich an der offiziellen Dokumentation von Microsoft (SQL Server 2008 R2)[MSS]. Die Struktur einer rekursiven CTE ist ähnlich zu rekursiven Routinen in anderen Programmiersprachen. Mit dem Unterschied, dass eine rekursive CTE mehrere Zeilen als Ergebnis zurückgeben kann anstatt eines einzelnen Wertes. Eine rekursive CTE besteht aus drei Elementen: 1. Aufruf der Routine Der erste Aufruf der rekursiven CTE besteht aus einer oder mehreren CTE query definitions verbunden mit UNION ALL, UNION, EXCEPT, oder INTERSECT Operatoren. Das Ergebnis die3 4 https://en.wikipedia.org/wiki/SQL:1999, zuletzt besucht am 08.06.2016 Lucas Ohlicher KAPITEL 1. EINLEITUNG ser Query-Definitionen bildet die Basis der CTE-Struktur und wird als anchor oder Initialisierung bezeichnet. 2. Rekursiver Aufruf der Routine Ein Aufruf beinhaltet eine oder mehrere CTE query definitions verbunden mit UNION ALL Operatoren, welche die CTE selbst referenzieren. Diese werden als recursive members oder als Schrittfall bezeichnet. 3. Abbruchbedingung Eine Abbruchbedingung ist bereits beinhaltet. Die Rekursion stoppt, wenn ein Aufruf keine Zeilen als Ergebnis zurückgibt. Dabei muss beachtet werden, dass die Rekursion nicht in eine Endlosschleife gerät. Dies kann zum Beispiel auftreten, wenn die Daten auf eine bestimmte Abfrage einen Zykel enthält und somit immer ein Ergebnis liefert. Falls dies eintritt, muss die Abfrage manuell auf eine maximale Anzahl an Rekursionsschritten eingeschränkt werden. 1.3.2 Pseudocode und Semantik Eine rekursive CTE muss mindestens einen anchor (Initialisierung) und einen recursive member (Schrittfall) enthalten. Folgendes Beispiel beinhaltet jeweils eine Initialisierung sowie einen Schrittfall. 1 WITH cte name ( column name [ , . . . n ] ) AS 2 ( 3 C T E q u e r y d e f i n i t i o n −− I n i t i a l i s i e r u n g 4 UNION ALL 5 C T E q u e r y d e f i n i t i o n −− S c h r i t t f a l l 6 ) 7 SELECT ∗ FROM cte name −− A b f r a g e d e r CTE Die Semantik der rekursiven Ausführung lässt sich folgendermaßen erläutern: 1. Aufteilen der CTE in Initialisierung und Schrittfall. 2. Ausführen der Initialisierung ergibt das Basisergebnis (T0 ). 3. Ausführen der Schrittfälle mit Ti als Input und Ti+1 als Output. 4. Wiederholen von Schritt 3 bis ein leeres Ergebnis zurückgegeben wird. 5. Zurückgeben der Ergebnismenge. Diese ist eine Vereinigung (UNION ALL) von T0 bis Tn . Lucas Ohlicher 5 KAPITEL 1. EINLEITUNG abflug Wien Frankfurt Wien Wien Venedig Wien Zürich Paris Madrid ankunft Frankfurt London Budapest Venedig Rom Zürich Paris Madrid Lissabon Tabelle 1.1: Beispieltabelle flug 1.3.3 Beispiel Im folgenden wird die Funktionsweise einer rekursiven CTE an einem praktischen Beispiel gezeigt. Es basiert auf einer Datenbank bestehend aus einer Tabelle flug (siehe Tabelle 1.1), die Flugverbindungen beinhaltet. Es sollen alle Flughäfen berechnet werden, die von Wien beginnend mit beliebig vielen Zwischenschritten erreichbar sind. 1 2 3 4 5 6 7 8 9 WITH ReichweiteWien ( a b f l u g , a n k u f t ) AS ( SELECT ∗ FROM f l u g WHERE a b f l u g=”Wien” UNION ALL SELECT ∗ FROM s t e p . a b f l u g , s t e p . a n k u n f t FROM ReichweiteWien as r JOIN f l u g as s t e p ON ( r . a n k u n f t = s t e p . a b f l u g ) ) SELECT ∗ FROM ReichweiteWien 1. Die rekursive CTE ReichweiteWien definiert jeweils eine Initialisierung sowie einen Schrittfall. 2. Die Initialisierung liefert das Basisergebnis T0 . Es beinhaltet alle Flughäfen, die direkt mit Wien verbunden sind (siehe Tabelle 1.2). 3. Der Schrittfall liefert die weiteren direkten Verbindungen zurück, die aus dem Ergebnis der Initialisierung hervorgehen. Mit anderen Worten, die von Wien aus mit einem Zwischenschritt erreichbaren Flughäfen. Man erhält dieses Ergebnis mit einer Join-Operation zwischen flug und der CTE ReichweiteWien. Dabei wird die eigentlich Rekursion ausgeführt. Die Verbindungen der CTE ReichweiteWien als Input (Ti ) verknüpft mit dem Join r.ankunft = 6 Lucas Ohlicher KAPITEL 1. EINLEITUNG abflug Wien Wien Wien Wien ankunft Frankfurt Budapest Venedig Zürich Tabelle 1.2: Initialisierung abflug Wien Wien Wien Wien Frankfurt Venedig Zürich ankunft Frankfurt Budapest Venedig Zürich London Rom Paris Tabelle 1.3: Zwischenergebnis nach dem 1. Iterationsschritt step.abflug ergibt als Output (Ti+1 ) die Flughäfen, die von Ti direkt erreichbar sind. Das Ergebnis der ersten Iteration ist in Tabelle 1.3 dargestellt. 4. Der Schrittfall wird wiederholt ausgeführt. Die zweite Iteration benutzt nun das Ergebnis der ersten Iteration (siehe Tabelle 1.3), die dritte Iteration das Ergebnis der zweiten und so weiter. Dies wird so lange fortgesetzt, bis kein neues Ergebnis zurückgegeben wird (siehe Tabelle 1.4). 5. Das Endergebnis ist eine Vereinigung aller Ergebnisse der durchgeführten Iterationsschritte (siehe Tabelle 1.5). 1.3.4 Logisches Ausführungsmodell Generell wird eine Rekursion als Schleife umgesetzt, welche solange ausgeführt wird, bis sich das Ergebnis nicht mehr ändert. Dabei gibt es abflug Paris ankunft Madrid (a) 2. Iterationsschritt abflug Madrid ankunft Lissabon (b) 3. Iterationsschritt Tabelle 1.4: Weitere Zwischenergebnisse Lucas Ohlicher 7 KAPITEL 1. EINLEITUNG abflug Wien Wien Wien Frankfurt Venedig Zürich Paris Madrid ankunft Frankfurt Budapest Venedig London Rom Paris Madrid Lissabon Tabelle 1.5: Endergebnis grundsätzlich zwei verschiedene Ansätze, wie die entsprechende Iteration ausgeführt wird. • Naive Iteration Diese Variante berechnet solange Ergebnisse weiterer Schritte, bis diese sich nicht mehr verändern. Anhand des eben ausgeführten Beispiels ReichweiteWien lässt sich dieser Ansatz folgendermaßen formulieren: R1 := I n i t ; i :=1 repeat Ri+1 := ( Ri ◦ V) ∪ Ri ; u n t i l Ri+1 = Ri Der erste Schritt (R1 ) ist in diesem Fall die Initialisierung. Für die weiteren Schritte werden die gültigen Verbindungen (V) basierend auf dem aktuellen Ergebnis berechnet und schließlich damit vereinigt. Ein Nachteil dieser Umsetzung ist, dass bereits bekannte Ergebnisse immer neu berechnet werden. Um dies zu umgehen, wird für jeden Schritt jeweils nur ∆Vi neu berechnet. • Semi-naive Iteration (oder Delta-Iteration) In dieser Variante wird nicht das gesamte Ergebnis neu berechnet, sondern immer nur die Teilergebnisse der jeweiligen Schritte (∆i ). Diese beinhalten die Ergebnisse, die mit dem nächsten Schritt hinzugefügt werden. R0 := ∅ ; ∆1 := I n i t ; i : = 1 ; repeat Ri := Ri−1 ∪ ∆i ; ∆i+1 := ( ∆i ◦ V) − Ri ; i := i +1 u n t i l ∆i = ∅ 8 Lucas Ohlicher KAPITEL 1. EINLEITUNG Mit Hilfe der semi-naiven Iteration können diese Berechnungen effizienter ausgeführt werden, da die selben Ergebnisse mit einem geringeren Rechenaufwand erzielt werden. In SQL, und auch in diesem Übungstool, wird das Prinzip der semi-naiven Iteration vor allem aus diesem Grund verwendet. 1.3.5 Beispiel mit expliziter Abbruchbedingung Rekursionen in SQL können auch vorzeitig abgebrochen werden, falls nur eine bestimmte Anzahl von Iterationsschritten durchgeführt werden soll oder eine Endlosschleife verhindert werden soll. Dies lässt sich erreichen, indem der Schrittfall um eine Bedingung als Where-Statement erweitert wird. Die Abfrage aus 1.3.3 könnte zum Beispiel abbrechen, sobald ein beliebiger Flughafen erreicht wurde oder nachdem n Zwischenschritte ausgeführt wurden. Um eine Rekursion anhand der Anzahl der Schritte abzubrechen, muss der CTE eine weitere Spalte hinzugefügt werden, die als Zähler der Schritte fungiert. In 1.3 wird die Rekursion ReichweiteWien so erweitert, dass sie nach drei Zwischenschritten abbricht. Listing 1.3: Rekursion mit expliziter Abbruchbedingung 1 2 3 4 5 6 7 8 9 10 11 WITH ReichweiteWien ( s c h r i t t , a b f l u g , a n k u f t ) AS ( SELECT 1 , a b f l u g , a n k u n f t FROM f l u g WHERE a b f l u g=”Wien” UNION ALL SELECT r . s c h r i t t +1, s t e p . a b f l u g , s t e p . a n k u n f t FROM ReichweiteWien as r JOIN f l u g as s t e p ON ( r . a n k u n f t = s t e p . a b f l u g ) WHERE r . s c h r i t t < 3 ) SELECT ∗ FROM ReichweiteWien 1.4 Sinn des Übungstools Rekursionen sind für viele Studenten anfangs schwer verständlich, besonders im Vergleich zu einer iterativen Lösung des selben Problems. Das liegt oftmals daran, dass oft etwas Zeit benötigt wird, um mit Rekursionen vertraut zu werden und um ihre Eigenheiten zu verstehen. Iterationen erscheinen einfacher und intuitiver, wenn keine rekursive Definition des Problems vorliegt, die direkt übernommen werden kann. Außerdem sind rekursive Programme für Anfänger meist schwieriger zu lesen und zu verstehen, da diese kompakter ausfallen als ihr iteratives Gegenstück. Diese Punkte gelten vor allem für imperative ProgrammierLucas Ohlicher 9 KAPITEL 1. EINLEITUNG sprachen. Im Fall von SQL und anderen relational vollständigen Sprachen gibt es noch weitere Schwierigkeiten zu beachten. Zum Beispiel arbeitet man mit vollständigen Tabellen, die zuerst entsprechend abgefragt und verbunden werden müssen, anstatt mit einfachen Werten und Datenstrukturen. Ein weiteres Problem dabei ist es, die drei verschiedenen Abfragen, die für eine Rekursion mindestens notwendig sind, richtig zu formulieren und die Joins korrekt auszuführen. Letztlich muss auch eine Abbruchbedingung in den Schrittfall eingebaut werden, um eine Endlosschleife zu vermeiden. Rekursionen in SQL sind ein sehr mächtiges Werkzeug, welche mit einer gewissen Anfangshürde verbunden sind. Dieses Tool soll dabei helfen, diese Hürde (schneller) zu überwinden. Vor allem die grafische und interaktive Darstellung der Ergebnisse soll dazu führen, dass eine Rekursion in SQL nicht als ein unverständliches Gesamtkonstrukt betrachtet werden sollte, sondern als eine Kombination ihrer Einzelteile, welche meistens für sich selbst einfache SQL-Abfragen sind. 10 Lucas Ohlicher Kapitel 2 Übungstool Das folgende Kapitel befasst sich mit dem eigentlichen Übungstool. Zuerst werden Design-Entscheidungen diskutiert, die vor der Implementierung getroffen wurden, sowie deren Vor- und Nachteile hervorgehoben. Danach werden die wichtigsten Komponenten des Tools genauer beschrieben und ihre Funktionalität anhand von Beispielen erklärt. 2.1 Vorüberlegungen Die Grundidee dieses Tools ist es, ein webbasiertes Übungstool für Studenten zu erstellen, um ihnen die Rekursion in SQL näherzubringen. Die Aufgabe ist es, eine Webseite zu erstellen, die in irgendeiner Form fähig ist, SQL-Abfragen inklusive Rekursionen auszuführen. Zusätzlich ist es wichtig, dass das Tool für den Benutzer möglichst einfach und ohne größeren Aufwand bedienbar ist. Dabei stehen prinzipiell zwei verschiedene Herangehensweisen zur Auswahl, um das Ausführen von SQL im Tool zu verwirklichen. Eine Möglichkeit ist es, das Tool direkt mit einer fixen Datenbank zu verbinden. Diese Datenbank würde bereits über notwendige Datensätze verfügen und Abfragen werden direkt an die Datenbank weitergegeben und von ihr ausgeführt. Somit würde das Tool praktisch als grafische Datenbank-Schnittstelle, mit eventuell weiteren Funktionen, dienen. Der Vorteil dieser Herangehensweise ist es, dass die Umsetzung sehr einfach ist. Allerdings überwiegen dabei die Nachteile stark. Zuerst wird eine Datenbank benötigt, die möglichst immer erreichbar ist - was nicht garantiert werden kann. Es kann zu Ausfällen oder auch zu Überlastungen, zum Beispiel durch Endlosschleifen bei missglückten Rekursionen, kommen. Des Weiteren muss die Datenbank manuell mit Daten gefüllt werden und kann auch nur von einem Administrator erweitert werden, womit dem Benutzer nur wenige statische Datensätze zur Verfügung stehen würden. Dieses Problem könnte behoben werden, indem es dem Benut11 KAPITEL 2. ÜBUNGSTOOL zer ermöglicht wird, sich mit einer beliebigen, auch lokalen, Datenbank zu verbinden und diese zu verwenden. Allerdings werden die vorher genannten negativen Eigenschaften lediglich an den Benutzer selbst weitergegeben. Eine weitere Überlegung bei dieser Implementierung ist es, welches Datenbankmanagementsystem (DBMS) dabei zu verwenden wäre. Die SQL am naheliegendste Open-Source-Alternative ist MySQL[MyS], welche allerdings rekursive Anfragen nicht unterstützt. Andere Datenbankmanagementsysteme, die Rekursionen unterstützen, zum Beispiel PostgreSQL[PGS], würden zwar den Anforderungen entsprechen, aber weisen oft Unterschiede zu SQL auf - vor allem, was die Syntax betrifft. Da das Tool zur Übung von SQL ausgelegt wird, ist eine abweichende Syntax nicht zielführend. Auch zu beachten ist, dass Anfragen an Datenbanken oft optimiert werden und dadurch Ergebnisse eventuell in anderer Reihenfolge als erwartet ausgeben werden können. Hier wird ebenfalls wieder auf den Übungszweck verwiesen - das Tool soll das Ergebnis möglichst so ausgeben, wie es sich der Benutzer erwarten würde. Die andere Möglichkeit zur Implementierung ist es, Abfragen direkt lokal im Browser auszuführen. Somit fallen alle Probleme weg, die mit der Verwendung eines DBMS zusammenhängen, da zur Benutzung des Tools lediglich ein Browser benötigt wird. Der Nachteil dabei ist vor allem der höhere Programmieraufwand - das Tool selbst muss SQL-artige Abfragen parsen sowie auf Arrays ausführen können, inklusive der Rekursion. Außerdem müssen dabei möglichst viele SQL-Befehle und -Funktionen implementiert werden, damit sich das Tool für den Benutzer kaum von einem echten DBMS in Sachen Funktionalität unterscheidet. Der größere Programmieraufwand rechtfertigt sich vor allem durch die zusätzliche Flexibilität, die damit erreicht wird. Da der Parser für die Eingabe im Tool eigens dafür erstellt wird, kann sich die Syntax beliebig an SQL orientieren und es können auch auf das Tool zugeschnittene Fehlermeldungen generiert werden. Außerdem ist es für den Benutzer möglich, eigene Datensätze zu verwenden oder an andere weiterzugeben, da diese lediglich in Textform vorliegen müssen, um für das Tool verwendbar zu sein. Ein weiterer Punkt, der als Nachteil gesehen werden könnte, ist die Performance des Tools. Die Ausführung von SQL-artigen Abfragen ist definitiv langsamer und unoptimierter als die selben Abfragen auf einem DBMS. Allerdings ist dies für das Tool kaum bis gar nicht relevant, da es nicht für die Verwendung von großen Datensätzen gedacht ist. Es ist aus übungstechnischen Gründen nicht sinnvoll, eine Datenbank mit einigen tausend Einträgen pro Tabelle zu verwenden, wenn beispielsweise zwanzig Einträge pro Tabelle die selbe Wirkung erzielen. Mit kleinen Datensätzen unterscheiden sich die Abfragezeiten kaum von denen eines DBMS - besonders wenn zuvor erst eine Verbindung zu einer externen 12 Lucas Ohlicher KAPITEL 2. ÜBUNGSTOOL Datenbank hergestellt werden muss. Aufgrund der vorher genannten Vor- und Nachteile beider Implementierung fiel die Wahl letztlich eindeutig auf eine komplett clientseitige Implementierung. Ein Übungstool sollte möglichst benutzerfreundlich sowie schnell und einfach zu bedienen sein. Der Benutzer soll nach Aufruf der Seite ohne Aufwand eine Abfrage eingeben und ausführen können. Dennoch steht es dem Benutzer bei Bedarf zur Verfügung, eigene Datensätze zu verwenden, ohne sich direkt an den DatenbankAdministrator wenden zu müssen. 2.2 Graphische Benutzeroberfläche Das Tool besteht aus wenigen, für den Benutzer sichtbaren, Einzelteilen (Abbildung 2.1). Es gibt ein Menü zur Auswahl der zu verwendenden Datenbank, einen Überblick über die aktuelle Datenbank (Tabellen- und Spaltennamen mit Datentyp) sowie einen Editor, mit Syntax-Highlighting für SQL, zur Eingabe von Abfragen. Im Hintergrund erfolgt das Parsen der Eingabe und nach einem Klick wird, falls die Eingabe korrekt ist, die Abfrage ausgeführt und das Ergebnis ausgegeben. Falls eine rekursive CTE als Eingabe erkannt wird, kann diese schrittweise ausgeführt werden. Dieser Vorgang wird im nächsten Kapitel (3) genauer erläutert. Abbildung 2.1: Benutzeroberfläche am Beginn; Überblick über aktuellen Datensatz links, Editor rechts 2.2.1 Auswahl der Daten Ein Basis-Datensatz wird beim Laden des Tools bereits ausgewählt und ähnelt dem Datensatz, der in Beispiel 1.3.3 verwendet wird. Des Weiteren bietet der Button ’Select Database’ zwei Möglichkeiten, andere Lucas Ohlicher 13 KAPITEL 2. ÜBUNGSTOOL Datensätze zu laden. Zum einen können verschiedene vordefinierte Datensätze verwendet werden, die für alle Benutzer verfügbar sind. Diese liegen lokal vor und können in einem simplen Dropdown-Menü ausgewählt werden (siehe 2.2). Zum anderen können externe Datensätze in Form von GitHub Gists1 geladen werden. Bereits vorhandene Gists können per Eingabe der ID in das Tool geladen und werden temporär gespeichert. Das bedeutet, geladene Datensätze stehen solange zur Verfügung, bis das Tool geschlossen wird. Abbildung 2.2: Dropdown-Menü zur Datensatz-Auswahl Erstellung eigener GitHub Gists Mit GitHub Gists ist es möglich, eigene Datensätze für das Tool zu erstellen. Eine Gist-Datei enthält eine Datenbank in Textform, mit beliebig vielen Tabellen und Einträgen. Der Benutzer wird allerdings angehalten, das Ausmaß einer solchen Datenbank klein zu halten (max. 100 Einträge), da das Tool nicht für große Datenmenge ausgelegt ist und auch dem Hauptziel des Tools, dem Übungszweck, entgegenwirkt. Dabei wird eine Syntax verwendet, die bereits in einem anderen Tool Verwendung findet: Einem Rechner für relationale Algebra, RelaX - relational algebra calculator [Kes14]. Dadurch ist es möglich, Gist-Dateien in beiden Tools zu verwenden. Anhand eines Beispiel wird nun erläutert, wie eine solche Datei aufgebaut ist. group : Flug−Datenbank d e s c r i p t i o n [ [ E i n f a c h e r B e i s p i e l d a t e n s a t z , [ Databases and I n f o r m a t i o n Systems ] ( d b i s . uibk . ac . a t ) ] ] example [ [ /∗ B e i s p i e l −Query 1∗/ SELECT ∗ FROM f l u g WHERE i d > 2 ; /∗ B e i s p i e l −Query 2 ∗/ SELECT ∗ FROM f l u g h a f e n ]] flug = { a b f l u g : s t r i n g , a n k u n f t : s t r i n g , i d : number ’ VIE ’ , ’FRA’ , 0 ’FRA’ , ’LHR’ , 1 1 14 https://gist.github.com/, zuletzt besucht am 08.06.2016 Lucas Ohlicher KAPITEL 2. ÜBUNGSTOOL ’ VIE ’ , ’BUD’ , 2 } flughafen = { s t a n d o r t , land , i a t a , i d ’ Wien ’ , ’Ös t e r r e i c h ’ , ’ VIE ’ , 0 ’ F r a n k f u r t ’ , ’ Deutschland ’ , ’FRA’ , 1 ’ London ’ , ’ England ’ , ’LHR’ , 2 ’ Budapest ’ , ’ Ungarn ’ , ’BUD’ , 3 } • group Mit diesem Attribut wird der Datensatz mit einem Titel versehen. Dieser kann beliebige Zeichen enthalten und wird bis zum ersten Zeilenumbruch eingelesen. Der Titel scheint nach dem Laden im Dropdown-Menü zur Datenbankauswahl auf. • description (optional) Hier kann der Datensatz genauer beschrieben werden und es kann auf etwaige Quellen verwiesen werden. Falls das Attribut einzeilig ist, wird die selbe Syntax wie in group verwendet und es wird bis zum ersten Zeilenumbruch eingelesen. Eine mehrzeilige Beschreibung ist mit Hilfe von doppelten Klammern ebenfalls möglich, wie im Beispiel zu sehen. Die Beschreibung, wenn vorhanden, erscheint im Tool unterhalb des Editors. Dieses Attribut unterstützt die Markdown2 -Syntax um die Beschreibung direkt in äquivalenten HTML-Code umzuwandeln. Falls keine Beschreibung verwendet wird, kann das Attribut weggelassen werden. • example (optional) Mit diesem Attribut können Beispiel-Abfragen zum Datensatz hinzugefügt werden. Es wird die selbe Syntax wie für die mehrzeilige Beschreibung verwendet und es können beliebig viele Beispiele angegeben werden. Mehrere Beispiele werden mittels Strichpunkt getrennt. Diese Beispiele können nach dem Laden des Datensatzes über Buttons unterhalb des Editors direkt eingefügt werden. Dabei werden die Abfragen so übernommen, wie diese in der Gist-Datei angegeben werden, inklusive aller Zeilenumbrüche und Tabulatoren. • Datensätze Am Ende folgt die Definition der Datensätze. Die Angabe von Tabellen ähnelt dem Zuweisen von Variablen in diversen Program2 https://de.wikipedia.org/wiki/Markdown, zuletzt besucht am 08.06.2016 Lucas Ohlicher 15 KAPITEL 2. ÜBUNGSTOOL miersprachen. In diesem Fall wird der Name der Variable als Name der Tabelle verwendet und in den geschwungenen Klammern werden die Einträge dieser Tabelle definiert. Dabei dient die erste Zeile dazu, die Spalten nach dem Schema name : datentyp zu beschreiben. Die Angabe des Datentyps ist optional, wie im Beispiel zu sehen, und dient hauptsächlich zur genaueren Beschreibung der Spalten. Es werden nur string und number als Datentypen unterstützt, alles andere wird als String behandelt. In den weiteren Zeilen folgen die eigentlichen Einträge, wobei Strings mit einfachen Anführungszeichen versehen werden sollten. Nach Erstellung einer Gist-Datei kann diese nun unter der Verwendung der zugehörigen ID, zu finden am Ende der URL, ins Tool importiert werden. Somit ist es auch möglich, Datensätze einfach mit anderen Benutzer zu teilen - es wird nur die ID einer gültigen Gist-Datei benötigt. 2.2.2 SQL-Editor Der Editor wird mit Hilfe von CodeMirror[Cod] erstellt. CodeMirror ist ein, in JavaScript implementierter, Texteditor, der sich auf das Bearbeiten von Code spezialisiert. Es stehen sogenannte Modes für diverse Programmiersprachen, wie zum Beispiel SQL, zur Verfügung. Des Weiteren verfügt CodeMirror über eine umfangreiche API sowie CSSFunktionen, um den Editor an die Anwendung anzupassen. Für das Tool selbst dient CodeMirror hauptsächlich als visuelle Unterstützung in Form von Syntax-Highlighting und dem Anzeigen der Zeilennummern. In den Editor eingegebene Abfragen werden in Echtzeit geparst, auf korrekte Syntax und Semantik überprüft sowie gegebenenfalls ergänzt. Dieser Vorgang erfolgt in drei Schritten: 1. Überprüfung der Syntax Die Überprüfung der Syntax erfolgt mit Hilfe eines Parsers, der eigens für das Tool erstellt wurde und auf dessen Anforderungen zugeschnitten wurde. Auf den Parser wird in einem späteren Abschnitt genauer eingegangen (siehe Abschnitt 2.3). Liefert der Parser keinen Fehler zurück, wird mit dem nächsten Schritt fortgesetzt, ansonsten wird eine entsprechende Fehlermeldung angezeigt. 2. Überprüfung der Semantik Das Ergebnis des Parsers wird in diesem Schritt mit JavaScript überprüft. Jede Tabelle oder Spalte, die in der Abfrage referenziert wird, muss in der aktuellen Datenbank vorhanden sein. Falls alle Tabellen und Spalten in der Datenbank gefunden werden, oder ein entsprechender Alias in der Abfrage vorhanden ist, wird mit dem nächsten Schritt fortgesetzt, ansonsten wird eine Fehlermeldung, die auf die fehlerhafte Tabelle oder Spalte hinweist, angezeigt. 16 Lucas Ohlicher KAPITEL 2. ÜBUNGSTOOL 3. Fehlende Tabellen ergänzen Um dem Benutzer die Eingabe zu erleichtern, ist es nicht notwendig, Tabelle und Spalte immer exakt zu referenzieren. Falls eine Spalte einer Tabelle eindeutig zugeordnet werden kann, wird diese im Hintergrund zur Abfrage hinzugefügt. Existiert eine Spalte in mehreren verwendeten Tabelle, kann diese nicht eindeutig zugeordnet werden und es wird eine Fehlermeldung angezeigt. Nach erfolgreichem Abschluss dieser Schritte wird der Button unterhalb des Editors aktiv und die Abfrage kann ausgeführt werden. Diese wird direkt im Browser ausgeführt (siehe Abschnitt 2.4) und das Ergebnis bei nicht-rekursiven Abfragen direkt unterhalb ausgegeben. Rekursive Abfragen werden speziell ausgegeben und werden in einem späteren Kapitel genauer behandelt (siehe Kapitel 3). Abbildung 2.3: Ergebnis einer nicht-rekursiven Abfrage 2.3 Parsen von SQL mit PEG.js Da das Tool über keine Anbindung an eine Datenbank verfügt, erfolgt das Parsen der Eingabe ebenfalls clientseitig im Browser. Sobald sich der Inhalt des Editors verändert, wird versucht, diese Eingabe zu Parsen. Dabei wird der Inhalt des Editors an eine Parsing-Funktion übergeben, die mit Hilfe von PEG.js[Peg] erstellt wurde. Lucas Ohlicher 17 KAPITEL 2. ÜBUNGSTOOL 2.3.1 PEG.js PEG.js, entwickelt von David Majda, ist ein freier Parser-Generator für JavaScript. Ein Parser kann per Kommandozeile oder mit Hilfe einer JavaScript API generiert werden. Der Parser selbst ist ein JavaScript Objekt mit einer einfachen API. PEG.js basiert auf der parsing expression grammar (PEG)[For04]. PEG gehört zum Typ der analytisch-formalen Grammatiken, das bedeutet, eine formale Sprache wird mit Hilfe von Regeln zur String-Erkennung beschrieben. Syntaktisch erinnern PEGs an kontextfreie Grammatiken (CFG), werden aber unterschiedlich interpretiert. CFGs können mehrdeutig sein, während PEGs immer eindeutig sind. Wenn ein String erfolgreich geparst wird, gibt es genau einen gültigen Syntaxbaum. Daher eignen sich PEGs sehr gut, um Computersprachen zu parsen, während sich CFGs eher für natürliche Sprachen anbieten. Mit PEG.js erstellte Grammatiken ähneln syntaktisch JavaScript, sind nicht zeilenorientiert und ignorieren Leerzeichen zwischen Tokens. Außerdem ist es möglich, Kommentare wie in JavaScript zu verwenden. Auf oberster Ebene befinden sich rules, die über einen Namen identifiziert werden, und eine parsing expression, die das Muster definiert, nach dem der Input-Text gematcht wird. Der Parsing-Ausdruck kann zusätzlich JavaScript-Code enthalten, der ausgeführt wird, wenn das Muster erfolgreich gematcht wird. Zusätzlich kann vor der ersten Regel ein sogenannter initializer eingefügt werden. Dieser beinhaltet JavaScript-Code, der vor dem eigentlichen Parsing-Prozess ausgeführt wird. Alle Variablen und Funktionen, die im initializer definiert werden, können in den rules und parsing expressions verwendet werden. Damit können Hilfsfunktionen und Variablen definiert werden, die den Parsing-Prozess vereinfachen sollen. Der Parser dieses Tools basiert auf einem bereits vorhandenen SQLParser sql.pegjs3 . Dieser wurde an die Anforderungen des Tools angepasst und um Funktionen erweitert, insbesondere wurde die Syntax für CTEs zum Parser hinzugefügt. Eine korrekte CTE nach diesen Regeln besitzt im Allgemeinen die Form des Beispiels in 1.3.3. Mit dem Unterschied, dass die Query innerhalb der With-Klausel eine beliebige Form haben kann und nicht zwingend rekursiv sein muss. Es muss beachtet werden, dass die Anzahl der Attribute der CTE mit der Anzahl der Spalten der inneren Query übereinstimmen müssen. Falls die Eingabe korrekt ist, also der Parser nicht aufgrund eines Fehlers abbricht, wird ein JavaScript-Objekt zurückgegeben, welches die geparste Abfrage enthält. Dieses Objekt wird in weiterer Folge verwendet, um die Semantik zu überprüfen sowie die Abfrage auszuführen. Beispielswei3 18 https://github.com/alsotang/sql.pegjs, zuletzt besucht am 08.06.2016 Lucas Ohlicher KAPITEL 2. ÜBUNGSTOOL se liefert die Abfrage SELECT ∗ FROM f l u g WHERE f l u g . a b f l u g=”Wien” folgendes Objekt zurück: [ [ { ” stmt ” : ” s e l e c t ” , ” s e l e c t c o r e s ” : [ { ” r e s u l t s ” : [ { ” column ” : ” ∗ ” } ] , ” from ” : [ { ” t a b l e ” : ” f l u g ” } ] , ” where ” : [ { ” column ” : ” a b f l u g ” , ” t a b l e ” : ” f l u g ” } , ”=” , {” l i t e r a l ” : [ ” \ ” ” , [ ”W” , ” i ” , ” e ” , ” n ” ] , ” \ ” ” ] } ] } ] } ] 2.4 Ausführung von SQL mit SQLike Die Ausführung der Abfrage auf den Arrays wird ebenfalls gänzlich in JavaScript, mit Hilfe von SQLike[Fra], verwirklicht. SQLike wurde von Thomas Frank entwickelt und ist eine kleine Query-Engine für JavaScript und ActionScript. Ihre Syntax ist ähnlich zu SQL und kann verwendet werden, um Arrays von Objekten und Arrays von Arrays im Stile von SQL abzufragen. SQLike beherrscht viele grundlegende Funktionen und Befehle von SQL und musste für das Tool nur geringfügig erweitert werden. Damit ist es auch möglich, im Select-Abschnitt der Abfrage zu Rechnen beziehungsweise Strings zu konkatenieren. Um eine Abfrage auszuführen, muss die entsprechende Funktion von SQLike aufgerufen werden, und als Parameter ein Objekt mit der Query übergeben werden. Die Funktion liefert das Ergebnis der Query in Form eines Arrays zurück. 2.4.1 SQLike Beispiele Dieser Abschnitt zeigt anhand von Beispielen, wie Objekte für SQLikeQueries aufgebaut sind. Außerdem wird gezeigt, wie die neu hinzugefügten Funktionen verwendet werden. Dies ist lediglich eine kurze Aufzählung und befasst sich vor allem mit Funktionen, die für rekursive Abfragen unbedingt notwendig sind. Eine Dokumentation mit allen Funktionen ist auf der Webseite von SQLike zu finden4 . Der Aufruf der Funktion SQLike.q(queryObject) führt die Abfrage, die im Parameter als queryObject spezifiziert ist, aus. Die Aufgabe in der Verwendung von SQLike besteht darin, ein solches Objekt zu erstellen. Im Folgenden werden Beispiele gezeigt, wie diese aufgebaut werden. 4 http://www.thomasfrank.se/SQLike/, zuletzt besucht am 08.06.2016 Lucas Ohlicher 19 KAPITEL 2. ÜBUNGSTOOL Listing 2.1: Datensätze in SQLike (Auszug) var db = { } ; db . f l u g = [ { ” a b f l u g ” : ” VIE ” , ” a n k u n f t ” : ”FRA” , ” i d ” : 0 } , {” a b f l u g ” : ”FRA” , ” a n k u n f t ” : ”LHR” , ” i d ” : 1 } , {” a b f l u g ” : ” VIE ” , ” a n k u n f t ” : ”BUD” , ” i d ” : 2 } ] ; db . f l u g h a f e n = [ { ” l a n d ” : ” Ös t e r r e i c h ” , ” s t a n d o r t ” : ” Wien ” , ” i a t a ” : ” VIE ” , id :0} , {” l a n d ” : ” Deutschland ” , ” standort ”:” Frankfurt ” , ” i a t a ” : ”FRA” , id : 1 } ] ; Listing 2.2: SQLike Select-From-Where var q u e r y O b j e c t = { } ; queryObject . S e l e c t = [ ’ ∗ ’ ] ; q u e r y O b j e c t . From = db . f l u g ; q u e r y O b j e c t . Where = f u n c t i o n ( ) { r e t u r n t h i s . i d > 2 ; } ; var r e s u l t = SQLike . q ( q u e r y O b j e c t ) ; In Listing 2.1 wird auszugsweise gezeigt, wie Datensätze in SQLike aufgebaut sind. Im Beispiel wird eine Datenbank db mit den Tabellen flug und flughafen erstellt und je mit Einträgen versehen. In 2.2 wird die Basis-Funktionalität von SQLike gezeigt. Es wird ein Objekt erstellt und mit den notwendigen Attributen versehen. Das Select-Attribut ist ein Array mit allen Spaltennamen, die im Ergebnis angezeigt werden sollen beziehungsweise ein Stern für alle Spalten, wie in SQL. Das FromAttribut beinhaltet das Array oder das Array von Arrays, in dem die Daten zu finden sind, im Beispiel wird lediglich die entsprechende Variable referenziert. Where-Bedingungen werden als JavaScript-Funktionen übergeben und können damit auch sehr komplex werden. Dabei ist zu beachten, dass JavaScript- und SQL-Syntax unterschiedlich sein kann zum Beispiel gilt && in JavaScript als logisches Und, während in SQL dafür AND verwendet wird. 20 Lucas Ohlicher KAPITEL 2. ÜBUNGSTOOL Listing 2.3: SQLike Join mit On var q u e r y O b j e c t = { } ; queryObject . S e l e c t = [ ’ flug abflug ’ , ’ flug ankunft ’ , ’ flughafen standort ’ ] ; q u e r y O b j e c t . From = db . f l u g ; q u e r y O b j e c t . J o i n = db . f l u g h a f e n ; q u e r y O b j e c t . On = f u n c t i o n ( ) { r e t u r n t h i s . f l u g . a b f l u g == t h i s . f l u g h a f e n . i a t a } ; var r e s u l t = SQLike . q ( q u e r y O b j e c t ) ; Listing 2.4: SQLike Join mit Using var q u e r y O b j e c t = { } ; queryObject . S e l e c t = [ ’ flug abflug ’ , ’ flug ankunft ’ , ’ flughafen standort ’ ] ; q u e r y O b j e c t . From = db . f l u g ; q u e r y O b j e c t . J o i n = db . f l u g h a f e n ; q u e r y O b j e c t . Using = [ ’ id ’ ] ; var r e s u l t = SQLike . q ( q u e r y O b j e c t ) ; Wie in den Listings 2.3 und 2.4 zu sehen, werden Joins ebenfalls über Attribute des Query-Objekts realisiert. Dazu wird der Join-Partner sowie die Join-Bedingung in Form von Arrays angegeben, beziehungsweise im Fall von Joins mit On als JavaScript-Funktion. Außerdem ist es möglich, wie in SQL, einen impliziten Join auszuführen und die Join-Bedingung in das Where-Attribut zu verschieben. Dabei entfällt auch das JoinAttribut und der Join-Partner wird in das From-Attribut als weiteres Element aufgenommen. Listing 2.5: SQLike Union All var q u e r y O b j e c t = { } ; var query1 = { } ; var query2 = { } ; query1 . S e l e c t = [ ’ ∗ ’ ] ; query1 . From = db . f l u g ; query1 . Where = f u n c t i o n ( ) { r e t u r n t h i s . a b f l u g==”VIE ” } ; query2 . S e l e c t = [ ’ ∗ ’ ] ; query2 . From = db . f l u g ; query2 . Where = f u n c t i o n ( ) { r e t u r n t h i s . a b f l u g==”FRA” } ; q u e r y O b j e c t . UnionAll = [ query1 , query2 ] ; var r e s u l t = SQLike . q ( q u e r y O b j e c t ) ; Lucas Ohlicher 21 KAPITEL 2. ÜBUNGSTOOL Listing 2.5 zeigt die Funktionalität von Union All sowie anderer Mengenoperationen (Intersect, Except), die nach dem selben Schema durchgeführt werden. Dabei werden mehrere SQLike-Queries in einem Array zusammengefasst und als entsprechendes Attribut hinzugefügt. Listing 2.6: SQLike Funktionen var q u e r y O b j e c t = { } ; queryObject . S e l e c t = [ ’ | concat | ’ , [ abflug , ” − ” , ankunft ] , ’ | as | ’ , ” Strecke ” , ’ | calc | ’ , [ 2 , ’ ∗ ’ , id ] ] ; q u e r y O b j e c t . From = db . f l u g ; var r e s u l t = SQLike . q ( q u e r y O b j e c t ) ; Die Verwendung von Funktionen wird in Listing 2.6 demonstriert. Dabei wird im entsprechenden Select-Array zuerst die zu verwendende Funktion angegeben. Damit diese als Funktion von SQLike erkannt wird, wird sie anhand von Pipes am Anfang beziehungsweise am Ende des Ausdrucks gekennzeichnet. Das nächste Element des Arrays beinhaltet das Attribut bei Aggregat-Funktionen in Form einer Spalte oder einem Array von Attributen bei Rechnungen und String-Konkatenationen. Die Alias-Funktion kann nach dem selben Prinzip sowohl in Select- als auch From-Attributen verwendet werden. 2.5 Unterstützte Funktionen Da das Tool lokal und ohne Datenbank im Hintergrund arbeitet, ist nicht alles, was in SQL möglich ist, auch im Tool möglich. Im folgenden Abschnitt werden die vom Tool unterstützten SQL-Funktionen und Befehle aufgelistet sowie falls notwendig, genauer beschrieben. • Select (Distinct) - From - Where • And und Or • Order By • Like • Wildcards • Between • Not • Aliases mit As • Joins mit Using/On 22 Lucas Ohlicher KAPITEL 2. ÜBUNGSTOOL • Left/Right/Full/Cross/Natural Join • Group By und Having • Limit • Union/Except/Intersect (All) • Aggregat-Funktionen (Avg, Count, Min, Max, Sum) • String-Konkatenation mit concat() in Select • Grundrechnungsarten und Modulo in Select • Einfach-rekursive Abfragen (siehe Kapitel 3) • Kommentare Vollständigkeitshalber folgt nun eine Aufzählung verschiedener Funktionen, die entweder aus technischen Gründen nicht implementiert wurden oder aufgrund der Anforderungen des Tools nicht benötigt werden. • Subqueries • In • Is (Not) Null • Komplexere Datentypen (zum Beispiel Datum) • Data Definition Language (zum Beispiel CREATE TABLE) • Data Manipulation Language (zum Beispiel INSERT INTO oder UPDATE) 2.5.1 Hinweise zur Verwendung Um Unklarheiten zu vermeiden, werden einige Funktionen und Befehle etwas genauer erläutert und anhand von Beispielen erklärt. • Referenzierung Es ist nicht notwendig, Tabellen immer exakt anzugeben. Falls diese eindeutig sind, reicht der Name der Spalte. Der Parser wird eine entsprechende Fehlermeldung ausgeben, falls eine Spalte nicht eindeutig zugeordnet werden kann. Lucas Ohlicher 23 KAPITEL 2. ÜBUNGSTOOL • Joins Joins jeglicher Art werden von links nach rechts ausgeführt. Dabei muss, vor allem bei Abfragen mit mehreren Joins, beachtet werden, dass verschiedene Joins unter Umständen unterschiedliche Spaltennamen zurückliefern können und somit ein falsches Ergebnis erzeugen können. Zum Beispiel liefert ein Join mit Using nur eine Spalte zurück, während der selbe Join mit On zwei Spalten mit dem jeweiligen Präfix der Tabelle zurück gibt. Es wird geraten, verschiedene Join-Arten nach Möglichkeit nicht zu mischen. • Group By und Having Die Verwendung von Having ist nur in Kombination mit Group By möglich und auch nur dann, wenn Having direkt auf den Group By-Ausdruck folgt. • String-Konkatenation mit concat() Im Select-Teil der Abfrage kann eine Mischung aus Spalten und Strings beliebig konkateniert werden. Dabei werden der concat()Funktion die Daten als Attribute übergeben. SELECT c o n c a t ( ” Das i s t Flug ” , id , ” nach ” , a n k u n f t ) FROM f l u g • Grundrechnungsarten und Modulo in Select Rechnungen mit Kommazahlen können direkt als Spalte angegeben werden. Alternativ können Rechnungen auch in eine calc()Funktion, ähnlich zu concat(), verschoben werden. SELECT 2∗ i d FROM f l u g SELECT c a l c (2+ i d ) FROM f l u g • Kommentare Kommentare können im Editor mit der selben Syntax verwendet werden wie in SQL. −− e i n z e i l i g e r Kommentar /∗ m e h r z e i l i g e r Kommentar ∗/ 2.6 Browserkompatibilität Das Tool basiert komplett auf HTML, CSS und JavaScript und wurde zum Großteil unter der Verwendung der Frameworks AngularJS[Ang] 24 Lucas Ohlicher KAPITEL 2. ÜBUNGSTOOL und Twitter Bootstrap[Boo] erstellt. Des Weiteren werden einige wichtige Funktionen mit Hilfe der JavaScript-Bibliothek jQuery[jQu] umgesetzt. Dadurch hängt die Kompatibilität des Tools vor allem mit der Kompatibilität des Browsers mit AngularJS, Twitter Bootstrap und jQuery zusammen. AngularJS wird aktiv und weitreichend für die aktuellen Versionen von Chrome, Firefox, Safari und Safari für iOS sowie Internet Explorer der Versionen 9 bis 11 getestet. Für weitere Browser wird das Framework zwar nicht aktiv unterstützt, aber es kann ebenfalls fehlerfrei funktionieren - allerdings ohne Garantie. Für weitere Informationen zur Kompatibilität von AngularJS siehe https://docs.angularjs.org/misc/faq. Die Abbildungen 2.4 und 2.5 zeigen die jeweils von Twitter Bootstrap beziehungsweise jQuery aktiv unterstützen Browser. Es ist möglich, dass diese auf älteren Versionen oder anderen Browsern ebenfalls fehlerfrei funktionieren, allerdings kann dies von den Herstellern nicht garantiert werden. Abbildung 2.4: Twitter Bootstrap: Unterstützte Browser. Quelle: http: //getbootstrap.com/getting-started/#support Abbildung 2.5: Aktiv unterstützte Browser von jQuery. Quelle: https: //jquery.com/browser-support/ Wie zu erkennen ist, sind die jeweiligen Einzelteile des Tools im Allgemeinen in allen modernen und häufig verwendeten Browsern lauffähig. Des Weiteren wurde auch die Gesamt-Funktionalität des Tools in verLucas Ohlicher 25 KAPITEL 2. ÜBUNGSTOOL Browser Mozilla Firefox 44 Chromium 37 Google Chrome 50 Microsoft Edge 25 Microsoft Internet Explorer Google Chrome 50 Mozilla Firefox 46 Opera 37 Microsoft Internet Explorer Microsoft Internet Explorer Microsoft Internet Explorer Microsoft Internet Explorer 11 11 10 9 <9 Plattform Ubuntu 12.04 Ubuntu 12.04 Windows 10 Windows 10 Windows 10 Windows 7 Windows 7 Windows 7 Windows 7 Windows 7 Windows 7 Windows 7 lauffähig? ja ja ja ja ja ja ja ja ja ja eingeschränkt nein Tabelle 2.1: Liste getesteter Browser schiedenen Browsern und Umgebungen getestet. Ein Browser wird dabei auf die Ausführung rekursiver und nicht-rekursiver Anfragen getestet sowie dem Laden externe Datensätze via GitHub Gists. Dabei wurde festgestellt, dass das Übungstool auf verschiedenen modernen Browsern mit allen Funktionen verwendbar ist (siehe Tabelle 2.1). Microsofts Internet Explorer 9 funktioniert nur eingeschränkt, da externe Tabelle via GitHub Gists nicht geladen werden können. 26 Lucas Ohlicher Kapitel 3 Rekursive Anfragen im Tool In den vorherigen Kapiteln wird das Tool im Allgemeinen behandelt und anhand von nicht-rekursiven Anfragen gezeigt, wie es grundsätzlich funktioniert. Die Basis beinhaltet dabei das Parsen von SQL, den syntaktischen und semantischen Prüfungen der Eingabe, sowie letztlich das Ausführen der Query. All diese Teile sind notwendig, um rekursive Anfragen auszuführen und schrittweise darzustellen. 3.1 Erkennung rekursiver Anfragen Wie in Abschnitt 2.3 erläutert, erkennt der Parser des Tools die grundlegende Syntax einer Common Table Expression (CTE). Damit kann aber noch nicht festgestellt werden, ob die Eingabe eine rekursive Anfrage ist, da lediglich die Syntax und nicht die Semantik der Eingabe überprüft wird. Daher wird, nachdem die drei Schritte des Parsing-Prozesses (siehe Abschnitt 2.2.2) erfolgreich abgeschlossen wurden, überprüft, ob es sich um eine Rekursion handelt. Es ist zu beachten, dass das Tool nur einfache Rekursionen behandelt, die nach einem festgelegten Muster (siehe Abschnitt 1.3.3) aufgebaut sein muss. Eine rekursive Anfrage muss mehrere Anforderungen erfüllen, um vom Tool als solche erkannt zu werden: 1. With-Klausel Eine CTE ist notwendig, da dies die einzige Möglichkeit ist, um rekursive Anfragen in SQL zu tätigen. Besitzt die Eingabe keine With-Klausel, dann wird sie vom Tool automatisch als nichtrekursiv eingestuft. 2. Union All In SQL liefert eine Rekursion nur dann das gewünschte Ergebnis, wenn Initialisierung und Schrittfall mit dem Union All -Operator 27 KAPITEL 3. REKURSIVE ANFRAGEN IM TOOL verbunden sind. Falls das Tool keinen Union All -Operator in der CTE vorfindet, wird diese nicht als rekursiv eingestuft. 3. Rekursive Referenz und Join im Schrittfall Das zweite Select-Statement in der CTE muss in irgendeiner Weise auf sich selbst referenzieren - die eigentliche Rekursion. Des Weiteren ist es notwendig, dass dieses Statement einen Join beinhaltet, damit garantiert wird, dass das Ergebnis schrittweise wachsen kann. 4. Auswahl der With-Klausel Die Abfrage außerhalb der CTE muss zumindest eine Spalte der zuvor ausgeführten Rekursion beinhalten, ansonsten kann die Abfrage beliebig durchgeführt werden. Diese Anforderung dient dazu, um den Benutzer darauf hinzuweisen, dass das eigentliche Ergebnis der Rekursion erst mit dieser Abfrage ausgegeben werden kann. Sofern die Eingabe des Benutzers all diese Anforderungen erfüllt, wird diese als rekursive Abfrage erkannt. Es ist zu beachten, dass mit diesen Punkten noch kein Ergebnis garantiert werden kann - die Aufgabe einer richtigen Eingabe obliegt weiterhin dem Benutzer. Diese Punkte dienen dazu, um rekursive und nicht-rekursive Abfragen unterscheiden zu können. Diese Unterscheidung wird benötigt, da rekursive Anfragen in anderer Art und Weise ausgeführt und dargestellt werden müssen als nicht-rekursive Anfragen. 3.2 Ausführung rekursiver Anfragen Rekursive Anfragen können mit SQLike nicht direkt ausgeführt werden. Daher ist es notwendig, solche Anfragen iterativ zu betrachten und Schritt für Schritt auszuführen. Dieser Vorgang ist vergleichbar mit dem Ausführen einer rekursiven Anfrage per Hand. Es wird zuerst die Initialisierung ausgeführt und das Ergebnis zwischengespeichert, danach der erste Schrittfall. Das Ergebnis dieser beiden Abfragen wird vereinigt und es wird wieder der Schrittfall ausgeführt und mit dem vorigen Ergebnis vereinigt - so lange, bis dem Ergebnis keine neuen Zeilen mehr hinzugefügt werden. Dieser Vorgang wird vom Tool folgendermaßen ausgeführt: 1. Aufteilen der Abfrage in Initialisierung, Schrittfall und äußerer Query. Union All wird dabei ignoriert, da davon ausgegangen wird, dass Initialisierung und Schrittfall mittels Union All vereinigt werden. 2. Ausführung der Initialisierung mit Hilfe von SQLike. Das Resultat wird als Ergebnis der Rekursion zwischengespeichert. 28 Lucas Ohlicher KAPITEL 3. REKURSIVE ANFRAGEN IM TOOL 3. Ausführung des Schrittfalls mit Hilfe von SQLike und Vereinigung (mittels Union All ) mit dem Ergebnis der Rekursion. Dieser Schritt wird solange wiederholt, bis ein Schrittfall keine neuen Einträge zum Ergebnis hinzufügt. 4. Sobald die Rekursion abgeschlossen ist, wird auf dem Ergebnis die äußere Query ausgeführt. Diese ergibt das Endergebnis der rekursiven Anfrage. 3.3 Interaktive Darstellung der Rekursion Das Ziel dieses Tools ist es, dem Benutzer den Ablauf einer Rekursion in SQL zu veranschaulichen. Dazu wird als Beispiel eine ähnliche Abfrage wie in Abschnitt 1.3.3 ausgeführt um erreichbare Flughäfen rekursiv zu berechnen. Abbildung 3.1: Eingabe einer korrekten rekursive Abfrage Abbildung 3.1 zeigt, dass bei Eingabe einer korrekten rekursiven Abfrage der Benutzer eine Rückmeldung erhält, dass diese als rekursive angesehen wird. Weiters bedeutet dies, dass die interaktive Darstellung des Ergebnisses zur Verfügung steht. Sobald die Eingabe mit einem Klick auf den Button ausgeführt wird, erscheinen unterhalb des Editors die ersten beiden Teile der schrittweisen Lösung (siehe Abbildung 3.2). Es werden die für die Rekursion benötigten Statements herausgeschrieben, um sie später entsprechend hervorzuheben. Außerdem ist bereits die Ergebnistabelle der Rekursion sichtbar (Tabelle ’RecRel’ im Beispiel), die allerdings derzeit noch keine Einträge besitzt. Mit einem Klick auf ’Initialization’ kann der erste Schritt ausgeführt werden (siehe Abbildung 3.3). Dies ist die Initialisierung der Rekursion, also die Ausführung des ersten Select-Statements. Es wird farblich hervorgehoben, welcher Teil der Query dafür zuständig ist. Außerdem wird Lucas Ohlicher 29 KAPITEL 3. REKURSIVE ANFRAGEN IM TOOL Abbildung 3.2: Start der schrittweisen Darstellung das Ergebnis dieser Abfrage im Ergebnis markiert. Der nächste Schritt beinhaltet den ersten Join (siehe Abbildung 3.4). Es wird wieder das zuständige Select-Statement markiert sowie die JoinBedingung hervorgehoben. In diesem Schritt wird auch die Tabelle des Join-Partners dargestellt und es werden die für den Join zuständigen Spalten beider Tabellen farblich markiert. Nun wird das Resultat des Joins mit dem Inhalt der Tabelle ’RecRel’ vereinigt (Abbildung 3.5). Spalten, die in diesem Schritt hinzugefügt werden, sind farblich markiert. Dies entspricht dem Ergebnis der Rekursion nach dem ersten Schritt. Die nächsten Schritte beinhalten abwechselnd Join sowie Vereinigung und fügen so dem Ergebnis weitere Schritte hinzu. Dies wiederholt sich solange, bis keine Join-Partner mehr vorhanden sind und somit keine neuen Ergebnisse mehr hinzugefügt werden können. Die Abbildungen 3.6 und 3.7 zeigen jeweils den Join für den zweiten Schritt beziehungsweise das Ergebnis nach zwei Schritten. Sobald die Rekursion stoppt, enthält die Tabelle ’RecRel’ das vollständige Ergebnis. Damit kann nun, als letzter Schritt im Tool, die äußere Abfrage durchgeführt werden. Mit dem Button ’show final result’ wird die äußere Abfrage ausgeführt, womit das Endergebnis berechnet wird. Dieser Button ist von Anfang an verfügbar, um die schrittweise Lösung bei Bedarf zu überspringen. Der Benutzer kann in der Darstellung der Rekursion beliebig vor und zurück navigieren. Die Erklärungen und Hervorhebungen werden in beiden Fällen entsprechend angepasst. 30 Lucas Ohlicher KAPITEL 3. REKURSIVE ANFRAGEN IM TOOL Abbildung 3.3: Initialisierung der Rekursion Abbildung 3.4: Erster Schrittfall Lucas Ohlicher 31 KAPITEL 3. REKURSIVE ANFRAGEN IM TOOL Abbildung 3.5: Ergebnis des ersten Schritts Abbildung 3.6: Zweiter Schrittfall Abbildung 3.7: Ergebnis nach zwei Schritten 32 Lucas Ohlicher KAPITEL 3. REKURSIVE ANFRAGEN IM TOOL Abbildung 3.8: Ende der Rekursion Abbildung 3.9: Ergebnis der äußeren Query Lucas Ohlicher 33 Kapitel 4 Zusammenfassung In dieser Bachelorarbeit wurde ein Übungstool für die Rekursion in SQL erstellt. Es präsentiert schrittweise das Ergebnis einer rekursiven Abfrage, um es dem Benutzer zu ermöglichen, die einzelnen Teile einer solchen Abfrage interaktiv mitzuverfolgen. Ziel dabei ist es, Rekursionen besser zu veranschaulichen und somit verständlicher zu machen. Das Tool arbeitet komplett clientseitig und alle Berechnungen werden direkt im Browser ausgeführt. Die Eingabe von Abfragen erfolgt in einem Code-Editor, der über passendes Syntax-Highlighting verfügt. Bereits während der Eingabe wird diese durch einen Parser, der an die Anforderungen des Tools angepasst wurde, auf syntaktische und semantische Korrektheit überprüft. Falls keine Fehler angezeigt werden, kann die eingegebene Abfrage ausgeführt werden. Diese wird direkt im Browser mit Hilfe von JavaScript berechnet und benötigt keine externe Datenbank. Bei nicht-rekursiven Abfragen wird das Ergebnis direkt angezeigt, während bei rekursiven Abfragen der Benutzer sich über Buttons Schritt für Schritt durch die Zwischenergebnisse klicken oder direkt zum Endergebnis springen kann. Des Weiteren werden bei Rekursionen immer die derzeitigen Schritte farblich hervorgehoben, um den Vorgang sowie die zuständigen Tabellen und Code-Abschnitte besser zu veranschaulichen. Es liegen bereits vordefinierte Datensätze vor, die von jedem Benutzer verwendet werden können. Zusätzlich werden Beispiele für Rekursionen zur Verfügung gestellt, die sich über Buttons direkt in den Editor einfügen lassen. Außerdem ist es möglich, externe oder selbsterstellte Datensätze zu verwenden, welche in Form von Textdateien als GitHub Gists geladen werden können. Dabei können die Datensätze auch zusätzlich mit Beispiel-Abfragen versehen werden, die ebenfalls über Buttons direkt eingefügt werden können. Dadurch ist es möglich, Datensätze und Abfragen einfach mit anderen Benutzern zu teilen und wiederzuverwenden. 34 KAPITEL 4. ZUSAMMENFASSUNG Ausblick Das Übungstool unterstützt die Eingabe und Ausführung der meisten wichtigen SQL Funktionen und Befehle, die sich mit der Abfrage von Datensätzen befassen. Zusätzlich werden auch rekursive Abfragen unterstützt, die einem bestimmten Muster entsprechen. Nicht implementierte Funktionen wurden meist aus technischen Gründen nicht umgesetzt oder weil sie aufgrund des Übungszwecks des Tools nicht erforderlich waren. So entfällt zum Beispiel der komplette Block der Data Definition Language und der Data Manipulation Language, da Datensätze über andere Weise erstellt und manipuliert werden können. Es wäre allerdings durchaus vorstellbar, Datensätze auch direkt über den Editor zu erstellen und zu bearbeiten. Das Tool unterstützt derzeit nur einfach-rekursive Funktionen, das heißt es ist nicht möglich, Rekursionen zu verschachteln, um zum Beispiel das Ergebnis einer Rekursion als Initialisierung einer weiteren Rekursion zu verwenden. Dies gilt für die Berechnung und die interaktive Darstellung. Das liegt daran, dass sich einfache Rekursionen zur Erklärung am besten eignen und sich verschachtelte Rekursion von der Funktionsweise kaum von einfachen Rekursionen unterscheiden. Es wäre dennoch wünschenswert, verschachtelte Rekursionen zumindest berechnen zu können und dabei auf eine interaktive Darstellung zu verzichten, da diese wahrscheinlich sehr komplex ausfallen würde. Lucas Ohlicher 35 Literaturverzeichnis [Ang] AngularJS - Javascript Webframework by Google, https:// angularjs.org/, [Online; Stand 08. Juni 2016]. [Boo] Twitter Bootstrap - CSS-Framework, http://getbootstrap. com/, [Online; Stand 08. Juni 2016]. [Cod] CodeMirror - A Text Editor for JavaScript, https:// codemirror.net/, [Online; Stand 08. Juni 2016]. [For04] B. Ford: Parsing Expression Grammars: A Recognition-based Syntactic Foundation, Proceedings of the 31st ACM SIGPLANSIGACT Symposium on Principles of Programming Languages, POPL ’04, ACM, New York, NY, USA, pages 111–122, URL http://doi.acm.org/10.1145/964001.964011. [Fra] T. Frank: SQLike - a small query engine, http://www. thomasfrank.se/sqlike.html, [Online; Stand 08. Juni 2016]. [jQu] jQuery - JavaScript library, https://jquery.com/, [Online; Stand 08. Juni 2016]. [KE13] A. Kemper and A. Eickler: Datenbanksysteme: Eine Einführung, 9. Auflage, Oldenbourg Verlag, 2013. [Kes14] J. Kessler: Übungstool für Relationale Algebra, RelationenTupel-Kalkül und SQL, Bachelor Thesis, Department for Computer Science, University of Innsbruck, http://dbis-uibk. github.io/relax/, 2014, [Online; Stand 08. Juni 2016]. [MSS] Recursive Queries Using Common Table Expression (Microsoft SQL Server 2008 R2), https://msdn.microsoft.com/en-us/ library/ms186243.aspx, [Online; Stand 08. Juni 2016]. [MyS] MySQL, https://www.mysql.de/, [Online; Stand 08. Juni 2016]. [Peg] PegJS - Parser Generator for JavaScript, http://pegjs.org/, [Online; Stand 08. Juni 2016]. 37 LITERATURVERZEICHNIS [PGS] PostgreSQL, https://www.postgresql.org/, [Online; Stand 08. Juni 2016]. [Vos08] G. Vossen: Datenmodelle, Datenbanksprachen und Datenbankmanagementsysteme. 5. Auflage, Oldenbourg Verlag, 2008. 38 Lucas Ohlicher