11.01.2015 Aufrufe

pdf (1820 Kb) - Fachgebiet Datenbanken und Informationssysteme ...

pdf (1820 Kb) - Fachgebiet Datenbanken und Informationssysteme ...

pdf (1820 Kb) - Fachgebiet Datenbanken und Informationssysteme ...

MEHR ANZEIGEN
WENIGER ANZEIGEN

Erfolgreiche ePaper selbst erstellen

Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.

Leibniz Universität Hannover<br />

Fakultät für Elektrotechnik <strong>und</strong> Informatik<br />

Institut für Praktische Informatik<br />

<strong>Fachgebiet</strong> <strong>Datenbanken</strong> <strong>und</strong> <strong>Informationssysteme</strong><br />

Bachelorarbeit<br />

im Studiengang Informatik<br />

Relationenalgebra als Datenbankanfragesprache<br />

Stefanie Bernhardt<br />

Matr.-Nr. 2517460<br />

12. Mai 2009<br />

Prüfer <strong>und</strong> Betreuer: Prof. Dr. Udo Lipeck<br />

Zweitprüfer: Dr. Hans Hermann Brüggemann


Inhaltsverzeichnis<br />

1 Einleitung 3<br />

1.1 Zielsetzung der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />

1.2 Gliederung der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />

2 Gr<strong>und</strong>lagen 4<br />

2.1 Die Relationenalgebra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4<br />

2.1.1 Gr<strong>und</strong>operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4<br />

2.1.2 Ableitbare Operationen . . . . . . . . . . . . . . . . . . . . . . . . . 5<br />

2.1.3 Erweiterung der Relationenalgebra . . . . . . . . . . . . . . . . . . 6<br />

2.2 Prinzipieller Aufbau eines Compilers . . . . . . . . . . . . . . . . . . . . . 7<br />

2.3 JavaCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8<br />

2.4 Die JavaCC-Grammatikdatei . . . . . . . . . . . . . . . . . . . . . . . . . . 9<br />

3 Anforderungen im Detail 12<br />

3.1 Mindestanforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />

3.2 Verbesserungsmöglichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . 13<br />

4 Entwurf 14<br />

4.1 Syntax der Anfragesprache . . . . . . . . . . . . . . . . . . . . . . . . . . . 14<br />

4.2 Analyse der Eingabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />

4.2.1 Lexikale Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />

4.2.2 Syntaktische Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . 19<br />

4.2.3 Semantische Analyse: . . . . . . . . . . . . . . . . . . . . . . . . . . 34<br />

4.2.4 Die Symboltabelle . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />

4.3 SQL-Code-Erzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39<br />

5 Implementierung 48<br />

5.1 Package-Struktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48<br />

5.2 Die Grammatikdatei <strong>und</strong> generierte Klassen . . . . . . . . . . . . . . . . . 48<br />

5.3 Beschreibung der nicht generierten Klassen . . . . . . . . . . . . . . . . . . 49<br />

6 Die Benutzerschnittstelle 56<br />

Abbildungsverzeichnis 58<br />

Tabellenverzeichnis 59<br />

2


Kapitel 1<br />

Einleitung<br />

1.1 Zielsetzung der Arbeit<br />

Ziel dieser Arbeit ist die Entwicklung einer Datenbankanfragesprache auf Basis der Relationenalgebra.<br />

Dazu soll eine graphische Benutzerschnittstelle bereitgestellt werden, die<br />

auf komfortable Weise die Eingabe von Anfragen in der Relationenalgebra ermöglicht,<br />

sowie Ausgaben <strong>und</strong> ggf. Fehlermeldungen anzeigt. Die Anfragen werden intern in ihrer<br />

Struktur analysiert <strong>und</strong> auf Fehler geprüft. Im Fall eines Fehlers soll eine aussagekräftige<br />

Fehlermeldung ausgegeben werden. Ist die Anfrage fehlerfrei, so wird sie vereinfacht, in<br />

SQL übersetzt <strong>und</strong> an das Oracle-DBS gesendet. Die Ergebnistabellen sollen dem Nutzer<br />

in einem Ausgabebereich angezeigt werden.<br />

Der Hauptteil dieser Arbeit wird sich mit der Analyse, der Vereinfachung <strong>und</strong> der Übersetzung<br />

von Anfragen beschäftigen. Daneben spielen die Bereitstellung der Benutzerschnittstelle,<br />

die Verbindung zum Datenbanksystem <strong>und</strong> die Erweiterbarkeit eine große<br />

Rolle.<br />

Nach seiner Fertigstellung soll das Programm Studenten als Lernmittel dienen, um den<br />

sonst nur theoretischen Umgang mit der Relationenalgebra durch praktische Anwendung<br />

besser verinnerlichen zu können.<br />

1.2 Gliederung der Arbeit<br />

Im Anschluss an diese kleine Einleitung sollen zunächst die nötigen Gr<strong>und</strong>lagen erläutert<br />

werden, bevor etwas detaillierter auf die Anforderungen eingegangen wird. Im vierten Kapitel<br />

soll der Entwurf vorgestellt werden. Hier wird zunächst die Anfragesprache entworfen.<br />

Die folgenden Unter-Kapitel beschäftigen sich mit dem Entwurf der einzelnen Analyse<strong>und</strong><br />

Übersetzungs-Komponenten. Kapitel 5 gibt einen Überblick über die implementierten<br />

Klassen. Abschließend wird die Benutzeroberfläche vorgestellt.<br />

3


Kapitel 2<br />

Gr<strong>und</strong>lagen<br />

2.1 Die Relationenalgebra<br />

Die Relationenalgebra ist die Menge aller endlichen Relationen zusammen mit Operationen.<br />

Durch Kombination von Operationen lassen sich Terme formulieren, die man als<br />

prozedurale Datenbank-Anfragesprache auffassen kann.<br />

Im Folgenden werden die einzelnen relationalen Operationen beschrieben. Dabei sei jeweils<br />

für die Relation R das Schema (A 1 : D 1 , ..., A n : D n ), sowie für die Relation S das<br />

Schema (B 1 : D 1 , ..., B m : D m ) mit Attributen A i , B j <strong>und</strong> Datentypen D k vorausgesetzt.<br />

2.1.1 Gr<strong>und</strong>operationen<br />

• R ∪ S<br />

Die Vereinigung R ∪ S vereinigt alle Tupel zweier Relationen R <strong>und</strong> S unter der<br />

Vorraussetzung, dass beide Relationen die gleichen Schemata besitzen.<br />

Voraussetzung: Schema(R)=Schema(S)=(A 1 , ..., A n )<br />

Ergebnis-Schema: (A 1 , ..., A n )<br />

Ergeibnis-Relation: {t | t ∈ R ∨ t ∈ S}<br />

• R − S<br />

Die Differenz R − S liefert alle R-Tupel, die nicht in S enthalten sind.<br />

Voraussetzung: Schema(R)=Schema(S)=(A 1 , ..., A n )<br />

Ergebnis-Schema: (A 1 , ..., A n )<br />

Ergebnis-Relation: {t | t ∈ R ∧ t /∈ S}<br />

• σ ϕ (R)<br />

Die Selektion σ ϕ (R) liefert eine Teilmenge aller Tupel einer Relation R entsprechend<br />

der Selektionsformel ϕ. Dabei ist ϕ entweder eine atomare Selektionsformel<br />

oder eine Zusammensetzung atomarer Selektionsformeln mit den logischen Operatoren<br />

∧, ∨, ¬, ⇒, ⇔. Eine atomare Selektionsformel sei ϕ = θ (α 1 , ..., α n ) mit θ:<br />

n-stelliger Vergleichsoperator <strong>und</strong> α k : Attributterm aus R-Attributen, Datentyp-<br />

Operationen <strong>und</strong> Datentyp-Konstanten.<br />

Ergebnis-Schema: Schema(R)=(A 1 , ..., A n )<br />

Ergebnis-Relation: {t | t ∈ R ∧ t erfüllt ϕ}<br />

4


• π Ā (R)<br />

Die Projektion π Ā (R) liefert alle Spalten einer Relation R entsprechend der Attributliste<br />

Ā. Die Attributliste besteht aus R-Attributtermen <strong>und</strong> jeweils einem<br />

optionalen Aliasnamen.<br />

Voraussetzung: Ā = (α 1 C 1 , ..., α k C k )) α i : Attributterm, C i : Aliasname<br />

Ergebnis-Schema: (C 1 , .., C k )<br />

Ergebnis-Relation: {t : (C 1 , .., C k ) | r ∈ R, t = (π α1 (r) , ..., π αk (r))}<br />

• R × S<br />

Das Kartesische Produkt R×S bildet alle möglichen Kombinationen von Tupeln<br />

aus R <strong>und</strong> S. Vorraussetzung ist, dass die Attributmengen von R <strong>und</strong> S disjunkt<br />

sind.<br />

Voraussetzung: {A 1 , ..., A n } ∩ {B 1 , ..., B m } = ∅<br />

Ergebnis-Schema: (A 1 , ..., A n , B 1 , ..., B m )<br />

Ergebnis-Relation: {t | r ∈ R, s ∈ S, t = r · s}<br />

2.1.2 Ableitbare Operationen<br />

• R ∩ S = R − (R − S)<br />

Der Durchschnitt R ∩ S liefert alle Tupel, die in R <strong>und</strong> S enthalten sind.<br />

Voraussetzung: Schema(R)=Schema(S)=(A 1 , ..., A n )<br />

Ergebnis-Schema: (A 1 , ..., A n )<br />

Ergebnis-Relation: {t | t ∈ R ∧ t ∈ S}<br />

• R ⊲⊳ ϕ<br />

S = σ ϕ (R × S)<br />

Der Verb<strong>und</strong> bzw. Join liefert alle Kombinationen von R- <strong>und</strong> S-Tupeln bei denen<br />

die Verb<strong>und</strong>sbedingung ϕ erfüllt ist. Dabei ist die Verb<strong>und</strong>sbedingung ϕ eine<br />

Mengen von ∧-verknüpften Vergleichen A i θB j von Attributtermen von R <strong>und</strong> S mit<br />

Vergleichsoperator θ. Für θ ≡= handelt es sich um einen Equijoin.<br />

Ergebnis-Schema: (A 1 , ..., A n ) · (B 1 , ..., B m )<br />

Ergebnis-Relation: {r · s | r ∈ R ∧ s ∈ S ∧ r, s erfüllen ϕ}<br />

• R ⊲⊳ S = R ⊲⊳ ϕ S mit ϕ = ∧ A i = B j S, wobei A i , B j jeweils namensgleiche<br />

Attribute von R <strong>und</strong> S sind. Der Natural Join ist somit ein Equijoin über alle<br />

namensgleichen Attribute von R <strong>und</strong> S.<br />

Ergebnis-Schema: {A 1 , ..., A n } ∪ {B 1 , ..., B m }<br />

Ergebnis-Relation: {t mit Attributen {A 1 , ..., A n } ∪ {B 1 , ..., B m } |<br />

π A1 ,...,A n<br />

(t) ∈ R ∧ π B1 ,...,B m<br />

(t) ∈ S<br />

• R ⊲< S<br />

Der Semijoin R ⊲< ϕ S = π Attribute(R) (R ⊲⊳ ϕ S) liefert alle R-Tupel, die mindestens<br />

einen Joinpartner in S haben.<br />

Ergebnis-Schema: Schema(R)=(A 1 , ..., A n )<br />

Ergebnis-Relation: {t ∈ R | ∃ (s ∈ S) t, s erfüllen ϕ}<br />

5


• R ⊲< ϕ<br />

S<br />

Der Anti-Semijoin R ⊲< ϕ S = R − (R ⊲< ϕ S) liefert alle R-Tupel, die keine<br />

Joinpartner in S haben.<br />

Ergebnis-Schema: Schema(R)=(A 1 , ..., A n )<br />

Ergebnis-Relation: {t ∈ R | ¬∃ (s ∈ S) t, s erfüllen ϕ}<br />

• R ⊐⊲⊳⊏ ϕ S<br />

Der Outerjoin liefert alle Tupel aus dem Join zwischen R <strong>und</strong> S mit zusätzlich<br />

allen R <strong>und</strong> S-Tupeln, die keinen Joinpartner haben (mit Nullwerten aufgefüllt)<br />

Ergebnis-Schema: (A 1 , ..., A n ) · (B 1 , ..., B m )<br />

Ergebnis-Relation: {t · s | t ∈ R , s ∈ S , t, s erfüllen ϕ}<br />

∪ { t · (⊥, · · · , ⊥) | t ∈ R , t /∈ (R ⊲⊳ S) t erfüllt ϕ}<br />

∪ { (⊥, · · · , ⊥) · s | s /∈ (R ⊲⊳ S) s erfüllt ϕ}<br />

Varianten:Left Outer Join, Right Outer Join<br />

Hierbei werden nur die Tupel der linken (bzw rechten) Relation mit Nullwerten<br />

aufgefüllt.<br />

• Division R ÷ S<br />

Für R(A 1 , ..., A n ) <strong>und</strong> S(B 1 , ..., B m ) <strong>und</strong> unter der Voraussetzung (B 1 , ..., B m ) =<br />

(A n−m+1 , ..., A n ) selektiert die Division R ÷ S alle (A 1 , ..., A n−m )-Tupel aus R die<br />

mit allen S-Tupeln in R auftreten.<br />

Voraussetzung: Schema(S) ⊆ Schema(R), S ≠ ∅<br />

Ergebnis-Schema: (A 1 , . . . , A n−m ),<br />

Ergebnis-Relation: { t: (A 1 , . . . , A n−m ) | ∃(r ∈R) ( t=π A1 ,...,A n−m<br />

(r) ∧ ∀(s∈S) t·s ∈<br />

R ) }<br />

• Umbenennungen<br />

Jeder Relationsoperand darf mit einem Alias versehen werden. Für eine Relation<br />

R <strong>und</strong> ein Alias X, ist die Schreibweise (R X). Auf die Attribute der umbenannten<br />

R-Relation wird mit X.A i zugegriffen.<br />

2.1.3 Erweiterung der Relationenalgebra<br />

• Γ Ḡ# ¯F (R)<br />

Die Gruppierung Γ Ḡ# ¯F (R) liefert alle Spalten entsprechend ¯F von Tupel-Gruppen<br />

mit gleichen Ḡ-Werten. ¯F enthält eine Liste von gruppeninvarianten R-Attributtermen<br />

<strong>und</strong> auf R-Attributterme angewendete Aggregatfunktionen wie count, min,<br />

max, sum, avg. Für Ḡ = ∅ bilden alle Tupel eine Gruppe.<br />

Beispiel: Γ ∅#count(∗) (ST UDENT EN)<br />

Voraussetzung: ¯F = (α1 C 1 , ..., α k C k )) α i : Attributterm oder Aggr.-Funktion, C i :<br />

Aliasname<br />

Ergebnis-Schema: (C 1 , .., C k )<br />

6


2.2 Prinzipieller Aufbau eines Compilers<br />

Im Allgemeinen bestehen Compiler aus sechs Funktionseinheiten mit klar abgegrenzten<br />

Aufgabenbereichen. Dieses Vorgehen ermöglicht eine hohe Wiederverwendbarkeit einzelner<br />

Komponenten <strong>und</strong> vereinfacht nachträgliche Änderungen durch Austauschen einzelner<br />

Einheiten. In der Praxis gibt es durchaus Abweichungen in der Anzahl der Komponenten,<br />

sowie in der Verteilung ihrer Aufgaben, was aber an der gr<strong>und</strong>legenden Funktionsweise<br />

nichts ändert. Im folgenden Abschnitt werden kurz die typischen Funktionseinheiten<br />

erläutert.<br />

Lexikale Analyse<br />

Die aktive Komponente der lexikalen Analyse ist der Scanner bzw. Lexer. Der Scanner liest<br />

den zu übersetzenden Code zeichenweise ein <strong>und</strong> erzeugt daraus eine Tokenfolge. Jedes<br />

Token besteht dabei aus einer Tokenklasse <strong>und</strong> ggf. einem Tokenwert. Ein Tokenwert ist<br />

ein Zeiger auf den entsprechenden Eintrag in der Symboltabelle, in welcher Informationen<br />

über die auftretenden Tokens abgespeichert werden.<br />

Syntaktische Analyse<br />

Die aktive Komponente der syntaktischen Analyse ist der Parser. Er analysiert die vom<br />

Scanner erzeugte Tokenfolge anhand einer Grammatik, die die Syntax der Quellsprache<br />

beschreibt, <strong>und</strong> erzeugt daraus einen Syntaxbaum.<br />

Semantische Analyse<br />

Die letzte Analysephase ist die semantische Analyse. Die Hauptaufgaben der semantischen<br />

Analyse sind die Datentypprüfung <strong>und</strong> die Untersuchung von Gültigkeitsbereichen. Bei<br />

behebbaren Typfehlern kann ggf. eine Typanpassung vorgenommen werden.<br />

Zwischencodeerzeugung<br />

Damit ist die Analyse der Eingabe abgeschlossen. In manchen Fällen wird an dieser Stelle<br />

schon direkt der Zielcode erzeugt. Im Allgemeinen wird jedoch zunächst ein Zwischencode<br />

erzeugt, der sich leicht produzieren lässt <strong>und</strong> sich leicht in die Zielsprache übersetzen lässt.<br />

Der Zwischencode kann als Basis für verschiedene Übersetzungen in ähnliche Sprachen<br />

dienen <strong>und</strong> soll Optimierungen vereinfachen.<br />

Codeoptimierung<br />

Der Zwischencode wird nun in Hinblick auf Laufzeitverhalten <strong>und</strong> Speicherplatzbedarf<br />

optimiert. Dazu werden zum Beispiel überflüssige Berechnungen entfernt oder Anweisungen<br />

verschoben, sofern sie im Zielcode dadurch seltener ausgeführt werden müssen <strong>und</strong><br />

die Verschiebung semantisch nichts verändert.<br />

Zielcodeerzeugung<br />

In der letzten Phase wird nun aus dem optimierten Zwischencode der Zielcode erzeugt.<br />

7


2.3 JavaCC<br />

JavaCC steht für Java Compiler Compiler <strong>und</strong> ist gleichzeitig Scanner- <strong>und</strong> Parsergenerator,<br />

der in Java implementiert ist <strong>und</strong> Java-Code erzeugt. JavaCC ist Open Source <strong>und</strong><br />

unter den Bedingungen der BSD-Lizenz herausgegeben. Optional kann statt Scanner <strong>und</strong><br />

Parser auch nur eine der Komponenten erzeugt werden. Ein einfacher generierter Scanner<br />

(TokenManager) erzeugt eine Tokenfolge <strong>und</strong> speichert für jedes Token den Tokenwert, die<br />

Tokenklasse <strong>und</strong> die Position. Der Tokenmanager wirft Fehler vom Typ TokenMgrError,<br />

die die Fehlerquelle gut beschreiben. Für Fehler in der Eingabe wird eine Fehlermeldung<br />

mit der genauen Position des unerwarteten Zeichens <strong>und</strong> einigen zusätzlichen Informationen<br />

ausgegeben. Auch interne Fehler, wie Endlosschleifen oder der Versuch einer zweiten<br />

Instanzierung des Tokenmanagers werden identifiziert. Der generierte Parser ist ein LL(n)-<br />

Parser, wobei standardmäßig n=1 ist. Das Lookahead kann aber nicht nur optional größer<br />

eingestellt werden, sondern auch dynamisch während des Parsens angepasst werden. (ParseException<br />

noch erklären )<br />

Einfache Scanner <strong>und</strong> Parser können mit wenig Aufwand erzeugt werden. JavaCC bietet<br />

aber auch zahlreiche Möglichkeiten komplexere Strukturen zu scannen <strong>und</strong> zu parsen.<br />

Für den Tokenmanager können verschiedene Zustände (lexical states) definiert werden,<br />

in denen er unterschiedliche Token erkennt, was zum Beispiel zur Handhabung von Kommentaren<br />

hilfreich sein kann. Neben normalen Token <strong>und</strong> zu überlesenden Zeichenketten<br />

lassen sich SpecialTokens definieren, die an jeder beliebigen Stelle im Programm unabhängig<br />

von der Grammatik auftreten dürfen <strong>und</strong> keinen Einfluss auf den Parsingvorgang<br />

haben. Um dies zu gewährleisten tauchen die SpecialToken in der normalen Tokenfolge<br />

nicht auf. Stattdessen besitzt jedes Token einen zweite Referenz, auf ein SpecialToken<br />

unmittelbar vor dem Token. Jedes SpecialToken besitzt eine Referenz zum nächsten SpecialToken.<br />

Somit kann bei Bedarf auf die normale Tokenfolge ohne Specialtokens, auf die<br />

Specialtokenfolge <strong>und</strong> auf die Position der SpecialTokens innerhalb der Tokenfolge zugegriffen<br />

werden. Zusätzlich gibt es noch eine vierte Möglichkeit mit gescannten Zeichen<br />

umzugehen, dabei wird die eingelesene Zeichenkette zunächst zwischengespeichert <strong>und</strong><br />

dem nächsten erkannten Token zugefügt. Auch diese Funktion kann zum Beispiel beim<br />

Umgang mit Kommentaren hilfreich sein.<br />

Die Token für die Lexikale Analyse, sowie die Grammatik für den Parser, Einstellungen<br />

des Compilers <strong>und</strong> Javacode-Fragmente werden zusammen in einer Grammatikdatei<br />

definiert. Dabei werden die Token entweder als Strings oder als reguläre Ausdrücke notiert.<br />

Die Parser-Produktionen werden in EBNF oder in Javacode angegeben. Aus der<br />

Grammatikdatei erzeugt der JavaC-Compiler alle nötigen Dateien. Die erzeugten Klassen<br />

umfassen dabei die Klassen für den Parser, den Scanner (TokenManager) <strong>und</strong> eine<br />

Constants-Klasse, welche die definierten Token auf Konstanten abbildet. Diese Klassen<br />

werden bei jedem Kompiliervorgang neu erzeugt. Zusätzlich werden, sofern sie nicht bereits<br />

vorhanden sind, vier weitere Klassen erzeugt: Token.java, SimpleCharStream.java,<br />

TokenMgrError.java <strong>und</strong> ParseException.java.<br />

Ein großer Vorteil von JavaCC ist, dass man sehr viele Möglichkeiten hat die Erzeugung<br />

von Klassen <strong>und</strong> deren Verhalten zu beeinflussen. Beispielsweise bietet JavaCC verschiedene<br />

Optionen, die zu Beginn in der Grammatikdatei auf die gewünschten Werte gesetzt<br />

werden können. Zum Beispiel lässt sich mit den Optionen USER_CHAR_STREAM <strong>und</strong><br />

USER_TOKEN_MANAGER die Erzeugung der konkreten Charstream- <strong>und</strong> TokenManager-<br />

Klassen unterbinden <strong>und</strong> stattdessen Interfaces erzeugt. Über die Option BUILD_PARSER<br />

8


lässt sich die Erzeugung des Parsers abstellen. COMMON_TOKEN_ACTION ist eine<br />

Option die standardmäßig false ist. Setzt man sie auf true, so wird nach jedem gescannten<br />

Token die Methode CommonTokenActen(Token t) aufgerufen, die man selbst definieren<br />

kann. Dies eignet sich zum Beispiel sehr gut um die Symboltabelle mit den verschiedenen<br />

Token zu füllen. Natürlich gibt es noch eine Vielzahl weiterer Optionen, auf die ich aber<br />

an dieser Stelle aber nicht weiter eingehen möchte.<br />

Zusätzlich lässt sich die Grammatikdatei an vielen Stellen mit Javacode anreichern. Das<br />

Gr<strong>und</strong>gerüst der Parserklasse wird standardmäßig in der Grammatikdatei definiert. Für<br />

den Scanner lassen sich neben der CommonTokenAction auch lexikale Aktionen definieren:<br />

Jedem regulärem Ausdruck kann ein Java-Block folgen, in dem die Aktion definiert<br />

wird, die nach Erkennung eines Wortes des Ausdrucks ausgeführt werden soll. Ähnlich<br />

können für den Parser parser_actions definiert werden, die bei Anwendung einer Produktion<br />

ausgeführt werden, dies könnten zum Beispiel Anweisungen zum Konstruieren eines<br />

Syntaxbaumes sein. Zum Erstellen eines Syntaxbaumes kann auch optional ein Präprozessor<br />

wie JJTree oder JTB vorgeschaltet werden. In diesem Fall wird die eigentliche<br />

.jj-Grammatikdatei aus einer, im Fall von JJTree, .jjt-Grammatikdatei generiert. JJTree<br />

ergänzt den Code dabei um den Aufbau des Syntaxbaumes.<br />

Man erkennt schnell dass sich JavaCC sowohl für einfache, sowie auch für komplexe<br />

Scanner <strong>und</strong> Parser eignet. Die von vorne herein vorhandene gute Fehlererkennung, die<br />

zahlreichen Einstellmöglichkeiten <strong>und</strong> vor Allem die Möglichkeit einzelne Produktion mit<br />

Javacode anzureichern, machen die Nutzung dieses Tools für diese Arbeit möglich. Wie<br />

bereits oben erwähnt, bringt die Nutzung eines Generators erhebliche Vorteile in Punkt<br />

Erweiterbarkeit mit sich, weswegen JavaCC hier auf jeden Fall benutzt werden soll.<br />

2.4 Die JavaCC-Grammatikdatei<br />

Der Aufbau der Grammatikdatei soll hier nur grob erläutert werden. Eine ausführliche<br />

Beschreibung findet sich unter https://javacc.dev.java.net/doc/javaccgrm.html .<br />

Die Grammatikdatei beginnt bei Bedarf mit einem Optionenblock. Einige der möglichen<br />

Optionen habe ich oben bereits erwähnt, JavaCC bietet aber eine Vielzahl weiterer möglicher<br />

Optionen zur Anpassung der generierten Klassen an die jeweiligen Bedürfnisse des<br />

Entwicklers. Der Optionenblock wird mit dem Schlüsselwort options eingeleitet. Zwischen<br />

geschweiften Klammern werden dann den Optionen die gewünschten Werte zugewiesen.<br />

Ein Optionenblock könnte also wie folgt aussehen:<br />

options<br />

{<br />

LOOKAHEAD=2;<br />

IGNORECASE=true;<br />

COMMON_TOKEN_ACTION=true;<br />

}<br />

Möchte man die Standardeinstellungen beibehalten, kann der Optionenblock weggelassen<br />

werden.<br />

Als nächstes folgt compiliation_unit, eingeschlossen in die Schlüsselwörter<br />

PARSER_BEGIN(parser_name) <strong>und</strong> PARSER_END(parser_name). Hier erfolgt die<br />

Definition der Parser-Klasse, die beim Kompiliervorgang von JavaCC mit Code gefüllt<br />

werden soll. Außerdem kann hier eine Package-Deklaration angegeben sein, die für alle ge-<br />

9


nerierten Klassen gelten soll, so wie Import-Anweisungen für die Parser <strong>und</strong> TokenManger-<br />

Klassen. Ein Beispiel für eine compilation_unit:<br />

PARSER_BEGIN(TestParser)<br />

package foo;<br />

public class TestParser {<br />

public static void main(String args[]) throws ParseException {<br />

eg1 parser = new eg1(System.in);<br />

try {<br />

TestParser.StartSymbol();<br />

System.out.println("Thank␣you.");<br />

} catch (Exception e) {<br />

System.out.println(e.getMessage());<br />

TestParser.ReInit(System.in);<br />

}<br />

}<br />

}<br />

PARSER_END(TestParser)<br />

Anschließend müssen die Produktionen für den Scanner <strong>und</strong> den Parser definiert werden.<br />

Der gesamte Aufbau der Grammatikdatei ist auf der javacc-Homepage in EBNF dokumentiert.<br />

Da die Produktionen sehr unterschiedlich aufgebaut sein können, ist die Definition<br />

in EBNF hier auch sehr sinnvoll, weswegen ich diese übernehme.<br />

Eine regular_expression_production wird durch eins der vier Schlüsselwörter TOKEN,<br />

SKIP, SPECIAL_TOKEN oder MORE eingeleitet, die den Umgang mit Wörtern dieses<br />

Ausdrucks festlegen. Gegebenenfalls wird dem Schlüsselwort eine Liste der Zustände, in<br />

denen der reguläre Ausdruck erkannt werden soll, vorangestellt.<br />

regular_expression_production ::=<br />

["" | ""]<br />

( TOKEN | SKIP | SPECIAL_TOKEN | MORE ) [IGNORE_CASE]":"<br />

"{" regexpr_spec ( | regexpr_spec )∗ "}"<br />

regexpr_spec ::=<br />

regular_expression [java_block_for_lexical_action][":" identifier_for_lexical_state ]<br />

regular_expression ::=<br />

java_string_literal<br />

| ""<br />

| ""<br />

| ""<br />

complex_regular_expression_choices ::=<br />

complex_regular_expression ( "|" complex_regular_expression )∗<br />

complex_regular_expression ::=<br />

( complex_regular_expression_unit )∗<br />

complex_regular_expression_unit ::=<br />

java_string_literal<br />

| ""<br />

| character_list<br />

| "(" complex_regular_expression_choices ")" [ "+" | "∗" | "" ]<br />

Beispiel:<br />

10


TOKEN :<br />

{<br />

<br />

| <br />

| <br />

| <br />

| <br />

}<br />

Das für die regular_expression angegebene Symbol "#"dient der Markierung von privaten<br />

regulären Ausdrücken, die nur zur Hilfe definiert werden <strong>und</strong> selbst kein Token darstellen.<br />

Die Methode CommonTokenAction sowie Variablen <strong>und</strong> Methoden, die für lexikale Aktionen<br />

benötigt werden an einer zentralen Stelle definiert, eingeleitet durch das Schlüsselwort<br />

TOKEN_MGR_DECLS.<br />

token_manager_decls ::= "TOKEN_MGR_DECLS" ":" java_block<br />

Die Produktionen der Grammatik für den Parser werden in BNF notiert. Da jedes nichtterminale<br />

Symbol nach der Generierung des Parsers durch eine Methode dargestellt wird,<br />

ist es durchaus möglich für Nichtterminale Übergabeparameter <strong>und</strong> Rückgabewerte zu definieren,<br />

weswegen die Nichtterminalen in der Grammatikdatei mit einem Java-Methodenkopf<br />

inklusive Rückgabewert <strong>und</strong> Parameterliste definiert werden. Nach einem ":"folgt ein<br />

Java-Block, der entweder leer ist oder Deklarationen <strong>und</strong> Anweisungen enthält, die bei<br />

jeder Anwendung der Produktion ausgeführt werden sollen. Die Deklarationen gelten für<br />

alle Produktionen. Im Methodenrumpf ist die rechte Seite der BNF-Produktion definiert.<br />

Nichtterminale, die hier auftreten, werden wie Methodenaufrufe notiert, Terminale sind<br />

entweder einzelne Zeichen oder Tokens. An jeder beliebigen Position der Produktion kann<br />

ein weiterer Java-Block definiert werden, der ausgeführt wird, sobald er passiert wird.<br />

bnf_production ::=<br />

java_access_modifier java_return_type java_identifier "(" java_parameter_list ")" ":"<br />

java_block<br />

"{" expansion_choices "}"<br />

expansion_choices ::= expansion ( "|" expansion )∗<br />

expansion ::= ( expansion_unit )∗<br />

expansion_unit ::= local_lookahead<br />

| java_block<br />

| "(" expansion_choices ")" [ "+" | "∗" | "" ]<br />

| "[" expansion_choices "]"<br />

| [ java_assignment_lhs "=" ] regular_expression<br />

| [ java_assignment_lhs "=" ] java_identifier "(" java_expression_list ")"<br />

Es gibt noch eine zweite Möglichkeit Produktionen für den Parser anzugeben. Die Produktion<br />

wird dabei komplett in Javacode notiert <strong>und</strong> durch das Schlüsselwort JAVACODE<br />

eingeleitet. Da diese Variante aber für Grammatiken gedacht ist, die mit der kontextfreien<br />

Darstellung der BNF nicht auskommen, wird sie für diese Arbeit nicht benötigt.<br />

11


Kapitel 3<br />

Anforderungen im Detail<br />

3.1 Mindestanforderungen<br />

Die Benutzerschnittstelle<br />

Die graphische Benutzerschnittstelle soll dem Nutzer eine bequeme Eingabe der Anfragen<br />

ermöglichen <strong>und</strong> muss in der Lage sein, Fehlermeldungen <strong>und</strong> die Ergebnistabellen anzuzeigen.<br />

Dazu soll sie jeweils ein eigenes Teilfenster bereitstellen, wobei die Fenster für<br />

die Ergebnistabellen <strong>und</strong> die Eingabe natürlich entsprechend groß sein müssen. Ist der<br />

dargestellte Inhalt zu groß für den Anzeigebereich, so soll der Überlauf über Scrollbalken<br />

erreichbar sein. Außerdem soll die Copy&Paste-Funktion unterstützt werden.<br />

Die Sprache<br />

Die zu entwerfende Anfragesprache soll sich an der Relationenalgebra orientieren, wie<br />

sie in der Vorlesung „Gr<strong>und</strong>lagen der DBS“ im Sommersemester 08 eingeführt wurde.<br />

Dabei können griechische Symbole durch Schlüsselwörter ersetzt werden <strong>und</strong> die Eingabe<br />

linear, also ohne Tiefstellung von Parametern, erfolgen. Die Sprache sollte natürlich<br />

unter der Voraussetzung, dass man die Relationenalgebra kennt, möglichst intuitiv sein.<br />

Zu implementieren sind zunächst alle Gr<strong>und</strong>operationen, der Gamma-Operator, sowie<br />

alle abgeleiteten Operationen mit Ausnahme des Outerjoins <strong>und</strong> der Division. Die relationenalgebraische<br />

Definition der Attributterme, lässt einen sehr komplexen Aufbau zu,<br />

weswegen diese zunächst nur eingeschränkt implementiert werden sollen. Es soll mit Zeichenketten<br />

<strong>und</strong> Zahlen umgegangen werden können. Die Syntax der Anfragesprache ist<br />

über eine kontextfreie Grammatik zu definieren.<br />

Das Ausführungssystem<br />

Eine eingegebene Anfrage soll korrekt analysiert <strong>und</strong> in SQL übersetzt werden. Bei Fehlern<br />

in der Anfrage sind aussagekräftige Fehlermeldungen auszugeben, die dem Nutzer<br />

eine schnelle Korrektur ermöglichen. Die Übersetzung sollte mindestens nach dem Prinzip<br />

geschachtelter From-Klauseln erfolgen, wobei für jeden Operanden einer Operation<br />

eine Unteranfrage in der From-Klausel des jeweiligen SQL-Blocks ausgeführt wird. Da<br />

dieses Vorgehen zu einer stark verschachtelten SQL-Anfrage führt, die nicht nur schwer<br />

lesbar, sondern auch für das DBMS schwer optimierbar ist, ist ein besserer Übersetzungsalgorithmus<br />

natürlich wünschenswerter. Es soll zunächst ausreichen, wenn der erzeugte<br />

12


SQL-Code nur auf kleine <strong>Datenbanken</strong> angewendet werden kann. Neben der Analyse <strong>und</strong><br />

der Übersetzung ist noch die Zusammenarbeit mit dem Datenbanksystem zu regeln. Es<br />

muss möglich sein, eine Verbindung per Login aufzubauen, die Anfrage auszuführen <strong>und</strong><br />

das Ergebnis entgegen zu nehmen. Außerdem muss es möglich sein, Fehlermeldungen von<br />

Oracle entgegen zu nehmen <strong>und</strong> dem Nutzer als Laufzeitfehler anzuzeigen.<br />

3.2 Verbesserungsmöglichkeiten<br />

Der Umgang mit großen Datenmengen muss ermöglicht werden. Außerdem klammert die<br />

in den Mindestanforderungen festgelegte Funktionalität klammert noch einige Möglichkeiten<br />

der Relationenalgebra aus. Die Operationen Division <strong>und</strong> Outerjoin (damit auch<br />

der Left- <strong>und</strong> Right-Outerjoin) müssen noch integriert werden. Der Umgang mit dem<br />

Datentyp Date fehlt <strong>und</strong> die Attributterme sind bisher nur eingeschränkt implementiert.<br />

Außerdem muss der Umgang mit großen Datenmengen ermöglicht werden. Diese Erweiterungen<br />

sollten natürlich die höchste Priorität erhalten.<br />

In SQL gibt es durch die Order-By-Klausel die Möglichkeit Ergebnistabellen nach beliebigen<br />

Spalten sortieren zu lassen, was sich auch auf die zu entwickelnde Relationenalgebra-<br />

Anfragesprache übertragen lässt. Hierbei wäre es denkbar am Ende einer Anfrage die<br />

textuelle Eingabe einer Order-by-Anweisung zuzulassen oder stattdessen eine Order-by-<br />

Option auf der Benutzeroberfläche zur Verfügung zu stellen, bei der die jeweiligen Spalten<br />

eingetragen oder eventuell sogar ausgewählt werden könnten.<br />

Durch einen frühzeitigen Verbindungsaufbau zum DBS, wäre es auch möglich den Nutzer<br />

allgemein bei der Eingabe von Relationen- <strong>und</strong> Spaltennamen zu unterstützen.<br />

Zur besseren Bedienbarkeit <strong>und</strong> Lesbarkeit kann eine Syntaxunterstützung eingebaut<br />

werden. Es wäre in einigen Situationen auch Vorteilhaft, wenn die Möglichkeit besteht,<br />

den erzeugten SQL-Code auf der Benutzerschnittstelle auszugeben.<br />

Desweiteren bietet es sich an, dem Nutzer die Möglichkeit der Übungsabgabe <strong>und</strong> eine<br />

Benutzungshistorie zur Verfügung zu stellen. Auch die Bereitstellung einer englischen<br />

Version der Benutzeroberfläche wäre denkbar.<br />

Die Relationenalgebra sieht allgemein nur lesende Zugriffe auf <strong>Datenbanken</strong> vor, daher<br />

würde es zu weit gehen über eine Integrierung von Create/Update-Kommandos nachzudenken.<br />

Um Datenbank-Relationen zu erzeugen oder zu ändern ware also eine SQL-<br />

Schnittstelle nötig. Die Benutzerfre<strong>und</strong>lichkeit könnte hier allerdings dadurch erhöht werden,<br />

dass ein SQL-Modus angeboten wird, in dem der Nutzer statt Relationenalgebra-Anfragen,<br />

SQL-Anweisungen eingeben kann. Dies würde die Nutzung einer zweiten Schnittstelle<br />

für schreibende Zugriffe auf die Datenbank unnötig machen.<br />

13


Kapitel 4<br />

Entwurf<br />

4.1 Syntax der Anfragesprache<br />

Die Syntax der Anfragesprache soll möglichst intuitiv sein, das heißt sich möglichst gut<br />

an die Syntax der Relationenalgebra anlehnen. Da die Eingabe von den für die Relationenalgebra<br />

typischen Symbolen <strong>und</strong> Indizes am Computer allerdings schwierig ist, soll die<br />

Syntax hier etwas abgeändert werden.<br />

Die relationenalgebraischen Symbole werden durch Bezeichner ersetzt, welche natürlich<br />

der Bequemlichkeit wegen nicht zu lang sein sollten <strong>und</strong> trotzdem eindeutig <strong>und</strong> intuitiv<br />

auf das zugeordnete Symbol schließen lassen müssen. Die definierten Bezeichner sind der<br />

Tabelle 4.1 zu entnehmen. Der einheitlichen Darstellung wegen sind in der Tabelle alle<br />

Schlüsselworte in Großbuchstaben dargestellt, die zu entwickelnde Sprache soll aber bezüglich<br />

der Schlüsselworte nicht case-sensitiv sein. Die Eingaben „SEL“, „sel“ oder „Sel“<br />

werden gleich behandelt.<br />

Die Bedingungen <strong>und</strong> Attributlisten, die gewöhnlich im Index eines Symbols auftauchen,<br />

werden den Symbolen nun in eckigen Klammern nachgestellt. An der syntaktischen Struktur<br />

der Anfragen soll gegenüber der Relationenalgebra nichts verändert werden, jedoch<br />

gibt es vorerst ein paar Einschränkungen bzgl. der Sprachmächtigkeit:<br />

1. Es wird auf den Datentyp Date <strong>und</strong> auf die Operation Division verzichtet.<br />

2. Es wird auf die logischen Operatoren „=>“ <strong>und</strong> „ verzichtet.<br />

3. Ein mehr als zweistelliger Vergleich in einer Bedingung ist nur durch eine Und-<br />

Verknüpfung von zweistelligen Vergleichsoperationen möglich. Ausdruck (a) ist damit<br />

unzulässig, Ausdruck (b) zulässig.<br />

(a) SEL[A


Operation Symbol Sprachelement Beispiel<br />

Selektion σ SEL SEL[A>10](R)<br />

Projektion π PROJ PROJ[A Aneu, C](R)<br />

Vereinigung ∪ UNION R UNION S<br />

Durchschnitt ∩ INTERSECT R INTERSECT S<br />

Differenz - MINUS R MINUS S<br />

Kartesisches Produkt × CARTESIAN R CARTESIAN S<br />

Join ⊲⊳ JOIN R JOIN[A=B] S<br />

Natural Join ⊲⊳ NJOIN R NJOIN S<br />

Semijoin ⊲< SJOIN R SJOIN[A=B] S<br />

Anti-Semijoin ⊲< ASJOIN R ASJOIN[A=B] S<br />

Gruppierung Γ GROUP GROUP[A # A, avg(B)](R)<br />

Logisches Und ∧ AND SEL[A>2 AND B>2](R)<br />

Logisches Oder ∨ OR SEL[A>2 OR B>2](R)<br />

Logisches Nicht ! NOT SEL[NOT A=B] S<br />

Leere Menge ∅ {} GROUP[{} # count(*)](R)<br />

Tabelle 4.1: Sprachelemente<br />

Was an dieser Stelle noch anzumerken ist, ist die Klammerung. Die unären Operationen<br />

Selektion, Projektion <strong>und</strong> Gruppierung klammern ihre Operanden generell. Die ist auch<br />

aus der Relationenalgebra bekannt. Intuitiv klammert man einen Operanden einer binären<br />

Operation, wenn es sich bei dem Operanden wieder um eine binäre Operation handelt,<br />

um die Ausführungsreihenfolge eindeutig festzulegen. Für die zu entwickelnde Sprache soll<br />

eine Klammerung von Operanden gr<strong>und</strong>sätzlich immer möglich sein. Tritt eine zweistellige<br />

Operation als Operand auf, so wird eine Klammerung vorausgesetzt. Ein Ausdruck der<br />

Form ’1.’ wäre damit unzulässig <strong>und</strong> müsste geklammert werden. Die Ausdrücke 2. <strong>und</strong><br />

3. wären zulässig. Auch eine Klammerung der gesamten Anfrage ist möglich.<br />

1. R ASJOIN[A=B] S UNION T<br />

2. (R ASJOIN[A=B] S) UNION T<br />

3. R ASJOIN[A=B] (S UNION T)<br />

Die zu implementierenden Operatoren <strong>und</strong> Operationen können der folgenden Tabelle<br />

entnommen werden. Syntax- <strong>und</strong> Prioritätsregeln entsprechen dabei den bekannten<br />

Regeln aus SQL.<br />

Gruppe<br />

Operatoren/Operationen<br />

logische Operatoren AND, OR, NOT<br />

Vergleichsoperatoren =, , , >=, =


4.2 Analyse der Eingabe<br />

4.2.1 Lexikale Analyse<br />

Für den Scanner sind in erster Linie die regulären Ausdrücke für die Token zu definieren.<br />

Das zeichenweise Lesen der Eingabe, die Zuordnung zu den Token <strong>und</strong> die Erstellung<br />

der Tokenfolge, erledigt der generierte Scanner ohne weiteres Zutun. Für jedes Token<br />

t wird dabei automatisch die Tokenklasse, die zugehörige Zeichenkette in der Eingabe,<br />

sowie deren Position in den Variablen t.kind, t.image, t.beginLine <strong>und</strong> t.beginColumn<br />

abgespeichert.<br />

Definition der Tokenklassen<br />

Es müssen zunächst alle Schlüsselwörter der Sprache in regulären Ausdrücken untergebracht<br />

werden. Da meistens einer Operation bzw. einem Operator genau ein Schlüsselwort<br />

zugeordnet ist, wird für den Namen der Tokenklasse einfach das zugehörige Schlüsselwort<br />

gewählt, der jeweilige reguläre Ausdruck entspricht dem Schlüsselwort als Zeichenkette.<br />

Der reguläre Ausdruck für die Tokenklasse CONCAT wäre zum Beispiel einfach „concat“.<br />

Auf Groß- <strong>und</strong> Kleinschreibung muss hierbei nicht geachtet werden, da diese durch die<br />

JavaCC-Option „IGNORE_CASE“ ignoriert wird. Auf diese Weise werden definiert:<br />

1. Je eine Tokenklasse für jeden boolschen Operator, also and, or <strong>und</strong> not.<br />

2. Je eine Tokenklasse für die Vergleichsoperatoren between, like <strong>und</strong> in, sowie je eine<br />

Tokenklasse für die Schlüsselwörter is <strong>und</strong> null.<br />

3. Je eine Tokenklasse für die Schlüsselwörter aller String-Operationen, also concat,<br />

substr, length, instr, lpad, rpad, replace, trim, upper, lower, <strong>und</strong> initcap.<br />

4. Ebenso für die Schlüsselwörter der numerischen <strong>und</strong> Datentyp-unabhängigen Operationen,<br />

also für ro<strong>und</strong>, trunc, mod, nvl, nvl2, nullif <strong>und</strong> coalesce. .<br />

Sowie je eine Tokenklasse für die Aggregatfunktionen count, sum, min, max, avg.<br />

Da für die relationenalgebraischen Operationen zum Teil mehrere Schlüsselwörter zugelassen<br />

sind, sind die zugehörigen regulären Ausdrücke nicht mehr ganz trivial. Sie sind<br />

der Tabelle 4.3 zu entnehmen.<br />

Die bisher nicht aufgeführten Vergleichsoperatoren verhalten sich syntaktisch völlig gleich.<br />

Da als Operand eines Vergleichsoperators auch kein Vergleich zugelassen ist, unterscheiden<br />

sie sich auch nicht bezüglich ihrer Priorität <strong>und</strong> können daher in einer Tokenklasse<br />

zusammengefasst werden.<br />

COMP: „=“ | „“ | „>=“ | „


Tokenklasse Regulärer Ausdruck<br />

PROJ „proj“ | „pj“<br />

SEL<br />

„sel“<br />

GROUP „goup“ | „gp“<br />

JOIN „join“<br />

NJOIN „natural join“ | „njoin“ | „nj“<br />

SJOIN „semijoin“ | „sjoin“ | „sj“<br />

ASJOIN „antisemijoin“ | „asjoin“ | „asj“<br />

OJOIN „outer join“ | „ojoin“ | „oj“<br />

ROJOIN „right outer join“ | „rojoin“ | „roj“<br />

LOJOIN „left outer join“ | „lojoin“ | „loj“<br />

CARTESIAN „cartesian“ | „cross join“<br />

UNION „union “<br />

INTERSECT „intersect“<br />

MINUS „minus“<br />

Tabelle 4.3: Tokenklassen-Definitionen für Relationenalgebraische Operationen<br />

Tokenklasse Regulärer Ausdruck<br />

APLUS „+“<br />

AMINUS „-“<br />

ADIV „/“<br />

AMUL „*“<br />

Tabelle 4.4: Tokenklassen-Definitionen für arithmetische Operatoren<br />

INFCONCAT: “||“<br />

PT: “.“<br />

EMPTYSET: “{}“<br />

Für Klammern <strong>und</strong> Kommas werden explizit keine Tokenklassen erzeugt. Sie sind wichtig<br />

für die syntaktische Analyse der Anfrage. Da nach dem Parsing die syntaktische Struktur<br />

der Anfrage im Syntaxbaum gespeichert ist, ist eine Speicherung dieser Zeichen über das<br />

Parsing hinaus nicht notwendig. JavaCC erzeugt für jedes in der Grammatik auftretende<br />

Zeichen automatisch eine anonyme Tokenklasse, die an dieser Stelle völlig ausreicht.<br />

Es bleiben noch die Tokenklassen für Bezeichner <strong>und</strong> Literale zu definieren. Es ist wichtig,<br />

dass sich die Definition der Bezeichner-Klasse unterhalb den Schlüsselwort-Definitionen<br />

befindet, damit ein auftretendes Schlüsselwort auch als solches erkannt wird. Da es sich<br />

bei Bezeichnern in relationenalgebraischen Anfragen um Namen von Tabellen- <strong>und</strong> Spalten<br />

einer Oracle-Datenbank handelt, sind hier die Oracle-spezifischen Namenskonventionen<br />

einzuhalten. Namen von Tabellen <strong>und</strong> Spalten im Oracle-System beginnen mit einen<br />

Buchstaben, sind maximal 30 Zeichen lang <strong>und</strong> können neben großen <strong>und</strong> kleinen Buchstaben<br />

die Zeichen „$“, „_“ <strong>und</strong> „#“ beinhalten. Es ist in JavaCC zwar möglich die<br />

maximale Länge eines Ausdrucks festzulegen, dies würde bei Eingabe eines zu langen Bezeichners<br />

aber dazu führen, dass der Scanner die ersten 30 Zeichen als Bezeichner-Token<br />

erkennt <strong>und</strong> für die restlichen Zeichen ein neues Token erzeugt. Es ist leicht nachvollziehbar,<br />

dass dies beim Parsing zu verwirrenden Fehlermeldungen führen würde. Die Länge<br />

der Bezeichnern soll deshalb erst vom Parser überprüft werden, welcher dadurch in der<br />

17


Lage ist eine aussagekräftige Fehlermeldung auszugeben.<br />

IDENTIFIER: ([A-Z,a-z])([A-Z,a-z] | „$“ | „_“ |„#“)*<br />

Auftretende Literale teilen sich grob in Zeichenketten <strong>und</strong> Zahlen. Beide Gruppen müssen<br />

aber noch genauer differenziert werden. Einige Operationen wie zum Beispiel „substr“ erwarten<br />

ganzzahlige Operanden, während es an vielen anderen Stellen durchaus möglich ist<br />

beliebige Zahlen zu verwenden. Der Parser muss also die Möglichkeit haben beiden Klassen<br />

zu unterscheiden, weshalb der Scanner hier unterschiedliche Tokenklassen bereitstellen<br />

muss.<br />

INTEGER_LITERAL: [1-9][0-9]*<br />

NUM_LITERAL: [0-9]+ “.“ [0-9]+<br />

Gleiches gilt für Zeichenketten. Ein Character-Literal könnte als Zeichenkette betrachtet<br />

werden. Da aber zum Beispiel die Operationen rpad <strong>und</strong> lpad für einen Operand ein einzelnes<br />

Zeichen <strong>und</strong> keine Zeichenkette erwarten, müssen String- <strong>und</strong> Character-Literale<br />

definiert werden. Es wird zunächst ein Hilfsausdruck CHAR definiert, der ein beliebiges<br />

Zeichen annehmen kann. Ein Character-Literal ist dann ein einzelnes CHAR-Zeichen in<br />

Hochkommata, ein String ist eine Aneinanderreihung von CHAR-Zeichen in Hochkommata.<br />

Anmerkung zur Symboltabelle<br />

Der lexikale Scanner speichert zu Beginn implizit die Position eines Tokens in der Eingabemaske,<br />

das Abbild des Tokens als Zeichenkette <strong>und</strong> die Tokenklasse, in der Token-<br />

Datenstruktur. Jedoch arbeitet nur noch der Parser direkt mit den Tokens weiter, so dass<br />

diese Informationen zusätzlich an einer anderen Stelle gespeichert werden müssen. Auch<br />

während der syntaktischen <strong>und</strong> semantischen Analyse werden weitere Informationen gewonnen,<br />

die in irgendeiner Form abgespeichert werden müssen. Hierzu eignet sich zum Teil<br />

der vom Parser erzeugte Syntaxbaum <strong>und</strong> zum Teil eine globale Symboltabelle. Welche<br />

Informationen abzuspeichern sind, <strong>und</strong> ob sich zur Speicherung die Symboltabelle oder<br />

ein Knoten-Objekt des Syntaxbaumes besser eignet, soll an den jeweils geeigneten Stellen<br />

diskutiert werden. Der Aufbau der Symboltabelle wird daher erst nach dem Entwurf der<br />

Analyse-Stufen vorgestellt.<br />

18


4.2.2 Syntaktische Analyse<br />

Für den Parser muss eine LL(1)-Grammatik entworfen werden. Um das eigentliche Parsing<br />

kümmert sich hier wieder der JavaCC-Generator. Die Produktionen sollen hier in Anlehnung<br />

an Javacc in erweiterter Backus-Naur-Form angegeben werden. Token werden dabei<br />

durch die Symbole „“ markiert. Für Klammern wurden keine Token definiert,<br />

weswegen sie einfach als Symbole in den Produktionen vorkommen. Ein nichtterminales<br />

Symbol entspricht in JavaCC einem Methodenaufruf <strong>und</strong> ist daher durch eine zunächst<br />

leere Parameterliste gekennzeichnet.<br />

Entwurf der Grammatik<br />

Ein Ausdruck der Relationenalgebra besteht aus Verschachtelungen von unären <strong>und</strong> binären<br />

Ausdrücken. Eine Produktion der Form<br />

RAExpression(): UnaryRAExpression() | BinaryRAExpression()<br />

würde die LL(1)-Bedingung verletzen, da ein Binärer Ausdruck wieder mit einem Unären<br />

Ausdruck beginnt. Umformuliert kann man sagen, ein Ausdruck der Relationenalgebra<br />

besteht aus einem unären Ausdruck, optional gefolgt von einer binären Operation <strong>und</strong><br />

einem zweiten unären Ausdruck.<br />

RAExpression(): UnaryRAExpression() [ BinaryRAOperation() UnaryRAExpression()]<br />

Es ist hier möglich die Binären Operationen unter einem Nichtterminal zusammenzufassen,<br />

da festgelegt wurde, dass alle binären relationenalgebraischen Operationen die gleiche<br />

Priorität besitzen <strong>und</strong> untereinander geklammert werden müssen. Für die binären Operationen<br />

ergibt sich damit folgende Produktion.<br />

BinaryRAOperation(): |<br />

|<br />

„[„ JoinCondition() „]“ |<br />

„[„ JoinCondition() „]“ |<br />

„[„ JoinCondition() „]“ |<br />

„[„ JoinCondition() „]“ |<br />

„[„ JoinCondition() „]“ |<br />

„[„ JoinCondition() „]“ |<br />

|<br />

|<br />

|<br />

<br />

Ein unärer Ausdruck besteht hingegeben aus einem Tabellennamen, einem geklammerten<br />

Ausdruck mit einer optionalen Umbenennung oder einer unären Operation, also einer<br />

Projektion, einer Selektion oder einer Gruppierung.<br />

19


UnaryRAExpession(): RelationIdent() |<br />

„(„ RAExpression() [ Alias() ] „)“ |<br />

„[„ AttList() „]“ „(„ RAExpression() „)“ |<br />

„[„ Condition() „]“ „(„ RAExpression() „)“ |<br />

„[„ GroupList() „“ AggrList() „]“ „(„ RAExpression() „)“<br />

Für Selektionsbedingung muss ein anderes Nichtterminal verwendet werden als für die<br />

Joinbedingungen, da hier nicht nur Und-Verknüpfungen, sondern beliebige boolsche Verknüpfungen<br />

von Vergleichen erlaubt sind.<br />

Für die Relationen- <strong>und</strong> Aliasnamen ergeben sich folgende Produktionen.<br />

RelationIdent(): <br />

Alias(): <br />

Mit den bisher gegebenen Produktionen lassen sich auf relationenalgebraischer Ebene<br />

bereits beliebige Ausdrücke rekursiv erstellen, es fehlen aber noch die Produktionen für<br />

die Bedingungen <strong>und</strong> Attribut-Listen.<br />

Eine Selektionsbedingung besteht aus logischen Verknüpfungen von Vergleichs-Ausdrücken.<br />

Da die logischen Operatoren unterschiedliche Prioritäten besitzen, müssen hier mehrere<br />

Produktionen erstellt werden. Stellt man sich einen Ableitungsbaum vor, so müssen sich<br />

ganz allgemein Operatoren mit einer niedrigen Priorität möglichst weit entfernt von den<br />

terminalen Operanden-Symbolen, also möglichst weit oben im Baum befinden. Bezogen<br />

auf die Grammatik bedeutet dies, dass für jede Prioritätsstufe eine Produktion vorhanden<br />

sein muss. Die Produktion werden hierarchisch durchlaufen. In der ersten Produktion<br />

können optional Symbole mit niedrieger Priorität erkannt werden oder direkt in die<br />

nächste Produktion gewechselt werden. Bei den logischen Operatoren besitzt die Oder-<br />

Verknüpfung die niedrigste Priorität, eine Und-Verknüpfung eine höhere <strong>und</strong> der Not-<br />

Operator die höchste. Zusäzlich muss die Änderung der Prioritäten durch Klammerung<br />

mit einbezogen werden. Es ergeben sich folgende Produktionen.<br />

Conditon(): AndExpression() ( AndExpression())∗<br />

AndExpression(): NotExpression() (AND NotExpression)∗<br />

NotExpression(): [] ( Comparison() | "‘("‘ Condition() "‘)"’␣)<br />

Da keine Schachtelung von Vergleichen möglich ist, müssen hier keine Prioritäts-Regeln<br />

beachtet werden. Es muss daher nur eine Produktion vorhanden sein.<br />

Comparison(): AttributeTerm() ( AttriuteTerm() |<br />

[] |<br />

|<br />

Literal() Literal() )<br />

Ein Literal kann hierbei ein beliebiges Literal sein.<br />

20


Literal(): StrLiteral() | CharLiteral() | NumLiteral() | IntLiteral()<br />

StrLiteral(): <br />

CharLiteral(): <br />

NumLiteral(): <br />

IntLiteral(): <br />

Für die Attributterme müssen wieder die Prioritäten beachtet werden, weswegen zunächst<br />

eine Produktion für die Addition <strong>und</strong> die Subtraktion benötigt wird <strong>und</strong> eine zusätzliche<br />

für Multiplikation <strong>und</strong> Division.<br />

AttributeTerm(): Term() ( ( | ) Term())*<br />

Term(): Factor() (AND Factor())*<br />

Ein Factor ist entweder ein Attribut, ein geklammerter Ausdruck oder eine Datentyp-<br />

Operation.<br />

Factor():<br />

Attribute() |<br />

“(“ AttributeTerm() “)“ |<br />

( | | ) “(„ StrOperand “)“ |<br />

“(“ StrOperand() “,“ StrOperand() “)“ |<br />

“(“StrOperand() “,“ IntLiteral() “,“ IntLiteral() “)“ |<br />

“(“ StrOpeand() “)“|<br />

“(“ StrOperand() “,“ StrOperand() “)“|<br />

“(“ StrOperand() “,“ StrOperand() “,“ StrOperand() “)“|<br />

( | ) „(„ StrOperand() “,“ IntLiteral() “,“ CharLiteral() “)“|<br />

....|<br />

( | | ) “(“ NumberOperand() “,“ IntLiteral() “)“<br />

( | | | ) “(„ AtomicElement() “,“ AtomicElement() “)“<br />

Bei einem Datentyp-Operand kann es sich jeweils um ein Literal des jeweiligen Typs oder<br />

um ein Attribut handeln.<br />

StrOperand(): StrLiteral() | Attribute()<br />

NumberOperand(): NumLiteral() | IntLiteral() | Attribute()<br />

Ein Attribut ist ein Bezeichner, dem optional ein anderer Bezeichner als Relationennamen-<br />

Prefix, gefolgt von einem Punkt, vorangestellt wird. Dies ist unter Einhaltung der LL(1)-<br />

Bedingung nicht so einfach darstellbar, da der optionale, so wie auch der nicht optionale<br />

Teil mit einem Bezeichner-Token beginnen <strong>und</strong> sich der optionale Teil am Anfang der<br />

Produktion befindet. Der Parser könnte mit nur einem Lookahead-Symbol also nicht wissen,<br />

ob es sich bei dem aktuellen Bezeichner um den Attributnamen oder um einen Präfix<br />

handelt. Abhilfe schafft folgende Produktion.<br />

Attribute(): [ ]<br />

21


Für einen Attribut-Zugriff, der nur über den Attributnamen erfolgt, symbolisiert der<br />

erste Bezeichner den Attributnamen. Für einen Attribut-Zugriff mit Punkt-Notation symbolisiert<br />

der zweite Bezeichner den Attributnamen. Beide Zugriffsmöglichkeiten werden<br />

korrekt geparst.<br />

Auf der Basis der bisherigen Produktionen können nun beliebige Selektionsbedingungen<br />

erkannt werden. Außerdem lässt sich sehr einfach die Produktion für eine Joinbedingung<br />

formulieren. Joinbedingungen sind optional Und-verknüpfte Vergleiche, die Vergleichs-<br />

Produktionen wurden bereits definiert.<br />

JoinContition(): Comparison() ( Comparison() )*<br />

Eine Attributterm-Liste einer Projektion besteht aus mit Komma getrennten Attribut-<br />

Termen <strong>und</strong> jeweils einem opionalem Aliasnamen. Produktionen für Attributterme wurden<br />

bereits definiert.<br />

AttList(): AttributeTerm() [ Alias() ] [“,“ AttList()]<br />

Eine Gruppierungsliste enthält durch Kommas getrennte Attributterme oder das Token<br />

für die leere Menge.<br />

GroupList(): ( AttributeTerm() (“,“ AttributeTerm()())* ) | <br />

Bleibt zuletzt die Aggregierungsterm-Liste von Gruppierungen. Diese enthält aggregierte<br />

<strong>und</strong> unaggregierte Attributterme, jeweils optional gefolgt von einem Alias. Die Aggregierungsfunktion<br />

count kann neben einem Attributterm auch ein “*“ als Operand besitzen.<br />

AggrList(): (AttributeTerm() | AggrTerm()) [Alias()] [“,“ AggrList()]<br />

AggrTerm(): ( | | | ) “(“ AttributeTerm() „)“ | |<br />

„(„ (AttributeTerm() | ) „)“<br />

Die Produktionen der Grammatik können in der JavaCC-Grammatikdatei mit Parser-<br />

Aktionen in Form von Java-Code angereichert werden. Auf diese Weise lassen sich neu<br />

gewonnene Informationen über Tokens direkt in die Symboltabelle eintragen <strong>und</strong> der<br />

Syntaxbaum aufbauen.<br />

Aufbau des Syntaxbaumes<br />

Die naheliegenste Variante wäre es, einen Ableitungsbaum für die eingegebene Anfrage zu<br />

erzeugen, dieses wäre auch automatisch aus Javacc heraus möglich. Nichtterminale Symbole<br />

in einer Grammatik dienen der Beschreibung der Struktur von Worten der Grammatik.<br />

Aber auch ein Syntaxbaum ist eine Datenstruktur zur Speicherung von syntaktischen<br />

Strukturen. Sofern man also im Nachhinein nicht nachvollziehen muss welche Produktionen<br />

der Grammatik in welcher Reihenfolge angewendet wurden, ist es nicht notwendig die<br />

Nichtterminalen Symbole der Grammatik in den Syntaxbaum mit aufzunehmen. Es wäre<br />

sogar nachteilig diese mit aufzunehmen, da zum Einen der entstehende Baum mit Nichtterminalen<br />

Symbolen um ein Vielfaches größer wäre als nur mit terminalen Symbolen <strong>und</strong><br />

zum Anderen ein zusätzlicher Schritt notwendig wäre um aus dem Ableitungsbaum einen<br />

Nichtterminal-freien Baum als Basis für die Optimierung <strong>und</strong> SQL-Code-Erzeugung zu<br />

erzeugen.<br />

22


Aus der Grammatik lässt sich direkt ein Operatorbaum ableiten, also ein Baum, der<br />

die hierarchische Struktur der Operationen in der Anfrage repräsentiert. Im Gegensatz<br />

zum aus der Relationenalgebra bekannten Operatorbaum, müssen hier natürlich auch die<br />

Bedingungen <strong>und</strong> Attributlisten in einer Baumstruktur abgespeichert werden.<br />

Um Ausdrücke der Relationenalgebra repräsentieren zu können sind verschiedene Knoten-<br />

Typen notwendig.<br />

1. Knoten, die nur Operanden-Kindknoten besitzen. Dazu gehören Knoten für die Vereinigung,<br />

den Durchschnitt, die Differenz, die Division, das Kartesische Produkt, den<br />

Natürlichen Verb<strong>und</strong>, sowie alle in Bedingungen auftretende arithmetische Operatoren,<br />

Operatoren für Zeichenketten, boolsche Operatoren <strong>und</strong> Vergleichsoperatoren.<br />

2. Knoten, die zusätzlich zu ihren Operanden-Kindknoten Kindknoten für eine Bedingung<br />

enthalten. Hierzu zählen der Join, der Semijoin, der Antisemijoin, der Outerjoin<br />

<strong>und</strong> die Selektion.<br />

3. Knoten, die Attributterm-Listen abspeichern können müssen. Hierbei muss noch<br />

genauer differenziert werden, da für eine Gruppierung im Gegensatz zur Projektion<br />

zwei Listen abgespeichert werden können müssen.<br />

Beim Parsing kann einfach auf Basis des aktuell gelesen Tokens die Art des zu erzeugenden<br />

Knotens bestimmt werden. Für jedes Token, dass im Operatorbaum gespeichert werden<br />

soll, muss ein Knoten erzeugt <strong>und</strong> dem Baum hinzugefügt werden. Die Hierarchie der<br />

Knoten wird durch die Reihenfolge der angewendeten Produktionen bestimmt. Es wird<br />

ein Zeiger definiert, der immer auf das Knoten-Objekt zeigt, an dem der nächste Knoten<br />

angefügt werden muss. Wird dem Operatorbaum ein Knoten-Objekt hinzugefügt, so wird<br />

der Zeiger darauf gesetzt. Je nach Knotenart wird nun ggf. erst auf den Bedingungs-,<br />

Attributterm-Listen oder Gruppierungs-Listen-Nachfolger gewartet, auf den dann wiederum<br />

der Zeiger gesetzt wird. Dieses Vorgehen entspricht strukturell der Abarbeitung<br />

der Produktionen in der Grammatik. Trifft der Parser auf ein nichtterminales Symbol,<br />

wird vor der weiteren Abarbeitung der aktuellen Produktion, zunächst das nichttermiale<br />

Symbol mit Hilfe einer passenden Produktion verarbeitet. Ist der Parser am Ende einer<br />

Produktion angelangt, die einen vollständigen Ausdruck beschreibt, so ist im Operatorbaum<br />

der zugehörige Teilbaum fertig aufgebaut <strong>und</strong> der aktuell zu bearbeitende Knoten<br />

muss eine Ebene hoch navigiert werden. Parallel navigiert auch der Parser in der Hierarchie<br />

der angewendeten Produktionen eine Ebene hoch <strong>und</strong> setzt dort die Abarbeitung<br />

einer Produktion fort.<br />

Bei diesem Vorgehen kommt noch eine Schwierigkeit dazu, da viele Teilausdrücke Infix-<br />

Operatoren enthalten. Der Parser liest die Tokenfolge von links nach rechts <strong>und</strong> für jedes<br />

Token, dass in den Baum aufgenommen werden soll, wird sofort ein Knoten erzeugt <strong>und</strong><br />

dem Baum hinzugefügt. Es ist daher logisch, dass ein Teilbaum für einen beliebigen Ausdruck,<br />

der linker Operand eines Infix-Operators ist, noch nicht an der richtigen Stelle<br />

hängen kann. Stattdessen befindet er sich an der Position an der normalerweise der Infix-<br />

Operator-Knoten hängen müsste. Wenn der Parser das Token für den Infix-Operator liest,<br />

befindet sich, durch die Produktions-bedingte Navigation im Operatorbaum, der Zeiger<br />

an der richtigen Stelle, also auf dem späteren Vaterknoten des Infix-Operator-Knotens, an<br />

dem im Moment noch der Teilbaum hängt, der den linken Operanden des Infix-Operators<br />

repräsentiert. Der Infix-Operator-Knoten muss nun also zwischen dem aktuellen Knoten<br />

23


<strong>und</strong> dem zuletzt eingefügten Kind-Teilbaum eingefügt werden. Der Zeiger wandert wieder<br />

auf den zuletzt eingefügten Knoten, das heißt auf den Infix-Operator-Knoten. Um<br />

für den Wurzelknoten keine Sonderbehandlung einfügen zu müssen wird zu Beginn des<br />

Parsings ein leerer root-Knoten erzeugt, der nur einen Nachfolger, nämlich den eigentlichen<br />

Wurzelknoten des Baumes, haben soll. Zusammenfassend ist der Aufbau-Prozesses<br />

des Operatorbaumes als Flussdiagramm in Abbildung 4.1 dargestellt. Zusätzlich soll die<br />

Funktionsweise durch folgendes Beispiel verdeutlicht werden. Die eingegebene Anfrage sei<br />

Abbildung 4.1: Aufbau des Operatorbaumes<br />

SEL [A+1


werden. Es existieren die folgenden Produktionen, die in Anlehnung an JavaCC in BNF<br />

angegeben sind. Die Tokenklassen sind durch „“ markiert.<br />

1. Start(): Expr()<br />

2. Expr(): UnaryExpr() [ Expr() #]<br />

3. UnaryExpr(): ( „[„ RelExpr() „]“ „(„ Expr() „)“ #) | ( #)<br />

4. RelExpr(): Term() Term() #<br />

5. Term(): AtomicElement() [„+“ Term() # ]<br />

6. AtomicElement(): ( | ) #<br />

Die #-Symbole markieren die Stellen, an den ein Ausdruck abgeschlossen ist <strong>und</strong> der<br />

Zeiger im Operatorbaum eine Stufe hochgestellt werden muss. Sie stehen hier nur repräsentativ<br />

für entsprechenden JavaCode zur Naviagation im Baum.<br />

Vor Beginn des Parsings wird ein Root-Knoten erzeugt, sowie ein Zeiger, der auf Root<br />

zeigt.<br />

Das erste Token ist SEL. Der Parser wendet also nacheinander Produktion 1, 2 <strong>und</strong> 3 an<br />

<strong>und</strong> verarbeitet nun das SEL-Token. Es wird ein Knoten-Objekt erzeugt, dass neben den<br />

normalen Operanden auch einen Bedingungs-Teilbaum aufnehmen kann. Dieses wird an<br />

die nächste frei Position im aktuellen Knoten, also im root-Knoten gehängt. Der Zeiger<br />

wandert auf den gerade eingefügten Knoten.<br />

Das nächste Token ist „[„. Klammern haben keinen Einfluss auf den Operatorbaum. Der<br />

Parser liest das Token <strong>und</strong> bleibt in Produktion 3. Nun ist das Lookahead-Symbol ein<br />

Bezeichner mit Wert „A“. Der Parser trifft in Produktion 3 auf ein nichtterminales Symbol<br />

<strong>und</strong> kann Produktion 4, 5 <strong>und</strong> 6 anwenden um „A“ zu verarbeiten. Ist „A“ gelesen, so<br />

wird wieder ein Knoten-Objekt erzeugt, dass an die nächste freie Position im aktuellen<br />

Knoten-Objekt gespeichert wird. Das aktuelle Knoten-Objekt ist der Selektions-Knoten,<br />

sein Bedingungsteilbaum muss zuerst aufgebaut werden. Der Zeiger wandert danach wieder<br />

auf den eingefügten Knoten.<br />

25


Das Lookahead ist nun „+“. Der Parser beendet Produktion 6 <strong>und</strong> liest dabei die, hier als<br />

Raute gekennzeichnete Anweisung, den Zeiger eine Ebene hochzusetzen. Der Ausdruck<br />

„A“ wurde komplett verarbeitet.<br />

Das Lookahead ist immernoch „+“. Da der Parser Produktion 6 beendet hat kann er nun<br />

die Bearbeitung von Produktion 5 fortsetzen. Für das „+“-Token wird ein Knoten-Objekt<br />

erzeugt. Da „+“ ein Infix-Operator ist, wird das Knoten-Objekt nicht an die nächste freie<br />

Position, sondern zwischen den aktuellen Knoten <strong>und</strong> den zuletzt eingefügten Teilbaum<br />

gehängt. Der Zeiger wandert wieder mit.<br />

Nun wird eine „1“ gelesen. Der Parser trifft auf das nichtterminale Symbol Term() <strong>und</strong><br />

wendet noch einmal Produktion 5 <strong>und</strong> 6 an, liest dann die „1“ <strong>und</strong> erzeugt ein Knoten-<br />

Objekt, welches an die nächste freie Position des aktuellen Knotens gehängt wird.<br />

26


Das Lookahead ist nun „


Der Parser trifft in Produktion 4 auf das nichtterminale Symbol Term(). Das Lookahead<br />

ist „B“. Es werden nacheinander wieder die Produktionen 5 <strong>und</strong> 6 angewendet. Ein neuer<br />

„B“-Knoten wird angefügt.<br />

Die Produktion 6 kann wieder beendet werden, der Teilausdruck „B“ wurde komplett<br />

verarbeitet, der Zeiger im Baum wird eine Ebene hoch gesetzt.<br />

Da das Lookahead eine Klammer <strong>und</strong> kein „+“ kann auch Produktion 5 beendet werden.<br />

Dieses Mal aber ohne Lesen einer Raute, für den erkannten Teilausdruck „B“ wurde der<br />

Zeiger bereits versetzt. Nun kann die Verarbeitung von Produktion 4 fortgesetzt werden,<br />

aber auch hier ist der Parser bereits am Ende angekommen. Beim Lesen der Raute wird<br />

wieder der Zeiger eine Ebene hoch gesetzt, der Ausdruck „A+1


Die Bearbeitung von Produktion 3 wird fortgesetzt. Hierbei liest der Parser nacheinander<br />

die beiden Token „]“ <strong>und</strong> „)“, die keinen Einfluss auf den Operatorbaum haben. Danach<br />

trifft er auf das Expr()-Nichtterminal. Das Lookahead ist „R“. Es müssen also nach<br />

einander die Produktionen 1, 2 <strong>und</strong> 3 angewendet werden, wobei für UnaryExpr() die<br />

Produktion gewählt wird, deren rechte Seite mit einem Bezeichner beginnt. Für „R“ wird<br />

wieder ein Knoten-Objekt erzeugt. Der Zeiger zeigt zur Zeit auf den Selektions-Knoten.<br />

Da „R“ kein Infix-Operator ist, muss das neue Knoten Objekt an die nächste freie Position<br />

im Selektions-Knoten gehängt werden, also als Operand.<br />

Der Parser beendet Produktion 3 <strong>und</strong> setzt den Zeiger eine Ebene hoch. Der Teilausdruck<br />

„R“ wurde komplett verarbeitet.<br />

29


Das Lookahead ist „)“. Der Parser setzt die Verarbeitung von Produktion 2 durch Beendigung<br />

fort, ohne dabei eine Raute zu lesen. Bei der Weiterverarbeitung von Produktion<br />

3 kann er nun die Klammer lesen. Das neue Lookahead ist eine Vereinigung, also beendet<br />

er Produktion 3 <strong>und</strong> setzt den Zeiger eine Ebene hoch. Der Ausdruck „SEL[A+1


Das Lookahead ist nun der Bezeichner „S“. Um S zu verarbeiten muss der Parser die<br />

Produktionen 2 <strong>und</strong> 3 anwenden. Für „S“ wird ein Knoten-Objekt erzeugt <strong>und</strong> an die<br />

nächste freie Position gehängt. Der Zeiger wandert wieder mit.<br />

Die Produktion 3 wird mit hoch setzen des Zeigers beendet, da der Ausdruck „S“ komplett<br />

verarbeitet wurde.<br />

31


Auch Produktion 2 kann beendet werden. Der Ausdruck „SEL[A+1


Zeichenkette nicht von Bedeutung, da diese nur zur Identifikation der zugehörigen Tokenklasse<br />

diente. Die Übersetzung muss auf Basis der Tokenklasse <strong>und</strong> der in SQL üblichen<br />

Syntax für die jeweilige Operation erfolgen <strong>und</strong> ist daher unabhängig vom Abbild in der<br />

Eingabe. Allgemein hängen sehr viele Entscheidungen, wie ein Element zu Prüfen oder<br />

zu Übersetzen ist, von der jeweiligen Tokenklasse ab. Es macht also Sinn, neben der Position<br />

auch die Tokenklasse in der Baumstruktur zu speichern <strong>und</strong> auf die Aufnahme der<br />

Schlüsselwörter in der Symboltabelle ganz zu verzichten.<br />

Anders ist es bei auftretenden Bezeichnern <strong>und</strong> Literalen. Das Abbild als Zeichenkette<br />

hängt hier nicht von der Tokenklasse ab <strong>und</strong> muss zusätzlich gespeichert werden. Für<br />

auftretende Bezeichner gibt es auch noch mehr Informationen, die abgespeichert werden<br />

müssen. In der Anfrage<br />

PROJ[A Aneu](R)<br />

treten drei Bezeichner auf, die alle unterschiedliche Rollen spielen. Beim ersten Bezeichner<br />

handelt es sich um ein Attribut bzw. einen Spaltennamen, beim zweiten um ein Alias<br />

<strong>und</strong> beim dritten um eine Relation bzw. einen Tabellennamen. Die Symboltabelle sollte<br />

daher in der Lage sein diese „Bezeichner-Typen“ abzuspeichern. Die unterschiedlchen<br />

Bezeichner-Typen werden während des Parsings erkannt <strong>und</strong> können direkt in die Symboltabelle<br />

eingetragen werden. Hierzu müssen lediglich die Produktionen<br />

RelationIdent(): <br />

Alias(): <br />

Attribute(): [ ]<br />

mit passenden Symboltabellen-Methoden-Aufrufen angereichert werden. Für die dritte<br />

Produktion ist dies etwas schwieriger, da es sich beim ersten Bezeichner-Token sowohl um<br />

ein Attribut als auch um eine Relation handeln kann. Für den ersten Bezeichner muss<br />

der Bezeichner-Typ „Relation“ eingetragen werden falls das Lookahead ein Punkt ist,<br />

ansonsten handelt es sich um ein Attribut. Falls der zweite Bezeichner auftritt, handelt<br />

es sich dabei auf jeden Fall um ein Attribut, hier ist also keine Abfrage nötig. Man sieht<br />

an dieser Stelle warum es wichtig war den Punkt explizit als Tokenklasse zu definieren,<br />

ein Vergleich mit einer anonymen Tokenklasse wäre nicht möglich gewesen.<br />

Da während des Parsing alle nötigen Informationen über die Tokens in der Token-<br />

Datenstruktur gespeichert sind, ist es nicht notwendig, dass vor Beginn des Parsings schon<br />

Einträge in der Symboltabelle vorhanden sind. Der Parser kann also die erste Eintragung<br />

eines Bezeichners in die Symboltabelle vornehmen <strong>und</strong> gleichzeitig den Bezeichner-Typ<br />

angeben. Natürlich müsste der Parser dann auch die erste Eintragung der Literale vornehmen.<br />

Hierzu sind die Literal-Produktionen mit entsprechendem Code anzureichern.<br />

Der zugehörige Ausdruck eines Aliasnamens ist nur schwer in der Symboltabelle abspeicherbar.<br />

Die Speicherung des Ausdrucks als Zeichenkette wäre zwar möglich, für die<br />

weitere Verarbeitung aber wenig geeignet. Die Speicherung von Teilbäumen in der Symboltabelle<br />

wäre ebenfalls möglich, würde aber zu vielen red<strong>und</strong>anten Informationen <strong>und</strong><br />

im schlechtesten Fall zu einer enormen Symboltabelle führen. Der Ausdruck sollte also<br />

aus dem Syntaxbaum abgeleitet werden <strong>und</strong> wird nicht in der Symboltabelle gespeichert.<br />

33


4.2.3 Semantische Analyse:<br />

Bezüglich der Ausdrücke der Relationenalgebra hat die semantische Analyse folgende<br />

Aufgaben zu bewältigen.<br />

1. Konsistenz-Prüfung<br />

In der Anfrage auftretende Attribut- <strong>und</strong> Relationennamen müssen sich in der Datenbank<br />

widerspiegeln.<br />

2. Prüfung von Gültigkeitsbereichen<br />

Attribut-, Relationen- <strong>und</strong> Aliasnamen sowie Gruppierungen von Attributen besitzen<br />

innerhalb der Anfrage einen gewissen Gültigkeitsbereich. Dieser muss untersucht<br />

werden, um einen semantisch korrekten Zugriff zu garantieren.<br />

3. Datentyp-Prüfung<br />

(a) Attribut-Ebene<br />

In Selektions- <strong>und</strong> Join-Bedingungen auftretende Operatoren, müssen mit Attributen<br />

<strong>und</strong> Literalen passender Datentypen verwendet werden.<br />

(b) Relationen-Ebene<br />

Operationen auf Relationen-Ebene sind ebenfalls nicht mit beliebigen Operanden<br />

möglich. Voraussetzungen, die an die Schemata der Operanden gestellt<br />

werden, müssen daher ebenfalls geprüft werden.<br />

Wie man leicht sieht wird hier eine Verbindung zum Datenbanksystem notwendig. Hierzu<br />

werden die Login-Daten des Nutzers benötigt, die von der Steuerung der Benutzeroberfläche<br />

abgefragt werden können. Zur Interaktion mit dem Datenbanksystem bietet die<br />

Java-Umgebung das Package java.sql. Sobald die Verbindung steht, können die benötigten<br />

Informationen, also Tabellennamen <strong>und</strong> Spaltennamen, mit den zugehörigen Datentypen,<br />

aller benötigter Tabellen, über die Dictionary-View „all_tab_colums“ abgerufen werden.<br />

Das Ergebnis ist ein ResultSet, aus dem die benötigten Informationen nun gewonnen werden<br />

können. Es wird noch eine Datenstruktur benötigt um die jeweiligen Schemata der<br />

Tabellen abzuspeichern.<br />

Konsistenz-Prüfung<br />

Die Relationennamen der Anfrage, die konsistent zur Datenbank verwendet werden müssen,<br />

<strong>und</strong> deren Attribute mit zugehörigen Datentypen benötigt werden, können von der<br />

Symboltabelle abgefragt werden. Die Existenz der verwendeten Relationennamen in der<br />

Datenbank kann implizit beim Abfragen der benötigten Daten erfolgen. Ist ein Relationenname<br />

nicht als Tabelle in der Datenbank vorhanden, so muss dies dem Nutzer durch<br />

eine Fehlermeldung mitgeteilt werden.<br />

Zugriffe auf Attribute finden an verschiedenen Stellen im Operatorbaum statt <strong>und</strong> können<br />

daher nicht gleich beim Abfragen der Relationen-Daten vom Datenbank-System überprüft<br />

werden. Stattdessen werden alle Attribute einer Relation mit Datentypen in einer<br />

Schema-Datenstruktur für jede auftretende Relation in der Symboltabelle abgespeichert.<br />

Da den Datentypen im Oracle-System Längenangaben mitgegeben sind, müssen diese<br />

der Typinformation der Schema-Datenstruktur ebenfalls mitgegeben <strong>und</strong> später in der<br />

Prüfung einbezogen werden.<br />

34


Schema-Berechnung <strong>und</strong> Typ-Prüfung auf Relationenebene<br />

Um zu Prüfen, ob auf Attribute semantisch korrekt zugegriffen wird, muss das Schema<br />

der Operanden der jeweiligen Operation betrachtet werden. Handelt es sich bei einem<br />

Operanden um einen einfachen Tabellennamen, also einen Blattknoten im Operatorbaum,<br />

so ist das Schema bekannt. Handelt es sich bei dem Operanden aber um einen komplexeren<br />

relationenalgebraischen Ausdruck bzw um einen inneren Knoten im Operatorbaum, so<br />

muss das Schema des Ausdrucks berechnet werden. Für die Anfrage<br />

PROJ [A](R NJOIN S)<br />

mit Schema(R)=(R.A:D1, R.B:D2) <strong>und</strong> Schema(S)=(S.B:D2, S.C:D3) muss zunächst das<br />

Schema des Ausdrucks „R njoin S“ berechnet werden, bevor überprüft werden kann, ob<br />

A darin enthalten ist.<br />

Es gibt in der Relationenalgebra per Definition für jede Operation eine Vorschrift wie das<br />

Ergebnis-Schema aus den Schemata der jeweiligen Operanden berechnet werden kann. Im<br />

Beispiel gilt<br />

Schema(R NJOIN S) = (R.A:D1, B:D2, S.C:D3)<br />

Um für einen beliebigen Knoten ein Schema zu berechnen, müssen die Schemata seiner<br />

Operanden bekannt sein. Die Berechnung erfolgt dabei nach den, in der Definition<br />

der einzelnen relationenalgebraischen Operationen, festgelegten Regeln. Da zu Beginn die<br />

Schemata der in der Anfrage aufgetretenden Tabellen, also den Blattknoten im Operatorbaum,<br />

bekannt sind, kann die Schema-Berechnung Bottom-up durchgeführt werden.<br />

Zusätzlich müssen Umbennenungen von Ausdrücken mit in die Schema-Berechnung einfließen.<br />

Die Umbenennung eines Ausdrucks führt zur Umbennenung aller Relationennamen-Prefixe,<br />

der im jeweiligen Schema enthaltenden Attribute, ohne dass ein späterer<br />

Zugriff auf die ursprünglichen Relationennamen möglich ist. Die Anfrage 1) führt zu<br />

einem Fehler, korrekt wären hingegen Anfrage 2) oder 3)<br />

1. PROJ[R.A]((R NJOIN S) T)<br />

2. PROJ[T.A]((R NJOIN S) T)<br />

3. PROJ[A]((R NJOIN S) T)<br />

Schema((R NJOIN S) T)= (T.A:D1, T.B:D2, T.C:D3)<br />

Per Definition müssen die meisten Operanden von relationenalgebraischen Operationen<br />

gewisse Voraussetzungen bezüglich ihrer Schemata erfüllen, damit die Operation möglich<br />

ist. Bevor also eine Schema-Berechnung an einem Knoten durchgeführt werden kann,<br />

müssen die Vorausseztungen der Schemata der Operanden überprüft werden. Für jede<br />

Vereinigung, jeden Durchschnitt <strong>und</strong> jede Differenz muss geprüft werden, ob die Schemata<br />

der Kindknoten identisch sind. Nach der Prüfung ergibt sich das Ergebnis-Schema direkt<br />

aus den Schemata der Kindknoten. Für jede Projektion, Selektion, Gruppierung <strong>und</strong> für<br />

jede Art von Join muss zunächst geprüft werden, ob Attribute, die in der Bedingung<br />

vorkommen, auch in den Schemata ihrer Kindknoten vorhanden sind. Danach kann das<br />

Ergebnis-Schema entsprechend den in der Definition der Operation festgelegten Regeln<br />

berechnet werden.<br />

35


Prüfung von Gültigkeitsbereichen <strong>und</strong> Datentyp-Prüfung auf Attributebene<br />

Der Zugriff auf Attribute erfolgt in der Bedingung, Attributtermliste oder Gruppierungsliste<br />

einer Operation. Um zu Prüfen, ob auf Attribute semantisch korrekt zugegriffen<br />

wird, muss das Schema der Operanden der jeweiligen Operation betrachtet werden. Hierzu<br />

müssen die Schemata der Operanden zwischengespeichert werden. Die Prüfung der<br />

Attributterme erfolgt Bottom-up. Für die Blattknoten, also die Attribute, kann mittels<br />

den zwischengespeicherten Schemata einfach überprüft werden, ob die ungruppiert im<br />

Schema Attribute existieren, außerdem können die Datentypen ausgelesen werden. Für<br />

die inneren Knoten muss überprüft werden, ob die Operanden passende Datentypen besitzen<br />

<strong>und</strong> der Ergebnis-Typ abgespeichert werden. Hierzu müssen die Typ-Ausdrücke aller<br />

möglichen Operationen definiert sein.<br />

Attribut- <strong>und</strong> Aggregierungstermlisten von Projektionen <strong>und</strong> Gruppierungen müssen<br />

nicht nur überprüft werden, sondern können zusätzlich ein Schema verändern. Aliasnamen<br />

oder Gruppierungen von Attributen müssen daher im zwischengespeicherten Operanden-<br />

Schema gespeichert werden. Natürlich müssen diese dann herangezogen werden um das<br />

Schema des jeweiligen Gruppierungs- oder Projektions-Knotens zu berechnen. Eine Umbenennung<br />

eines Attributes führt zur Umbenennung des Attributes im jeweiligen Schema,<br />

nach der Umbenennung kann nicht mehr auf den ursprünglichen Namen zugegriffen werden.<br />

Die Anfrage<br />

PROJ [A]( PROJ [A Aneu ](R NJOIN S))<br />

führt zu einem Fehler, da zum Zeitpunkt des äußeren Zugriffs auf A kein Attribut namens<br />

A mehr im Schema existiert.<br />

Schema(PROJ[A Aneu](R NJOIN S))=(R.Aneu:D1, B:D2, S.C:D3)<br />

Auch die Gruppierung von Attributen muss im Schema vermerkt werden, da ein späterer<br />

Zugriff auf einzelne Elemente der Gruppe nicht mehr möglich sein darf.<br />

Schema(GROUP[A,B#A,B,C](R NJOIN S))=(group(A:D1, B:D2), S.C:D3)<br />

Für Gruppierungen ist zusätzlich zu Prüfen, ob in der Aggregierungsterm-Liste auftretende<br />

nicht-aggregierte Attribute den gruppierten Attributen entsprechen. Die Gruppierungsliste<br />

muss daher zuerst durchlaufen werden.<br />

Gesamtablauf<br />

Zu Beginn der Prüfung werden die vorhandenen Spaltennamen mit zugehörigen Datentypen<br />

von der Datenbank abgefragt <strong>und</strong> zunächst in die Symboltabelle eingetragen. Danach<br />

wird der Operatorbaum bottom-up. Für jeden Knoten werden folgende Schritte durchgeführt:<br />

1. (a) Ist der Knoten ein Blattknoten, so hole das zugehörige Schema aus der Symboltabelle<br />

<strong>und</strong> speichere es im Knoten.<br />

(b) Ist der Knoten ein Knoten mit Bedingung, so speichere die Schemata der Operanden<br />

zwischen. Durchlaufe die Attributterme bottom-up. Für jeden Blattknoten<br />

prüfe, ob das Attribut in den zwischengespeicherten Schemata ungruppiert<br />

vorhanden ist <strong>und</strong> speichere den Datentyp im Knoten. Für die inneren<br />

Knoten prüfe die Datentypen der Operanden <strong>und</strong> berechne Ergebnis-Typ.<br />

36


(c) Ist der Knoten eine Projektion, so durchlaufe <strong>und</strong> prüfe die Attributterme<br />

genau wie die Attributterme in Bedingungen, aber speichere zusätzlich Umbenennungen<br />

von Attributen im zwischengespeichertem Schema.<br />

(d) Ist der Knoten eine Gruppierung, so durchlaufe zuerst die Gruppierungsliste<br />

<strong>und</strong> speichere eine Gruppierung der aufgetretenden Attribute im zwischengespeichertem<br />

Schema. Durchlaufe dann die Aggregierungsterm-Liste genau wie<br />

die Attributliste einer Projektion, aber prüfe zusätzlich, ob auftretende nichtaggregierte<br />

Attribute den gruppierten Attributen des zwischengespeicherten<br />

Operanden-Schemas entsprechen.<br />

2. Überprüfe die Operations-bedingten Voraussetzungen, die die Schemata der Operanden<br />

erfüllen müssen.<br />

3. Berechne das aktuelle Schema aus den Schemata den zwischengespeicherten Operandenschemata.<br />

Semantische Analyse während des Parsings<br />

Allgemein lässt sich die semantische Analyse oft während des Parsings über eine attributierte<br />

Grammatik durchführen. Die zu prüfenden Eigenschaften, hier also die Datentypen<br />

<strong>und</strong> die Schemata, werden dabei den nichtterminalen Symbolen als Attribute mitgegeben.<br />

Die Vorschriften, wie ein Datentyp- oder ein Schema-Attribut berechnet wird, sowie die<br />

durchzuführenden Prüfungen, werden den Produktionen als semantische Regeln angefügt.<br />

In einer attributierten Grammatik gibt es synthetische Attribute, die, aus der Sicht des<br />

Ableitungsbaumes eines Ausdrucks, nur vom unteren Teilbaum eines Symbols abhängen.<br />

In einem top-down-Parsing-Verfahren, wie in diesem Fall, können sie in einer Produktion<br />

einfach mittels Rückgabewerten von auftretenden Nichtterminalen <strong>und</strong> den semantischen<br />

Regeln, für das nichtterminale Symbol auf der linken Seite berechnet <strong>und</strong> als Rückgabewert<br />

an die nächst höhere Produktion weitergegeben werden.<br />

Daneben gibt es inherite Attribute, dich nicht vom unteren Teilbaum im Ableitungsbaum,<br />

sondern von anderen Symbolen im Baum abhängen. Hängen die inheriten Attribute<br />

nur von im Baum weiter links liegenden Symbolen ab, so sind sie ebenfalls bei einem<br />

top-down-Parsing-Verfahren während des Parsings berechenbar. Da der weiter links<br />

liegende Teil eines Ableitungsbaum immer bereits durchlaufen wurde, sind die nötigen<br />

Attributwerte bereits bekannt <strong>und</strong> können den nichtterminalen Symbolen als Parameter<br />

mitgegeben werden.<br />

Diese Eigenschaft trifft auf die Relationenalgebra jedoch nicht zu. Die Schemata <strong>und</strong> die<br />

Datentypen können zwar synthetisch berechnet werden, aber um Prüfen zu können, ob auf<br />

Attribtnamen-Datentyp-Kombinationen, die in einer Bedingung oder Attributterm-Liste<br />

auftreten, in den Schemata der Operanden vorhanden sind, müssen erst die Schemata<br />

der Operanden bekannt sein. Einer der Operanden befindet sich aber im zugehörigen<br />

Ableitungsbaum weiter rechts. Auf diese Weise wäre eine attributierte Grammatik hier<br />

also nicht möglich.<br />

37


4.2.4 Die Symboltabelle<br />

Nach den vorangegangenen Überlegungen muss die Symboltabelle in der Lage sein Bezeichner<br />

<strong>und</strong> Literale, jeweils mit der Position in der Eingabe <strong>und</strong> dem Abbild als Zeichenkette,<br />

abzuspeichern. Außerdem muss für jeden Bezeichner der Bezeichner-Typ „Relation“,<br />

„Attribut“ oder „Alias“ gespeichert werden können. Für Attribute <strong>und</strong> Literale<br />

ist zusätzlich ein Datentyp, für Relationen ein Relationenschema abzuspeichern.<br />

Position Image IdentType Datatype Schema<br />

[1, 6] A Attribute string -<br />

[1, 8] Aneu Alias - -<br />

[1, 11] B Attribute int -<br />

[1, 13] 3 - int -<br />

[1, 16] R Relation - (R.A:string, R.C:int)<br />

[1, 20] S Relation - (S.B:int, S.D:int)<br />

Tabelle 4.5: Symboltabelle für die Eingabe PROJ[A Aneu, B+3](R NJOIN S)<br />

38


4.3 SQL-Code-Erzeugung<br />

In der SQL-Code-Erzeugung muss nun aus dem vorhandenen Operatorbaum, die SQL-<br />

Anfrage erzeugt werden.<br />

Der Algorithmus zur Erzeugung des SQL-Codes sollte sich nicht auf die Annahme stützen,<br />

dass gewisse syntaktische Strukturen aufgr<strong>und</strong> von durchgeführten Optimierungen<br />

nicht auftreten. Wenn mit Operatorbäumen beliebiger Struktur umgegannen werden kann,<br />

ist es möglich nachträglich beliebige Optimierungsstufen zu ergänzen.<br />

Um eine Anfrage in der Relationenalgebra in SQL zu übersetzen, müssen zunächst die<br />

einzelnen Operationen betrachtet werden. Die Tabelle 4.6 gibt Auskunft darüber, wie sich<br />

einzelne Operationen der Relationenalgebra in SQL-Anfragen abbilden lassen.<br />

Operation SQL-Pendant Beispiel SQL-Code<br />

Selektion Where-Klausel SEL[A>10](R) SELECT *<br />

FROM R<br />

WHERE A>10<br />

Projektion Select-Klausel PROJ[A Aneu, C](R) SELECT A Aneu, C<br />

FROM R<br />

Vereinigung UNION R UNION S SELECT * FROM R<br />

UNION<br />

SELECT * FROM S<br />

Durchschnitt INTERSECT R INTERSECT S analog<br />

Differenz MINUS,<br />

R MINUS S<br />

analog<br />

EXCEPT<br />

Kart. Produkt From-Klausel R CARTESIAN S SELECT *<br />

FROM R,S<br />

Join From-Klausel R JOIN[A=B] S SELECT *<br />

FROM R JOIN S<br />

ON(A=B)<br />

Natural Join From-Klausel R NJOIN S SELECT *<br />

FROM R NATURAL<br />

JOIN S<br />

Semijoin R SJOIN[A=B] S analog<br />

Anti-Semijoin<br />

Gruppierung<br />

Unteranfrage<br />

in Where-Klausel<br />

Group-by-/<br />

Select-Klausel<br />

R ASJOIN[A=B] S SELECT *<br />

FROM R<br />

WHERE NOT EXISTS<br />

( SELECT *<br />

FROM S<br />

WHERE A=B )<br />

GROUP[A#A,avg(B)](R)<br />

Tabelle 4.6: Übersetzung einzelner Operationen in SQL<br />

SELECT A, avg(B)<br />

FROM R<br />

GROUP BY A<br />

Das Prinzip der verschachtelten From-Klauseln<br />

Man könnte die in der Tabelle angegebenen Relationen R <strong>und</strong> S nun statt als Tabellennamen<br />

als Variablen betrachten. Falls R in der Relationenalgebra-Anfrage ein nicht-trivialer<br />

39


Ausdruck ist, wird in der SQL-Anfrage statt einem Tabellennamen eine Unteranfrage<br />

eingetragen. Hierdurch hätte man einen sehr einfachen Algorithmus, der mit beliebig verschachtelten<br />

Relationenalgebra-Ausdrücken umgehen kann. Als Beispiel soll die folgende<br />

Übersetzung betrachtet werden.<br />

Anfrage:<br />

Übersetzung:<br />

PROJ[B,C](SEL[A>1]()R) NJOIN PROJ[C,D](S)<br />

SELECT ∗<br />

FROM (SELECT B,C<br />

FROM (SELECT ∗<br />

FROM R<br />

WHERE A>1))<br />

NATURAL JOIN (SELECT C,D<br />

FROM S)<br />

Das Problem bei diesem Verfahren ist die stark verschachtelte Struktur. Die Anfragen<br />

werden dadurch sehr schlecht lesbar <strong>und</strong> in den meisten Fällen weniger effizient.<br />

Entwurf einer verbesserten Übersetzung<br />

SQL bietet in jedem SQL-Block sechs Klauseln, die für eine Anfrage benutzt werden<br />

können. Bei der oben angegeben Übersetzung der Relationenalgebra-Ausdrücke werden<br />

jedoch nur zwei bis drei Klauseln von einer Operation benutzt <strong>und</strong> jede weitere Operation<br />

erzeugt eine neue Unteranfrage der From-Klausel. Sinnvoller wäre es mit mehreren Operationen<br />

die Klauseln eines SQL-Blocks zu füllen. Hierzu soll folgendes Beispiel betrachtet<br />

werden.<br />

Anfrage:<br />

Übersetzung 1:<br />

Übersetzung 2:<br />

PROJ[B](SEL[A>1](R))<br />

SELECT B<br />

FROM (SELECT ∗<br />

FROM R<br />

WHERE A>1)<br />

SELECT B<br />

FROM R<br />

WHERE A>1<br />

Die zweite Übersetzungs-Variante kommt ohne Unteranfrage aus <strong>und</strong> ist deshalb der ersten<br />

vorzuziehen. Für die beiden Übersetzungen war keine Umformung der ursprünglichen<br />

Anfrage nötig. Da sich in einem SQL-Block mehr als eine Operation unterbringen lässt,<br />

ist keine eindeutige Abbildung eines Relationenalgebra-Ausdrucks auf eine SQL-Anfrage<br />

möglich. Beide Übersetzungen können daher als SQL-seitige Interpretation des selben<br />

Relationenalgebra-Ausdrucks betrachtet werden.<br />

Der gesuchte Algorithmus soll eine Übersetzung liefern die äquivalent zur eingegebenen<br />

Anfrage ist, also nicht auf Umformungen beruht, also nur auf Basis unterschiedlicher<br />

Interpretationen arbeiten.<br />

Geht man nun davon aus, dass die Anfrage nicht weiter umgeformt werden darf, so muss<br />

zum Beispiel die Anfrage<br />

40


R NJOIN PROJ[A,C](S)<br />

eine Unteranfrage enthalten. Es gäbe sonst keine Möglichkeit den Join nur auf den Spalten<br />

A <strong>und</strong> C der Tabelle S auszuführen. Unter gewissen Umständen wäre es zwar möglich die<br />

Projektion nach außen zu ziehen <strong>und</strong> so auf die Unteranfrage zu verzichten, aber nach<br />

obiger Festlegung wäre dies hier nicht erlaubt. Es ergibt sich folgende SQL-Anfrage:<br />

SELECT ∗<br />

FROM R NATURAL JOIN (SELECT A, C<br />

FROM S)<br />

Das Beispiel lässt sich leicht auf ähnliche Situationen übertragen. Ersetzt man die Projektion<br />

durch eine Selektion oder ein Gruppierung, so ist ebenfalls eine Unteranfrage nötig.<br />

In SQL gibt es die Möglichkeit eine Folge von Joins in einer From-Klausel anzugeben, daher<br />

würde das Ersetzen der Projektion durch eine weitere Join-Operation nicht zu einer<br />

Unteranfrage führen. Die Anfrage „R NJOIN (S NJOIN T)“ führt zu der SQL-Anfrage:<br />

SELECT ∗<br />

FROM R NATURAL JOIN S<br />

NATURAL JOIN T<br />

An den bisherigen Beispielen lässt sich gut erkennen, dass die Entscheidung, ob eine Unteranfrage<br />

in der From-Klausel auszuführen ist, nicht nur an der aktuell betrachteten<br />

Operation, sondern an der Kombination von Operation <strong>und</strong> Operand hängt. Man kann<br />

sich also vorstellen, dass der Operatorbaum Knoten für Knoten durchlaufen wird <strong>und</strong> für<br />

jeden Knoten in Abhängigkeit seines Vaterknotens entschieden wird, ob eine Unteranfrage<br />

stattfinden muss. Eine Projektion löst als Operand eines Joins eine Unteranfrage aus,<br />

als Operand einer Selektion aber nicht. Einige andere Operanden des Joins, wie ein anderer<br />

Join oder eine Tabellenname, lösen hingegen keine Unteranfrage aus. Es wäre sehr<br />

aufwendig alle möglichen Kombinationen von Operation <strong>und</strong> Operanden zu untersuchen,<br />

aber es lassen sich gewisse Gesetzmäßigkeiten erkennen.<br />

Die Mengenoperationen<br />

Die Mengenoperationen Vereinigung, Durchschnitt <strong>und</strong> Minus unterscheiden sich von den<br />

übrigen Operationen dadurch, dass sie nicht die Klauseln eines SQL-Blocks füllen, sondern<br />

zwei unterschiedliche SQL-Blöcke verbinden. Es ist daher nachvollziehbar, dass sobald sie<br />

als Operand einer Nicht-Mengenoperation auftreten, eine Art Unteranfrage ausgeführt<br />

werden muss, da die Aufteilung der Teilanfrage in zwei SQL-Blöcke sonst nicht möglich<br />

wäre. Für die Kindknoten dieser Operationen muss hingegen nie eine Unteranfrage in der<br />

From-Klausel ausgeführt werden, da für den linken <strong>und</strong> den rechten Operanden ein neuer<br />

unbefüllter SQL-Block zur Verfügung steht. Die Anfrage PROJ[A]((R NJOIN S) UNION<br />

SEL[B>1](T)) ist somit ohne Umformung nicht ohne Unteranfrage in der From-Klausel<br />

darstellbar. Die zugehörige SQL-Anfrage wäre:<br />

SELECT A<br />

FROM (SELECT ∗<br />

FROM R NATURAL JOIN S<br />

UNION<br />

SELECT ∗<br />

41


FROM T<br />

WHERE B>1)<br />

Die Projektion ist in diesem Beispiel die erste Operation <strong>und</strong> kann daher benutzt werden<br />

um die Select-Klausel des ersten SQL-Blocks zu füllen. Der nächste Knoten im zugehörigen<br />

Operatorbaum wäre die Vereinigung. Da diese hier als Kindknoten einer Nicht-<br />

Mengenoperation auftritt, ist eine Unteranfrage in der From-Klausel nötig. Die Vereinigung<br />

verbindet zwei neue SQL-Blöcke über einen UNION-Operator. Die beiden SQL-<br />

Blöcke werden dann von den Operanden der Vereinigung, also des Joins <strong>und</strong> der Selektion<br />

befüllt. Auch in diesem Beispiel gilt, dass ein nach-innen-ziehen der Projektion sinnvoller<br />

wäre, als die Anfrage direkt zu übersetzen, aber dies ist wieder die Aufgabe der Code-<br />

Optimierung.<br />

Der (Anti-)Semijoin<br />

Ähnlich lassen sich auch die Semijoins <strong>und</strong> Antisemijoins untersuchen. Ein Antisemijoin<br />

ist nicht in einem einzelnen SQL-Block darstellbar, da er eine Unteranfrage in der<br />

Where-Klausel auslöst. Semijoins lassen sich zwar durch Umformung in einem SQL-Block<br />

darstellen, dies soll aber wieder Aufgabe der Codeoptimierung sein. Somit wird auch hier<br />

eine Unteranfrage in der Where-Klausel ausgelöst. Semijoins <strong>und</strong> Antisemijoins können<br />

also wieder gleich behandelt werden. Auftretend als Operand einer Operation, die keinen<br />

neuen SQL-Block zu Verfügung stellt, lösen sie eine Unteranfrage in der From-Klausel<br />

aus. Die Anfrage R NJOIN (S ASJOIN[A=B] T) ist ohne Unteranfrage nicht darstellbar.<br />

Auch als Operand von einstelligen Operationen wird für einen (Anit-)Semijoin eine<br />

Unteranfrage erzeugt, dies kann durch Umformungen wie<br />

PROJ[A](R ASJOIN[A=B] S) = PROJ[A](R) ASJOIN[A=B] S in der Codeoptimierungsstufe vermieden<br />

werden. Für die Operanden eines (Anti-)Semijoins wird jeweils ein SQL-Block<br />

zur Verfügung gestellt, in dem bereits eine Where-Bedingung existiert. Im rechten SQL-<br />

Block ist die Where-Bedingung die Join-Bedingung, im linken besteht sie aus „(NOT)<br />

EXISTS“ <strong>und</strong> einer Unteranfrage. Legt man nun fest, dass durch Operanden auftretende<br />

Where-Bedingungen mit der vorhandenen Where-Bedingung Und-verknüpft werden, so<br />

wird, abgesehen von den Mengenoperationen, für keinen Operanden eine Unteranfrage<br />

in der From-Klausel ausgelöst. Diese Festlegung widerspricht nicht der Voraussetzung,<br />

dass keine Umformungen in der Relationenalgebra vorgenommen werden dürfen, es handelt<br />

sich hier lediglich um eine mögliche Interpretation. Die Anfrage R ASJOIN[A=B] (S<br />

ASJOIN[B=C] T) führt damit zu folgendem SQL-Code.<br />

SELECT ∗<br />

FROM R<br />

WHERE NOT EXISTS<br />

(SELECT ∗<br />

FROM S<br />

WHERE A=B<br />

AND NOT EXISTS<br />

(SELECT ∗<br />

FROM T<br />

WHERE B=C))<br />

42


Die Anfrage wirkt sehr verschachtelt, es handelt sich bei den Verschachtelungen aber um<br />

die unumgänglichen Unteranfragen in der Where-Klausel. Unteranfragen in der From-<br />

Klausel finden nicht statt. Auch ein (Anti-)Semijoin als linker Operand kommt ohne<br />

Unteranfrage in der From-Klausel aus. Die Übersetzung der Anfrage (R ASJOIN[A=B]<br />

S) ASJOIN[B=C] T führt zu folgendem Code.<br />

SELECT ∗<br />

FROM R<br />

WHERE NOT EXISTS<br />

(SELECT ∗<br />

FROM S<br />

WHERE A=B AND NOT EXISTS<br />

(SELECT ∗<br />

FROM T<br />

WHERE B=C))<br />

Der Join, der Natural Join, der Outerjoin <strong>und</strong> das kartesische Produkt<br />

Joins, Natural Joins, Outerjoins <strong>und</strong> Kartesische Produkte können ebenfalls in einer Gruppe<br />

von Operationen zusammengefasst werden. Sie alle sind zweistellige Operationen, die<br />

sich auf die From-Klausel eines SQL-Blocks abbilden lassen. Sie können innerhalb einer<br />

From-Klausel in beliebiger Kombination, beziehungsweise mehrfach auftreten. Das heißt<br />

hintereinander ausgeführte Operationen dieser Gruppe können immer im selben SQL-<br />

Block in der From-Klausel dargestellt werden. Sofern sie als Operand auftreten führen sie<br />

also nie zu einer Unteranfrage, was allerdings nicht ausschließt, dass für ihre Operanden<br />

Unteranfragen ausgeführt werden müssen. Handelt es sich bei den Operanden wieder um<br />

Operationen dieser Gruppe oder um Tabellennamen, muss keine Unteranfrage ausgeführt<br />

werden. Alle anderen Operationen würden hier als Operand zu einer Unteranfrage führen,<br />

da die Unterbringung der jeweiligen Information im aktuellen SQL-Block sich auf das<br />

Ergebnis der Operation <strong>und</strong> nicht nur auf den jeweiligen Operanden beziehen würde.<br />

Die unären Operationen<br />

Eine Projektion füllt die Select-Klausel eines SQL-Blocks, eine Gruppierung die Group-by<br />

<strong>und</strong> die Select-Klausel. Da die Hintereinanderausführung mehrerer Projektion oder Gruppierungen<br />

semantisch nicht das selbe ist wie die Aufnahme in die selbe Select-Klausel,<br />

müssen hierbei Unteranfragen ausgeführt werden. Hintereinander ausgeführte Selektionen<br />

könnten zwar in eine Where-Klausel aufgenommen werden, dies ist aber auch durch<br />

eine Umformung darstellbar, in der die Selektionsbedingungen zweier Selektionen <strong>und</strong>verknüpft<br />

werden. Somit kann in einen SQL-Block eine Projektion oder Gruppierung,<br />

<strong>und</strong> eine Selektion aufgenommen werden, ansonsten ist eine Unteranfrage auszuführen.<br />

Im Zusammenhang mit den anderen Operationen wurden die einstelligen Operationen<br />

bereits diskutiert.<br />

Die Übersetzung<br />

Zur Übersetzung wird der gegebene Operatorbaum durchlaufen, wobei die Knoten unterschiedlichen<br />

SQL-Block-Objekten zugeordnet werden. Ein SQL-Block enthält neben<br />

Varibalen für die üblichen Klauseln eine zweite Where-Klausel für (Anti-)Semijoins. Bis<br />

43


auf die From-Klausel darf jede Klausel nur einmal befüllt werden. Das Zusammenfügen<br />

der Where-Klauseln geschieht beim Auslesen der Anfrage aus dem Block-Objekt. Da neben<br />

normalen SQL-Blöcken auch Mengenoperationen möglich sind, müssen auch Objekte<br />

einer Mengenoperationsklasse erzeugt werden können, die jeweils zwei Operanden <strong>und</strong><br />

eine Operation aufnehmen können.<br />

Man kann sich gut vorstellen, dass Operationen, die sich nach der Übersetzung in einem<br />

SQL-Block befinden, im Operatorbaum zusammenhängend sein müssen. Wird an einer<br />

Stelle im Operatorbaum festgestellt, dass für einen Operanden eine Unteranfrage erzeugt<br />

werden muss, so befinden sich alle Operationen, die die Klauseln der Unteranfrage füllen,<br />

im Teilbaum, der den betrachteten Operand als Wurzel besitzt. Die Operationen innerhalb<br />

dieses Teilbaumes können entweder den SQL-Block der Unteranfrage füllen, oder wieder<br />

neue Unteranfragen erzeugen; sie können nicht (direkt) die Klauseln des äußeren SQL-<br />

Blocks füllen.<br />

Der Baum muss nun durchlaufen <strong>und</strong> nach den Knoten durchsucht werden, an denen<br />

neue SQL-Blöcke beginnen. Dies wird immer aus Sicht der Operation für die jeweiligen<br />

Operanden entschieden, da für Operationen der Mengenoperations- <strong>und</strong> der Semijoin-<br />

Gruppe unabhängig von den Operanden neue SQL-Blöcke erzeugt werden müssen <strong>und</strong> es<br />

von der Operation abhängt, ob die verschiedene SQL-Blöcke über eine Mengenoperation,<br />

über die Where-Klausel oder über die From-Klausel verb<strong>und</strong>en werden.<br />

Es soll zunächst beispielhaft der union-Knoten aus dem Beispiel-Baum 4.2 betrachtet<br />

werden. Für seine Operanden müssen immer neue SQL-Blöcke erzeugt werden. Bei einem<br />

rekursiven Baum-Durchlauf könnte auf der union-Ebene also für den linken Operanden<br />

ein neuer SQL-Block erzeugt werden, der dann rekursiv den linken Teilbaum des union-<br />

Knotens durchläuft <strong>und</strong> befüllt wird. Bei der Rückgabe des Blocks an die union-Ebene ist<br />

dieser nach den obigen Überlegungen komplett. Er könnte also bereits als Zeichenkette<br />

ausgelesen werden <strong>und</strong> im Mengenoperations-Block untergebracht werden. Der linke Teilbaum<br />

wäre damit abgearbeitet <strong>und</strong> der als Zeichenkette ausgelesene SQL-Block verhält<br />

sich im weiteren nicht anders wie eine Zeichenkette für einen Tabellennamen.<br />

Auf dieser Basis kann eine Übersetzungs-Methode definiert werden, die rekursiv für jeden<br />

Knoten mit einem Block-Objekt block1 aufgerufen wird <strong>und</strong> folgender Weise vorgeht:<br />

1. Knoten=Mengenoperation: Rufe Operanden-Ebenen mit neuen SQL-Blöcken auf,<br />

lese die Blöcke als Strings aus <strong>und</strong> führe diese in einem Mengenoperations-Block<br />

zusammen.<br />

2. Knoten=(Anti-)Semijoin: Rufe linke Operanden-Ebene mit übergebenen Block-Element<br />

block1 auf. Rufe dann rechte Operanden-Ebene mit neuem Block-Element block2<br />

auf, lese block2 als String aus <strong>und</strong> speichere ihn mit (NOT) EXISTS in der Where-<br />

Klausel von block1.<br />

3. Knoten=Joingruppen-Element: Rufe erst erste Operanden-Ebene auf, speichere dann<br />

Join-Operator als Zeichenkette in der From-Klausel von block1 <strong>und</strong> rufe die zweite<br />

Operanden-Ebene auf.<br />

(a) Für Operanden aus Semijoin-, Mengenoperations- oder unärer Gruppe, rufe<br />

Operanden-Ebene mit neuem Block-Element block2 auf, lese block2 als String<br />

aus, füge zwei Klammern hinzu <strong>und</strong> speichere String in der From-Klausel von<br />

block1<br />

(b) Sonst: Rufe Operanden-Ebene mit block1 auf.<br />

44


4. Knoten=Element der unären Gruppe: Lese Selektionsbedingung bzw. Attribut- oder<br />

Gruppierungslisten in entsprechende Klauseln von block1 ein. Falls diese bereits<br />

belegt: Erzeuge neues Block-Element block2<br />

(a) Falls Operand aus Semijoin- oder Mengenoperations-Gruppe, rufe Operanden-<br />

Ebene mit neuem Block-Element block3 auf. Füge block3 als String mit zusätzlichen<br />

Klammern in der From-Klausel von, falls vorhanden block2, sonst<br />

block1, ein.<br />

(b) Sonst: rufe Operanden-Ebene mit, falls vorhanden block2, sonst block1, auf.<br />

Gegebenenfalls wird block2 als String mit Klammern in block1 eingetragen.<br />

5. Falls Operand=Tabellenname: Trage Tabellennamen in die From-Klausel von block1<br />

ein.<br />

Zur besseren Verdeutlichung soll das Verfahren im Folgenden durch die Übersetzung des<br />

Beispielbaumes 4.2 angewendet werden.<br />

Übersetzung des Beispielbaumes<br />

1.Aufruf<br />

Njoin1-Ebene<br />

Union-Ebene<br />

Proj-Ebene<br />

Njoin2-Ebene<br />

R-Ebene<br />

Njoin2-Ebene<br />

S-Ebene<br />

Njoin2-Ebene<br />

Proj-Ebene<br />

Der Aufruf des Wurzelknoten-Ebene erfolgt mit einem leeren SQL-Block-<br />

Objekt. (hier: block1)<br />

Da es sich beim linken Operanden um eine Mengen-Operation handelt,<br />

wird für union ein neues Mengen-Opearations-Objekt erzeugt.(hier: mengenBlock1)<br />

Für Nachfolger von Mengenoperationen wird immer ein neuer SQL-Block<br />

Erzeugt, daher wird die Projektions-Ebene wieder mit einem neuen Block<br />

aufgerufen. (hier: block2)<br />

Aus der Attributliste der Projektion wird eine Zeichenkette erstellt <strong>und</strong> in<br />

die Select-Klausel von block2 eingetragen. Für ein Joingruppen-Element<br />

als Operand eines Elements der unären Gruppe wird keine Unteranfrage<br />

erzeugt. Daher wird die njoin-Ebene mit block2 aufgerufen.<br />

Tabellennamen als Operanden führen nie zu Unteranfragen, daher wird<br />

die R-Ebene mit block2 aufgerufen.<br />

„R“ wird an die From-Klausel von block2 angehängt, die R-Ebene ist abgearbeitet,<br />

block2 wird zurück gegeben.<br />

In der Njoin-Ebene wird nun „NATURAL JOIN“ an die From-Klausel von<br />

block2 gehängt. Da der linke Operand keine Unteranfrage erzeugt, wird<br />

die S-Ebene mit block2 aufgerufen.<br />

„S“ wird an die From-Klausel von block2 angehängt, die S-Ebene ist abgearbeitet,<br />

block2 wird zurück gegeben.<br />

Die njoin-Ebene ist abgearbeitet block2 wird zurück gegeben.<br />

Die Proj-Ebene ist abgearbeitet, block2 wird zurückgegeben.<br />

45


Abbildung 4.2: Beispiel-Baum für die Code-Erzeugnung<br />

Union-Ebene<br />

Die linke Teilanfrage der Vereinigung ist komplett <strong>und</strong> kann als String<br />

„Select A From R NATURL JOIN S“<br />

für den linken Operanden von mengenBlock1 eingetragen werden. Die<br />

Mengenoperation in mengenBlock1 wird auf „UNION“ gesetzt. Für den<br />

rechten Operand einer Mengenoperation wird wie für den linken ein neuer<br />

SQL-Block übergeben. (hier block3)<br />

T-Ebene<br />

Union-Ebene<br />

„T“ wird an die From-Klausel von block3 angehängt, die S-Ebene ist abgearbeitet,<br />

block3 wird zurück gegeben.<br />

Die rechte Teilanfrage der Vereinigung ist komplett <strong>und</strong> kann als String<br />

„Select * From T“ 1<br />

1 Dass für eine unbefüllte Select-Klausel „Select *“ ausgeben werden muss, muss in der toString-<br />

Methode der SQL-Block-Klasse beachtet werden<br />

46


für den rechten Operanden von mengenBlock1 eingetragen werden. Die<br />

union-Ebene ist abgearbeitet <strong>und</strong> mengenBlock1 kann zurückgegeben werden.<br />

Njoin1-Ebene<br />

In der Njoin-Ebene wird mengenBlock1 als Zeichenkette mit zusätzlichen<br />

Klammern in die From-Klausel von block1 aufgenommen. Die Zeichenkette<br />

dazu lautet nun<br />

„(Select A From R NATURL JOIN S<br />

UNION<br />

Select * From T)“<br />

Danach wird „NATURAL JOIN“ angehängt. Der rechte Operand des<br />

njoin-Knotens ist eine unäre Operation <strong>und</strong> löst als Operand eines Elementes<br />

der Join-Gruppe eine Unteranfrage aus. Die Sel-Ebene wird also<br />

mit einem neuem Block-Element block4 aufgerufen.<br />

Sel-Ebene<br />

U-Ebene<br />

Sel-Ebene<br />

Njoin1-Ebene<br />

Der Bedinungs-Teilbaum der Selektion wird als String ausgelesen <strong>und</strong> in<br />

die Where-Klausel von block4 eingetragen. Da Tabellennamen als Operand<br />

von unären Operationen keine Unteranfrage auslösen, wird die U-Ebene<br />

mit block4 aufgerufen.<br />

„U“ wird an die From-Klausel von block4 angehängt, die U-Ebene ist<br />

abgearbeitet, block4 wird zurück gegeben.<br />

Die Sel-Ebene ist abgearbeitet, block4 wird zurückgegeben.<br />

Da block4 für eine From-Unteranfrage erzeugt wurde, wird block4 als<br />

Zeichenkette mit zusätzlichen Klammern<br />

„( Select * FROM U WHERE A>1 )“<br />

in die From-Klausel von block1 eingetragen. Block1 wird zurückgegeben<br />

<strong>und</strong> enthält die gesamte Anfrage<br />

Select ∗<br />

From (Select A From R NATURL JOIN S<br />

UNION<br />

Select ∗ From T)<br />

NATURAL JOIN<br />

(Select ∗ From U Where A>1)<br />

47


Kapitel 5<br />

Implementierung<br />

5.1 Package-Struktur<br />

Das Projekt gliedert sich in zwei Packages<br />

1. de.ra2sql.core<br />

2. de.ra2sql.gui<br />

Das core-Package enthält alle Klassen, die zur internen Verarbeitung der Anfrage nötig<br />

sind. Hierzu gehören die Symboltabelle, der Scanner, der Parser, die Semantische-Analyse-<br />

Einheit, die SQL-Code-Erzeugung, eine Klasse, die die Datanbank-Anfragen regelt, die<br />

TreeBuilder-Klasse zum Aufbau des Operatorbaumes <strong>und</strong> diverse Datenstrukturen zur<br />

Speicherung von Informationen wie zum Beispiel Datenstrukturen für die Knoten-Objekte<br />

<strong>und</strong> die Schema-Datenstruktur.<br />

Die Haupt-Komponenten Parser, Semantische-Analyse-Einheit <strong>und</strong> Code-Erzeugung arbeiten<br />

unabhängig voneinander. Sie müssen von außen mit einem zu verarbeitenden Input<br />

aufgerufen werden <strong>und</strong> geben der aufrufenden Klasse den verarbeiteten Output zurück.<br />

Das gui-Package enthält die Klassen für die Graphische Benutzerschnittstelle. Die Haupt-<br />

Klasse GUI ist für die Visualisierung der Benutzerschnittstelle sowie für die Ansteuerung<br />

der Haupt-Komponenten des core-Packages zuständig.<br />

5.2 Die Grammatikdatei <strong>und</strong> generierte Klassen<br />

Die Grammatikdatei besteht aus einem Optionen-Block, einem Parser-Block, der zusätzlichen<br />

Code für den generierten Parser enthalten kann, den Token-Definitionen <strong>und</strong> den<br />

Produktionen der Grammatik.<br />

Da die zu entwickelnde Anfrage-Sprache nicht case-sensitive sein soll, ist im Optionenblock<br />

die Option „IGNORE_CASE“ zu setzen.<br />

Der Parser-Block enthält neben der Package-Angabe eine statische Instanz der Klasse<br />

TreeBuilder, zum Aufbau des Operatorbaumes, <strong>und</strong> eine parse-Methode. Die Method erwartet<br />

die zu parsende Anfrage in einem java.io.Reader-Objekt <strong>und</strong> gibt den Wurzelknoten<br />

des erzeugten Operatorbaumes zurück. Ihre Aufgaben bestehen darin, alle verwendeten<br />

statischen Klassen <strong>und</strong> Instanzen zu reinitialisieren, sich selbst <strong>und</strong> damit auch implizit<br />

48


den Scanner mit der übergebenen Anfrage zu reinitialisieren, das Start-Symbol der Grammatik<br />

aufzurufen <strong>und</strong> den Wurzelknoten des Operatorbaumes beim TreeBuilder-Objekt<br />

abzufragen <strong>und</strong> dem Aufrufer zurück zu liefern.<br />

Die Token-Definitionen <strong>und</strong> die Grammatik können Kapitel 4.2.1 <strong>und</strong> 4.2.2 entnommen<br />

werden. Zusätzlich wird innerhalb der Produktionen der Grammatik der Tree-Builder<br />

angesteuert <strong>und</strong> Relationen, Aliase <strong>und</strong> Literale in die Symboltabelle aufgenommen.<br />

Die generierten Klassen<br />

Aus der Grammatik-Datei werden sieben Klassen generiert. Die Klassen<br />

• SimpleCharStream.java<br />

• Token.java<br />

• TokenMgrError.java<br />

• ParseException.java<br />

werden nur generiert sofern sie nicht existieren, da sie unabhängig vom Inhalt der Grammatikdatei<br />

sind. Außerdem werden die Klassen<br />

• Parser.java<br />

• ParserConstants.java<br />

• ParserTokenManger.java<br />

erzeugt. ParserConstants enthält die Konstanten für die in der Grammatikdatei definierten<br />

Tokenklassen. Der ParserTokenManager stellt den lexikalen Scanner dar, der anhand<br />

der in der Grammatikdatei definierten regulären Ausdrücke für die Tokenklassen, aus dem<br />

SimpleCharStream, der die Eingabe enthält, eine Tokenfolge erstellt. Der Scanner muss<br />

nicht expilzit aufgerufen werden, sondern wird automatisch durch den generierten Parser<br />

angesteuert. Der Parser enthält für jede Produktion der Grammatik eine eigene Methode,<br />

die eine ParseException werfen kann. Das aktuelle Token-Objekt ist in der Variable token<br />

gespeichert, das Lookahead kann über die Methode jj_ntk abgefragt werden. Der Parser<br />

ist statisch <strong>und</strong> muss daher über eine ReInit()-Methode reinitialisert werden, welche<br />

automatisch auch den TokenManager reintialisiert.<br />

5.3 Beschreibung der nicht generierten Klassen<br />

Die Knoten-Datenstrukturen<br />

Die Knoten-Datenstrukturen werden für den Operatorbaum benötigt. Von allen Knoten-<br />

Objekten gespeichert werden müssen die Operanden, die ebenfalls wieder Knoten-Objekte<br />

sind, der Vaterknoten, die Tokenklasse, die Position zur Identifizierung eines Symboltabellen-<br />

Eintrags <strong>und</strong> zur Ermöglichung einer gezielten Fehler-Ausgabe, ein Relationenschema <strong>und</strong><br />

ein Datentyp.<br />

Die benötigten Getter- <strong>und</strong> Setter- Methoden werden im Interface INode spezifiziert.<br />

Die Klasse Node implementiert das Interface INode ohne zusätzliche Funktionalität. Alle<br />

Elemente der Anfrage, die keine Attributliste oder Bedingung beinhalten, können durch<br />

49


Node-Objekte dargestellt werden. Hierzu gehören alle Elemente innerhalb einer Attributliste<br />

oder einer Bedingung, alle Mengenoperationen, das kartesische Produkt, der natürliche<br />

Verb<strong>und</strong>, so wie alle Bezeichner.<br />

Die Knoten-Datenstruktur zur Speicherung von Operationen mit Bedingung, also die<br />

Selektion <strong>und</strong> die Joins, erweitert die Klasse Node um die Speicherung eines Bedingungs-<br />

Knotens <strong>und</strong> die zugehörigen Getter <strong>und</strong> Setter.<br />

Die Knoten-Datenstruktur zur Speicherung von Operationen mit Attributliste erweitert<br />

die Node-Klasse um die Speicherung eines Arrays, welches die einzelnen Wurzelknoten<br />

der Attributterm-Listenelemente enthält, sowie ebenfalls um die Getter- <strong>und</strong> Setter-<br />

Methoden.<br />

Da die Gruppierung zwei Listen enthält, erweitert die zugehörige Knoten-Datenstruktur<br />

die Listen-Knoten-Klasse um eine zweite Liste, die Gruppierungsliste, sowie um die nötigen<br />

Getter <strong>und</strong> Setter für die Gruppenelemente.<br />

Das Klassendiagramm 5.1 soll die Knoten-Hierarchie verdeutlichen.<br />

SymbolTableEntry<br />

Objekte der Klasse SymbolTableEntry stellen Symboltabellen-Einträge dar. Sie speichern<br />

jeweils die Position, das Abbild, den Bezeichner-Typ, ein Relationen-Schema <strong>und</strong> einen<br />

Datentyp. Außerdem werden die nötigen Getter- <strong>und</strong> Setter-Methoden bereitgestellt.<br />

SymbolTable<br />

Die Klasse SymbolTable enthält eine Liste von Symboltabellen-Einträgen der Klasse SymbolTableEntry.<br />

Die Methoden setAttribute, setAlias, setRelation <strong>und</strong> setLiteral werden<br />

vom Parser mit einem Token-Parameter aufgerufen <strong>und</strong> legen jeweils ein neues Eintrag-<br />

Objekt mit bekannter Position, bekanntem Abbild <strong>und</strong> Bezeichner-Typ an. Die Bezeichner-<br />

Typen sind als Konstanten definiert. Neben den bereits genannten Methoden <strong>und</strong> den noch<br />

fehlenden Gettern <strong>und</strong> Settern, wird noch ein Methode benötigt, die alle unterschiedlichen<br />

Relationennamen der Tabelle zurück gibt. Diese wird benötigt um die Schemata<br />

aller Relationen aus der Datenbank abzufragen. Alle angebotenen Methoden, sowie auch<br />

die Liste mit Einträgen, werden statisch implementiert, daher wird noch eine Methode<br />

ReInit() benötigt, die die Einträge der Symboltabelle entfernt um diese auf die nächste<br />

Anfrage vorzubereiten.<br />

TreeBuilder<br />

Die TreeBuilder-Klasse wird vom Parser angesteuert <strong>und</strong> ist für den Aufbau des Operatorbaumes<br />

verantwortlich.<br />

Der TreeBuilder speichert global den root-Knoten, an welchem der Wurzel-Knoten des<br />

eigentlichen Operatorbaumes angehängt werden soll. Außerdem muss der Zeiger auf den<br />

aktuell zu bearbeitenden Knoten global gespeichert werden. Die, durch den Parser gesteuerte,<br />

Navigation im Baum geschieht durch die Methoden add <strong>und</strong> up. Ob es sich bei<br />

einem Token um eine Infix-Operation handelt, entscheidet der TreeBuilder intern. Da für<br />

das An- oder Umhängen des ersten Knotens einer Attributliste, einer Gruppierungsliste<br />

oder einer Bedingung spezielle Zugriffsmethoden benutzt werden müssen, muss der Tree-<br />

Builder wissen, wann dies der Fall ist. Hierzu werden vier Modi als statische Konstanten<br />

definiert <strong>und</strong> eine Variable, die die Position des Zeigers, als Tiefe innerhalb des aktuellen<br />

50


Abbildung 5.1: Klassenhierarchie der Knoten-Objekte<br />

Bedingungs-, Attributterms- oder Gruppierungslisten-Teilbaums angibt. Eine besondere<br />

Zugriffsmethode, ist immer dann nötig, wenn der Modus nicht auf Normal steht <strong>und</strong> die<br />

Tiefe Null ist. Die Art des gesonderten Zugriffs hängt vom Modus ab. Dieser wird vom<br />

Parser in den jeweiligen Produktionen gesetzt.<br />

51


Abbildung 5.2: Die Klasse TreeBuilder<br />

Attribute<br />

Ein Attribut speichert einen Attributnamen, einen (optional leeren) Relationennamen-<br />

Präfix, <strong>und</strong> einen Datentyp. Es werden die nötigen Getter- <strong>und</strong> Setter-Methoden bereitgestellt,<br />

sowie eine equals-Methode <strong>und</strong> eine isCompatibleTo-Methode bereitgestellt. Zwei<br />

Attribute sind gleich, wenn sie den gleichen Namen, den gleichen Präfix <strong>und</strong> den gleichen<br />

Datentyp besitzen. Sie sind kompatibel, wenn Name <strong>und</strong> Datentyp übereinstimmen. Die<br />

Präfixe dürfen sich dabei also unterscheiden. Die Methode wird an mehreren Stellen von<br />

der Semantischen Analyse benötigt. Zum Beispiel müssen für eine Vereinigung zweier<br />

Relationen, alle Attribute der Schemata kompatibel sein.<br />

RelationsSchema<br />

Ein Relationenschema-Datenstruktur muss eine Liste von Attributen speichern können.<br />

Außerdem müssen neben den üblichen Getter- <strong>und</strong> Setter-Methoden, Methoden bereitgestellt<br />

werden, die ein Schema-Objekt mit einem gegebenen Schema-Objekt auf Gleichheit,<br />

Teilmengen-Beziehungen, oder Kompatiblität überprüfen. Zwei Schema-Objekte sind<br />

kompatibel, wenn sie gleich viele Attribute besitzen <strong>und</strong> diese zueinander kompatibel<br />

sind. Zusätzlich beinhaltet die Schema-Datenstuktur eine toString-Methode, die es möglich<br />

macht bei einem semantischen Fehler involvierte Schemata mit auf dem Bildschirm<br />

auszugeben.<br />

SchemaOperations<br />

Die Klasse SchemaOperations bietet statische Methoden zur Berechnung von Schemata<br />

an.<br />

Semantische Analyse<br />

Die Klasse SemanticalAnalysisUnit implementiert die semantische Prüfung der Schemata<br />

<strong>und</strong> Datentypen im Operatorbaum.<br />

52


Sie wird über die öffentliche Methode „analyse“ aufgerufen, die als Paramenter den Wurzelknoten<br />

des zu Prüfenden Operatorbaumes erwartet <strong>und</strong> den Wurzelknoten des überarbeitenden<br />

Baumes zurückgibt. Im Fehlerfall wird eine SemanticalException geworfen.<br />

Bevor mit der Analyse begonnen wird, wird die Methode „readOutRelationsSchemata“<br />

der Klasse DatabaseFrontend aufgerufen, welche die Schemata aller in der Anfrage verwendeten<br />

Relationennamen von der Datenbank abfragt <strong>und</strong> in die Symboltabelle einträgt.<br />

Die Analyse erfolgt rekursiv über die Methode „recursiveAnalyse“. Für die Blattknoten<br />

werden die jeweiligen Schemata aus der Symboltabelle abgefragt <strong>und</strong> im Knoten-Objekt<br />

eingetragen. Ist in der Symboltabelle für einen Blattknoten kein Schema-Eintrag vorhanden,<br />

so wird eine SemanticalException geworfen. Die Schemata der inneren Knoten werden<br />

nach den jeweiligen Prüfungen über die Methode computeSchema berechnet, die zur<br />

Berechnung die statischen Methoden der Klasse SchemaOperations nutzt. Für jedes von<br />

Node abgeleitete Knoten-Objekt, werden die Schemata des linken <strong>und</strong> des rechten Operanden<br />

in den globalen Schema-Variablen currentSchemaLeft <strong>und</strong> currentSchemaRight<br />

abgespeichert <strong>und</strong> je nach Knotentyp die passende Methode zur Prüfung der Bedingung,<br />

der Attributliste oder der Gruppierungsliste aufgerufen.<br />

Abbildung 5.3: Klassendiagramm für die Semantische Analyse<br />

SQLBlock<br />

Die Klasse SQLBlock wird für die SQL-Code-Erzeugung benötigt. Sie bietet die Methoden<br />

setSelect, addToFrom, setWhere, setWhereExists, setGroupBy, setHaving <strong>und</strong> toString<br />

an. Die Methode set-Methoden speichert den übergebenen String in der globalen Variable<br />

falls dieser zuvor null war <strong>und</strong> gibt true zurück. Ist die Variable bereits initialisiert, so<br />

wird false zurückgegeben. Auf diese Weise weiß die aufrufende ConversionUnit, ob die<br />

Speicherung einer Klausel im aktuellen SQL-Block möglich ist oder ob ein neuer Block<br />

53


erzeugt werden muss. Die Methode addToFrom <strong>und</strong> fügt den übergebenen String der<br />

globalen From-Liste hinzu.<br />

Operations<br />

Die Klasse Operations beinhaltet die statischen Konstanten für die Einteilung der relationenalgebraischen<br />

Operationen in Gruppen, sowie eine statische Methode zur Zuordnung<br />

der Gruppenvariablen zu den einzelnen Operationen. Sie erwartet eine Konstante für die<br />

Tokenklasse <strong>und</strong> gibt eine Konstante für die Gruppe zurück.<br />

ConversionUnit<br />

Die Klasse ConversionUnit implementiert die Übersetzung der Anfrage. Die Klasse bietet<br />

eine öffentliche statische decodeToSQL-Methode an, die den Wurzelknoten des Operatorbaumes<br />

als Parameter erwartet <strong>und</strong> die SQL-Anfrage als String zurückgibt. Die Übersetzung<br />

findet dann rekursiv in der privaten Methode „decode“ statt. Diese erwartet ein<br />

Objekt der Klasse SQL-Block, das aktuelle Knoten-Objekt <strong>und</strong> einen String, der Leerzeichen<br />

oder Tabulatoren zur Formatierung der SQL-Anfrage enthalten kann. Die Zuordnung<br />

der Gruppen-Variablen zu den verschiedenen Relationenalgebraischen Operationen erfolgt<br />

über die statischen Methoden der Klasse „Operations“. Die Übersetzung der Bedingungen,<br />

Attributlisten <strong>und</strong> Gruppierungslisten erfolgt wieder in eigenen Methoden.<br />

Abbildung 5.4: Klassendiagramm für die Code-Erzeugnunge<br />

54


DatabaseFrontend<br />

Die Klasse DatabaseFrontend ist für die Datenbank-Anfragen zuständig. Sie beinhaltet<br />

die Methoden initConnection, readOutRelationsSchemas, executeSQLQuery <strong>und</strong> close-<br />

Connection.<br />

Die Methode initConnection erwartet einen Benutzernamen <strong>und</strong> ein Passwort <strong>und</strong> baut<br />

die Verbindung zur Datenbank auf. ExecuteQuery erwartet eine SQL-Anfrage als String,<br />

führt diese aus <strong>und</strong> gibt einen java.io.Reader zurück, der die formatierte Ergebnis-Tabelle<br />

enthält. Die Methode readOutRelationsSchemas fragt alle in der Anfrage aufgetretenden<br />

Relationennamen über die Methode getDistinctRelationEntries von der Symboltabelle ab,<br />

fragt alle Attribut-Datentyp-Paare von der Datenbank ab, erzeugt daraus Schema-Objekte<br />

<strong>und</strong> speichert diese in der Symboltabelle.<br />

55


Kapitel 6<br />

Die Benutzerschnittstelle<br />

Die Benutzerschnittstelle besteht im Wesentlichen aus einem Eingabe- <strong>und</strong> zwei Ausgabe-<br />

Bereichen, zwei Toolbars <strong>und</strong> einer Menüzeile.<br />

Abbildung 6.1: Die Benutzer-Schnittstelle<br />

Die Eingabe<br />

Die Eingabe der Anfrage erfolgt im oberen Teilfenster. Sie kann mehrzeilig sein <strong>und</strong> beliebig<br />

viele Leerzeichen oder Tabulatoren enthalten. Die Schlüsselworte der Sprache können<br />

optional händisch eingegeben, oder durch Anklicken des entsprechenden Symbols in der<br />

linken Toolbar an der aktuellen Cursor-Position in der Eingabemaske eingefügt werden.<br />

56


Senden der Anfrage<br />

Eine Anfrage kann durch Anklicken des Send-Buttons gesendet werden. Da hierzu ein<br />

Datenbank-Login nötig ist, öffnet sich, falls der Nutzer die Login-Daten nicht bereits<br />

eingegeben hat, automatisch ein Fenster zur Entgegennahme des Benutzernamens <strong>und</strong><br />

des Passworts. Im Vorfeld kann die Eingabe der Login-Daten auch über einen Button in<br />

der Toolbar oder über die Menüzeile -> Optionen -> Einstellungen erfolgen. Tritt bei<br />

der Anfrage-Analyse ein Fehler auf, so wird dieser im unteren Ausgabe-Bereich angezeigt,<br />

ansonsten wird die Ergebnistabelle im Ausgabebereich dargestellt.<br />

Eine Anfrage kann durch Anklicken des ToSQL-Buttons auch optional als SQL-Anfrage<br />

im Ausgabe-Bereich ausgegeben werden.<br />

Bedienbarkeit<br />

Zum besseren Verständnis werden für alle Buttons der Oberfläche Tooltips angezeigt,<br />

die kurz die Funktionalität erläutern. Außerdem lässt sich unter dem Menüpunkt Hilfe<br />

eine Hilfedatei anzeigen, die die Benutzeroberfläche <strong>und</strong> die Sprache noch einmal kurz<br />

erläutert.<br />

Weitere Funktionen<br />

Alle Eingabe- <strong>und</strong> Ausgabe-Bereiche lassen sich scrollen sobald der dargestellte oder eingegebene<br />

Inhalt zu groß wird. Zusätzlich können aber einzelne Teilfenster durch kleine<br />

Pfeile in den Trennbalken ein- <strong>und</strong> ausgeblendet werden oder in der Größe durch ziehen<br />

des Trennbalkens manuell verändert werden. Die Anordnung der Fenster lässt sich durch<br />

zwei Buttons in der oberen Toolbar oder ober das Ansicht-Menü verändern. CopyPaste<br />

wird in der Eingabemaske unterstützt, in den Ausgabe-Bereichen ist nur das Kopieren<br />

des Auswahlbereiches möglich. Die Funktionen finden sich im Rechtsklick-Menü, im<br />

Bearbeiten-Menü, oder können über die Standard-Tastenkombinationen benutzt werden.<br />

57


Abbildungsverzeichnis<br />

4.1 Aufbau des Operatorbaumes . . . . . . . . . . . . . . . . . . . . . . . . . . 24<br />

4.2 Beispiel-Baum für die Code-Erzeugnung . . . . . . . . . . . . . . . . . . . 46<br />

5.1 Klassenhierarchie der Knoten-Objekte . . . . . . . . . . . . . . . . . . . . . 51<br />

5.2 Die Klasse TreeBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52<br />

5.3 Klassendiagramm für die Semantische Analyse . . . . . . . . . . . . . . . . 53<br />

5.4 Klassendiagramm für die Code-Erzeugnunge . . . . . . . . . . . . . . . . . 54<br />

6.1 Die Benutzer-Schnittstelle . . . . . . . . . . . . . . . . . . . . . . . . . . . 56<br />

58


Tabellenverzeichnis<br />

4.1 Sprachelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15<br />

4.2 Operatoren/Operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15<br />

4.3 Tokenklassen-Definitionen für Relationenalgebraische Operationen . . . . . 17<br />

4.4 Tokenklassen-Definitionen für arithmetische Operatoren . . . . . . . . . . . 17<br />

4.5 Symboltabelle für die Eingabe PROJ[A Aneu, B+3](R NJOIN S) . . . . . 38<br />

4.6 Übersetzung einzelner Operationen in SQL . . . . . . . . . . . . . . . . . . 39<br />

59

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!