Fakultät für Elektrotechnik und Informatik Institut für Praktische Informatik Fachgebiet Datenbanken und Informationssysteme Entwurf, Realisierung und Betrieb einer temporalen Erweiterung von relationalen Datenbanken Masterarbeit im Studiengang Informatik Marcell Salvage Matrikelnummer: 2946190 Prüfer: Prof. Dr. Udo Lipeck Zweitprüfer: Dr. Hans Hermann Brüggemann Betreuer: Prof. Dr. Udo Lipeck 20.04.2015 Inhaltsverzeichnis 1 Einleitung 1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Ziel und Aufbau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Grundlagen 2.1 Temporale Daten . . . . 2.1.1 Zeitmodelle . . . 2.1.2 Zeitpunkte . . . . 2.1.3 Zeitintervalle . . 2.1.4 Zeitspannen . . . 2.1.5 Zeitdimensionen . 2.2 PostgreSQL . . . . . . . 2.2.1 Extensions . . . . 2.2.2 Datentyp Range . 2.3 Bison & Flex . . . . . . 1 1 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 4 5 6 12 12 13 13 16 19 3 Temporale Tabellen 3.1 Valid Time-Tabellen . . . . . . . . . . . . 3.1.1 Anforderungskatalog: Constraints . 3.1.2 Anforderungskatalog: Anfragen . . 3.1.3 Anforderungskatalog: Modifikation 3.2 Transaction Time-Tabellen . . . . . . . . . 3.3 Bitemporale Tabellen . . . . . . . . . . . . 3.4 Herausforderungen & Probleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 24 25 28 30 34 36 36 4 Temporale Datenbanksprachen 4.1 TSQL2 . . . . . . . . . . . . . . . 4.2 SQL:2011 . . . . . . . . . . . . . 4.2.1 Valid Time-Tabellen . . . 4.2.2 Transaction Time-Tabellen 4.2.3 Bitemporal-Tabellen . . . 4.3 VTSQL2+ . . . . . . . . . . . . . 4.4 Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 38 41 42 45 48 48 52 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . III . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Entwurf und Implementierung 5.1 SQL-Erweiterung . . . . . . . . . . . . 5.1.1 DDL/DML-Kommandos . . . . 5.1.2 Anfragen . . . . . . . . . . . . . 5.2 Funktionen der PostgreSQL Extension 5.2.1 Allen-Relationships-Funktionen 5.2.2 Funktionen für DDL/DML . . . 5.2.3 Trigger . . . . . . . . . . . . . . 5.2.4 Anfrage-Hilfsfunktionen . . . . 5.3 Übersetzung der SQL-Erweiterung . . . 5.3.1 Parser . . . . . . . . . . . . . . 5.3.2 Abstract Syntax Tree (AST) . . 5.3.3 Übersetzung . . . . . . . . . . . 5.4 PHP-Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 55 55 58 59 60 61 62 66 67 68 68 69 78 6 Beispieldatenbank Modulkatalog 6.1 Datentyp Semester . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Studiengänge und Kompetenzbereiche . . . . . . . . . . . . . . . . . . . . 6.3 Lehrveranstaltungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 82 83 85 7 Fazit & Ausblick 87 A PostgreSQL-Extension: Datentyp Semester 89 Abbildungsverzeichnis 95 Tabellenverzeichnis 97 Literaturverzeichnis 99 IV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kapitel 1 Einleitung Entwurf, Realisierung und Betrieb einer temporalen Erweiterung; das sind 3 Aufgaben, welche historisch bereits viele Male angegangen wurden. In der Einleitung soll zunächst mithilfe der Motivation dargestellt werden, warum vorliegende Ausarbeitung dieses Thema aufgreift. Nach der Motivation wird die Struktur in kurzen Sätzen dargestellt. Dadurch wird ersichtlich, in welchem Kapitel die Aufgaben aufgegriffen und gelöst werden. 1.1 Motivation Die Vergangenheit und Zukunft gehören zu Datenbanken wie die Gegenwart, jedoch liefern die üblichen relationalen DBMS kaum Unterstützung, temporale Datenbanken zu pflegen. Versuche, temporale Elemente in den SQL-Standard zu integrieren, gab es schon vor 20 Jahren, genauer genommen seit 1994. Es wurde eine Initiative gegründet, angeführt von Snodgrass, um SQL mit temporalen Elementen zu erweitern. 1992 gab es bereits Diskussionen in der Community und nach einem Vorschlag von Snodgrass wurde ein Komitee gegründet, welches mit der Entwicklung von TSQL21 beauftragt wurde. Die Sprache versank etwas in Vergessenheit, bis der SQL-Standard SQL:2011 das Thema erneut aufgegriffen hat, jedoch gibt es immer noch keine Einigung über die tatsächliche Realisierung der einzelnen Bestandteile. Viele Unternehmen speichern historische Daten auf ihre eigene Weise, so dass Interoperabilität zwischen den Daten ausgeschlossen ist. Es besteht ein großer Markt für die Verwaltung temporaler Daten aller Art, insbesondere, wenn sie transparent und über einen langen Zeitraum geschehen soll. Ein Beispielszenario für temporale Daten ist, wie in der Literatur oft erwähnt, eine Datenbank über Beschäftigungsverhältnisse. Beschäftigungsverhältnisse verändern sich über einen Zeitraum, Mitarbeiter wechseln ihre Position im Unternehmen und es verändern sich Arbeitsumstände, wie Gehalt oder auch Beziehungen wie der Manager einer Abteilung, der für diverse Angestellte verantwortlich ist. Ein anderes Szenario fernab der gängigen Literatur, auf das in dieser Ausarbeitung eingegangen wird, ist die Verwaltung von Veranstaltungen in Studiengängen in einem Online-Modulkatalog. Dort wird festgehalten, welche Veranstaltungen in dem aktuellen Semester angeboten 1 Eine SQL-92 Erweiterung, näher beschrieben in Kapitel 4.1 1 werden, einschließlich ihrer Eigenschaften, wie Semesterwochenstunden, Leistungspunkte, Beschreibung, Lehrkraft und vielen weiteren. Jedoch interessiert häufig nicht nur der aktuelle Zustand. Studenten in späteren Semestern referenzieren ältere Veranstaltungen, spätestens bei ihrem Abschluss auf dem Abschlusszeugnis. Oft wechseln angebotene Veranstaltungen oder sie verschwinden gar vollständig. Trotzdem dürfen Informationen über sie nicht verloren gehen, sie bleiben historisch relevant. Außerdem interessieren Veranstaltungen aus dem vorherigen Semester, wenn Nachholtermine für Prüfungen angeboten werden. Andere Beispiele für temporale Daten sind Data Warehouses und das Bankenwesen. Dort werden sie auch regelmäßig für Analyseverfahren angefragt und ausgewertet. Diese Ausarbeitung befasst sich mit den Forschungsergebnissen der letzten Jahre im Zusammenhang mit aktuellen Erkenntnissen und Technologien, um eine Schnittstelle zu erstellen, welche die genannte Erfassung und Verwaltung temporaler Daten vereinfacht und unterstützt. Zusätzlich soll sie ohne große Schwierigkeiten in ein bestehendes Datenbanksystem integriert werden können und Abwärtskompatibilität zu der bisherigen Schnittstelle gewährleisten. So soll es einfach möglich sein, ein bisheriges Schema mit wenigen Modifikationen zu temporalisieren und, wenn nötig, temporale Daten abzufragen, wenn diese benötigt werden. Das gewährleistet höchste Kompatibilität mit bekannten Schnittstellen und trotzdem Flexibilität. Dafür werden PostgreSQL-Extensions verwendet, welche mit einem Parser eine abwärtskompatible Erweiterung von SQL lesen und mit der Extension Informationen austauschen. 1.2 Ziel und Aufbau Nach der Einleitung im ersten Kapitel werden im zweiten Kapitel die nötigen Grundlagen erläutert, die eine temporale Datenhaltung erst möglich machen. Zu den Grundlagen gehören zunächst Grundbegriffe der temporalen Datenhaltung wie die existierenden Zeitmodelle, verschiedene Zeittypen wie beispielsweise Zeitpunkte und Zeitintervalle sowie die Zeitdimensionen, das heißt Valid Time und Transaction Time. Zusätzlich werden in den nachfolgenden Unterkapiteln die verwendeten Technologien beschrieben, PostgreSQL sowie die Entwicklung von Extensions und Bison und Flex für die Erstellung eines Parsergenerators. Das dritte Kapitel bespricht die verschiedenen Arten von temporalen Tabellen und welche Probleme und Herausforderungen dazukommen, wenn die Entscheidung für eine temporale Datenbasis getroffen wird. Anhand von Beispielen werden temporale Aktionen gezeigt und die Lösung in SQL implementiert dargestellt. Das gilt als Grundlage für die im Kapitel 5 gezeigte Implementierung. Im vierten Kapitel werden vergangene temporale SQL-Erweiterungen des SQLStandards betrachtet, um eine Basis für die in der Ausarbeitung entwickelte SQLErweiterung zu schaffen. Dafür werden Beispiele für übliche Aktionen dargestellt und anschließend die Sprachen in ihrer Funktionalität evaluiert. Die Erkenntnisse aus diesem Kapitel fließen in die Entwicklung der SQL-Erweiterung dieser Ausarbeitung mit ein. Anschließend wird im fünften Kapitel der Entwurf der drei Bestandteile der ent- 2 wickelten DB-Erweiterung erklärt. Als erstes wird der Aufbau der Erweiterung des SQL-Standards vorgestellt. Dafür wird gezeigt, welche neuen Schlüsselwörter hinzugekommen sind und wie die neue Semantik der einzelnen SQL-Anfragen ist. Danach wird die PostgreSQL-Extension entworfen, welche die benötigten Funktionen und Trigger erklärt. Dabei wird dargestellt, welche Methoden in welchem Teil der SQL-Erweiterung benutzt werden und anhand von Beispielen ihre Funktionalität beschrieben. Im letzten Teil des Kapitels wird die Übersetzung der SQL-Erweiterung in Standard-SQL anhand von Beispielen gezeigt. Dafür wird der benötigte abstrakte Syntaxbaum und wie er in der Übersetzung benutzt wird beschrieben. Im sechsten Kapitel findet eine Testimplementierung einer temporalen Datenbank mit Hilfe der entwickelten Bausteine statt. Dafür wird der momentan vorhandene Modulkatalog der Fakultät mit den entwickelten Konzepten neu aufgebaut. Anschließend werden die Unterschiede analysiert und die Daten überführt. An die entstehende Datenbank werden analog passende Anfragen gestellt, die den Anforderungen an den Modulkatalog angepasst sind. Den Abschluss der Arbeit bildet das Fazit, welches die Ergebnisse zusammenfasst und auf mögliche weitere Bestandteile der Erweiterung von SQL hinweist. 3 Kapitel 2 Grundlagen Im folgenden Kapitel werden nötige Grundlagen für den Aufbau der Erweiterung beschrieben. Dabei ist es wichtig zunächst die Bestandteile von temporalen Daten im Allgemeinen kennen zu lernen, um den Aufbau von temporalen Datenbanken und Datenbanksprachen verstehen zu können. Anschließend wird eine kleine Einführung in PostgreSQL mit Fokus auf Extensions und dem Datentyp Range gegeben. 2.1 Temporale Daten Temporale Daten sind Informationen, welche mit Zeitangaben abgespeichert werden. Dieser Vorgang, Daten mit einem Zeitstempel zu versehen und anschließend mit dieser Angabe abzuspeichern, klingt vorerst einfach. Dabei entstehen jedoch zusätzliche Anforderungen und Probleme. Nachfolgend wird zunächst die Zeit im Allgemeinen erklärt; welche Formen von Zeit existieren? Anschließend werden die verschiedenen Zeitdimensionen betrachtet. Dadurch entsteht ein Überblick der Arten von temporalen Tabellen, die in der Literatur besprochen werden. 2.1.1 Zeitmodelle Zeitmodelle sind ein Bestandteil von temporalen Informationen. Sie beschreiben die Beschaffenheit von Zeit in den Daten. In [TYT11] wird zwischen drei verschiedenen Modellen unterschieden. Das Modell, welches der Realität am nächsten kommt, ist das kontinuierliche Modell. Dort ist jeder Zeitpunkt mit einer reellen Zahl dargestellt. Dieses Modell hat eine hohe Genauigkeit, ist jedoch dank der Gegebenheit von Computern immer mit Verlust verbunden. In der Realität wird dieses Modell selten eingesetzt. Im diskreten Modell wird die Zeit in Chronons (näher beschrieben in Kapitel 2.1.2) unterteilt, zum Beispiel Kalendermonate oder Jahre. So gibt es in diesem Modell für jedes Chronon genau einen Abschnitt. Ein drittes Modell ist das nicht-temporale Modell, welches Daten mit Zeit beinhaltet, die nicht durch ihre Zeit identifiziert werden, zum Beispiel das Geburtsdatum. In temporalen Datenbanken beschäftigen wir uns mit dem diskreten Modell, da es sich für die Abbildung von Daten mit temporalen Eigenschaften am besten 4 eignet und Anfragen sich logischer gestalten lassen, wie man in den nächsten Kapiteln über die Zeitpunkte und Zeitintervalle sehen kann. 2.1.2 Zeitpunkte Zeitpunkte, wie in [JW10] beschrieben, sind eine Momentaufnahme der Zeit. Sie beschreiben ein Chronon in der Zeitlinie. Verschiedene Zeitpunkte können nicht nur unterschiedliche Punkte in der Zeitlinie darstellen, es gibt auch Optionen, die bestimmen wie fein die Zeit abgebildet wird. Diese Feinheit der Zeit bezeichnet man als Granularität. Beispiele für solche Granularitäten sind Tage, Kalendermonate oder Jahre. So wäre ein Zeitpunkt 2012 mit der Granularität Jahr behaftet, 2012-04-04 12:30:50 hätte die Granularität Sekunde. Die kleinste darstellbare Zeit, die sich mit einer angegebenen Granularität darstellen lässt, heißt Chronon. So ist das Chronon vom gegebenen Zeitpunkt 2012 genau ein Jahr. Dieses Jahr lässt sich nicht teilen, das heißt, wenn zwei Ereignisse in 2012 passiert sind, lassen sie sich nicht mit unterschiedlichen Zeitpunkten in der Granularität Jahr darstellen. Es existieren auch Granularitäten, die nur in bestimmten Domänen sinnvoll sind. So wird im Modulkatalog die Granularität eines Semesters gewählt, da die Gültigkeit der einzelnen Elemente sich innerhalb eines Semesters nicht verändern. Weitere Beispiele für ungewöhnliche, aber dennoch mögliche Granularitäten, wären eine Dekade (10 Jahre) oder ein Trimester, welches auch im akademischen Raum benutzt wird. Zu den Zeitpunkten mit Granularitätsangaben kommt zudem noch das Problem der Konvertierung zwischen verschiedenen Granularitäten. In welcher Beziehung stehen zwei Zeitpunkte mit unterschiedlicher Granularität? Ist der Zeitpunkt 2014-03-20 in der Granularität Tag vor oder nach dem Zeitpunkt 2014 mit der Granularität Jahr? Auch die Frage, wie man eine Konvertierung bei kleiner werdender Granularität vornimmt, kommt dabei auf. Wenn man genau definieren würde, dass bei der Konvertierung von Jahr zu Tag der Zeitpunkt 2012 den Zeitpunkt 2012-01-01 repräsentieren würde, wäre das vorher genannte Problem gelöst. Verschiedene DBMS verfahren in diesem Fall jedoch unterschiedlich. Interessant wird es auch bei ungewöhnlichen Granularitäten, wie dem zuvor genannten Semester, da der Semesteranfang sowohl von Hochschule zu Hochschule, als auch von Jahr zu Jahr unterschiedlich ist. Es gibt somit keine genaue Einteilung und es kann nicht mit Sicherheit gesagt werden, dass ein Semester exakt 6 Monate nach dem letzten startet. Es ist nicht einmal klar, an welchem Wochentag es startet. In dem Fall müsste man eine Interoperabilität mit Zeitpunkten mit anderen Granularitäten wie beispielsweise einem Tag ausschließen. Doch das ist nicht das einzige Problem. Zusätzlich kommen noch die Zeitzonen hinzu, die insbesondere beim Austausch von Daten mit einer Granularität von Stunden und feiner eine große Rolle spielen. So gelten zwischen Zeitzonen verschiedene Konventionen und es existieren verschiedene Annahmen über die Zeit in einem Jahr, wie die Sommerzeit und Winterzeit. Des Weiteren enthalten Zeitberechnungen wesentlich größere Probleme als man zunächst annimmt Als Beispiel sei ein astronomisches Element in einer Datenbank 5 vermerkt, von dem wir wissen, dass es nach exakt einem Sonnenjahr1 erneut auftreten wird. So würde es nicht reichen, lediglich das Jahr um eins zu erhöhen, da ein Sonnenjahr länger als ein Jahr, approximiert etwa 365.242190417 Tage, lang ist. Zeitberechnungen sind genau deshalb nicht trivial und man sollte bei der Implementierung solcher Systeme auf domänenspezifische Anforderungen an die Zeit Rücksicht nehmen. Beispielsweise sollte man sich beim Austausch zwischen Zeitzonen auf eine Zeitzone festlegen, die in der Datenbank physikalisch gespeichert wird und bei der Ausgabe bei Bedarf zurück konvertiert wird. 2.1.3 Zeitintervalle Zeitintervalle eignen sich für die Beschreibung von zeitlich verankerten Zeiträumen, die Vergangenheit, Gegenwart oder Zukunft umfassen. Sie beinhalten einen Anfangs- und einen Endzeitpunkt. Dabei gibt es 4 verschiedene Arten, ein Zeitintervall darzustellen; [], [), (] und (), wobei eine runde Klammer ein offenes Ende symbolisiert, das heißt, dass der angegebene Zeitpunkt nicht im Intervall enthalten ist. Analog stehen eckige Klammern für ein geschlossenes Ende, welches den angegeben Zeitpunkt miteinbezieht. Als Beispiel würde das Zeitintervall [2014-01-01,2015-01-01) das ganze Jahr 2014 beschreiben, wohingegen [2014-01-01,2015-01-01] das ganze Jahr 2014 mit zusätzlich dem 01.01.2015 beschreiben würde. Für diese Ausarbeitung sind [) oder (]-Intervalle am besten geeignet, da sie die Darstellung von mehreren benachbarten Intervallen vereinfachen. Bei der Intervalldarstellung gilt: wenn der Endzeitpunkt vom ersten Intervall identisch mit dem Anfangszeitpunkt des zweiten Intervalls ist, lassen sich beide zu einem zusammenfassen. So ergeben die Intervalle [2014-01-01,2014-06-01) und [2014-06-01,2015-01-01) zusammengefasst [2014-01-01,2015-01-01). Dieses Beispiel zeigt ein weiteres Problem der []-Notation; ergeben zwei benachbarte Intervalle ein zusammengefasstes Intervall oder existieren zwischen ihnen noch relevante Zeitpunkte? Es lässt sich nicht mit Bestimmtheit sagen, dass [2014-01-01,2014-05-31] und [2014-06-01,2015-01-01] zusammengefasst [2014-01-01,2015-01-01) ergeben, da zwischen den Intervallen noch Zeit liegt. [) oder (]-Intervalle erleichtern somit Operationen für den Zusammenschluss von Intervallen und die Temporalisierung von Daten. Zur Vereinfachung wird in den folgenden Kapiteln ausschließlich [) verwendet, obwohl (] analog funktioniert. Bis zum Ende der Zeit, oder auch 9999-12-31 In Zeitintervallen wird zusätzlich ein Konstrukt benötigt, das einen Zeitraum beschreibt, welcher sich von einem festen Zeitpunkt aus erstreckt und bis an das Ende der Zeit reicht. Das heißt, jeder Zeitpunkt ab dem Startzeitpunkt ist in diesem Intervall enthalten. Dieses Konstrukt wird benötigt, wenn ein Zustand eines Objektes beschrieben wird, von dem nicht bekannt ist, wann er ungültig ist. Es ist nicht bekannt, wann sich dieses Objekt verändern wird. Darüber hinaus ist nicht bekannt, ob sich das Objekt jemals 1 Zeitraum, innerhalb dessen die Erde alle Jahreszeiten durchläuft 6 verändern wird. Deshalb wird ein Endzeitpunkt eingeführt, der eben dies bedeuten soll. In der Literatur wird dieser Endzeitpunkt unterschiedlich genannt; in [JW10] wird 9999-12-31 benutzt, das heißt das Ende dieses Dekamillenniums. In [TYT11] wird dieser Endzeitpunkt mit UC gekennzeichnet. Auch der Begriff FOREVER tritt in einigen Werken auf. In PostgreSQL ist es möglich, den Start- und/oder Endzeitpunkt leer zu lassen, um ebendies zu erreichen. Now Now beschreibt den Zeitpunkt, in dem die Funktion aufgerufen wird. So würde der Zugriff auf Now am 2015-04-20 um 13:50:34 genau diesen Zeitpunkt ausgeben und eine Sekunde später den selben Zeitpunkt um eine Sekundenzahl erhöht. Die Funktion ist also vom Zeitpunkt des Aufrufes abhängig. Verschiedene DBMS nennen diese Funktion unterschiedlich, so ist in PostgreSQL ein Now() üblich, der SQL Standard beschreibt die Funktion mit CURRENT_TIMESTAMP und diversen anderen Funktionen seit der Standardversion SQL2008. So gibt CURRENT_TIMESTAMP den momentanen Zeitpunkt mitsamt Zeitzonen-Informationen zurück. Zusätzlich existiert CURRENT_DATE, welches das Datum ohne Tageszeitangabe zurückgibt. Die Funkion LOCAL-TIME stellt dagegen den Zeitpunkt ohne Zeitzonen-Informationen dar. In älteren DBMS nannte man diese Funktion auch getdate(). Die Now-Funktion wird in der temporalen Datenhaltung für mehrere Aspekte benötigt. So müssen in der Transaktionszeit bei einer erfolgreich abgeschlossenen Transaktion die Tabellenspalten, die verändert oder gelöscht wurden, archiviert werden. Dabei wird bei der Transaktionszeit angegeben, dass die Tabellen nun nur noch bis Now gelten und gegebenenfalls die neuen Spalten zum Zeitpunkt der Transaktion ab dem Zeitpunkt Now gelten. Auch bei der Gültigkeitszeit benötigt man das, wenn man ausdrücken will, dass ein bestimmter Umstand ab jetzt nicht mehr gilt. Die bereits im Kapitel für die Zeitintervalle 2.1.3 besprochenen Zeitzonen stellen auch für Now ein Problem dar. So muss, je nach Implementierungsentscheidung, Now stets in eine vorher definierte Zeitzone umgewandelt werden, um mit dem Rest der Datenbank konform zu sein, insbesondere wenn die gewählte Granularität sehr fein ist. Allen-Relationen zwischen Zeitintervallen Allen-Relationships wurden zuerst 1983 im Artikel [All83] von James F. Allen erläutert. Sie beschreiben die Beziehungen zwischen Zeitintervallen auf einer gemeinsamen Zeitlinie. In der Tabelle 2.1 werden die 11 Beziehungen erläutert. Dabei sind t1 und t2 Zeitintervalle. Die linke Spalte zeigt eine Funktion mit zwei Zeitintervallen als Argumente, die einen Wahrheitswert zurückgibt. Die rechte Spalte beschreibt, wann die Funktion den Wahrheitswert true oder false zurückgibt. In der Abbildung 2.1 findet sich eine grafische Erklärung der Beziehungen. Auf dieser Grafik sind auch die Paare der Relationen R, S mit R(t1 , t2 ) ⇔ S(t2 , t1 ) zu erkennen. So gilt, dass Before mithilfe des Vertauschens von Argumenten After darstellen kann. Es 7 gilt somit Before(t1 , t2 ) ⇔ After(t2 , t1 ). Analog gilt dasselbe für die anderen gegenübergestellten Beziehungen. Equals ist kommutativ, also Equals(t1 , t2 ) ⇔ Equals(t2 , t1 ). In der Abbildung 2.2 wird eine Einteilung mittels einer Baumstruktur vorgenommen. Dabei sind die Blätter des Baums die in der Tabelle 2.1 vorgestellten Relationships (jeweils eine übersetzbare Relationship pro Paar). Die restlichen Knoten sind von [JW10] zusätzlich hinzugefügte allgemeine Beziehungen zwischen Zeitintervallen. Entweder sie schneiden sich und besitzen somit eindeutige gemeinsame Punkte, oder sie schneiden sich nicht und es besteht kein gemeinsamer Punkt. Bei zwei nicht schneidenden gibt es außerdem die Unterscheidung zwischen berührenden und nicht berührenden Intervallen. Bei geschnittenen Intervallen gibt es die Unterscheidung, ob sie auch noch nicht gemeinsame Punkte haben. Wenn sie nur noch gemeinsame Punkte haben, kann zusätzlich unterschieden werden, ob sie identisch sind und eines der Intervalle lediglich ein Teil des anderen ist. Bei Teilintervallen wird außerdem danach unterschieden, ob sie am Anfang oder Ende des Intervalls oder irgendwo in der Mitte liegen. Before(t1 , t2 ) After(t1 , t2 ) During(t1 , t2 ) Contains(t1 , t2 ) Overlaps(t1 , t2 ) Overlapped-by(t1 , t2 ) Meets(t1 , t2 ) Met-by(t1 , t2 ) Starts(t1 , t2 ) Started-by(t1 , t2 ) Finishes(t1 , t2 ) Finished-by(t1 , t2 ) Equals(t1 , t2 ) t1 tritt vor t2 auf und die Zeitintervalle haben keine gemeinsamen Zeitpunkte t2 tritt vor t1 auf und die Zeitintervalle haben keine gemeinsamen Zeitpunkte Intervall t1 startet nach dem Anfangszeitpunkt von t2 und endet vor dem Endzeitpunkt von t2 Intervall t2 startet nach dem Anfangszeitpunkt von t1 und endet vor dem Endzeitpunkt von t1 Intervall t1 startet vor dem Anfangszeitpunkt von t2 und endet zwischen dem Anfangs- und Endzeitpunkt von t2 Intervall t2 startet vor dem Anfangszeitpunkt von t1 und endet zwischen dem Anfangs- und Endzeitpunkt von t1 Endzeitpunkt von t1 ist identisch mit dem Anfangszeitpunkt von t2 Endzeitpunkt von t2 ist identisch mit dem Anfangszeitpunkt von t1 t1 und t2 haben den gleichen Anfangszeitpunkt und t1 endet vor t2 t1 und t2 haben den gleichen Anfangszeitpunkt und t2 endet vor t1 t1 und t2 haben den gleichen Endzeitpunkt und t1 startet nach t2 t2 und t1 haben den gleichen Endzeitpunkt und t2 startet nach t1 t2 und t1 haben den gleichen Anfangs- und Endzeitpunkt Tabelle 2.1: Die 13 Relationen zwischen Zeitintervallen nach James F. Allen 8 Abbildung 2.1: Darstellung der Relationen in Tabelle 2.1 (Grafik entnommen aus [TYT11]) Relationen zwischen Punkt und Intervall Neben den Relationen zwischen Zeitintervallen sind auch Relationen zwischen Zeitpunkten und Intervallen für die Entwicklung der temporalen Datenbank von Interesse. Diese sind beschrieben in [TYT11]. Dabei sind diese Relationen eine Spezialisierung der gegebenen 13 Relationships nach Allen. Ein Punkt ist lediglich ein Intervall mit der Größe eines Chronons seiner Granularität. Das heißt ein Intervall [2014-03-20, 2014-03-21) oder auch [2014-03-20, 2014-03-20], gegeben die Granularität eines Tages, ist gleichbedeutend mit dem Zeitpunkt 2014-03-20. Wenn nun die 13 Relationships auf ein Intervall mit einer Größe > 1 angewendet werden, bleiben nur noch wenige übrig, da andere niemals erfüllt werden können. So gibt es kein Overlaps und Overlapped-by, da dafür ein Intervall außerhalb und innerhalb des anderen vorhanden sein muss. Das ist bei einem Intervall mit der Größe 1 nicht möglich. Started-by und Finished-by haben ebenfalls keinen Nutzen mehr, da ein Zeitpunkt nicht von einem Intervall gestartet beziehungsweise abgeschlossen werden kann. Contains ist zusätzlich ausgeschlossen, da ein größeres Intervall nicht in ein kleineres passt. Zudem ist Equals ausgeschlossen, da die beiden Intervalle nicht die gleiche Größe besitzen können. In der Tabelle 2.2 sind 9 Abbildung 2.2: Taxonomie der Allen-Relationships (Grafik entnommen aus [JW10]) alle übrigen Relationships vermerkt. Dabei fällt auf, dass nun auch einige Relationships durch die Einschränkung eines Intervalls identisch sind; so ist Meets dasselbe wie Starts und analog Met-by dasselbe wie Finishes. In der Abbildung 2.3 findet sich eine grafische Veranschaulichung, ähnlich wie bei der Abbildung 2.1, für die Relationen zwischen den Intervallen. Diese werden benötigt, um Anfragen an die Datenbank zu stellen. Eine häufige Anfrage an eine Valid Time-Tabelle ist: Welche Daten galten zum Zeitpunkt t? Dafür eignet sich During, da es genau das beschreibt; ist der Zeitpunkt im gegebenen Zeitintervall enthalten? Wenn er enthalten ist, so war der Datensatz zu diesem Zeitpunkt gültig und kann ausgegeben werden. Andere Anfragen benutzen die beschriebenen Relationships analog; auch bei Transaction Time-Tabellen. So lässt sich die Frage Welche Datensätze wurden exakt zum Zeitpunkt t ungültig gemacht? mithilfe der Finishes-Relation ausdrücken. Weitere Einsatzzwecke für die Relationen finden sich im Kapitel Entwurf und Implementierung. Before(p, t) After(p, t) Meets(p, t) Starts(p, t) Met-by(p, t) Finishes(p, t) During(p, t) Punkt p ist vor dem Anfangszeitpunkt von t Punkt p ist nach dem Endzeitpunkt von t Der Anfangszeitpunkt von t und Punkt p sind identisch Der Endzeitpunkt von t und Punkt p sind identisch Punkt p ist zwischen dem Anfangs- und Endzeitpunkt von t Tabelle 2.2: Temporale Relationen zwischen Zeitpunkt und Zeitintervall 10 Abbildung 2.3: Darstellung der Relationen in Tabelle 2.2 (Grafik entnommen aus [TYT11]) Relationen zwischen zwei Punkten Die Relationen lassen sich noch weiter spezialisieren, indem man die Einschränkungen aus dem vorherigen Unterkapitel auf beide Intervalle anwendet, so dass beide Intervalle die Größe 1 haben. Dadurch entsteht eine Gruppe von Relationen zwischen zwei Zeitpunkten. Diese sind recht mühelos zu verstehen, da sie identisch zu den üblichen Vergleichsoperatoren sind; =, <, >. Sie lassen sich ähnlich wie die Relationen aus dem vorherigen Unterkapitel ableiten, indem in die Relationen zwei Intervalle der Länge 1 eingesetzt werden. In der Tabelle 2.3 sind die drei Relationen analog beschrieben. Dabei sind p und q jeweils Zeitpunkte. Die Abbildung 2.4 zeigt eine grafische Veranschaulichung. Before(p, q) After(p, q) Equals(p, q) Punkt p ist vor dem Punkt q Punkt p ist nach dem Punkt q Punkt p und Punkt q sind identisch Tabelle 2.3: Temporale Relationen zwischen zwei Zeitpunkten Abbildung 2.4: Darstellung der Relationen in Tabelle 2.3 (Grafik entnommen aus [TYT11]) 11 2.1.4 Zeitspannen Eine Zeitspanne beschreibt einen nicht verankerten Zeitraum wie 1 Tag oder 4 Wochen. Mit ihnen lässt sich der Unterschied zwischen zwei Zeitpunkten feststellen oder aus einem bestehenden Zeitpunkt ein neuer Zeitpunkt erstellen. So ergibt der Zeitpunkt 2014-04-01 mit der Zeitspanne 1 Tag den Zeitpunkt 2014-04-02. Im SQL-92 wurde ein Datentyp verabschiedet, der einer Zeitspanne entsprechen sollte; das INTERVAL. Das sei nicht mit dem zuvor besprochenen Zeitintervall zu verwechseln. Der Datentyp wurde mit einer ganzen Reihe anderer Datentypen eingeführt, beispielsweise dem DATE, TIME und TIMESTAMP, alles Datentypen, welche die Abspeicherung von Zeit über den Standard ermöglichen. 2.1.5 Zeitdimensionen In der temporalen Datenhaltung existieren unterschiedliche Zeitdimensionen, die jeweils eine andere Bedeutung im Kontext der Speicherung, Verwaltung und Abfrage der Daten haben. Zuerst vorgestellt wurden die Begriffe der Zeitdimension im Artikel [SA86]. Die Implementierung und Verwaltung der verschiedenen Zeitdimensionen und ihrer Kombinationen unterscheidet sich untereinander erheblich und muss deshalb gesondert betrachtet werden. Valid Time Valid Time, auch Application Time oder Effective Time2 in Englisch oder Gültigkeitszeit in Deutsch, beschreibt die Gültigkeit von Objekten in der entworfenen Welt. Valid Time beschreibt das, was wir glauben, was in der modellierten Welt zu den abgebildeten Zeiten wahr ist. Bei dem Entwurf einer Datenbank, wo sich Objekte und ihre Beziehungen über die Zeit hinweg verändern, ist oft die Vergangenheit und Zukunft genauso interessant wie die Gegenwart. Daher werden Objekte mit ihrer Valid Time abgespeichert und das heißt auch, dass sie nun mehrmals in einer Tabelle, für verschiedene Zeiträume, vorkommen können. Transaction Time Transaction Time, auch System Versioned oder Asserted Versioning in Englisch oder Transaktionszeit in Deutsch, wird, ähnlich wie die Valid Time, in Tabellen mithilfe eines Versionsstempels an den Datensätzen abgebildet. Sie bedeuten jedoch etwas anderes; sie bilden ab, wie die Datenbank zu den angegeben Zeitpunkten beschaffen war. Transaktionszeit erläutert demnach, was wir glaubten, was für die modellierte Welt wahr war. Bei der Valid Time ist es möglich, auch zukünftige und vergangene Datensätze zu editieren, um Annahmen zu korrigieren oder zu erweitern, wohingegen dies bei der Transaktionszeit nicht erlaubt ist. Die vergangenen Datensätze werden erschaffen, wenn aktuelle verändert werden. Eine Ausnahme stellt dabei die in [JW10] beschriebene Asserted Versioning 2 in [JW10] 12 dar, welche auch DEFERRED TRANSACTION zulässt. Damit werden Transaktionen bezeichnet, deren Zeitintervall in der Zukunft liegen. Dies ist von Nutzen, wenn wir abbilden wollen, dass sich unser Wissen in der Zukunft verändert, jedoch die aktuellen Daten nicht geändert werden dürfen, auch wenn sie nicht zwangsweise korrekt sind. Bitemporal Bitemporal heißen Tabellen, die sowohl die Valid Time als auch die Transaction Time eines Objekts abspeichern. 2.2 PostgreSQL PostgreSQL ist ein quelloffenes objekt-relationales Datenbanksystem, entwickelt seit 01.05.1995. Die Software wird mit C entwickelt und wird unter der selbst entwickelten PostgreSQL-Lizenz vertrieben. Die vorliegende Ausarbeitung wird PostgreSQL in der Version 9.2 verwenden. PostgreSQL wurde auf einige Betriebssysteme portiert, darunter die üblichen Linux-Distributionen, Microsoft Windows und andere Unix-Klone wie Mac OS X3 und BSD-Derivate. Das Projekt wird auch im kommerziellen Betrieb zahlreich verwendet, zum Beispiel im U.S. State Department und bei Red Hat4 . Zum Support von SQL:2011, welches im Kapitel ?? näher betrachtet wird, steht in der Dokumentation von PostgreSQL 9.3.5 folgende Bemerkung im Kapitel Appendix D. SQL Conformance 5 : PostgreSQL supports most of the major features of SQL:2011. Out of 179 mandatory features required for full Core conformance, PostgreSQL conforms to at least 160. In addition, there is a long list of supported optional features. It might be worth noting that at the time of writing, no current version of any database management system claims full conformance to Core SQL:2011. Es fehlen jedoch alle im Kapitel 4.2 genannten Features. Periods lassen sich dennoch mithilfe des Range-Datentyps simulieren. 2.2.1 Extensions PosgreSQL ermöglicht die Programmierung von selbst definierten Funktionen und Datentypen, die in SQL benutzt werden können. Dabei werden verschiedene Programmiersprachen unterstützt, darunter C, welches die Programmiersprache ist, mit der PostgreSQL implementiert wurde. Zusätzlich bietet PostgreSQL mithilfe von Modulen weitere Programmiersprachen für die Entwicklung von Erweiterungen an: PL/pgSQL, einer Sprache, die Oracles PL/SQL ähnelt, PL/TCL, PL/Python und PL/Perl. Diese 3 Seit Mac OS X 10.7 Lion sogar die Standard-Datenbank für die Server Edition und installiert auf allen Desktop Versionen. 4 siehe http://www.postgresql.org/about/users/ 5 http://www.postgresql.org/docs/9.3/static/features.html 13 Sprachen werden im Kontext der Erweiterungen prozedurale Programmiersprachen genannt und werden durch zusätzlich installierte Module gelesen, übersetzt und verarbeitet, um für die Entwicklung zur Verfügung zu stehen. In dieser Ausarbeitung werden SQL, C und PL/pgSQL als Programmiersprachen benutzt. Diese haben den Vorteil, dass sie keinen zusätzlichen Interpreter auf dem Server benötigen und damit leichter auf verschiedene PostgreSQL-Installationen zur Verfügung zu stellen sind, da C, SQL und PL/pgSQL auf jedem PostgreSQL-System ab der Version 9.0 verfügbar sind. Nachdem einige logisch zusammenhängende6 Funktionen, Objekte und Operatoren geschrieben wurden, bietet PostgreSQL die Möglichkeit, diese Objekte in eine Extension zusammenzufassen. Das hat den Vorteil, dass sie mit wenigen Befehlen auf eine andere PostgreSQL-Installation übertragen werden kann. Man kann sie zudem leichter dokumentieren und warten und auch besser in ihren Rechten bei der Installation einschränken. Zusätzlich ist es möglich die definierte Extension mitsamt ihrer Definitionen mit einem einzigen Statement entfernen. Für eine Extension werden zunächst mindestens zwei Dateien benötigt, eine Datei in der Form <name>-<version>.sql und eine weitere in der Form <name>.control. Diese beiden werden in das Extension-Verzeichnis von PostgreSQL verschoben und können anschließend mit CREATE EXTENSION <name>; geladen werden. Mit einem DROP EXTENSION <name>; kann die Extension entfernt werden. Um die Installation zu vereinfachen, bietet sich ein Makefile an. Das folgende Beispiel stammt aus der offiziellen Dokumentation von PostgreSQL. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 CREATE TYPE pair AS ( k text, v text ); CREATE OR REPLACE FUNCTION pair(anyelement, text) RETURNS pair LANGUAGE SQL AS ’SELECT ROW($1, $2)::pair’; CREATE OR REPLACE FUNCTION pair(text, anyelement) RETURNS pair LANGUAGE SQL AS ’SELECT ROW($1, $2)::pair’; CREATE OR REPLACE FUNCTION pair(anyelement, anyelement) RETURNS pair LANGUAGE SQL AS ’SELECT ROW($1, $2)::pair’; CREATE OR REPLACE FUNCTION pair(text, text) RETURNS pair LANGUAGE SQL AS ’SELECT ROW($1, $2)::pair;’; CREATE OPERATOR ˜> (LEFTARG = text, RIGHTARG = anyelement, PROCEDURE = pair); CREATE OPERATOR ˜> (LEFTARG = anyelement, RIGHTARG = text, PROCEDURE = pair); CREATE OPERATOR ˜> (LEFTARG = anyelement, RIGHTARG = anyelement, PROCEDURE = pair); CREATE OPERATOR ˜> (LEFTARG = text, RIGHTARG = text, PROCEDURE = pair); Quelltext 2.1: Datei: pair–1.0.sql 6 Logisch zusammenhängende Objekte sind beispielsweise eine Objektdefinition mitsamt selbstentwickelter Operatoren und Hilfsfunktionen 14 1 2 3 4 # pair extension comment = ’A key/value pair data type’ default version = ’1.0’ relocatable = true Quelltext 2.2: Datei: pair.control 1 2 3 4 5 6 EXTENSION = pair DATA = pair−−1.0.sql PG CONFIG = pg config PGXS := $(shell $(PG CONFIG) −−pgxs) include $(PGXS) Quelltext 2.3: Datei: Makefile In der pair-1.0.sql-Datei wird ein neuer Typ Pair erzeugt, der ein Paar aus Text-Variablen als eine Einheit darstellt, das heißt ein Zweier-Tupel aus Texten. Diese Art von Typen nennt man Composite-Types, weil sie genau das darstellen; eine Zusammensetzung aus Variablen. Für diesen Typ sind zusätzlich noch vier Funktionen in SQL definiert worden, die als Konstruktor gelten. Mit ihnen lassen sich neue Paare erzeugen. In den Funktionsargumenten findet man den Typ anyelement. Dies ist ein Pseudo-Typ7 , der polymorphe Programmierung erlaubt. Damit lassen sich Funktionen programmieren, die für mehr als einen bestimmten Datentyp gelten. Es wird erst zur Laufzeit überprüft, ob die übergebenen Argumente mit den benutzten Operationen in der definierten Funktion kompatibel sind und gegebenenfalls eine Fehlermeldung geworfen. Es existieren noch weitere Pseudo-Types wie anyarray und anyrange; letzteres wird in dieser Ausarbeitung mehrmals benutzt. Die definierten Funktionen werden anschließend in der Datei zu überladenen Operatoren zugewiesen. Das erleichtert den Umgang mit dem Datentyp. In der pair.control-Datei ist ein Kommentar über die Extension vermerkt, sowie die neueste stabile Version. Das Argument relocatable gibt an, ob sich eine Extension ohne Probleme in ein anderes Schema verschieben lässt. Das ist nur möglich, wenn die definierten Objekte in der Extension unabhängig vom gegebenen Schema sind. In diesem Beispiel ist dies der Fall, dementsprechend gilt relocatable = true. Weitere Angaben sind in der Dokumentation vermerkt. Die dritte Datei ist der Makefile, der mithilfe der PostgreSQL-Mechanismen pg_config die Installation von Extensions vereinfacht. Extensions eignen sich damit für die Verteilung von Objekten mit ihren Funktionen zwischen verschiedenen PostgreSQL-Systemen. Es ermöglicht dabei eine einfache Installation und Deinstallation. Außerdem bietet der Mechanismus noch weitere Optionen für vereinfachte Updates an, so dass Abwärtskompatibilität gewährleistet wird. So lassen sich mehrere Versionen einer Extension auf dem System installieren und die Datenbank oder das Schema kann angeben, welche Version zwingend benötigt wird. 7 http://www.postgresql.org/docs/9.3/static/datatype-pseudo.html 15 2.2.2 Datentyp Range Range ist ein in PostgreSQL integrierter Datentyp zur Darstellung von Intervallen. Diese Intervalle haben dabei einen Start- und Endpunkt. Es ist möglich, Intervalle in den im Kapitel 2.1.3 vorgestellten Möglichkeiten anzugeben, das heißt [], [), (] und (). Der zugrundeliegende Datentyp kann dabei unterschiedlich sein, in PostgreSQL selbst sind sechs Typen vordefiniert: int4range Intervall aus Ints (ganze Zahlen zwischen -2147483648 und +2147483647) int8range Intervall aus Bigints (ganze Zahlen zwischen -9223372036854775808 und 9223372036854775807) numrange Intervall aus Numerics (reelle Zahlen zwischen 131072 Vorkommastellen und 16383 Nachkommastellen) tsrange Intervall aus Zeitstempeln ohne Zeitzonenangabe tstzrange Intervall aus Zeitstempeln mit Zeitzonenangaben daterange Intervall aus Daten Es lassen sich mit CREATE TYPE <name> AS RANGE (<Typbeschreibung>) weitere Range-Typen aus bestehenden Basisdatentypen definieren, da die bereits angebotenen Range-Typen nicht für jeden Einsatzzweck geeignet sind. So existieren in temporalen Datenbanken mehr Granularitäten als die Zeitstempel und das Datum; es existiert beispielsweise die Granularität Monat, welche mit den gegebenen Typen nicht dargestellt werden können. In diesem Fall lassen sich selbst definierte Range-Datentypen nutzen. Es gibt hierbei verschiedene Möglichkeiten, eine Range zu erstellen; die erste Möglichkeit ist, die entsprechende Konstruktorfunktion aufzurufen. Diese heißt identisch zur Bezeichnung der Range und nimmt mindestens 2, jedoch höchstens 3 Argumente entgegen. Das erste Argument ist die untere Grenze des Intervalls, das zweite die obere Grenze. Bei der Angabe von NULL wird der Start- oder Endpunkt auf unendlich gesetzt. Im dritten optionalen Argument lässt sich angeben, von welcher Form das Intervall ist, das heißt, ob der Start oder das Ende geschlossen oder offen ist. Standardmäßig hat es die Form [). So erstellt der Aufruf int8range(3, 6) ein Intervall zwischen 3 und 6, wobei 6 nicht enthalten ist. Dieser Aufruf ist identisch mit int8range(3, 6, ’[)’). Für ein Intervall, in dem die 6 enthalten ist, wäre ein Aufruf mit int8range(3, 6, ’[]’) nötig. Neben dieser Möglichkeit lässt sich auch die Syntax :: von PostgreSQL verwenden. Der ::-Operator konvertiert alles vor :: in den Typ, der nach :: angegeben wird. So wird mit 2::float der Integer 2 in einen float 2.0 umgewandelt. Dasselbe funktioniert auch mit Range, da dies eine Konvertierung von Strings zu Range ermöglicht. So erstellt der Befehl ’[3, 6)’::int8range das zuerst genannte Beispiel. PostgreSQL übernimmt zusätzlich noch eine Konvertierung von [] zu [), falls der Datentyp es 16 zulässt, er also diskret ist. So wird ’[3, 6]’::int8range automatisch zu [3, 7), wohingegen dasselbe Beispiel mit numrange statt int8range nicht dazu in der Lage wäre, da es keine direkt benachbarte Zahl über 6 gibt, die dasselbe ausdrücken würde. Wenn eine Range erstellt wird, können der Anfangs- und Endzeitpunkt weggelassen werden. Sie definiert ein Intervall, bei dem entweder alle Punkte vor dem Endpunkt oder alle Punkte nach dem Startpunkt im Intervall enthalten sind. Möglich ist auch ein Intervall, in dem sowohl Anfangs- als auch Endpunkt weggelassen werden. Dieses Intervall enthält alle möglichen Elemente, abhängig vom definierten Range-Typ. Es ist auch möglich, statt dem weggelassenen Start- und Endpunkt ein infinity oder -infinity anzugeben, wenn der Datentyp der Range so eine Notation enthält. Dabei gibt es jedoch Unterschiede; so sind [today,] und [today,) identisch, [today,infinity] und [today,infinity) aber unterschiedlich, da im letzteren der Wert infinity enthalten ist, in dem davor jedoch nicht. Für die Ausarbeitung sind insbesondere tsrange, tstzrange und daterange interessant, da sie ein wie in Kapitel 2.1.3 besprochenes Zeitintervall darstellen und zusätzlich noch Operationen und Indexe anbieten. So übernimmt PostgreSQL viele Vorgänge, die sonst mühsam mithilfe von mehreren Spalten realisiert werden müssen, wie beispielsweise den Schnitt von zwei Intervallen. Beispiel Es folgen nun einige Beispiele. Zunächst wird eine Tabelle erstellt, die eine Daterange erstellt. Dabei wird ein übliches Beispiel einer Employee-Tabelle genommen, die eine Validtime-Historie enthält. Zusätzlich existieren als Spalten der Vor- und Nachname sowie das Jahresgehalt. 1 CREATE TABLE employee ( 2 e id SERIAL, 3 first name TEXT, 4 last name TEXT, 5 salary NUMERIC, 6 vt DATERANGE, 7 PRIMARY KEY (e id, vt) 8 ); Quelltext 2.4: Employee-Tabelle mit Daterange für Validtime Zu beachten ist dabei, dass e_id und vt den Primärschlüssel bilden, da in dieser Tabelle mehrmals dasselbe Objekt auftreten kann, das jedoch zu unterschiedlichen Zeiträumen gilt. Nun werden einige Daten in die Tabelle eingefügt; zwei Mitarbeiter mitsamt ihres Gehaltes, bei einem Mitarbeiter soll jedoch das Gehalt ab heute etwas höher sein. 1 2 3 4 5 INSERT INTO employee (e id, first name, last name, salary, vt) VALUES (1, ’Max’, ’Mustermann’, 2000, daterange(’2013−01−01’,NULL)), (2, ’Susi’, ’Kaufgern’, 2000, ’[2013−01−01,today)’::daterange), (2, ’Susi’, ’Kaufgern’, 3000, ’[today,)’::daterange); Quelltext 2.5: Inserts in employee 17 Dabei sind die zwei verschiedenen Arten der Definition von Ranges zu beobachten. In den Ranges selbst wird zusätzlich die Variable today verwendet. Diese gibt das aktuelle Datum zurück. Nun noch eine einfache Anfrage, welche alle Mitarbeiter zurück gibt, die momentan gültig sind. 1 SELECT e id, first name, last name, salary 2 FROM employee 3 WHERE vt @> date(’today’); Quelltext 2.6: Snapshot von employee Dafür wird der @>-Operator verwendet, welche überprüft, ob ein Objekt in einem Intervall vorhanden ist. Damit lässt sich ausdrücken, ob der jetzige Zeitpunkt im Intervall enthalten ist. PostgreSQL bietet auch eine einfache Syntax zur Definition von Zeitpunkten an. So lässt sich ein Datum-Intervall zwischen heute und in 12 Tagen leicht ausdrücken. 1 SELECT daterange(’today’, (now() + ’12 day’)::DATE); 2 3 Ausgabe: [2014−11−28,2014−12−10) Quelltext 2.7: Range und Intervall Liste der Operationen PostgreSQL bietet mit dem Range-Datentyp noch eine Reihe nützlicher Funktionen und Operatoren an, die sich auch für die Verarbeitung von Ranges mit Zeitpunkten eignen. Mit ihnen lassen sich die Allen-Relationships effizient programmieren, da die Operatoren mit optimiertem C implementiert und dabei auch Indexe beachtet wurden. In der Tabelle 2.4 sind die vordefinierten Operatoren zu entnehmen. Zusätzlich zu den Operatoren bietet PostgreSQL noch Funktionen an, die es ermöglichen, auf die einzelnen Elemente der Range zuzugreifen. Diese Funktionen sind in der Tabelle 2.5 vorgestellt. 18 Operator = <> < > <= >= @> @> <@ <@ && << >> &< &> -|+ * - Beschreibung Gleich Ungleich kleiner als größer als kleiner gleich größer gleich enthält range enthält element range ist enthalten in element ist enthalten in überlappen strikt links von strikt rechts von erweitert nicht rechts von erweitert nicht links von angrenzend union intersection difference Beispiel int4range(1,5) = ’[1,4]’::int4range numrange(1.1,2.2) <>numrange(1.1,2.3) int4range(1,10) <int4range(2,3) int4range(1,10) >int4range(1,5) numrange(1.1,2.2) <= numrange(1.1,2.2) numrange(1.1,2.2) >= numrange(1.1,2.0) int4range(2,4) @>int4range(2,3) int4range(1,55) @>42 int4range(2,4) <@ int4range(1,7) 42 <@ int4range(1,7) int8range(3,7) && int8range(4,12) int8range(1,10) << int8range(100,110) int8range(50,60) >> int8range(20,30) int8range(1,20) &<int8range(18,20) int8range(7,20) &>int8range(5,10) numrange(1.1,2.2) -|- numrange(2.2,3.3) numrange(5,15) + numrange(10,20) int8range(5,15) * int8range(10,20) int8range(5,15) - int8range(10,20) Ausg. t t t t t t t t t f t t t t t t [5,20) [10,15) [5,10) Tabelle 2.4: Range-Operatoren 2.3 Bison & Flex GNU Bison ist ein quelloffener Parsergenerator, welcher mit dem älteren Yacc kompatibel ist.8 Mit Bison lassen sich kontextfreie Grammatiken in einen Parser kompilieren. Dieser Parser kann in den Programmiersprachen C, C++ oder Java erzeugt werden. Bison wurde als Parsergenerator für dieses Projekt benutzt, weil ein Teil der Erweiterung in C geschrieben ist, und Bison sich in Projekten, die in C geschrieben sind, bewährt hat. So wird es von Ruby, PHP und von PostgreSQL selbst benutzt. Das Projekt wird Bison benutzen, um die SQL-Erweiterung in einen abstrakten Syntaxbaum (kurz AST) zu parsen und anschließend den AST in SQL zu übersetzen. Zusammen mit Bison wird häufig flex benutzt, um einen Lexer9 für den Parser zu erzeugen. flex übersetzt eine Definitionsdatei in einen Lexer in C, welcher die von Bison benötigte yylex-Funktion zur Verfügung stellt. Die yylex-Funktion soll dabei das nächste Token von einer Datei zurückgeben. 8 siehe https://www.gnu.org/software/bison/ Lexikalischer Scanner oder auch Tokenizer; zerlegt die Quelldatei in Token, das heißt in zusammenhängende Elemente 9 19 Funktion lower(anyrange) upper(anyrange) isempty(anyrange) lower inc(anyrange) upper inc(anyrange) lower inf(anyrange) upper inf(anyrange) Beschreibung Startpunkt Endpunkt Intervall leer? Links geschlossen? Rechts geschlossen? Startpunkt unendlich? Endpunkt unendlich? Beispiel lower(numrange(1.1,2.2)) upper(numrange(1.1,2.2)) isempty(numrange(1.1,2.2)) lower inc(numrange(1.1,2.2)) upper inc(numrange(1.1,2.2)) lower inf(’(,)’::daterange) upper inf(’(,)’::daterange) Erg. 1.1 2.2 false true false true true Tabelle 2.5: Range-Funktionen Beispiel In dem Beispiel wird ein Parser für eine Programmiersprache definiert, die arithmische Ausdrücke der Form 1+1 liest und direkt auswertet. Dabei wird zunächst ein Lexer benötigt, welcher die verschiedenen Tokens des Parsers definiert. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 %option noyywrap %{ #include <stdlib.h> #include <stdio.h> #include ”calc.tab.h” %} %% /∗ integers ∗/ [0−9]+ { yylval.intval = atoi(yytext); return NUM; } /∗ End of line and expression ∗/ [\n] { return END; }; /∗ operators ∗/ [+−/∗()] { return yytext[0]; } /∗ skip whitespace and tabs ∗/ [ \t] { } /∗ unknown characters produce error messsage ∗/ . { yyerror(”unknown character”); } %% Quelltext 2.8: Datei: calc.l Die Definition eines Lexers in flex besteht aus drei Teilen, die jeweils mit %% getrennt sind. Im Quelltext 2.8 von Zeile 1-8 ist der erste Block. Er enthält zunächst einen Abschnitt 20 welcher mit %{ eingeleitet und mit %} geschlossen wird. In ihm werden die benötigten Header-Dateien und damit auch Bibliotheken aufgezählt, die für den Lexer benötigt werden. In Zeile 5 ist der Header, der von Bison erstellt wird, eingebunden. Im zweiten Block, welcher von der Zeile 9 bis zum Ende des Quelltextes reicht, werden die Regeln der Tokens aufgezählt. Die Definition eines jeden Tokens passiert in zwei Teilen: zunächst ein regulärer Ausdruck, welcher beschreibt, wie der Token aufgebaut ist. Als zweites ein Codeblock, welcher aufgerufen wird, wenn der Lexer auf den Token trifft. Die Regel dieses Lexers enthält diesmal 5 Definitionen, wovon jedoch nur 3 tatsächlich Token sind. Die erste Definition ist ein Token, welcher ganze Zahlen repräsentiert. Eine ganze Zahl besteht aus mindestens einer Zahl. Im Codeblock wird der Text dieses Tokens via atoi in eine ganze Zahl umgewandelt und anschließend einer Variable, auf die Bison zugreifen kann, zugewiesen. Nun wird noch der Typ des Tokens zurückgegeben, damit Bison zwischen den Tokens unterscheiden kann. Die nächsten beiden Definitionen sind analog, nur dass diesmal kein Wert benötigt wird, da auf diese Tokens nur symbolisch in Bison zugegriffen wird. Die vorletzte Definition überspringt lediglich alle Tabs und Leerzeichen, da diese im Parsen nicht signifikant sind. (Das ist nicht in jeder Programmiersprache der Fall; so symbolisieren in der Programmiersprache Python die Leerzeichen, in welchem Block sich der Code befindet, ähnlich wie { und } in anderen Programmiersprachen wie C und Java). Die letzte Definition sammelt jedes andere Zeichen auf um eine Fehlermeldung auszuwerfen. Diese Definition hilft beim Debuggen des Lexers. Der dritte Block ist in diesem Fall leer; in ihm würde man zusätzliche Funktionen definieren, welche zu der erzeugten C-Datei hinzugefügt werden. Es ist möglich, dort eine Main-Funktion hinzuzufügen, falls man nur einen Lexer benötigt. In diesem Fall wird die Main-Funktion jedoch in der Bison-Datei definiert. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 %{ #include <stdio.h> int yyerror(const char ∗p) { fprintf(stderr, ”%s\n”, p); // print the error message } %} %union { int intval; }; %type <intval> expr %token <intval> NUM %token END %left ’+’ ’−’ %left ’∗’ ’/’ %start input %% Quelltext 2.9: Datei: calc.y (Block 1) Der Übersichtlichkeit halber wird die Bison-Datei aufgespalten. Im ersten Block (Quelltext 2.9) ist ähnlich wie bei der Lexer-Definition eine Aufzählung aller verwendeten Bibliotheken sowie einer Hilfsmethode yyerror in einem Codeblock angegeben. In Zeile 7-9 werden die Hilfsvariablen für den Lexer genannt. Anschließend werden noch die Typen, 21 Tokens und Operatoren definiert. Die Typendefinition bestimmt den Rückgabewert der Regeln. Via %left wird die Operatorenreihenfolge definiert. Die Reihenfolge der Regeln hier bestimmt auch, welche Operatoren zuerst ausgeführt werden, wenn der Parser ausgeführt wird. Da + und - zuerst angegeben sind, werden diese zuerst geparst und damit als letztes ausgeführt. Damit erreichen wir, dass Punkt- vor Strichrechnung evaluiert passiert. Mit %start wird bestimmt, welche Regel im Parser zuerst ausgeführt wird. 1 input: /∗ nil ∗/ | input line ; 2 3 line: END | expr END { 4 printf(”=> %d\n”, $1); 5 }; 6 7 expr: NUM { 8 $$ = $1; 9 } | expr ’+’ expr { 10 $$ = $1 + $3; 11 } | expr ’−’ expr { 12 $$ = $1 − $3; 13 } | expr ’∗’ expr { 14 $$ = $1 ∗ $3; 15 } | expr ’/’ expr { 16 $$ = $1 / $3; 17 } | ’−’ expr { 18 $$ = −$2; 19 } | ’(’ expr ’)’ { 20 $$ = $2; 21 }; Quelltext 2.10: Datei: calc.y (Block 2) Im zweiten Block im Quelltext 2.10 werden die Regeln der kontextfreien Grammatik angegeben. Die Codeblöcke hinter den Regeln werden ausgeführt, wenn der Parser die Regel parst. Da expr den Typ int besitzt, kann die Regel line am Ende das Ergebnis von expr als int via printf ausgeben. Es wäre auch möglich, mit den Codeblöcken einen AST anzulegen. 1 %% 2 int main() { 3 return yyparse(); 4 } Quelltext 2.11: Datei: calc.y (Block 3) Der letzte Block im Quelltext 2.11 ermöglicht es, ähnlich wie schon bei flex, Funktionen in C anzugeben. In diesem Fall soll die Main-Funktion angegeben werden, da der Parser bereits alles tut, was für das Beispielprogramm benötigt wird. Die Main-Funktion führt lediglich die yyparse() Funktion aus, welche den Parsevorgang startet, indem es von der Standardeingabe liest. Nun können die Bison-Datei und Flex-Datei mit dem Bison- und dem Flex-Befehl kompiliert werden. Dabei sollte beim Bison-Befehl die Zusatzoption -d angegeben werden, um zusätzlich einen Header anzulegen. Dieser Header ist für den 22 Lexer notwendig. Anschließend können die erzeugten C-Dateien mit einem C-Compiler wie gcc kompiliert werden. Die Main-Datei stammt dabei von der C-Datei von Bison. Wenn das kompilierte Programm ausgeführt wird, liest es von der Standardeingabe Ausdrücke, die der Parser dann auswertet. Dabei wird automatisch während des Parsevorgangs via den Codeblöcken der Ausdruck ausgewertet und via dem printf() Befehl in Zeile 4 vom Quelltext 2.10 ausgegeben. Durch die Definition der Regeln input und line können auch weitere Ausdrücke angegeben werden, die in der line-Regel ausgegeben werden. Im Quelltext 2.12 ist eine Beispielausführung des Programms zu sehen. 1 2 3 4 5 6 100 => 100 1+2∗2 => 5 (1+2)∗2 => 6 Quelltext 2.12: Ausführung des Beispielparsers 23 Kapitel 3 Temporale Tabellen Temporale Tabellen sind eine konkrete Realisierung der in 2.1.5 beschriebenen Zeitdimensionen. Sie unterscheiden sich von nicht temporalen Tabellen in einigen Punkten und erzeugen dadurch neue Probleme, die es zu lösen gilt. Einige Lösungen dieser Probleme sind mithilfe des Buchs [Sno00] entwickelt worden. In den folgenden Abschnitten werden Beispiele für die verschiedenen Arten von temporalen Tabellen vorgestellt. Die Basis stellt dabei das folgende Datenbankschema dar. ANG (Nr, Name, Gehalt) ARBEITET (Angestellter → ANG, Projekt → PROJEKT) PROJEKT (Name, Budget, Projektleiter → ANG) Nr 1 2 Name Max Bob Gehalt 2500 2600 Ang. 1 1 2 Prj. P1 P2 P2 Name P1 P2 Budget 100 200 PLeiter 1 2 Tabellen 3.1: Beispielausprägungen Die drei Relationen besitzen eine Beispielausprägung, die in den Tabellen 3.1 dargestellt wird. Dieses Schema besitzt dabei noch keine temporalen Tabellen; wir werden sie in den folgenden Abschnitten mit temporalen Attributen erweitern. Anschließend werden übliche zeitbezogene Anfragen ausgeführt und die Daten manipuliert. 3.1 Valid Time-Tabellen Im Kapitel 2.1.5 wurde die Valid Time als Konzept vorgestellt. Konkret im Datenbankmodell wird sie mit Zeitstempeln umgesetzt. Um eine Tabelle mit einer Valid TimeZeitdimension zu erweitern, fügen wir einen Zeitraum zu den Tupeln in der Datenbank hinzu. Dafür wird die Datenbank um zwei (Anfangszeitpunkt und Endzeitpunkt) oder, falls das Datenbanksystem einen Datentyp für Zeiträume besitzt, eine Spalte erweitert. Die Datenbank muss zusätzlich noch notieren, welche Intervallnotation die Zeiträume 24 benutzen (siehe Kapitel 2.1.3). Diese temporale Tabelle und auch die gesamte Datenbank benutzt die [)-Notation. Nun wird die Tabelle ARBEITET aus dem Beispielschema um Valid Time erweitert. Dafür wird eine Spalte hinzugefügt, welche den im Kapitel 2.2.2 vorgestellten Range-Datentyp verwendet. Ang. 1 1 2 Prj. P1 P2 P2 VT [2013-01-01,) [2013-01-01,) [2013-01-01,) Tabelle 3.2: Relation ARBEITET mit Valid Time In der Tabelle 3.2 ist nun die Beispielausprägung mit zusätzlicher Gültigkeitszeit versehen. Jeder Tupel ist seit 2013-01-01 gültig und der Endzeitpunkt ist leer, demnach gelten die Tupel für immer. Alle drei Tupel sind somit zum jetzigen Zeitpunkt gültig (vorausgesetzt, die Datenbank wird nach dem Zeitpunkt 2013-01-01 betrieben). Die Tabelle hat demzufolge keine zusätzlichen Informationen zu seinem vorherigen Zustand. Jetzt ist es jedoch möglich geworden, die Geschichte eines Mitarbeiters und seiner Projektzugehörigkeit aufzuzeichnen. Für diese Definition gibt es nun bestimmte Anforderungen, die ein gutes temporales Datenbanksystem erfüllen sollte, um dem User maximale Flexibilität im Umgang mit zeitbehafteten Daten zu geben. 3.1.1 Anforderungskatalog: Constraints Eine Anforderung die auch bei nicht-temporalen Tabellen existiert sind Constraints, welche die Ausprägungen der Tabellen einschränken; so existieren Bedingungen, welche garantieren, dass für ein eindeutig identifiziertes Objekt nicht mehrere Ausprägungen existieren oder das ein referenziertes Objekt via Fremdschlüsselbedingung existiert. Diese Constraints gelten in dieser Form für temporale Tabellen nicht mehr so wie vorher. Zuerst werden die Primärschlüssel betrachtet. Angenommen, vom Zeitraum 2012-01-01 bis 2013-01-01 sei der Mitarbeiter 1 im Projekt P1 tätig gewesen. Diese Daten würden jedoch das PRIMARY KEY-Constraint der nicht-temporalen Version verletzen, da nun der Schlüssel (1, P1) zweimal in der Ausprägung auftaucht. Das führt zum ersten Problem der temporalen Tabellen: Temporale Schlüssel. Temporaler Schlüssel In relationalen Datenbanken erlauben Primärschlüssel eine eindeutige Identifizierung eines Tupels über einen spezifizierten Wert. Dafür muss in den Objekten der Welt, die über die Datenbank festgehalten werden, eine Eigenschaft gefunden werden, die bei einer Instanz eines Objekts einzigartig ist. Diese Logik ist jedoch bei temporalen Tabellen nicht mehr anzuwenden. Dort gilt für eine Tabelle, dass zu einem Zeitpunkt nicht mehr als eine Ausprägung zu einem Objekt existieren darf. Der bestehende Primary Key-Constraint löst dieses Problem nicht, wie das zuvor genannte Beispiel zeigt. 25 Naiv wäre die Lösung, den Zeitraum als Primärschlüssel hinzuzufügen. Das würde jedoch das genannte Problem nicht lösen, sondern nur einige Spezialfälle ausschließen. So wäre, wenn nur der Zeitraum Teil des Schlüssels ist, die Ausprägung in Tabelle 3.3 erlaubt. Ang. 1 1 1 2 Prj. P1 P1 P2 P2 VT [2013-01-01,) [2013-06-01,2014-01-01) [2013-01-01,) [2013-01-01,) Tabelle 3.3: Ausprägung, die den temporalen Schlüssel verletzt Jedoch gibt es zu dem Zeitraum [2013-06-01, 2014-01-01) für den Primärschlüssel (1, P1) zwei Tupel, was den temporalen Schlüssel verletzt. Demnach gilt, dass eine Operation, welche die obige Ausprägung erzeugen würde, abgelehnt werden muss. Es muss demnach bei jeder Datenbankoperation überprüft werden, ob bereits ein Datensatz mit dem gleichen nicht-temporalen Schlüssel existiert, wo sich die Zeitpunkte schneiden. Wenn dies der Fall ist, muss die Operation abgelehnt werden, da sie sonst die Integrität der temporalen Tabelle verletzen würde. Eine Möglichkeit, dieses Constraint zu erzwingen ist via DBMS-Mitteln wie Trigger und Assertions. Temporale referentielle Integrität Nun soll zusätzlich zu der ARBEITET-Relation die Geschichte der Angestellten aufgezeichnet werden, da die Gehaltsänderungen verfolgt werden sollen. Dabei passiert dasselbe wie zuvor bei der ARBEITET-Relation. Das Resultat ist in der Tabelle 3.4 zu sehen. Nr 1 2 Name Max Bob Gehalt 2500 2600 VT [2013-01-01,) [2013-01-01,) Tabelle 3.4: Relation ANGESTELLTE mit Valid Time Nun soll zur Tabelle ARBEITET noch etwas Historie hinzugefügt werden. So hat der Mitarbeiter 2 von 2012-01-01 bis 2013-01-01 beim Projekt P1 gearbeitet. Danach wurde er jedoch aus dem P1 entlassen und hat das Projekt P2 zugewiesen bekommen. Die Tabelle 3.5 zeigt diese temporalen Daten. Dadurch entsteht jedoch ein neues Problem; im dritten Tupel der ARBEITETRelation wird der Angestellte 2 zum Zeitraum [2012-01-01, 2013-01-01) referenziert. Dieser Angestellte hat aber laut der Datenbank zu diesem Zeitpunkt gar nicht existiert! Das führt zum nächsten Problem im Entwurf der Datenbank: Temporale referentielle Integrität. Demnach muss eine Überprüfung stattfinden, dass zu jedem Zeitpunkt, in dem eine fremde Tabelle referenziert wird, auch tatsächlich ein Objekt mit dem entsprechenden 26 Ang. 1 1 2 2 Prj. P1 P2 P1 P2 VT [2013-01-01,) [2013-01-01,) [2012-01-01,2013-01-01) [2013-01-01,) Tabelle 3.5: Mitarbeiter 2 wurde versetzt Primärschlüsselattribut existiert. Das heißt, dass bei jeder Veränderung der referenzierten Tabelle sowie der referenzierenden Tabelle eine Überprüfung stattfinden muss. Falls die veränderten Daten dieser Bedingung nicht entsprechen, so muss die Operation, wie bei dem temporalen Schlüssel, abgelehnt werden. Eine solche Überprüfung kann wie der temporale Primärschlüssel durch einen Trigger geklärt werden. Wie diese Trigger programmiert werden und in einem dynamischen Kontext funktionieren, wird im Kapitel 5 gezeigt. Temporale Vollständigkeit Als drittes Constraint auf temporalen Tabellen existiert die Vollständigkeit. Sie garantiert, dass ein Objekt über die Zeit hinweg niemals Lücken enthält, demnach zwischen dem ersten Anfangszeitpunkt und dem letzten Endzeitpunkt kein Zeitpunkt existiert, der nicht in einem der Tupel des Objektes enthalten ist. So würde die folgende Ausprägung der Tabelle 3.6 ANGESTELLTE den Constraint verletzen. Nr 1 2 2 Name Max Bob Bob Gehalt 2500 2600 2800 VT [2013-01-01,) [2013-01-01,2014-01-01) [2014-06-01,) Tabelle 3.6: Relation ANGESTELLTE mit Valid Time Bob ist definiert vom 2013-01-01 bis zum Ende der Zeit, jedoch existiert eine Lücke zwischen dem 2014-01-01 bis 2014-06-01. Eine gültige korrigierte Ausprägung der ANGESTELLTE-Relation ist in Tabelle 3.12 zu sehen. Dort ist die Lücke gefüllt. Nr 1 2 2 2 Name Max Bob Bob Bob Gehalt 2500 2600 2700 2800 VT [2013-01-01,) [2013-01-01,2014-01-01) [2014-01-01,2014-06-01) [2014-06-01,) Tabelle 3.7: Relation ANGESTELLTE mit Valid Time 27 Coalesce Als letzte mögliche Integritätsbedingung der temporalen Tabellen wird das Coalescing betrachtet. Falls zeitlich benachbarte Daten die gleichen Tupelwerte besitzen, sollen sie zu einem Tupel verschmolzen werden, der den gesamten Zeitraum umfasst. Falls demnach nun die Ausprägung 3.8 existiert, wird sie automatisch zu der Ausprägung 3.9 transformiert. Nr 1 2 2 2 Name Max Bob Bob Bob Gehalt 2500 2600 2600 2600 VT [2013-01-01,) [2013-01-01,2014-01-01) [2014-01-01,2014-06-01) [2014-06-01,) Tabelle 3.8: Relation ANGESTELLTE mit Valid Time Nr 1 2 Name Max Bob Gehalt 2500 2600 VT [2013-01-01,) [2013-01-01) Tabelle 3.9: Relation ANGESTELLTE mit Valid Time 3.1.2 Anforderungskatalog: Anfragen Neben den besprochenen Unterschieden im Entwurf einer Valid Time-Tabelle und den Constraints gibt es auch Unterschiede in den Anfragen. Da Anfragen an nicht temporale Tabellen immer von einem jetzt gültigen Zustand ausgehen, ist es nicht möglich, die alten Anfragen zu übernehmen. In diesem Kapitel soll anhand von Beispielen repräsentiert werden, welche Arten von Anfragen existieren und wie sie sich von den nicht-temporalen Varianten unterscheiden. Temporale Schnappschussanfrage Als Anfang sei die Anfrage aus dem Quelltext 3.1 gegeben. 1 SELECT ∗ 2 FROM ang; Quelltext 3.1: Nicht temporale Anfrage an ANG Zuvor hat die Anfrage lediglich die beiden Mitarbeiter mit ihrem jetzigen Gehalt ausgegeben. Nach der Transformation in eine Valid Time-Tabelle ist nun die Ausgabe nicht mehr wie gewünscht der jetzige Zustand, sondern die Vergangenheit des Mitarbeiters Max ist zusätzlich mit angegeben. Wenn eine Anfrage für den jetzigen Zustand gewünscht ist, so muss dies zusätzlich angegeben werden. 28 1 SELECT Nr, Name, Gehalt 2 FROM ang a1 3 WHERE a1.vt <@ now(); Quelltext 3.2: Anfrage für momentane Angestellte Besonders ist im Quelltext 3.2 auf die Zeile 3 zu achten, da der <@-Operator verwendet wird, welcher überprüft, ob ein Element in einem gegebenen Intervall vorhanden ist. Wenn der jetzige Zeitpunkt im Zeitraum VT existiert, ist der Tupel jetzt gültig und damit bei einer Schnappschussanfrage auszugeben. Temporaler Join Bei einem temporalen Join wird, im Unterschied zu einem gewöhnlichen Join, die Zeit eines jeden Objekts berücksichtigt. So gilt, wenn zwei Tupel aus temporalen Tabellen durch die gegebene Join-Bedingung in Beziehung stehen, dass zusätzlich beachtet werden muss, ob beide einen gemeinsamen Zeitraum haben und wenn, wie dieser aussieht. Als Beispiel wird der natürliche Verbund zwischen ARBEITET und ANG betrachtet. Angenommen, Bob habe 2014 eine Gehaltserhöhung erhalten (siehe Tabelle 3.10). Nr 1 1 2 2 Name Max Max Bob Bob Gehalt 2000 2500 2600 3000 VT [2010-01-01,2013-01-01) [2013-01-01,) [2010-01-01,2014-01-01) [2014-01-01,) Tabelle 3.10: Zweite Gehaltserhöhung von Max Beim temporalen Verbund von ARBEITET und ANG würde dadurch der Tupel, der Bob dem Projekt 2 zuweist, in zwei Tupel aufgeteilt werden: zunächst ein Tupel, der im Zeitraum [2013-01-01, 2014-01-01) gilt und Bob mit dem Gehalt 2600 notiert und ein Tupel im Zeitraum [2014-01-01, FOREVER), der Bob mit einem Gehalt von 3000 notiert. Diese Aufspaltung ist in diesem Fall nötig, da für die Zeiträume verschiedene Daten durch den Verbund entstanden sind. Für den Zeitraum [2010-01-01, 2013-01-01) existieren in der resultierenden Tabelle keine Einträge, da es in diesen Zeitpunkten keine Zuweisung von Angestellten zu Projekten gab. Nr 1 1 2 2 2 Name Max Max Bob Bob Bob Gehalt 2500 2500 2600 2600 3000 Prj. P1 P2 P1 P2 P2 VT [2013-01-01,) [2013-01-01,) [2012-01-01,2013-01-01) [2013-01-01,2014-01-01) [2014-01-01,) Tabelle 3.11: Temporaler Join zwischen ANG und ARBEITET 29 Die dazugehörige Anfrage ist im Quelltext 3.3 zu sehen. 1 SELECT a1.nr, a1.name, a1.gehalt, a2.projekt, a1.vt ∗ a2.vt 2 FROM ang a1, arbeitet a2 3 WHERE a1.vt && a2.vt Quelltext 3.3: Temporaler Join Not Exists Zusätzlich zum Join existiert der Anti-Join, der mit Hilfe eines NOT EXISTS in der Where-Klausel dargestellt wird. Auch da muss die Gültigkeitszeit beachtet werden. Als Beispiel sei nach allen Projektnamen gefragt, in denen Bob nicht mitarbeitet. Der naive Ansatz wäre ein SQL-Befehl wie im Quelltext 3.4. 1 2 3 4 5 6 7 SELECT ar.projekt FROM arbeitet ar WHERE NOT EXISTS ( SELECT ∗ FROM ang an WHERE an.nr = ar.angestellter AND an.name = ’Bob’ ) Quelltext 3.4: Nicht temporales NOT EXIST Die Ausgabe dieser Anfrage wäre jedoch leer, was nicht dem temporalen Sinn entspricht. Tatsächlich wäre die Ausgabe mit der Gültigkeitszeit betrachtet wie in der Tabelle 3.12 beschrieben, da Bob im Projekt P1 seit 2013 nicht mehr mitwirkt. Die Anfrage funktioniert analog zum temporalen Join mit vier Fällen, die vereinigt werden. Prj. P1 VT [2013-01-01,) Tabelle 3.12: Projekte, in denen Bob nicht mitwirkt. Eine Umsetzung des NOT EXISTS, welche keine längliche UNION-Verkettung benötigt, ist im Kapitel 5.3 zu finden. Dort werden Funktionen benutzt, die zuvor in der Implementierung definiert werden. 3.1.3 Anforderungskatalog: Modifikation Zusätzlich ändern sich auch DML-Anweisungen bei temporalen Tabellen, da das traditionelle DELETE, UPDATE und INSERT auf Tupeln basiert und die Zeitstempel nicht beachtet. So bestehen die einzelnen Befehle unter Umständen aus mehreren Schritten. Es lassen sich weiterhin die üblichen DML-Anweisungen ausführen, jedoch sind diese gänzlich losgelöst von der Zeit; sie arbeiten tupelbasierend. Dadurch entstehen Ergebnisse, welche unter Umständen nicht gewünscht sind. Es werden nun zeitbehaftete Modifikationen betrachtet, welche zu den üblichen Angaben noch eine Zeitangabe besitzen, für die sie angewendet werden sollen. 30 Temporales Delete Das temporale Löschen löscht nicht in jedem Fall Tupel, häufig verändert es nur abhängig vom angegebenen Zeitrahmen die Zeitattribute. Die unterschiedlichen Möglichkeiten, die bei einem temporalen Löschvorgang auftreten können, umfassen unter Umständen mehrere traditionelle DML-Vorgänge. Es existieren vier mögliche Fälle, wie der Zeitraum für den Löschvorgang und der Zeitraum des betroffenen Tupels im Verhältnis zueinander stehen können. Diese vier Fälle müssen mitsamt Beispielen betrachtet werden, um später bei der Implementierung die einzelnen Schritte untersuchen zu können. Fall 1 Falls der angegebene Zeitraum einen betroffenen Tupel vollständig umfasst, gilt es, den Tupel zu löschen. Das ist der einfachste der vier Fälle; in diesem Fall wird die Operation ausgeführt, als sei der Löschzeitraum gar nicht angegeben. Nr 1 1 2 2 Name Max Max Bob Bob Gehalt 2000 2500 2600 3000 VT [2010-01-01,2013-01-01) [2013-01-01,) [2010-01-01,2014-01-01) [2014-01-01,) Tabelle 3.13: Ausgangsausprägung für das temporale Löschen Angenommen es existiert die Ausprägung 3.13 und es sollen temporal alle Angestellten gelöscht werden, die Bob heißen und 2010-01-01 bis 2014-01-01 existieren. Dann würde der dritte Tupel entfernt werden und die resultierende Ausprägung wäre die Tabelle 3.14. Nr 1 1 2 Name Max Max Bob Gehalt 2000 2500 3000 VT [2010-01-01,2013-01-01) [2013-01-01,) [2014-01-01,) Tabelle 3.14: Bob im Zeitraum [2010-01-01,2014-01-01) wird gelöscht Fall 2 Im Fall, dass der Zeitraum vor dem Anfangszeitpunkt des Tupels beginnt, jedoch vor dem Endzeitpunkt des Tupels endet, wird der Tupel verkürzt. Das führt dazu, dass kein Tupel tatsächlich gelöscht wird; es wird nur einer verändert. Die DELETE-Operation ist in diesem temporalen Kontext demnach ein UPDATE. Sei die Ausprägung 3.14 vom ersten Fall gegeben. Wenn nun alle Angestellten mit dem Namen Max für den Zeitraum [2010-01-01,2011-01-01) gelöscht werden sollen, so würde das erste Tupel eine Änderung erfahren, alle anderen Tupel wären unberührt und die Anzahl der Tupel würde sich nicht ändern. Die Änderung ist dabei, dass der Anfangszeitpunkt des VT-Attributs geändert wird. Das Ergebnis ist in der Tabelle 3.15 zu sehen. 31 Nr 1 1 2 Name Max Max Bob Gehalt 2000 2500 3000 VT [2011-01-01,2013-01-01) [2013-01-01,) [2014-01-01,) Tabelle 3.15: Max im Zeitraum [2010-01-01,2011-01-01) wird gelöscht Fall 3 Analog gilt dasselbe für den umgekehrten Fall. Schneidet der betroffene Tupel den Löschzeitraum auf dem rechten Rand, muss der Endzeitpunkt von VT verändert werden. Beispiel für eine solche Operation sei das Löschen von Max im Zeitraum [201201-01,2013-01-01) (vergleiche Ausgabetabelle 3.16). Nr 1 1 2 Name Max Max Bob Gehalt 2000 2500 3000 VT [2011-01-01,2012-01-01) [2013-01-01,) [2014-01-01,) Tabelle 3.16: Max im Zeitraum [2012-01-01,2013-01-01) wird gelöscht Fall 4 Falls jedoch der Zeitraum innerhalb der Gültigkeitszeit des Tupels liegt, entsteht eine Lücke. Es entstehen zwei neue Objekte. Demnach sind drei Schritte nötig, um den temporalen Schlüssel nicht zu verletzten. Im ersten Schritt wird der betroffene Tupel kopiert zwischengespeichert. Im zweiten Schritt wird der betroffene Tupel verändert, indem der Endzeitpunkt von VT auf den Anfangszeitpunkt des zu löschenden Zeitraums gesetzt wird. Als letzter Schritt wird der zwischengespeicherte Tupel eingefügt, wobei jedoch der Anfangszeitpunkt auf den Endzeitpunkt des Löschzeitraums gesetzt wird. Nr 1 1 2 Name Max Max Bob Gehalt 2000 2500 3000 VT [2011-01-01,2012-01-01) [2013-01-01,2014-01-01) [2014-01-01,) Tabelle 3.17: Max im Zeitraum [2014-01-01,2015-01-01) wird gelöscht (Schritt 2) Gegeben sei die Ausprägung 3.16 des vorherigen Falls. Falls nun erneut Max gelöscht werden soll, aber diesmal im Zeitraum [2014-01-01,2015-01-01), so tritt der besprochene Fall ein. Nach dem ersten Schritt wird die VT vom zweiten Tupel zwischengespeichert, um sie später einsetzen zu können. Anschließend wird der nächste Schritt ausgeführt, um die Ausprägung in Form der Tabelle 3.17 zu erhalten. Dann wird der letzte Schritt eingeführt und dadurch ein neuer Tupel eingefügt um die Ausprägung 3.18 zu erhalten. 32 Nr 1 1 1 2 Name Max Max Max Bob Gehalt 2000 2500 2500 3000 VT [2011-01-01,2012-01-01) [2013-01-01,2014-01-01) [2015-01-01,) [2014-01-01,) Tabelle 3.18: Max im Zeitraum [2012-01-01,2013-01-01) wird gelöscht (Schritt 3) Temporales Update Das temporale Update hat wie das temporale Delete vier Fälle. Diese vier Fälle haben die gleichen Bedingungen wie beim temporalen Delete, die einzelnen Schritte sind jedoch unterschiedlich. Fall 1 Falls der Updatezeitraum den Zeitraum eines betroffenen Tupels komplett umfasst, wird der Tupel aktualisiert ohne weitere Tupel hinzuzufügen. Fall 2 & Fall 3 Im zweiten und dritten Fall sind wie beim temporalen Delete die betroffenen Tupel entweder von vorne oder von hinten geschnitten und der Updatezeitrahmen liegt nicht zwischen dem Anfangs- und Endzeitpunkt der Gültigkeitszeit des Tupels. Im Gegensatz zum temporalen Löschen müssen aber nun zwei Schritte erfolgen; zunächst muss der Tupel zwischengespeichert werden und im betroffenen Tupel der Zeitraum auf den Updatezeitraum gesetzt werden. Danach muss der zwischengespeicherte unveränderte Tupel wieder eingefügt werden, jedoch so, dass der Zeitraum passt. Das ist vom Fall abhängig. Im zweiten Fall muss der eingefügte Tupel den Anfangszeitpunkt auf den Endzeitpunkt des Updatezeitrahmens setzen. Im dritten Fall wird jedoch der Endzeitpunkt auf den Anfangszeitpunkt des Updatezeitrahmens gesetzt. Gegeben sei die vorherige Ausprägung 3.18. Nun wird Bobs Gehalt für den Zeitraum von [2014-01-01,2015-01-01) auf 3400 gesetzt. Dadurch entstehen im ersten Schritt die Ausprägung 3.19 und im zweiten Schritt nach dem Einfügen des zwischengespeicherten unveränderten Tupels die Ausprägung 3.20. Zu sehen ist demnach, dass sich die Anzahl der Tupel durch das Update um eins erhöht hat. Nr 1 1 1 2 Name Max Max Max Bob Gehalt 2000 2500 2500 3400 VT [2011-01-01,2012-01-01) [2013-01-01,2014-01-01) [2015-01-01,) [2014-01-01,2015-01-01) Tabelle 3.19: Update Gehalt von Bob auf 3400 im Zeitraum [2014-01-01,2015-01-01) (Schritt 1) 33 Nr 1 1 1 2 2 Name Max Max Max Bob Bob Gehalt 2000 2500 2500 3400 3000 VT [2011-01-01,2012-01-01) [2013-01-01,2014-01-01) [2015-01-01,) [2014-01-01,2015-01-01) [2015-01-01,) Tabelle 3.20: Update Gehalt von Bob auf 3400 im Zeitraum [2014-01-01,2015-01-01) (Schritt 2) Fall 4 Im vierten Fall werden wie beim temporalen Delete mehrere Schritte benötigt. Jedoch wird diesmal noch ein Schritt mehr gebraucht, da der zwischengespeicherte Tupel zweimal eingefügt werden muss, nachdem der betroffene Tupel via Update verändert wurde. Die Schritte sind demnach analog zu den vorherigen Fällen, der zweite Schritt wird jedoch zweimal ausgeführt; jeweils einmal für den Zeitraum vor dem angegebenen Updatezeitraum und ein weiteres Mal für den danach. Temporales Insert Ein temporales Insert besitzt keine Fallunterscheidung wie das temporale Update oder Delete; es unterscheidet sich von einem üblichen Insert nur in der Tatsache, dass der eingefügte Datensatz noch zeitliche Informationen in Form der Gültigkeitszeit beinhaltet. Dabei muss beachtet werden, dass trotzdem die in den vorherigen Kapiteln definierten Integritätsbedingungen gelten. Falls temporale Schlüssel verlangt sind, kann kein Datensatz mit identischen zeitinvarianten Schlüsseln eingefügt werden, falls sich die Zeiträume schneiden. Falls temporale Vollständigkeit verlangt ist, muss der eingefügte Datensatz entweder komplett neu sein oder einem bestehenden Datensatz folgen. Nach der temporalen Einfügeoperation kann auch ein automatisches COALESCED geschehen. Dadurch ist es möglich, einen bestehenden Datensatz mithilfe eines temporalen INSERT zu verlängern und damit auf das UPDATE zu verzichten. Es existieren keine Fallunterscheidungen, da beim temporalen INSERT keine bestehenden Tupel betroffen sein können. Entweder die Einfügeoperation war erfolgreich und damit wurde ein weiterer Tupel hinzugefügt (außer COALESCE gilt und ein bestehender Tupel wurde verlängert) oder eine der Integritätsbedingungen wurde verletzt und damit war die INSERT-Operation nicht erfolgreich und wird einfach abgelehnt. 3.2 Transaction Time-Tabellen Transaction Time-Tabellen werden ähnlich wie Valid Time-Tabellen ausgezeichnet. Dabei besitzt jeder Tupel einen Zeitraum oder Zeitstempel. Doch im Gegensatz zu den Valid Time-Tabellen sollen diese nicht manuell durch DML-Anweisungen verändert werden. Sie sollen lediglich dazu dienen, die Geschichte der Datenbankausprägung zu repräsentieren. 34 So gilt es, dass die Tabelle die Zustände der Tupel vor der Modifikation nicht vergisst. Es soll möglich sein, den Stand der Welt zu einem bestimmten Zeitpunkt zu rekonstruieren. Es sind zwei Möglichkeiten genannt worden, Transaction Time-Tabellen auszuzeichnen. Zunächst die Methode mit Zeitstempeln, die den Zeitpunkt aufzeichnet, ab dem ein Datensatz gültig ist. Falls ein Tupel existiert, der das gleiche Objekt beschreibt, jedoch einen neueren Zeitstempel hat, so gilt stets der neueste als aktuellster Datensatz. Ein Beispiel für diese Variante ist in der Tabelle 3.21 zu sehen. Nr 1 2 2 Name Max Bobb Bob Gehalt 2500 2600 2600 TT 2013-01-01 12:00 2013-01-01 12:00 2014-01-01 12:00 Tabellen 3.21: Transaction Time-Tabelle mit Zeitstempeln So ist zu sehen, dass der Name von Bob zunächst falsch geschrieben wurde. Dieser Fehler wurde 2014 korrigiert. Dabei muss bei der Tabellendefinition beachtet werden, dass Nr nicht als Primärschlüssel ausreicht. TT muss als Schlüssel hinzugefügt werden, damit die obigen Daten die Bedingungen nicht verletzen. Das Problem mit diesem Ansatz ist, dass Löschoperationen nicht ohne Probleme aufgezeichnet werden können. Falls sich herausstellt, dass ein Objekt fälschlicherweise hinzugefügt wurde, so gibt es keine Möglichkeit, einen Zeitstempel anzusetzen, der Löschoperationen beschreibt. Diese Variante ist demnach nur sinnvoll, wenn lediglich UPDATE-Operationen an Daten aufgezeichnet werden sollen. Eine vollständigere Methode ist es, ähnlich wie bei den Valid Time-Tabellen, Zeiträume zu verwenden. Dadurch ist es möglich, Lücken darzustellen und die tatsächliche Ausprägungsvergangenheit der Tabellen zu beschreiben. Nr 1 2 2 3 Name Max Bobb Bob Maxima Gehalt 2500 2600 2600 2100 TT [2013-01-01 12:00,) [2013-01-01 12:00,2014-01-01 12:00) [2014-01-01 12:00,) [2012-01-01 12:00,2013-01-01 12:00) Tabellen 3.22: Transaction Time-Tabelle mit Zeiträumen Transaction Time-Tabellen werden mithilfe von Triggern auf den Tabellen realisiert. So werden bei einer UPDATE-Operation die alten Datensätze, die betroffen sind, zunächst kopiert und der Endzeitpunkt des Attributes TT des alten Tupels auf den jetzigen Zeitpunkt und der Startzeitpunkt von TT des neuen Tupels ebenfalls auf den jetzigen Zeitpunkt gesetzt. Dann wird die UPDATE-Operation dementsprechend auf die kopierten Tupel durchgeführt, um die Operation zu beenden. Bei Löschoperationen werden alle betroffenen Tupel als beendet gekennzeichnet, indem der Endzeitpunkt von TT auf den jetzigen Zeitpunkt gesetzt wird. 35 Anfragen auf Transaction Time-Tabellen sind dabei relativ einfach, da Dinge wie Coalesing und temporaler NOT EXIST keine Rolle spielen. Gefragt wird meist, was die aktuelle oder vergangene Ausprägung der Datenbank ist. Das funktioniert ähnlich wie schon bei den Valid Time-Tabellen, wenn dort nach den jetzt gültigen Objekten gefragt wurde. Interessant ist auch die Geschichte der Datensätze. Eine Anfrage an die Tabelle sortiert, nach den Zeiträumen erfüllt, diesen Zweck. 3.3 Bitemporale Tabellen Bitemporale Tabellen repräsentieren eine Kombination aus beiden Zeitdimensionen mithilfe der beschriebenen Methoden. Wenn davon ausgegangen wird, dass die Transaction Time mithilfe von Zeiträumen dargestellt wird, so gilt, dass jeder Tupel nun zwei Zeiträume enthält. Einer für die Transaktionszeit, der andere für die Gültigkeitszeit. So besitzen die einzelnen Objekte nun zwei Dimensionen. Dadurch zeichnet die Transaction Time noch zusätzlich Änderungen in den Gültigkeiten auf. Nr 1 2 2 2 Name Max Bob Bob Bob Gehalt 2500 2600 2600 2700 TT [2013-01-01,) [2013-01-01,2013-06-01) [2013-06-01,) [2013-06-01,) VTE [2013-01-01,) [2013-01-01,) [2013-01-01,2014-01-01) [2014-01-01,) Tabelle 3.23: Bitemporale Tabelle In der Tabelle 3.23 ist die Geschichte von Bobs Gehalt aufgezeichnet. So wurde 2013-06-01 in die Tabelle eine Änderung für Bobs Gehalt eingetragen. 2014 verdient Bob nun 2700. Dadurch wurde der alte Zustand mithilfe der Transaktionszeit gespeichert und der neue Zustand mit den neuen Gültigkeitszeiten eingetragen. 3.4 Herausforderungen & Probleme Der Betrieb temporaler Datenbanken erfordert spezielle Kenntnisse und Vorkehrungen, die wiederum zu unbekannten Herausforderungen und Problemen im Betrieb führen können. 1. Bestehende Anfragen haben nicht mehr die gleiche Semantik wie zuvor. Durch die Änderung des Tabellenaufbaus sind neue Anfragen vonnöten, um die Bedeutung wie vorher zu erreichen. Wenn ein SQL-Befehl zuvor den aktuellen Zustand der Daten ausgegeben hat, so ist nun zusätzlich die Geschichte enthalten. 2. Temporale Anfragen sind komplexer und größer. Dadurch, dass die Struktur der Tabellen sich verändert hat und Objekte über mehrere Tupel hinweg reichen, sind neue Anfragen anders und häufig verschachtelt zu gestalten. So gilt beispielsweise 36 beim temporalen Join, dass mehrere Fälle abgedeckt werden müssen und anschließend mit einer Vereinigung gearbeitet werden muss, um das temporal logische Ergebnis zu bekommen. 3. Es entsteht neuer Aufwand beim Einrichten der Datenbank, da bestimmte Vorgänge nun von der Standardimplementierung des DBMS abweichen. So gilt, dass es neue Integritätsbedingungen gibt und bestehende nun anders implementiert werden müssen. Das heißt, dass für jede bestehende Tabelle neue Operationen geschrieben und angewendet werden müssen. 4. Der Betrieb der Datenbank erfordert neue Methoden für die Änderungen der Daten. Bisherige DML-Anweisungen sind nicht mehr direkt anwendbar und müssen unter Umständen durch mehrere andere Anweisungen ersetzt werden. 5. Die Punkte 1-4 führen direkt dazu, dass neue Mitarbeiter nicht mehr mühelos an eine bestehende Datenbank herangeführt werden können. So gilt, dass neue Anfragen komplexer sind und der Betrieb anders funktioniert als bei konventionellen Datenbanken. Dies führt zu Mehraufwand bei der Einführung neuer Mitarbeiter in ein bestehendes Datenbankprojekt. Diese neu entstandenen Probleme sollen innerhalb dieser Ausarbeitung gelöst werden. Der Betrieb der Datenbank lässt sich durch das Einführen generischer Methoden als Teil einer Erweiterung von PostgreSQL lösen. Die gestiegene Komplexität der Anfragen und Verwaltung wird durch eine Erweiterung von SQL gelöst. Dafür werden zunächst bekannte temporale Datenbanksprachen betrachtet, um aus bestehenden Fehlern zu lernen und bestehende Elemente zu übernehmen. 37 Kapitel 4 Temporale Datenbanksprachen Da im Rahmen der Ausarbeitung eine temporale Erweiterung von SQL entwickelt wird, ist es hilfreich, eine Analyse der bestehenden temporalen Datenbanksprachen zu entwickeln. Dabei werden TSQL2 von Snodgrass, die neuen temporalen Idiome vom SQL Standard SQL:2011 sowie VTSQL2+ betrachtet. Ziel ist es, effiziente Problemlösungen zu entdecken und bekannte Probleme bei der Entwicklung der eigenen Spracherweiterung zu vermeiden. 4.1 TSQL2 TSQL2 wurde definiert in [Sno95] und ist eine Erweiterung von SQL-92. Die Sprache wurde entwickelt, nachdem Snodgrass 1992 vorgeschlagen hat, temporale Elemente in SQL einzufügen. Im selben Jahr wurde ein Komitee gegründet, welches die Sprache gestalten sollte. 1994 wurde die TSQL2 Language Specification [SAA+ 94a] veröffentlicht. TSQL2 behandelt dabei sowohl Valid Time-, Transaction Time- als auch bitemporale Tabellen. Im Standard [Sno95] ist die Unterstützung der Allen-Relationships angegeben. Alle Tabellen bei TSQL2 werden implizit als Coalesced angenommen; es ist nicht möglich, eine Tabelle ohne Coalescing anzulegen oder gar eine Anfrage zu gestalten, in der das Ergebnis nicht automatisch Coalesced ist. Im Folgenden wird ein Auszug aus der Sprache in Beispielen erläutert. Dabei soll nicht die genaue Sprachdefinition betrachtet werden, da dies den Rahmen sprengen würde, sondern lediglich ein Szenario mit temporalen Daten durchgespielt werden. Das Beispielszenario wird dabei dem Artikel [SAA+94b] entnommen. Beispiele Das Beispiel beschreibt einen Medienplan, das heißt einen Werbeplan für US TV-Events. Dieser Plan beschreibt, welche Werbung zu einem gegebenen Zeitpunkt in einem TVEvent gezeigt werden soll. Beispiele für solche Events sind berühmte Serien wie Roseanne oder Sportevents wie der Superbowl1 . 1 Der Superbowl ist das Finale der amerikanischen Football Liga. Es ist das weltweit größte Sportevent mit über 100 Millionen Zuschauern. Werbeplätze in diesem Ereignis sind heiß begehrt. 38 Zunächst wird eine Tabelle angelegt, welche die Kosten für Werbeverträge der verschiedenen TV-Events angibt. Dafür eignet sich eine Valid Time-Tabelle, die angibt, welche Angebote für den angegeben Zeitraum gültig sind. 1 CREATE TABLE NBCShows 2 (ShowName CHARACTER ( 30 ) NOT NULL, 3 InsertionLength INTERVAL SECOND, 4 Cost INTEGER) 5 AS VALID STATE YEAR ( 2 ) TO NDCSeason ; Quelltext 4.1: TSQL2-Statement für Valid Time-Tabelle Der Quelltext 4.1 ist ein TSQL2-Statement, welches die Valid Time-Tabelle erstellt. Es besteht aus drei Feldern, dem Namen der Show2 , der Werbelänge und den Kosten in USD. Die Werbelänge ist dabei als INTERVAL-Datentyp mit der Granularität von Sekunden gespeichert. Worin sich das Statement jedoch von einem üblichen SQL92Statement zum Erstellen einer Tabelle unterscheidet, ist die AS-Klausel in Zeile 5, welche die Tabelle als Valid Time definiert. Bei der Definition müssen zwei Optionen angegeben werden, zunächst die Range, welche die maximale Zeit angibt, die repräsentiert werden kann, und die Granularität. In diesem Fall ist Range YEAR ( 2 ), das heißt 102 = 100 Jahre. Als Granularität wird ein eigener Kalendertyp angegeben, TSQL2 erlaubt solche Definitionen. Es handelt sich dabei um die Season, in der Shows ausgestrahlt werden. Im Fernsehbusiness ist es üblich, Events über eine Season auszustrahlen und sein Programm in einer Season zu definieren, deswegen bietet sich hier der eigene Datentyp an. Nun verwenden wir ein INSERT Statement, um Daten in die Valid Time-Tabelle einzufügen. 1 INSERT INTO NBCShows 2 VALUES (’Roseanne’, 3 INTERVAL ’30’ SECOND, 4 251000) 5 VALID TIMESTAMP ’Spring Season 1994’ ; Quelltext 4.2: TSQL2 Insert Statement Quelltext 4.2 unterscheidet sich von einem normalen INSERT-Statement, da es zusätzlich das VALID TIMESTAMP-Keyword beinhaltet. Mittels des Keywords wird angegeben, in welchem Zeitraum der Eintrag gilt. In diesem Beispiel wird der selbst definierte Kalender verwendet, um eine für die Domäne passende Zeitangabe anzugeben. Für den Medienplan wird jedoch noch eine Tabelle benötigt, die den Plan definiert, welche Werbespots in welchem Event laufen. 2 Feldname dem Beispiel entnommen, in der Tabelle werden jedoch auch Sportevents gespeichert. 39 1 CREATE TABLE NBC FB Insertion 2 (GameName CHARACTER ( 30 ), 3 InsertionWindow INTERVAL FootballSegment, 4 InsertionLength INTERVAL SECOND ( 3, 0 ), 5 CommercialID CHARACTER ( 30 ) ) 6 AS VALID EVENT YEAR ( 2 ) TO HOUR 7 AND TRANSACTION ; Quelltext 4.3: TSQL2 Valid Time- und Transaction Time-Definition Der Quelltext 4.3 definiert eine Tabelle für den Einkauf von Werbeplätzen in einem Football-Event. Sie besteht aus vier Feldern, dem Spielnamen, in welchem Segment des Football-Spiels die Werbung ausgestrahlt wird, wie lang diese ist und schlussendlich den Namen der Werbung. Zusätzlich wird, wie in Quelltext 4.1, die Tabelle als Valid Time mit einer Range von 102 = 100 Jahren definiert, jedoch diesmal mit einer Granularität von Stunden. Es existieren dennoch zwei Unterschiede zur anderen Tabelle. Erstens wird sie als VALID EVENT definiert, was bedeutet, dass Valid Time nicht mit einem Intervall, sondern mit einem Zeitpunkt, in diesem Fall einer genauen Stunde, definiert ist. Zweitens soll sie zudem die Transaction Time dokumentieren. Die Tabelle ist demnach bitemporal. Einfügen geschieht analog. 1 SELECT N.GameName 2 FROM NBC FB Insertion AS N N2 3 WHERE N.GameName = N2.GameName AND 4 N.CommercialID <> N2.CommercialID Quelltext 4.4: TSQL2 List of football games broadcast by NBC that have two or more commercials Als nächstes kommen Beispielanfragen auf diese Tabellen. Die erste Anfrage im Quelltext 4.4 gibt zunächst alle Football-Events von NBC zurück, welche zwei oder mehr Werbeeinblendungen vermerkt haben. Zunächst fällt auf, dass keine Angaben zur Transaktionszeit gemacht wurden; wenn sie fehlt, wird angenommen, dass der aktuelle Zustand abgefragt wird und ältere Zustände werden nicht beachtet. Die Anfrage sieht zunächst aus wie normales SQL, es gibt jedoch einen syntaktischen Unterschied; die Tabelle hat NBC_FB_Insertion zwei Aliase. Dadurch wird implizit eine temporale Selektion durchgeführt. Das heißt, Elemente aus N und N2 werden zusammengehängt ausgegeben (joined), wenn sie zur gleichen Zeit gültig sind, ihre Valid Time demnach Überschneidungen enthält. Als letzter Schritt wird eine implizite Valid Time-Projektion ausgeführt. Neben dem Spielnamen werden zusätzlich die passenden Intervalle zurückgegeben. Zusammengefasst passieren drei Vorgänge bei dieser Anfrage: Transaktionszeit behandelt, Valid Time-Selektion, Valid Time-Projektion. 1 SELECT SNAPSHOT ShowName, 2 CAST(VALID(N) TO INTERVAL DAY) 3 FROM NBCShows(ShowName) AS N 4 WHERE N.ShowName = ’Roseanne’ Quelltext 4.5: TSQL2 How long has the Roseanne show run? 40 Die Anfrage im Quelltext 4.5 zeigt, wie lange die Show Roseanne schon im amerikanischen Fernsehen läuft. Dabei werden drei neue Konstrukte benutzt. Zunächst das Keyword SNAPSHOT, welches angibt, dass die Ausgabe keine Valid Time- oder Transactional Time-Tabelle sein soll. Damit wird die implizite Valid Time-Projektion vermieden und, wenn die ausgegebene Tabelle in anderen Anfragen verwendet wird, stehen keine temporalen Operationen zur Verfügung. In der TEXT-Klausel findet sich neben der Tabellenangabe eine Angabe eines Attributs in Klammern. Diese Angabe ermöglicht, das implizite Coalescing auf Attribute einzuschränken. Im Gegenzug dazu können jedoch nur diese Attribute der Tabelle benutzt werden. So gibt die Anfrage an, die Valid Time nach ShowName zusammenzufassen. In der SELECT-Klausel findet sich als letztes ein Cast, welche das Valid Time-Attribut in ein SQL-INTERVAL umwandelt (INTERVAL im Sinne von einer Zeitspanne, nicht einem Zeitintervall). Ausgegeben von dieser Anfrage wird eine Zeile mit zwei Spalten, eine davon Roseanne und die andere eine Zeitspanne in Tagen. Wenn wir annehmen, dass Roseanne etwa neun Jahre ausgestrahlt wurde, wäre die zweite Spalte ’3285 Days’. 1 SELECT ShowName, VALID CAST(BEGIN(VALID(A) AS DAY)) 2 FROM NBCShows(ShowName) (PERIOD) AS A 3 WHERE CAST(VALID(A) AS INTERVAL YEAR) >= 4 INTERVAL ’ 2 ’ YEAR Quelltext 4.6: TSQL2 List all shows broadcast by NBC that ran continuously for at least two years, and indicate the day that they began that run In der letzten Anfrage im Quelltext 4.6 des Beispielszenarios soll herausgefunden werden, welche Shows auf NBC mindestens zwei Jahre liefen und zusätzlich ausgegeben werden, wann die Ausstrahlung dieser Shows anfing. In der FROM-Klausel wird noch einmal ein Attribut angegeben, nach welchem ein benutzerdefiniertes Coalescing stattfindet. Zusätzlich wird das Argument (PERIOD) angegeben, welches die in der Tabellendefinition angegebene Valid Time-Granularität überschreibt und erzwingt, dass die Valid Time nun in einer Period mit einem Start- und Endzeitpunkt angegeben wird. In der WHERE-Klausel wird die Valid Time in ein INTERVAL umgewandelt und mit dem INTERVAL ’2’ YEAR verglichen. Als letzter Vorgang wird in der SELECT-Klausel anschließend der Startpunkt der Valid Time mithilfe der BEGIN-Funktion extrahiert und in die Granularität DAY umgewandelt. Die BEGIN-Funktion funktioniert nur auf Periods, deshalb die Umwandlung der selbstdefinierten Granularität in das Period in der FROM-Klausel. 4.2 SQL:2011 Der SQL-Standard wird von ISO3 und IEC4 veröffentlicht. In der Dezember 2011 veröffentlichten Version des SQL-Standards, genannt SQL:2011, kamen eine Reihe neuer Features im Bereich der temporalen Datenbanken hinzu. Anhand des Artikels [KM12] sollen diese Features nun vorgestellt werden. 3 4 International Organization for Standardization International Electrotechnical Commision 41 Zunächst das Feature PERIOD, welches als Eckstein der Unterstützung für die Verarbeitung von temporalen Informationen bezeichnet wird. Im Gegensatz zu vielen anderen temporalen Datenbanksystemen, wird in SQL:2011 kein neuer Datentyp hinzugefügt, der ein geordnetes Paar aus Zeitpunkten darstellt, sondern vielmehr kann bei der Definition einer Tabelle als Metadaten angegeben werden, welches Felderpaar eine Periode darstellt. Dabei kann definiert werden, ob die Felder als Valid Time oder als Transaction Time klassifiziert sind. Diese Period hat bestimmte Eigenschaften, die sie anschließend erfüllen muss. Der Anfangszeitpunkt muss stets vor dem Endzeitpunkt sein und die Period ist als ein Closed-Open Intervall definiert, wie im Kapitel 2.1.3 besprochen. Der Vorteil an diesem Ansatz ist, dass sich Periods leichter auf existierende Datenbanken anlegen lassen können, insbesondere wenn sie bereits ein temporales System angelegt haben, welches auf ein Intervallpaar von Feldern basiert. SQL:2011 schlägt auch die Unterscheidung in drei Typen von Tabellen vor; Application Time Period-Tabellen (auch Valid Time-Tabellen genannt), System Versioned-Tabellen (Transaction Time-Tabellen) und die Kombination von beidem, die bitemporalen Tabellen. Im SQL:2011 Standard sind jedoch nicht alle Fragen vollständig geklärt und für zukünftige Versionen noch Vorschläge offen gelassen worden. Es gibt noch keinen Vorschlag vom Standard zu Period-Normalisierung (auch bekannt als COALESCED Option). Es fehlt auch eine native Unterstützung von Period Joins, das heißt Joins von verschiedenen Tabellen unter temporalen Voraussetzungen, insbesondere Outer Joins, da sich Inner Joins mithilfe des OVERLAPS-Operator simulieren lassen. Im Gegensatz zu älteren Ansätzen hat SQL:2011 auch einige wesentliche Unterschiede. So verzichtet SQL:2011 komplett auf temporale Abwärtskompatibilität, um die Speicherung der Daten transparent zu halten. Die Entwickler halten außerdem eine Abwärtskompatibilität in Valid Time-Tabellen für sinnlos, da eine Anfrage an solch eine Tabelle immer auch Informationen mit Historie ausgeben soll. Außerdem wird auf Syntaxerweiterungen wie beispielsweise ein Präfix oder eine Anfrage verzichtet, da solche Syntaxregeln in vergangenen Vorschlägen unklare Definitionen beinhalteten. Im Folgenden sollen anhand eines Beispielszenarios die drei Tabellenarten mit Beispielanfragen erläutert werden. Daran lassen sich die Unterschiede in der Syntax und im Aufbau der Tabellen im SQL:2011-Standard erkennen. 4.2.1 Valid Time-Tabellen In SQL:2011 werden Valid Time-Tabellen Application Time Period-Tabellen genannt. Dabei wird nun am Beispiel einer Mitarbeiter Tabelle die Gültigkeitszeitdefinition sowie einige DDL-Statements und Anfragen veranschaulicht. 42 1 CREATE TABLE Emp( 2 ENo INTEGER, 3 EStart DATE, 4 EEnd DATE, 5 EDept INTEGER, 6 PERIOD FOR EPeriod (EStart, EEnd) 7 ) Quelltext 4.7: SQL:2011 Valid Time-Tabelle Zunächst wird in Quelltext 4.7 die Tabelle Emp für Employee mit vier Spalten angelegt. Primärschlüssel wurden bei dieser Definition mit Absicht weggelassen; sie werden im Quelltext 4.11 behandelt. Die vier Attribute werden dabei exakt wie in älteren SQLStandards angelegt. Dazu kommt jedoch die Zeile 6. In ihr wird angegeben, dass nun zwei der Attribute als eine PERIOD, das heißt ein Zeitintervall, dargestellt werden. Dabei sind PERIOD FOR definierte Schlüsselwörter. Danach kommt der Name dieses Intervalls und anschließend in Klammern die beiden Attribute. Die Reihenfolge ist dabei bedeutend, da das erste Argument den Startpunkt des Zeitintervalls angibt und das zweite Argument den Endpunkt. Dabei schränkt SQL:2011 ein, dass die Start- und Endzeitpunkte entweder vom Datentyp Date oder Timestamp sein müssen. Diese Einschränkung unterscheidet sich von TSQL2, wo eine eigene Granularität angegeben werden konnte. Diese Art der Definition von Zeitintervallen wurde gewählt, um bestehende Tabellen leichter mit temporalen Operatoren ausstatten zu können, ohne Zeilen zu entfernen oder Daten konvertieren zu müssen. In dieser Ausarbeitung werden die beiden Attribute zusammengefasst zu einer PostgreSQL Range (vergleiche Unterkapitel 2.2.2). 1 INSERT INTO Emp VALUES ( 2 22217, 3 DATE ’2010−01−01’, 4 DATE ’2011−11−12’, 5 3 6 ) Quelltext 4.8: SQL:2011 Insert in eine Valid Time-Tabelle ENo 22217 EStart 2010-01-01 EEnd 2011-11-12 EDept 3 Tabelle 4.1: Valid Time-Tabelle nach dem INSERT in 4.8 In Quelltext 4.8 werden nun Daten eingefügt. Dabei unterscheidet sich dieses Statement nicht von einem INSERT-Statement aus den vorherigen Versionen von SQL. In der Tabelle 4.1 ist das Ergebnis der Einfügeoperation zu sehen. 43 1 UPDATE Emp 2 FOR PORTION OF EPeriod 3 FROM DATE ’2011−02−03’ 4 TO DATE ’2011−09−10’ 5 SET EDept = 4 6 WHERE ENo = 22217 Quelltext 4.9: SQL:2011 Update ENo 22217 22217 22217 EStart 2010-01-01 2011-02-03 2011-09-10 EEnd 2011-02-03 2011-09-10 2011-11-12 EDept 3 4 3 Tabelle 4.2: Valid Time-Tabelle nach dem UPDATE in 4.9 Im Quelltext 4.9 ist ein UPDATE-Statement zu sehen. Dabei unterscheiden sich die Zeilen 1, 5 und 6 nicht von einem normalen Update; sie würden das Department vom Benutzer 22217 auf 4 ändern. Hinzugekommen bei SQL:2011 ist die FOR PORTIONSyntax. Mit ihr ist es möglich anzugeben, für welchen Zeitraum ein Update passieren soll. Demnach soll das Department in diesem Beispiel für das Zeitintervall [2011-02-03, 2011-09-10) aktualisiert werden, die restliche Zeit soll gänzlich unberührt bleiben. Hinter dieser Operation steht nicht nur ein Vorgang; zunächst werden alle betroffenen Zeilen, die in diesem Zeitraum gültig sind, lokalisiert. Danach gibt es mehrere Möglichkeiten, was passieren kann. Wenn eine Zeile komplett im angegebenen Zeitraum gültig ist, wird diese Zeile aktualisiert. Falls der angegebene Zeitraum jedoch nur einen Teil einer Zeile betrifft, wie in diesem Beispiel, gilt [2011-02-03, 2011-09-10) DURING [2010-01-01 2011-11-12). Dann wird diese Zeile in zwei Zeilen kopiert, wobei bei der ersten Zeile der Endzeitpunkt zum Anfangszeitpunkt des im Update angegebenen Zeitintervalls geändert wird und in der zweiten Zeile der Anfangszeitpunkt zum Endzeitpunkt des im Update angegebenen Zeitintervalls geändert wird. Nun wird die aktualisierte Zeile mit dem angegebenen Zeitintervall zwischen den beiden Zeilen eingefügt. Es existieren noch weitere Sonderfälle, wie, wenn das angegebene Intervall am Ende der Gültigkeit eines der Datensätze ist oder, wenn mehrere Zeilen betroffen sind. Die Tabelle 4.2 zeigt das Ergebnis dieser Aktualisierung. 1 DELETE FROM Emp 2 FOR PORTION OF EPeriod 3 FROM DATE ’2011−02−03’ 4 TO DATE ’2011−09−10’ 5 WHERE ENo = 22217 Quelltext 4.10: SQL:2011 Delete Das DELETE-Statement von Quelltext 4.10 funktioniert analog zum Update. Dabei kann eine Lücke entstehen, das heißt das Zeitintervall muss nicht zwangsweise genau 44 ENo 22217 22217 EStart 2010-01-01 2011-09-10 EEnd 2011-02-03 2011-11-12 EDept 3 3 Tabelle 4.3: Valid Time-Tabelle nach dem DELETE in 4.10 einen Datensatz betreffen. Es kann auch mehrere Datensätze umfassen und dadurch kann eine Lücke entstehen. 1 ALTER TABLE Emp 2 ADD PRIMARY KEY (ENo, 3 EPeriod WITHOUT OVERLAPS) Quelltext 4.11: SQL:2011 Without Overlaps Im Quelltext 4.7 wurde die Angabe eines Primary Keys mit Absicht weggelassen, um ihn jetzt im Quelltext 4.11 zu definieren. Dabei ist wichtig zu beachten, dass ENo als Primärschlüssel nicht ausreicht. Das ist auch schon an den Tabellen aus den vorherigen Beispielen zu beobachten, dort existieren mehrere Zeilen für einen ENo-Wert. Die Integrität in Valid Time-Tabellen ist nicht, dass zu jedem Primärschlüssel nur eine einzige Zeile existieren darf; es gilt, dass zu einem Primärschlüssel zu jedem Zeitpunkt nur ein Wert existieren darf. Deswegen ist zusätzlich die EPeriod als Primärschlüssel angegeben. Dazu kommt noch eine Option, die in SQL:2011 hinzugefügt wurde: WITHOUT OVERLAPS. Diese Option schließt im Beispiel Daten aus, wo der angegebene Wert in ENo identisch ist und die angegebenen Zeitintervalle in EPeriod überlappen. Das würde die temporale Integrität verletzen, da dadurch zu gegebenem Zeitpunkt zwei Zeilen zu einem Objekt existieren würden. 4.2.2 Transaction Time-Tabellen Neben den Valid Time-Tabellen bietet SQL:2011 auch zusätzliche Konstrukte für Transaction Time-Tabellen an. Im Kontext vom SQL:2011 werden diese jedoch Sytem VersionedTabellen genannt. Es wird das Beispiel von der Valid Time-Tabelle im vorherigen Unterkapitel aufgegriffen. 1 CREATE TABLE Emp 2 ENo INTEGER, 3 Sys start TIMESTAMP(12) GENERATED ALWAYS AS ROW START, 4 Sys end TIMESTAMP(12) GENERATED ALWAYS AS ROW END, 5 EName VARCHAR(30), 6 PERIOD FOR SYSTEM TIME (Sys start, Sys end), 7 PRIMARY KEY (ENo) 8 ) WITH SYSTEM VERSIONING Quelltext 4.12: SQL:2011 Transaction Time-Tabelle Beim Anlegen der Tabelle ist im Quelltext 4.12 zu sehen, dass neue Konstrukte in SQL:2011 existieren. Die Zeilen 1, 2 und 5 unterscheiden sich zunächst nicht von der Syntax des vorherigen SQL-Standards, in den anderen Zeilen finden sich jedoch 45 Unterschiede. So hat diese Tabelle mit vier Spalten zwei Spalten, die als TIMESTAMP definiert sind. Hinter den Spaltendefinitionen sind jedoch die Schlüsselwörter GENERATED ALWAYS AS ROW START angegeben. Damit wird vermieden, dass der Benutzer diesen Zeilen selbst beim INSERT einen Wert zuweist oder via UPDATE verändert. In der Zeile 6 werden die beiden Spalten als PERIOD definiert, damit die PERIOD-Operatoren für sie verfügbar sind. In der Zeile 8 wird die Tabelle WITH SYSTEM VERSIONING definiert. Dadurch wird jede DDL-Operation auf der Tabelle überwacht und gegebenenfalls bestehende Daten, wie bei Transaction Time üblich, archiviert bzw. aktualisiert. Dabei genügt als Primärschlüssel die ENo, da SQL:2011 implizit die temporale Integrität für Transaction Time-Tabellen gewährleistet. In den nächsten drei DDL-Statements wird die Auswirkung der Transaction TimeTabellen beschrieben. Dabei sind die Statements identisch zu den Varianten in den alten Standards. Jedoch ist das Ergebnis in der Tabelle durch die Transaction Time-Definition anders. So wird bei einem UPDATE nicht lediglich eine einzige Zeile verändert, es werden auch Zeilen hinzugefügt. 1 INSERT INTO Emp (ENo, EName) 2 VALUES (22217, ’Joe’) Quelltext 4.13: SQL:2011 Transaction Time-Tabelle Insert ENo 22217 Sys Start 2014-01-01 09:00:00 Sys End 9999-12-31 23:59:59 EName Joe Tabelle 4.4: Transaction Time-Tabelle nach dem INSERT in 4.13 Im Quelltext 4.13 wird eine INSERT-Operation durchgeführt. Dabei soll angenommen werden, dass sie zum Zeitpunkt 2014-01-01 09:00:00 abgeschlossen ist. Die Tabelle 4.4 zeigt das Ergebnis der Operation. Dabei ist zu beachten, dass im Statement keine Angaben zu Sys Start und Sys End gemacht worden sind. Sie werden implizit, abhängig vom Zeitpunkt der Operation, durchgeführt. Dadurch entsteht ein Sys Start mit dem Wert 2014-01-01 09:00:00, da dies der erste Zeitpunkt ist, zu dem dieser Zustand des Objekts mit den entsprechenden Werten in der Datenbank abgespeichert war. 1 UPDATE Emp 2 SET EName = ’Tom’ 3 WHERE ENo = 22217 Quelltext 4.14: SQL:2011 Transaction Time-Tabelle Update ENo 22217 22217 Sys Start 2014-01-01 09:00:00 2014-11-01 12:00:00 Sys End 2014-11-01 12:00:00 9999-12-31 23:59:59 EName Joe Tom Tabelle 4.5: Transaction Time-Tabelle nach dem UPDATE in 4.14 46 Nun wird im Quelltext 4.14 zum Zeitpunkt 2014-11-01 12:00:00 dieses Objekt aktualisiert. Der ursprünglich eingetragene Name war falsch, der richtige Name des Mitarbeiters ist Tom. Nun wird der alte Datenbestand nicht einfach verworfen, so wie es bei Schnappschuss-Tabellen üblich ist, sondern in einer neuen Zeile archiviert. Deshalb existieren für das Objekt 22217 nun zwei Zeilen in der Datenbank. Die erste Zeile stellt dar, was über das Objekt zwischen den Zeitpunkten 2014-01-01 09:00:00 und 2014-11-01 12:00:00 angenommen wurde. Das zweite Objekt zeigt an, was seit 2014-11-01 12:00:00 bekannt ist und auch für die Zukunft als richtig angenommen wird. Diese Operation besteht demnach aus zwei Versionen im Sinne der alten SQL-Standards. Zunächst wird der bestehende Datenbestand aktualisiert, indem er nur noch bis zum aktuellen Zeitpunkt gültig ist (Änderung bei Sys End). Dann wird eine neue Zeile eingefügt, welche die selben Werte wie die alten enthält, mit dem Unterschied der Daten, die via UPDATE geändert wurden. Diese gelten ab dem aktuellen Zeitpunkt und werden erstmal als für immer wahr eingestuft. 1 DELETE FROM Emp 2 WHERE ENo = 22217 Quelltext 4.15: SQL:2011 Transaction Time-Tabelle Delete ENo 22217 22217 Sys Start 2014-01-01 09:00:00 2014-11-01 12:00:00 Sys End 2014-11-01 12:00:00 2015-11-01 12:00:00 EName Joe Tom Tabelle 4.6: Transaction Time-Tabelle nach dem DELETE in 4.15 Nun werden die Daten via dem Befehl in Quelltext 4.15 ein Jahr später gelöscht. Dafür wird die aktuell gültige Datenzeile des Objekts als ab jetzt nicht mehr in der Datenbank bekannt angegeben, indem der Wert von Sys End auf den aktuellen Zeitpunkt gesetzt wird. 1 SELECT ENo,EName,Sys Start,Sys End 2 FROM Emp FOR SYSTEM TIME AS OF 3 TIMESTAMP ’2015−01−02 00:00:00’ Quelltext 4.16: SQL:2011 Anfrage ENo 22217 Sys Start 2014-11-01 12:00:00 Sys End 2015-11-01 12:00:00 EName Tom Tabelle 4.7: Ergebnis nach Anfrage 4.16 an Transaction Time-Tabelle Nun wurden in SQL:2011 Anfragen an Transaction Time-Tabellen erweitert. Wenn an einem SELECT ... FROM ein FOR SYSTEM_TIME AS OF TIMESTAMP <Zeitstempel> angehängt wird, so werden alle Zeilen zurückgegeben, welche vom Datenbanksystem zu diesem Zeitpunkt als wahr empfunden wurden. Dadurch lässt sich durch die Tabelle die Historie der Annahmen an die Datenbank verfolgen. 47 4.2.3 Bitemporal-Tabellen SQL:2011 beschreibt auch die Unterstützung für Tabellen, die sowohl Valid Time als auch Transaction Time enthalten, das heißt bitemporale Tabellen. Dabei wird als Beispiel eine Kombination der vorherigen beiden Unterkapitel genutzt. 1 CREATE TABLE Emp( 2 ENo INTEGER, 3 EStart DATE, 4 EEnd DATE, 5 EDept INTEGER, 6 PERIOD FOR EPeriod (EStart, EEnd), 7 Sys start TIMESTAMP(12) GENERATED ALWAYS AS ROW START, 8 Sys end TIMESTAMP(12) GENERATED ALWAYS AS ROW END, 9 EName VARCHAR(30), 10 PERIOD FOR SYSTEM TIME (Sys start, Sys end), 11 PRIMARY KEY (ENo, EPeriod WITHOUT OVERLAPS), 12 ) WITH SYSTEM VERSIONING Quelltext 4.17: SQL:2011 Bitemporal-Tabelle Im Quelltext 4.17 wird die bitemporale Tabelle erstellt. Dabei wird in den Zeilen 3, 4, 6 und 11 die Valid Time-Unterstützung definiert, wobei die Zeilen 7-10 sowie die Zeile 12 die Transaction Time zu der Tabelle hinzufügen. DDL-Statements und Anfragen funktionieren auf solchen Tabellen analog zu den vorangegangenen Kapiteln. 4.3 VTSQL2+ VTSQL2+ ist eine SQL-Spracherweiterung basierend auf VTSQL2, welche in der Diplomarbeit Objekt-relationale Realisierung einer temporalen Datenbanksprache von Frank Beier [Bei01] beschrieben wurde. Beier basierte seine Ausarbeitung auf eine ObjektRelationale Datenbank und erstellte ein Werkzeug zur Übersetzung und Ausführung von VTSQL2 Kommandos in einer Datenbank. VTSQL2+ unterscheidet sich von VTSQL2 beispielsweise in der Syntax. Es verwendet nicht das Schlüsselwort VALIDTIME um eine Anfrage mit einem temporalen Kontext zu beschreiben, sondern benutzt stattdessen das Schlüsselwort SEQUENCED. Beispiele Die folgenden Beispiele betrachten lediglich die Anfragen und ignorieren die DDL/DML Statements von VTSQL2+ , da diese schon effektiv im SQL:2011 Standard betrachtet wurden. Zunächst wird eine Tabelle in der Datenbank angelegt und mit einer Beispielausprägung gefüllt. 48 1 CREATE VALIDTIME COALESCED TABLE ang( 2 Nr NUMBER PRIMARY KEY, 3 Name VARCHAR(20), 4 Gehalt NUMBER, 5 Chefnr NUMBER REFERENCES ang); Quelltext 4.18: CREATE TABLE Klausel für die Beispiel Datenbank Quelltext 4.18 zeigt eine Erweiterung der CREATE TABLE-Anweisung um zwei Schlüsselwörter. VALIDTIME markiert die Tabelle als eine Valid Time-Tabelle und fügt implizit ein weiteres Attribut VT hinzu, welches die Gültigkeitszeit des Objekts in der Tabelle beschreibt. COALESCED heißt, dass der Zustand der Tabelle nach jeder Veränderung automatisch COALESCED sein soll. Demnach darf die Tabelle niemals in einem Zustand sein, in dem zwei zeitlich benachbarte Objekte mit der gleichen Identifikation und Werten existieren. Die Beispielausprägung ist aus der Tabelle 4.8 zu entnehmen. Nr 12 12 12 12 13 13 13 27 27 Name Mueller Mueller Mueller Mueller Meier Meier Meier Boss Boss Gehalt 5000 5000 5500 5000 5300 5300 5400 5200 5700 ChefNr 12 27 27 27 13 27 27 27 27 VT [01.03.2000,01.05.2000) [01.05.2000,01.07.2000) [01.07.2000,01.10.2000) [01.10.2000,FOREVER) [01.04.2000,01.08.2000) [01.08.2000,01.11.2000) [01.11.2000,FOREVER) [01.05.2000,01.09.2000) [01.09.2000,FOREVER) Tabelle 4.8: Beispielausprägung einer temporalen Tabelle inVTSQL2+ Abwärtskompatible Schnappschuss Anfragen Abwärtskompatibilität garantiert, dass SQL-Statements, die zuvor galten, weiterhin gültig sind und behandelt werden, als sei nur der aktuelle Zustand der Objekte gefragt. Das garantiert, dass Anwendungen wie bisher laufen, auch wenn eine temporale Funktionalität hinzugefügt wurde. 1 SELECT ∗ FROM ang; Quelltext 4.19: Temporal abwärtskompatible Schnappschuss Anfrage In der Anfrage 4.19 wird nach allen Entitäten aus der Relation ang gefragt. Sie besitzt keine spezielle Syntax und ist damit auch nicht temporal. Deswegen ist die Rückgabe in der Tabelle 4.9 alle Entitäten, die zum jetzigen Zeitpunkt noch gültig sind. Dabei ist noch zu beachten, dass aus der Ausgabe das Feld VT entfernt wurde. Die Anfrage ist damit komplett von der Gültigkeitszeit entkoppelt. Dadurch entsteht Kompatibilität zu alten Anfragen, die noch keine Gültigkeitszeit erwartet haben. 49 Nr 12 13 27 Name Mueller Meier Boss Gehalt 5000 5400 5700 ChefNr 27 27 27 Tabelle 4.9: Ergebnis von Anfrage 4.19 Sequentielle Anfragen Nun kommt die spezielle Syntax der Anfragesprache hinzu. Zunächst wird das SEQUENCED Schlüsselwort betrachtet, welches sequentielle Anfragen erzeugt. Jede sequentielle Anfrage gibt genau eine VT-Spalte zurück, welche die Gültigkeit der Entität beschreibt. Jede erwähnte Tabelle in der FROM-Klausel eines SEQUENCED SELECT muss eine Gültigkeitstabelle sein, das heißt sie muss eine VT-Spalte besitzen. 1 SEQUENCED SELECT name FROM ang WHERE Gehalt>5250; Quelltext 4.20: Sequentielle Anfrage Die erste Anfrage 4.20 beschreibt wann jeder Angestellte 5250 Euro verdient hat. In der Tabelle 4.10 sieht man die Ausgabe. Dabei ist zu beachten, dass Mueller nur vom 01.07.2000 bis 01.10.2000 die Bedingung erfüllt. Ohne das SEQUENCED Schlüsselwort wäre demnach zum Zeitpunkt 01.01.2013 Mueller nicht Bestandteil der Ausgabe, da er im Tupel 4 der Tabelle 4.8, welches zu dem Zeitpunkt gilt, nicht genug Geld verdienen würde. Name Mueller Meier Meier Meier Boss VT [01.07.2000,01.10.2000) [01.04.2000,01.08.2000) [01.08.2000,01.11.2000) [01.11.2000,FOREVER) [01.09.2000,FOREVER) Tabelle 4.10: Ergebnis von Anfrage 4.20 In der Tabelle 4.10 ist noch zusätzlich zu beobachten, dass zeitlich benachbarte Tupel mit gleichen Werten existieren. Diese lassen sich mithilfe des COALESCE Schlüsselworts zusammenfassen. 1 SEQUENCED COALESCE SELECT name FROM ang WHERE Gehalt>5250; Quelltext 4.21: Sequentielle Anfrage mit COALESCE Im Ausdruck 4.21 wird das COALESCE Schlüsselwort benutzt. Die Anfrage ist ansonsten identisch zu der Anfrage 4.20. Die Ausgabe fasst die Tupel 2-4 zusammen, da sie die gleichen Werte besitzen und zeitlich benachbart sind. Die endgültige Ausgabe ist in der Tabelle 4.11 zu sehen. 50 Name Mueller Meier Boss VT [01.07.2000,01.10.2000) [01.04.2000,FOREVER) [01.09.2000,FOREVER) Tabelle 4.11: Ergebnis von Anfrage 4.21 Nun wird zu der Anfrage 4.21 zusätzlich eingeschränkt, für welchen Zeitraum die Ausgabe gültig sein soll. 1 SEQUENCED [01.01.2000, 01.09.2000) 2 COALESCE SELECT name FROM ang WHERE Gehalt>5250; Quelltext 4.22: Sequentielle Anfrage mit COALESCE in einem spezifizierten Zeitraum So ist zwischen dem SEQUENCED und COALESCE ein Zeitraum mithilfe eines Intervalls in der Anfrage 4.22 angegeben. Diese Angabe schränkt die Ausgabe ein. Kein Tupel in der Tabelle 4.12 hat einen Zeitpunkt in VT, der außerhalb des angegebenen Zeitraums gilt. Da in diesem auch Boss nicht gültig war (siehe 4.11), fehlt dieser bei der Ausgabe komplett. Name Mueller Meier VT [01.07.2000,01.10.2000) [01.04.2000,01.09.2000) Tabelle 4.12: Ergebnis von Anfrage 4.22 Da wie angesprochen eine SEQUENCED SELECT Anfrage immer genau eine VT Spalte als Ausgabe enthält, kommt die Frage auf, was passiert, wenn zwei Tabellen in der FROM-Klausel angegeben werden. 1 SEQUENCED SELECT ∗ FROM ang a, ang b; Quelltext 4.23: Sequentielle Join Nun wir die Tabelle ang mit einem CROSS JOIN zweimal in der FROM-Klausel angegeben. In einem SEQUENCED JOIN werden jedoch nur zwei Tupel miteinander gejoint, die auch zu gemeinsamen Punkten gültig waren. So ist in der Ausgabetabelle 4.12 zu sehen, wie Mueller und Meier miteinander gejoint werden, jedoch nur wenn sie zur gleichen Zeit gültig waren. Zusätzlich wird in der VT-Spalte noch angegeben, zu welchen Zeitpunkten dieser CROSS JOIN gültig ist. Nicht-Sequentielle-Anfragen Als dritte und letzte Methode, Anfragen in VTSQL2+ zu stellen, existieren die NONSEQUENCED Anfragen. Sie unterscheiden sich zu abwärtskompatiblen Anfragen, da sie die VT-Spalte nicht ausblenden und alle Tupel, die zu diesem Moment nicht gültig sind, nicht einfach weglässt, sondern sowohl die Spalte als auch alle Tupel zu jedem Objekt 51 a.Nr ... 12 12 12 12 12 12 ... a.Name a.G a.CN b.Nr b.Name b.G b.CN VT Mueller Mueller Mueller Mueller Mueller Mueller 5000 5000 5500 5500 5000 5000 12 27 27 27 27 27 13 13 13 13 13 13 Meier Meier Meier Meier Meier Meier 5300 5300 5300 5300 5300 5400 13 13 13 27 27 27 [01.04.2000,01.05.2000) [01.05.2000,01.07.2000) [01.07.2000,01.08.2000) [01.08.2000,01.10.2000) [01.10.2000,01.11.2000) [01.11.2000,FOREVER) Tabelle 4.13: Ergebnis von Anfrage 4.23 (Namen der Übersichtlichkeit halber gekürzt) als normale Entitäten einer Relation behandelt. So lassen sich SEQUENCED Anfragen mithilfe von NONSEQUENCED emulieren. 1 NONSEQUENCED SELECT ∗, a.VT, b.VT FROM ang a, ang b; Quelltext 4.24: Nicht-Sequentielle-Anfrage Zunächst sieht das Beispiel 4.24 so aus wie die Anfrage 4.23, vom Schlüsselwort NONSEQUENCED abgesehen. Jedoch werden in der SELECT-Klausel darüber hinaus die VT-Spalten von beiden Tabellen verlangt. Außerdem wird hier der CROSS JOIN traditionell durchgeführt, sodass lediglich alle existierenden Tupel ungeachtet vom Zeitraum miteinander kombiniert werden. Mit Operatoren lässt sich anschließend die Anfrage aus 4.23 nachstellen. So lassen sich mithilfe von NONSEQUENCED komplexere Anfragen aufstellen, die von den Anforderungen von SEQUENCED abweichen. 4.4 Evaluation Es lassen sich nun die drei vorgestellten Sprachen vergleichen. Zunächst das etwas betagtere TSQL2. Es besitzt eine etwas obskure Syntax und fragwürdige sowie komplexe Ideen, was viel Interpretationsspielraum in der Implementierung ermöglicht. So sind alle temporalen Tabellen darin stets COALESCED und die Syntax für das Anlegen neuer temporaler Tabellen ist nicht intuitiv. Es ist wohl verständlich, dass diese Erweiterung keinen Platz im SQL-Standard gefunden hat. Der neue SQL-Standard hat gute Ansätze, ist jedoch noch nicht ausgereift. Er beschäftigt sich hauptsächlich mit den DDL/DML-Ausdrücken und bietet nur eine spezielle Syntax für Transaction Time-Tabellen, jedoch nicht für Valid Time-Tabellen. Auch solche Fragen wie COALESCE sind durch den Standard nicht beantwortet. Das Ziel war es wohl, einen schlanken Standard zu gestalten, der attraktiv für die Implementierung der DBMS ist. Es existiert jedoch zum Zeitpunkt der Ausarbeitung noch kein DBMS, welches diesen Teil des Standards vollständig implementiert. Als letzte Sprache wurde die hauseigene Spracherweiterung VTSQL2+ eingeführt. Sie besitzt eine klare und einfache Syntax und versucht Probleme zu lösen, die durch temporale Daten entstehen. Sie besitzt mächtige Mittel mit den drei Typen der Anfragen, 52 die nach kurzer Einführung selbsterklärend sind. Elemente aus diesen Sprachen werden sich hervorragend für die Entwicklung der eigenen Spracherweiterung eignen. Nachdem die drei Sprachen nun ausführlich analysiert und evaluiert wurden, stellt sich endgültig die Frage, welche Elemente der Sprache nun in die selbst entwickelte SQL-Sprache fließen werden. Dabei kann TSQL2 schon komplett gestrichen werden, da die Syntax antiquiert und nicht wie Teil einer modernen Spracherweiterung wirkt. Da der SQL-Standard 2011 keine Spracherweiterung für temporale SELECT-Anfragen bietet, eignet sich dafür die Syntax von VTSQL2+ . Es bleiben demnach DDL und DMLAusdrücke übrig. Für DML-Ausdrücke ist die Syntax vom Standard zwar sehr verbose, jedoch auch klar und intuitiv zu verstehen. Für den Gewinn der Zukunftskompatibilität eignen sich die DML-Statements vom SQL:2011-Standard demnach hervorragend. Bei den DDL-Ausdrücken wirkt die Syntax von VTSQL2+ wesentlich ausgereifter, da sie mit Schlüsselwörtern vor der Spaltendefinition arbeitet und somit die ursprüngliche Syntax deutlich mehr intakt lässt. Der SQL-Standard verwendet dafür Schlüsselausdrücke innerhalb der Spaltendefinition und wird deshalb nicht gewählt. Zusammenfassend kann demnach bestimmt werden, dass mit leichten Modifikationen, die SELECT-Anfragen sowie die DDL-Ausdrücke von VTSQL2+ gewählt werden, die DML-Ausdrücke jedoch vom Standard übernommen werden. Dadurch wird die Zukunftskompatibilität gewährleistet ohne auf eine klare Syntax zu verzichten. 53 Kapitel 5 Entwurf und Implementierung Dieses Kapitel beschäftigt sich mit der Umsetzung und Pflege der vorher genannten Erweiterung von SQL sowie der PostgreSQL-Extension. Zunächst wird die SQL-Sprache beschrieben; dabei wird die Syntax und Semantik dieser Sprache vorgestellt. Dadurch wird auch klar, welche Funktionen von der PostgreSQL-Extension benötigt werden. Diese werden anschließend im nächsten Unterkapitel beschrieben. Diese enthält Trigger und Funktionen, welche die Übersetzung der vorher genannten Sprache vereinfachen. Als letztes Unterkapitel wird die Übersetzung der SQL-Erweiterung beschrieben. Dafür wird der Bison/Flex-Parser vorgestellt und wie der davon erzeugte AST navigiert wird. Abbildung 5.1: Architektur Die formlose Abbildung 5.1 zeigt die Bestandteile der Software mitsamt der Abhängigkeiten. Daran sind die drei großen Teile der Architektur zu erkennen, welche nacheinander in Unterkapiteln beschrieben werden. Die PostgreSQL-Extension kann dabei noch feiner unterteilt werden, weil die Funktionen zu einzelnen Modulen zusammengefasst werden können. 54 5.1 SQL-Erweiterung Um den Betrieb der temporalen Datenbank zu vereinfachen, wurde im Rahmen der Ausarbeitung eine Erweiterung für den SQL-Standard entwickelt. Dabei soll die Syntax um Elemente ergänzt werden, welche die Kernkonzepte und Herausforderungen von temporalen Daten behandeln. Im Kapitel 4 wurden bereits historische Versuche dokumentiert; hier sollen nun Konzepte von dem Kapitel entnommen und verändert in eine neue Sprache zusammengefügt werden. Die im Kapitel 3 besprochenen Anforderungen an temporale Tabellen sollen mithilfe dieser Erweiterung der Sprache sichtlich erfüllt und die Bedienung dieser speziellen Art von Relation erleichtert werden. Dafür werden alle veränderten SQL-Befehle zunächst auf die Änderungen der Syntax betrachtet und anschließend anhand von Beispielen ihre Semantik erklärt, das heißt welche Operationen auf der Tabelle ausgeführt werden. Beispiele zu der definierten SQLErweiterung lassen sich im späteren Kapitel 5.3 finden; dort ist auch gleichzeitig die Übersetzung in traditionellem SQL angezeigt, welches die Semantik der Kommandos noch verdeutlichen kann. 5.1.1 DDL/DML-Kommandos Im Kapitel 3 war deutlich zu sehen, dass die Erstellung und Verwaltung von temporalen Daten sich von traditionellen Tabellen unterscheidet; es existieren neue Integritätsbedingungen, welche bei den DDL-Kommandos beachtet werden müssen und DML-Kommandos haben eine veränderte Semantik und benötigen unter Umständen einen Zeitraum, in dem sie gelten. Zunächst wird CREATE TABLE betrachtet. CREATE TABLE CREATE TABLE ist um drei Arten erweitert worden, die VALIDTIME, VALIDTIME COALESCED und die WITH SYSTEM VERSIONING-Varianten. Dabei können, wie aus dem Syntaxdiagramm 5.2 entnommen werden kann, VALIDTIME und VALIDTIME COALESCED mit WITH SYSTEM VERSIONING kombiniert werden. Alle diese zusätzlichen Schlüsselwörter sind optional; wenn sie weggelassen werden, wird die Tabelle nicht-temporal definiert. Das heißt, dass sich das CREATE TABLE-Kommando abwärtskompatibel verhält und in dieser Variante kein anderes Verhalten aufweist. Dabei ist zu beachten, dass die im Syntaxdiagramm angemerkten <Name> und <Columns> (Spaltendefinitionen) dabei analog zum traditionellen SQL funktionieren und keine speziellen Mechanismen besitzen. VALIDTIME Damit wird eine Tabelle mit Gültigkeitszeit ausgestattet; sie bekommt demnach eine zusätzliche Spalte mit dem Namen VT. In ihr wird die Valid Time eines Tupels ausgezeichnet. Zusätzlich gilt, dass die Tabelle den Constraint für temporale Schlüssel besitzt, demnach kann kein Objekt zweimal zur gleichen Zeit existieren (vergleiche Kapitel 3.1.1). 55 CreateTable: VT: TT: Abbildung 5.2: Syntax-Diagramm für CREATE TABLE VALIDTIME COALESCED Es gilt zunächst das gleiche wie bei VALIDTIME, jedoch wird noch zusätzlich die COALESCED-Integritätsbedingung zu der Tabelle hinzugefügt, die zeitlich benachbarte identische Tupel zu einem Tupel zusammenfügt. WITH SYSTEM VERSIONING Zu der Tabelle wird noch eine Spalte TT hinzugefügt, welche den Typ TSRANGE besitzt, der einem Intervall aus Zeitstempeln entspricht. Zusätzlich wird noch ein Trigger hinzugefügt, der eben diese Transaktionszeit verwaltet und bei der Veränderung der Daten durch DML-Kommandos die für die Transaktionszeit notwendigen Operationen durchführt. INSERT INSERT besitzt keine spezielle Syntax und braucht demzufolge auch kein Syntaxdiagramm. Die Gültigszeit eines Tupels wird lediglich beim einfügen wie ein normales Attribut eingefügt. Alle Spezialfälle, wie das Zusammenfügen von benachbarten Tupeln oder temporale Schlüssel werden mithilfe von Triggern gelöst, die bereits in CREATE TABLE angelegt wurden. Das erspart die Änderung von vorhandenen INSERT-Statements in der Software, die mit der Datenbank kommuniziert. UPDATE Die Syntax von UPDATE besitzt im Gegensatz zum traditionellem SQL ein neues Syntaxelement, im Syntaxdiagramm 5.3 als ForVT markiert. Dort kann angegeben werden, für welchen Zeitraum das Update ausgeführt werden soll und damit endgültig, welche Tupel betroffen werden. Falls das optionale ForVT weggelassen wird, verhält sich das Update abwärtskompatibel wie beim traditionellen SQL auf Tupel-Ebene und beachtet die Gültigkeitszeit gar nicht. Dabei sind <Set> und <Where> identisch zum traditionellen SQL 56 definiert. Dabei besteht jede Zeitpunktangabe aus zwei Elementen, dem RangeType und dem RangeElement, die nachfolgend näher vorgestellt werden. Update: ForVT: Date: Abbildung 5.3: Syntax-Diagramm für UPDATE <RangeType> Der <RangeType> gibt an, von welchem Typ das nachfolgend angegebene Datum ist; das ist wichtig, damit die Anfrage die Granularität des Zeitpunktes kennt und damit die Operationen entscheiden kann, die für die Anfrage benutzt werden. Es ermöglicht damit eine größere Flexibilität von Granularitäten. <RangeElement> Angabe eines Zeitpunktes in der vorher angegebenen Granularität. Dabei ist zu beachten, dass das Element in einfachen Anführungsstrichen geschrieben werden muss. Das gibt größere Freiheit bei Datentypen, die Elemente mit Leerzeichen zulassen und erleichtert zusätzlich den späteren Übersetzungsvorgang. Die Syntax ist äußerst verbose, jedoch abgesehen von der Datumsangabe in einfachen Anführungsstrichen vollkommen abwärtskompatibel zum SQL-Standard. Deshalb existiert auch eine Angabe der Spalte, welche die Gültigkeitszeit enthält. Da die SQL-Erweiterung keine anderen Spaltennamen zulässt, ist in der Syntax fest das VT als Spaltenangabe verankert. Das ist nur ein geringer Overhead bei der Programmierung für die zukünftige Abwärtskompatibilität. DELETE Das DELETE funktioniert analog zu dem Update, jedoch existiert keine <Set>-Angabe. Die Gültigskeitszeitangabe funktioniert allerdings gleich und es existiert ein abwärtskompatibler Modus, welcher auf Tupelebene arbeitet und die VT-Spalten vollkommen ignoriert. 57 Delete: ForVT: Date: Abbildung 5.4: Syntax-Diagramm für DELETE 5.1.2 Anfragen Die SQL-Erweiterung hat die größten Änderungen in den SQL-Anfragen. Wie in VTSQL2+ existieren nun drei Arten von Anfragen: Schnappschussanfragen, SEQUENCED und NONSEQUENCED. Die verschiedenen Varianten werden mit der Hilfe von Schlüsselwörtern vor der eigentlichen Anfrage als solche gekennzeichnet. Im Syntaxdiagramm 5.5 ist die klein wirkende Änderung notiert. Dabei ist zu beachten, das innere Anfragen die beispielsweise in der FROM- oder WHERE-Klausel von <Sel> auch mit SEQUENCED und NONSEQUENCED markiert werden können und den selben Regeln unterstehen wie eine äußere Frage. Im Klartext bedeutet das, dass eine beliebige Verschachtelungstiefe von den temporalen SELECT-Anfragen möglich ist. Insbesondere SEQUENCED-Unteranfragen in der WHERE-Klausel benötigen eine besondere Semantik, da sie im Zusammenhang mit der äußeren Anfrage stehen. in den folgenden Paragraphen werden die verschiedenen Varianten der neuen SQL-Anfragen analysiert und Beispiele vorgebracht, um die Semantik zu verstehen. Dafür ist es auch nötig, die abwärtskompatible Version zu betrachten. Abbildung 5.5: Syntax-Diagramm für SELECT-Anfragen Weder NONSEQUENCED noch SEQUENCED Falls eine Anfrage weder als NONSEQUENCED noch als SEQUENCED markiert ist, gilt sie als abwärtskompatibel. Das Verhalten der Anfrage ist dann abhängig von den referenzierten Tabellen. Falls die Tabellen nicht temporal sind, wird die Anfrage einfach als solche übernommen und 58 ausgeführt. Wenn jedoch temporale Tabellen in der FROM-Klausel sind, so werden diese als Schnappschuss referenziert. Das heißt, es werden nur die aktuell gültigen Objekte betrachtet. Das führt dazu, dass in einem bestehenden System die existierenden SQLAnfragen nicht angepasst werden müssen. NONSEQUENCED NONSEQUENCED verursacht, dass die in der folgenden SELECTAnfrage referenzierten Tabellen unverändert betrachtet werden, mitsamt ihrer VT- und TT-Attribute. Wenn mehrere Tabellen referenziert werden, wird kein temporaler Join ausgeführt. Im NONSEQUENCED-Modus lassen sich demnach die temporalen Daten auf Tupelebene betrachten und dies ermöglicht, dass solche Operationen wie der temporale Join im NONSEQUENCED-Modus nachprogrammiert werden können. SEQUENCED SEQUENCED-Anfragen werden als Anfragen betrachtet, wo die Zeiträume der temporalen Daten in jeder Aktion berücksichtigt werden. Die referenzierten Tabellen einer SEQUENCED-Anfrage müssen stets Gültigkeitszeit beinhalten. Das Ergebnis einer SEQUENCED-Anfrage ist stets eine Tabelle mit einer Gültigkeitszeit. Das führt dazu, dass SEQUENCED-Anfragen ineinander geschachtelt über FROM-Unteranfragen legal sind, sogar wenn die FROM-Unteranfrage einen temporalen Join enthält, da SEQUENCED garantiert, das stets nur ein Attribut mit dem Namen VT, welches die Gültigkeitszeit beschreibt, vorhanden ist. <TIntv> An dieser Stelle lässt sich in einfachen Anführungsstrichen in Intervallnotation angeben, für welchen Zeitraum die Tabelle abgefragt werden soll. Im Ergebnis sind dann keine Tupel mehr vorhanden, die außerhalb des angegebenen Zeitraums gültig sind. COALESCED COALESCED bedeutet, dass die Ausgabetabelle der Anfrage automatisch COALESCED ist, das heißt benachbarte identische Tupel zu einem Tupel verschmolzen werden. 5.2 Funktionen der PostgreSQL Extension Nachdem die SQL-Erweiterung definiert wurde, ist in einigen der neu definierten Kommandos deutlich geworden, das neue Funktionen und Trigger nötig sind, welche es ermöglichen neue temporale Daten hinzuzufügen, zu verändern, zu löschen oder anzufragen. Diese Funktionen werden als Extension von PostgreSQL realisiert, da man diese leicht auf neue Systeme installieren kann und sie schnell einsatzbereit sind. Dabei ist ein Großteil der Funktionen in der PostgreSQL eigenen Sprache PL/pgSQL geschrieben. Lediglich die Funktion, welche die SQL-Erweiterung von Kapitel 5.3 übersetzt, ist mithilfe von C geschrieben. Die Erweiterung besteht dabei aus vier verschiedenen Elementen; die Funktionen, welche die Allen-Relationships realisieren, DDL/DML-Funktionen, Anfrage-Hilfsfunktionen und eine Übersetzungsfunktion. Zunächst sollen die Funktionen der Allen-Relationship betrachtet werden, da sie die Grundlage für alle anderen Implementierungen bilden. 59 5.2.1 Allen-Relationships-Funktionen Mithilfe der Operationen des Range-Datentyps lassen sich die 13 Allen-Relationships darstellen. Diese definierten Funktionen sind nützlich im allgemeinen Kontext der Anfragen sowie für die Programmierung der folgenden Trigger und diversen anderen Funktionen, da sie häufig Anfragen an Intervallpaare haben (beispielsweise ob zwei Intervalle benachbart sind). In der Tabelle 5.1 sind die Übersetzungen der Relationen zwischen zwei Intervallen beschrieben, in der Tabelle 5.2 die Übersetzung der Relationen zwischen Zeitpunkten und Intervallen. Before(t1 , t2 ) After(t1 , t2 ) During(t1 , t2 ) Contains(t1 , t2 ) Overlaps(t1 , t2 ) Overlapped-by(t1 , t2 ) Meets(t1 , t2 ) Met-by(t1 , t2 ) Starts(t1 , t2 ) Started-by(t1 , t2 ) Finishes(t1 , t2 ) Finished-by(t1 , t2 ) Equals(t1 , t2 ) t1 << t2 t1 >> t2 t1 <@ t2 AND t1 <> t2 t1 @> t2 AND t1 <> t2 t1 && t2 AND t1 < t2 AND upper(t1 ) < upper(t2 ) t1 && t2 AND t1 > t2 AND upper(t1 ) > upper(t2 ) t1 -|- t2 AND t1 < t2 t1 -|- t2 AND t1 > t2 lower(t1 ) = lower(t2 ) AND upper(t1 ) < upper(t2 ) lower(t1 ) = lower(t2 ) AND upper(t1 ) > upper(t2 ) lower(t1 ) > lower(t2 ) AND upper(t1 ) = upper(t2 ) lower(t1 ) < lower(t2 ) AND upper(t1 ) = upper(t2 ) t1 = t2 Tabelle 5.1: Übersetzung der 13 Relationships-Arten von Allen zu Operationen auf den Range-Datentyp Before(p, t) After(p, t) Meets(p, t) Starts(p, t) Met-by(p, t) Finishes(p, t) During(p, t) p < lower(t) p > upper(t) p = lower(t) p = upper(t) p <- t Tabelle 5.2: Übersetzung der Relationen zwischen Zeitpunkt und Zeitintervall Es ist dabei überflüssig, eigene Methoden für den Vergleich zwischen Zeitpunkten anzulegen. PostgreSQL bietet dafür bereits eigene Methoden an, die dafür verwendet werden können. Es ist zudem auch schwierig diese Methoden generisch anzulegen; PostgreSQL bietet nur begrenzte Unterstützung für polymorphe Programmierung via den Pseudo-Typen anyelement und dessen Spezialisierungen an (vergleiche Tabelle 5.3 1 ). 1 http://www.postgresql.org/docs/9.3/static/datatype-pseudo.html 60 Name anyelement anyarray anynonarray anyenum anyrange Beschreibung Beliebiger Datentyp Beliebiges Array anyelement ohne Arrays Beliebiges Enum Beliebige Range Tabelle 5.3: Übersetzung der Relationen zwischen Zeitpunkt und Zeitintervall So sind die Funktionsdefinitionen der Allen-Relationships von der Polymorphie der Pseudotypen abhängig. Bei den Operationen zwischen Zeitintervallen sind die Definitionen mit zwei anyrange Datentypen verzeichnet. In den Relationen zwischen Zeitpunkten und Zeitintervallen ist der Zeitintervall-Datentyp mit anyrange verzeichnet, wohingegen der Zeitpunkt vom Typ anyelement ist. Wenn jetzt zusätzlich Funktionen für Punkt zu Punkt Relationen verzeichnet wären, müssten ihre Eingabewerte den Datentyp anyelement tragen. Dadurch würden sie die vorherigen Definitionen überdecken, da anyelement auch Ranges beinhaltet. Es gibt keine Möglichkeit, eine Range aus dem Datentyp auszuschließen. Dadurch entsteht die Einschränkung, dass die Allen-Relationships für Zeitpunktpaare nicht mit der passenden Benennung implementiert werden können. Da jedoch in PostgreSQL passende Operatoren existieren, die den gleichen Zweck erfüllen, schränkt das die Möglichkeiten zur Verarbeitung der Zeit nicht ein. Da nur drei Relationen von Allen für Punktpaare beschrieben wurden (Before, After und Equals) und diese mit logischen Vergleichsoperatoren (<, > und =) logisch zu ersetzen sind, ist dies auch keine Einschränkung für die Nutzer. 5.2.2 Funktionen für DDL/DML Da die SQL-Erweiterung es ermöglicht, DDL/DML im temporalen Kontext auszuführen, werden Funktionen benötigt, welche die nicht-temporalen Versionen ergänzen, falls die Anfrage das benötigt. So gilt, dass wenn ein DDL/DML-Kommando Syntax verwendet, welche im jetzigen SQL-Standard nicht vorgesehen war, eine von den hier definierten Funktionen aufgerufen werden muss, da die bereits vorhanden CREATE TABLE, DELETE und UPDATE in temporalen Datenbanken die benötigten Anforderungen nicht erfüllen. temporal create(tbl name TEXT, columns TEXT, vt BOOLEAN, coalesced BOOLEAN, tt BOOLEAN) Das temporal create führt als ersten Schritt stets ein CREATE TABLE der Form 5.1 aus. Dabei werden die TEXT-Variablen tbl name und columns in die CREATE TABLEAnweisung eingebunden und dadurch dynamisch eine Tabelle mit den Namen tbl name und den in columns definierten Spalten erstellt. 61 Nun existieren noch drei weitere boolsche Argumente, welche den Typ der Tabelle kennzeichnen. Falls vt wahr ist, wird die Tabelle mit zwei ALTER TABLE-Anweisungen mit einem neuen Schlüsselattribut VT erweitert. Dieses ist vom Typ RANGE und enthält die Gültigkeitszeit. Es sind zwei ALTER TABLE-Anweisungen notwendig, da die erste Anweisung das Attribut hinzufügen muss und die zweite Anweisung den Primärschlüssel-Constraint um VT erweitert. Anschließend bekommt die Tabelle einen temporal unique trigger(). 1 CREATE TABLE tbl name ( 2 columns 3 ) Quelltext 5.1: temporal create CREATE TABLE-Anweisung Falls coalesced wahr ist, wird der Tabelle der Trigger temporal coalesce trigger() hinzugefügt. Das garantiert, dass zeitlich benachbarte identische Tupel zusammengefasst werden. Ist tt wahr, so wird ähnlich wie bei VT ein neuer Primärschlüssel TT hinzugefügt, welcher die Transaktionszeit repräsentiert. Da das jedoch nicht ausreicht, um eine Transaktionstabelle zu erstellen, muss noch der Trigger transaction time trigger() zu der Tabelle hinzugefügt werden. temporal delete(tbl name TEXT, vt anyrange, where text TEXT DEFAULT ’true’) temporal update(tbl name TEXT, vt anyrange, set text TEXT, where text TEXT DEFAULT ’true’) Die Operationen temporal delete und temporal update funktionieren wie beschrieben im Kapitel 3.1.3. Dabei sind die Argumente der Funktionen selbsterklärend. Die Löschfunktion besitzt ein optionales Argument mit der WHERE-Klausel, falls es benötigt wird. Die UPDATE-Funktion hat ein Argument für die SET-Klausel und wie die Löschfunktion ein Argument für die WHERE-Klausel. 5.2.3 Trigger Jeder vorgestellte Trigger der temporalen Tabellen soll eine Integritätsbedingung erzwingen. Das heißt, dass diese Trigger durch eine Änderung der Daten aktiviert werden und den Vorgang abbrechen, falls eine der Integritätsbedingungen verletzt wurde. Die Herausforderung hierbei ist, dass diese Trigger nicht vollständig dynamisch erzeugt werden. Wenn mit CREATE TRIGGER ein Trigger an eine Relation gebunden wird, so wird bei jeder angegebenen Aktion auf dieser Relation eine Funktion aufgerufen, die beim CREATE TRIGGER angegeben wird. Ein Beispiel für eine solche Aktion ist im Quelltext 5.2 zu sehen. Dabei ist zu sehen, dass bei jedem INSERT oder UPDATE von VT die Funktion temporal_unique_trigger() für jede veränderte Zeile aufgerufen wird. Dieser Aufruf wird dynamisch im temporal_create(...) erzeugt und aufgerufen, wenn eine temporale Tabelle mit der entsprechenden Integritätsbedingung angelegt werden soll. 62 1 2 3 4 CREATE TRIGGER ang valid time constraints trigger BEFORE INSERT OR UPDATE OF vt ON ang FOR EACH ROW EXECUTE PROCEDURE temporal unique trigger(); Quelltext 5.2: CREATE TRIGGER-Beispiel Jedoch wird die Funktion temporal_unique_trigger() nicht dynamisch erzeugt und wurde einmal programmiert. Sie gilt damit als Blaupause für alle Trigger, die diese Integritätsbedingungen überprüfen und erzeugen, vollkommen unabhängig von der Tabelle, an der sie gebunden wird. Wie das funktioniert, wird im nächsten Abschnitt erklärt. Zunächst wird erläutert, welche Integritätsbedingungen der Trigger erfüllt und anschließend anhand eines Beispiels beim temporal_unique_trigger() wie das Prinzip der Blaupause funktioniert, welches analog zu den anderen Triggern verwendet wird. temporal unique trigger() Der Trigger wird aktiviert, wenn entweder neue Daten in die Tabelle eingefügt oder das Attribut VT eines Tupels verändert wird. Er verhindert, dass zwei Tupel mit den gleichen Schlüsselattributen (ohne VT) überschneidende Zeitpunkte besitzen. So wird verhindert, dass das UPDATE oder INSERT ausgeführt wird. Deswegen triggert ein DELETE den Trigger nicht; die temporale Einzigartigkeit kann damit nicht verletzt werden. Um den Trigger zu implementieren, werden via der selbst entwickelten Hilfsmethode get primary keys(tbl name TEXT) die Primärschlüssel ermittelt. Sie verwendet dafür die PostgreSQL Katalogtabelle pg attribute. Nun wird im Trigger via der NEW-Variable überprüft, ob bereits ein Datensatz mit den Schlüsselattributen und überschneidenden Zeitpunkten existiert. Dabei muss darauf geachtet werden, dass bei einem UPDATE der alte Datensatz ignoriert wird. Falls die obige Bedingung erfüllt wird, so wird eine Exception geworfen und die Datenmanipulation abgebrochen. 63 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 CREATE OR REPLACE FUNCTION temporal unique trigger() RETURNS trigger AS $$ DECLARE breaks constraint boolean; primary keys text[]; tt where TEXT := ’’; BEGIN primary keys := get primary keys(TG TABLE NAME); primary keys := array remove(primary keys, ’vt’); FOR i IN array lower(primary keys, 1)..array upper(primary keys, 1) LOOP primary keys[i] := ’tbl.’ || primary keys[i] || ’ = ($1).’ || primary keys[i]; END LOOP; IF (TG OP = ’INSERT’) THEN EXECUTE ’SELECT true = any(SELECT ’ || concat ws(’ AND ’, VARIADIC primary keys) || ’ FROM ’ || TG TABLE NAME || ’ AS tbl ’ || ’WHERE tbl.vt && ($1).vt’ || ’)’ USING NEW INTO breaks constraint; ELSIF (TG OP = ’UPDATE’) THEN EXECUTE ’SELECT true = any(SELECT ’ || concat ws(’ AND ’, VARIADIC primary keys) || ’ FROM ’ || TG TABLE NAME || ’ AS tbl ’ || ’WHERE tbl.vt && ($1).vt AND tbl.∗ <> $2’ || ’)’ USING NEW, OLD INTO breaks constraint; END IF; IF breaks constraint THEN RAISE EXCEPTION ’UNIQUE TEMPORAL CONSTRAINT VIOLATED’; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; Quelltext 5.3: temporal unique trigger() Im Quelltext 5.3 ist die Implementierung zu sehen. In der ersten Zeile ist zu sehen, wie die Funktionsdefinition aussehen muss, wenn ein Trigger programmiert werden soll. Die Funktion darf zunächst keine Argumente direkt entgegennehmen. Es ist jedoch möglich, Argumente zu benutzen, indem die spezielle Variable TG_ARGV[] verwendet wird. Beim CREATE TRIGGER-Aufruf können die Argumente wie bei jedem anderen Funktionsaufruf auch übergeben werden, lediglich in der Prozedur selbst müssen sie über die speziellen Variablen zugegriffen werden. Der Grund für die umständliche Definition ist wohl historisch. Neben TG_ARGV[] existieren weitere spezielle Variablen, die beim Aufruf implizit gesetzt werden. Interessant sind TG_OP, welche die Operation des Aufrufs beschreiben, beispielsweise ein Update oder ein Insert, NEW welches bei einem INSERT den eingefügten Tupel darstellt und bei einem UPDATE den veränderten Tupel, OLD welches bei einem UPDATE den veränderten Tupel vor seiner Änderung darstellt und beim DELETE den gelöschten Tupel annimmt und als letztes TG_TABLE_NAME welches 64 den Namen der Tabelle enthält, an welchen der Trigger gebunden ist. Mithilfe dieser Variablen lässt sich die Blaupause erst erzeugen. Die definierte Funktion hat als Rückgabetyp einen Trigger, obwohl in der Implementierung ein Tupel zurückgeliefert wird. Dieser Tupel ist abhängig von der Operation, die den Trigger aufruft. So benötigt ein DELETE den Tupel, der gelöscht werden soll, das UPDATE den veränderten Tupel und das INSERT den eingefügten. Falls NULL zurückgegeben wird, so wird die Operation einfach nicht ausgeführt. Mit dieser Mechanik ist es beispielsweise möglich, Tupel vor dem Einfügen zu manipulieren. In den Zeilen 7-12 wird nun mithilfe der speziellen Variable TG_TABLE_NAME und der Funktion get_primary_keys(<name>), welche in PostgreSQL so noch nicht existierte und im Rahmen der Ausarbeitung hinzugefügt wurde, der Primärschlüssel der Tabelle extrahiert und zu einem logischen Ausdruck zusammengefasst. In den Zeilen 14-26 wird dann bei einem INSERT oder UPDATE überprüft, ob der neu eingefügte oder veränderte Tupel sich mit einem bereits in der Tabelle bestehenden Tupel schneidet. Dafür wird mithilfe von EXECUTE und den speziellen Variablen dynamisches SQL erzeugt, welches unabhängig von der Tabelle die gewünschte Anfrage ergibt. Anschließend wird in den Zeilen 28-30 überprüft, ob die vorherigen Anfragen ein Ergebnis hatten und falls das der Fall ist, wird die Aktion abgebrochen, ansonsten wie gewollt ausgeführt. Das System ist extrem flexibel und benötigt, dass weniger Code dynamisch erzeugt werden muss. Für die anderen Trigger funktioniert das analog. temporal referential trigger() Um temporale referentielle Integrität zu implementieren, werden zwei verschiedene Trigger benötigt, da zwei Tabellen überprüft werden müssen; die referenzierte und die referenzierende Tabelle. Dabei werden diesmal Parameter für die Trigger benötigt, die via TG ARGV abgefragt werden können. Diese werden für die Angabe der Spalten benötigt, die referenziert werden. Die Implementierung richtet sich dabei an den definierten Check aus dem Kapitel 3.1.1. temporal coalesce trigger() Dieser Trigger richtet sich nach dem COALESCEDAttribut von VTSQL2, welches bestimmt, dass zeitlich benachbarte Daten mit ansonsten identischem Tupelinhalt zeitlich verschmolzen werden. Dafür wird nach jedem INSERT und UPDATE von VT, ähnlich wie beim temporal unique trigger(), überprüft, ob für die veränderten Daten nun identische, zeitlich benachbarte Tupel existieren. Falls das der Fall ist, wird der Tupel gelöscht und der Tupel aus NEW angepasst. Anschließend wird der Algorithmus nochmal ausgeführt bis es keine benachbarten Daten mehr gibt. Dadurch werden keine unnötigen Tupel angeschaut und die Methode bleibt effektiv. transaction time trigger() Der Trigger wird aktiviert, wenn ein beliebiges UPDATE oder DELETE auf der Tabelle geschieht. Zunächst wird sowohl bei einem Update als auch bei einem Delete überprüft, ob der betroffene Tupel den jetzigen Zeitpunkt enthält. Falls das nicht der Fall ist, so geschieht bei diesem Tupel nichts. Bei einem Update kopiert es den Tupel, passt den 65 TT-Zeitstempel an und wendet anschließend die UPDATE-Veränderung an. Bei einem DELETE werden bei allen beschriebenen Tupeln die TT-Zeitstempel abgeschlossen. Dabei gibt es jedoch noch ein Problem; wenn im Trigger selbst DELETE oder UPDATE verwendet wird, kommt es zu einer unendlichen Rekursion. Das lässt sich umgehen, indem mit INSERT gearbeitet wird. So wird beim UPDATE lediglich eine angepasste Kopie des OLD-Records via INSERT eingefügt und beim DELETE wird vor dem Löschvorgang auch der angepasste OLD-Record eingefügt, sodass er zweimal existiert, jedoch einer von ihnen gelöscht wird. Zusätzlich müssen die oben genannten DDL/DML-Funktionen und Trigger der Valid Time-Tabellen angepasst werden, sodass sie nur die Tupel beachten, welche den jetzigen Zeitpunkt im TT-Zeitraum besitzen. 5.2.4 Anfrage-Hilfsfunktionen Anfrage Hilfsfunktionen werden bei der Übersetzung benutzt, um Operationen effizienter und eleganter auszuführen. coalesced(tbl name TEXT, attr TEXT) Die Coalesced Funktion unterscheidet sich von den vorher genannten Funktionen. Die vorherigen waren Aktionen, das heißt wenn sie ausgeführt worden sind, hat sich der Datenbestand verändert. Coalesced ist jedoch eine Funktion, die im Rahmen einer Anfrage aufgerufen werden soll. Ihr Rückgabewert ist ein SET OF RECORDS. Das bedeutet, dass sie eine Tabelle zurückgibt. 1 SELECT ∗ 2 FROM coalesced( 3 ’(SELECT name, vt 4 FROM ang 5 WHERE gehalt > 5250)’, 6 ’name’ 7 ) AS (Name TEXT, vt DATERANGE) Quelltext 5.4: Beispiel Coalesced Im Quelltext 5.4 ist ein Beispiel für den Aufruf der Funktion gegeben. Benutzt wird sie in der FROM-Klausel und das Ergebnis wird wie eine Tabelle behandelt. Es existieren zwei Parameter. Der erste Parameter ist die Unteranfrage, welche am Ende Coalesced sein soll. Der zweite Parameter sind die Spalten, nach denen zusammengefasst werden soll. Da PostgreSQL zur Laufzeit die resultierende Tabelle nicht kennt, muss anschließend im Alias-Abschnitt eine Tabellendefinition angegeben werden. Der zweite Parameter und die Tabellendefinition sind dabei syntaktischer Overhead, da beides aus der inneren Anfrage ermittelt werden kann. In der SQL-Erweiterung kann dies ausgenutzt werden, um die Anfrage zu konstruieren. 66 relation split(vt anyrange, rel TEXT) Die Funktion relation split spaltet die Tupel der Relation rel auf. Die Aufspaltung basiert dabei auf dem ersten Argument vt. Ein Beispiel wäre, wenn die Funktion aufgerufen mit relation split(’[2011-01-01, 2012-01-01)’, ’ang’) wird, wobei die Ausprägung von ang wie in der Tabelle 5.4 angegeben wird. Nr 1 1 2 2 Name Max Max Bob Bob Gehalt 2000 2500 2600 3000 VT [2010-01-01,2013-01-01) [2013-01-01,) [2010-01-01,2014-01-01) [2014-01-01,) Tabelle 5.4: Ausgangsausprägung für das temporale Löschen Die Ausgabe der Funktion ist wie bei COALESCED ein Set aus Tupeln, welche in diesem Fall so aussehen wie in Tabelle 5.5 angegeben. Nr 1 1 1 1 2 2 2 2 Name Max Max Max Max Bob Bob Bob Bob Gehalt 2000 2000 2000 2500 2600 2600 2600 3000 VT [2010-01-01,2011-01-01) [2011-01-01,2012-01-01) [2012-01-01,2013-01-01) [2013-01-01,) [2010-01-01,2011-01-01) [2011-01-01,2012-01-01) [2012-01-01,2014-01-01) [2014-01-01,) Tabelle 5.5: Ausgangsausprägung für das temporale Löschen Das ist besonders nützlich bei Unteranfragen in der WHERE-Klausel, da es erlaubt, diese ungeachtet der Gültigkeitszeit zu betrachten. 5.3 Übersetzung der SQL-Erweiterung Nachdem die Syntax und Semantik der SQL-Erweiterung definiert wurde, fehlt noch die Übersetzung der SQL-Erweiterung in einen SQL-Dialekt, den PostgreSQL versteht. Dafür werden die aus dem vorherigen Kapitel entwickelten Hilfsmethoden verwendet, um eine einfache Übersetzung zu ermöglichen, die sich einfach verändern und entwickeln lässt. Der Vorteil, so viele Hilfsfunktionen der Extension zu verwenden, besteht darin, falls zusätzliche Funktionalität benötigt wird oder ein Fehler sich eingeschlichen hat, dass der komplexe Übersetzungsprozess nicht angefasst werden muss, sondern lediglich die 67 Funktion verändert werden muss und somit das Ergebnis der Übersetzung nicht anders ist, jedoch die endgültige Ausführung sich verändert. In diesem Kapitel wird zunächst der Parser und Lexer betrachtet, welcher die Sprache liest. Anschließend wird der Übersetzungsvorgang zu einem AST betrachtet, welcher den Zwischenschritt zur Transformation zu Standard-SQL darstellt. Als letzter Schritt wird die Übersetzung von der erweiterten SQL-Syntax in Standard-SQL anhand von Beispielen veranschaulicht. 5.3.1 Parser Zunächst wird ein Lexer und Parser in Bison und Flex entwickelt, welcher aus dem erweiterten SQL-Befehl zunächst einen AST generiert. Der Parser ist dabei eine Erweiterung des Beispielparser, welcher in [Lev09] zu finden ist. In dieser Lektüre wurde als Beispiel ein rudimentärer SQL-Parser entwickelt. In der Ausarbeitung wurde dieser vervollständigt, um einen Großteil des SQL-Standards zu parsen. Ansätze für die ASTGenerierung in diesem Beispielparser wurden bereits in einem anderen Projekt realisiert, welches auf der Webseite 2 zu finden ist. Diese Ansätze wurden als Inspiration und Basis genutzt, um die eigene AST-Generierung zu implementieren. Der generierte AST wird anschließend benutzt, um eine mit PostgreSQL kompatible Anfrage zu generieren. Dafür muss jedoch zunächst entschieden werden, welche Syntaxelemente in der Erweiterung zum traditionellen SQL hinzukommen. Die Entscheidung einen Parser mit Bison zu entwickeln entstand, weil einfache Mittel wie reguläre Ausdrücke keine beliebige Rekursionstiefe erlaubt hätten. Damit wären verschachtelte Anfragen wesentlich schwieriger zu übersetzen gewesen. Außerdem erlaubt ein Parser leichtere Erweiterbarkeit mit Elementen und Bison wird auch von PostgreSQL zum lesen der Anfragen benutzt. 5.3.2 Abstract Syntax Tree (AST) Bison und Flex ermöglichen es, direkt beim parsen Code auszuführen. Dadurch lassen sich Logging-Funktionalitäten hinzufügen, jedoch auch ein Übersetzer einfügen. Die Übersetzung direkt beim Lesen auszuführen wäre jedoch nicht möglich gewesen, da beim Übersetzungsvorgang eine Datenbankanbindung nötig ist. Die Datenbankanfragen während des Parsevorgangs auszuführen wäre demnach weder performant, noch modular. So lässt sich der AST gänzlich ohne Datenbankanbindung generieren und testen, was für den Entwicklungsvorgang von Vorteil war. In 5.5 ist ein Quelltext vermerkt, der in der Abbildung 5.6 als Syntaxbaum zu sehen ist. Das navigieren der beliebig tiefen Knoten in beispielsweise dem Tables-Abschnitt ist via dem Visitor-Pattern einfach umzusetzen und dafür geeignet, eine inkrementelle Übersetzung vorzunehmen. 2 https://github.com/hoterran/sqlparser 68 Statement Select IsSequenced True Columns name gehalt Tables Where ang Bool Comparison Operator Variable Operator Int gehalt > 5250 AND Comparison Variable Operator String name = ’Bob’ Abbildung 5.6: Beispiel AST 1 SEQUENCED SELECT name, vt 2 FROM ang 3 WHERE gehalt > 5250 AND name = ’Bob’ Quelltext 5.5: Beispiel Anfrage für AST-Umwandlung 5.3.3 Übersetzung Die Übersetzung der erweiterten SQL-Syntax in PostgreSQL-kompatibles SQL passiert mithilfe eines vorher kompilierten C-Moduls innerhalb des Extension-Ökosystems von PostgreSQL. Dabei ist der Parser, welcher den Quellcode, wie zuvor erläutert, in den AST umwandelt, vollkommen unabhängig von der Erweiterung. Das C-Modul bietet eine Methode sql translate an, welche einen String als Parameter anbietet. Dieser String soll die SQL-Anweisung mit der erweiterten Syntax beinhalten. In der Abbildung 5.7 ist der Ablauf vom sql translate Aufruf bis zum übersetzten Ausdruck zu verfolgen. Dabei wird zunächst der von Bison und Flex generierte Parser aufgerufen und anschließend der AST durchlaufen und ein neuer SQL-Befehl zusammengesetzt. Dieser wird anschließend zurückgegeben und an die Applikation weitergereicht. Zwischen der Rückgabe des übersetzten Befehls und der Generierung des ASTs kann es zur Kommunikation mit dem PostgreSQL Server kommen, da manche der Befehle abhängig von der Struktur der Tabelle sind. Im Folgenden werden anhand von Beispieltransformationen gezeigt, wie der Aufbau der Befehle durch die C-Methode realisiert wurde. 69 Temporales Create 1 CREATE VALIDTIME COALESCED TABLE ang( 2 Nr INT, 3 Name TEXT, 4 Gehalt INT, 5 PRIMARY KEY (Nr) 6 ) Quelltext 5.6: Temporal Create vor der Transformation 1 SELECT temporal create( 2 ’ang’, 3 ’Nr INT, Name TEXT, Gehalt INT, PRIMARY KEY (Nr)’, 4 true, 5 true, 6 false 7 ) Quelltext 5.7: Temporal Create nach der Transformation Die Übersetzung der temporalen CREATE-Anweisung besteht aus wenigen Schritten. Zunächst wird die Zeile 1 aus dem Quelltext 5.7 zur Ausgabe hinzugefügt. Diese ist für jeden VALIDTIME Create identisch, benötigt demnach keine Analyse der Eingabe. Dann wird, wie in der Zeile 2 zu sehen ist, der Name in einfachen Anführungsstrichen mit einem Komma am Ende angehängt. Anschließend werden die Spaltendefinitionen von Zeile 2-5 in einfachen Anführungsstrichen hinzugefügt. Die überflüssigen Zeilenumbrüche werden dabei entfernt. Die drei boolschen Parameter, die anschließend Teil der Operation sind, bezeichnen den Typ der Tabelle. Der erste bestimmt, ob die Tabelle VALIDTIME besitzt. Falls diese auf true gesetzt ist, kann der nächste Operator true oder false sein; er zeigt an, ob die Tabelle COALESCED ist. Der letzte Parameter ist für die Transaction Time zuständig. Falls die Tabelle WITH SYSTEM VERSIONING ausgezeichnet ist, so gilt, dass der Parameter auf true gesetzt wird. Ansonsten wird er auf false gesetzt. Die Übersetzung ist in einem SELECT verpackt, da dies die Syntax für Funktionenaufrufe in PostgreSQL ist. Das gleiche gilt für alle anderen Kommandos, welche die Daten definieren oder manipulieren (abgesehen vom INSERT, welches keine spezielle Syntax besitzt). Temporales Update 1 UPDATE ang 2 FOR PORTION OF vt 3 FROM DATE ’2014-01-01’ 4 TO DATE ’’ 5 SET gehalt = 3000 6 WHERE name = ’Bob’ Quelltext 5.8: Temporales Update vor der Transformation 70 1 SELECT temporal update( 2 ’ang’, 3 ’[2014-01-01,)’::DATERANGE, 4 ’gehalt = 3000’, 5 ’name = ”Bob”’ 6 ) Quelltext 5.9: Temporales Update nach der Transformation Ein temporales Update besteht aus vier Elementen: dem Tabellennamen, der Zeitangabe, einer SET-Klausel und einer optionalen WHERE-Klausel. Die Tabelle, SET und WHERE-Klausel werden in die temporal update Funktion eingesetzt. Dabei ist darauf zu achten, dass einfache Anführungszeichen escaped werden müssen, das heißt sie müssen gedoppelt werden, da sonst der String vorzeitig beendet und die Syntax für den Aufruf der temporal update Funktion damit ungültig wird. Die Zeitangabe ist zusammengesetzt aus zwei Zeitpunkten, die aus dem temporalen Update zu extrahieren sind. Der Datentyp DATERANGE ist dabei auch mit angegeben. Wäre die Angabe FROM TS ’...’ TO TS ’...’ wäre der Typ TSRANGE, welches einen Timestamp-Zeitraum darstellt. Das ermöglicht selbst-definierte Datentypen für Zeitangaben in temporalen Updates zu verwenden. In diesem Fall ist die Angabe für den Endzeitpunkt leer, das heißt das FOREVER als Endzeitpunkt gilt. Dieses Vorgehen ist identisch zu der PostgreSQL-Syntax für Intervalle, die unendlich in die Zukunft reichen. Temporales Delete 1 DELETE FROM ang 2 FOR PORTION OF vt 3 FROM DATE ’2014-01-01’ 4 TO DATE ’2015-01-01’ 5 WHERE name = ’Bob’ Quelltext 5.10: Temporales Update vor der Transformation 1 SELECT temporal delete( 2 ’ang’, 3 ’[2014-01-01,)’::DATERANGE, 4 ’name = ”Bob”’ 5 ) Quelltext 5.11: Temporales Update nach der Transformation Temporales Delete funktioniert analog zum temporalen Update. Bei der Transformation gibt es diesmal keine SET-Spalte, deshalb besitzt die resultierende Ausgabe einen Parameter weniger. 71 Temporale Schnappschussanfrage 1 SELECT a.name, a.gehalt 2 FROM ang a Quelltext 5.12: Abwärtskompatible Anfrage nach den aktuellen Angestellten 1 SELECT a.name, a.gehalt 2 FROM ( 3 SELECT nr, name, gehalt 4 FROM ang 5 WHERE ’[now,now]’ <@ vt 6 ) AS a Quelltext 5.13: Transformation von Anfrage 5.12 Die temporale Tabelle ang wird mit einer abwärtskompatiblen Schnappschussanfrage abgefragt. Die Anfrage soll dabei die temporalen Elemente der Tabelle verwerfen und lediglich die aktuell gültigen Tupel anzeigen. Die Gültigkeitszeit ist in der Ausgabe der Anfrage nicht enthalten. Bei der Transformation wird demnach zuerst überprüft, ob die angefragten Tabellen temporal sind. Die Erweiterung erfragt dafür beim DBMS via der SPI-Schnittstelle, ob die Tabelle ein VT-Attribut enthält. Wenn das der Fall ist, so wird diese Tabelle durch eine innere Anfrage in der FROM-Klausel ersetzt. Diese innere Anfrage benutzt den vorgestellten <@ Operator, um auf Daten einzuschränken, dessen VT den Zeitpunkt NOW enthalten. Dabei wird speziell mit einem Intervall mit genau einem Element gearbeitet, da das einen flexiblen Range-Datentyp für VT erlaubt. Wäre VT nicht vom Typ DATERANGE sondern TSRANGE so wäre der Aufruf ’[now,now]’ <@ vt weiterhin in Ordnung, da NOW auch ein valider Timestamp ist. Dabei besitzt die Unteranfrage noch den gleichen Alias wie in der temporalen Schnappschussanfrage, damit die Felder in SELECT-Klausel nicht umbenannt werden müssen. Im SELECT der Unteranfrage werden alle Spalten außer VT ausgewählt, damit das Ergebnis vollkommen unabhängig von der VALID TIME ist. 1 SELECT ang.name, ang.gehalt, projekt.name 2 FROM ang NATURAL JOIN projekt Quelltext 5.14: Abwärtskompatible Anfrage mit einer nicht temporalen Tabelle 1 SELECT ang.name, ang.gehalt, projekt.name 2 FROM ( 3 SELECT nr, name, gehalt 4 FROM ang 5 WHERE ’[now,now]’ <@ vt 6 ) AS ang NATURAL JOIN projekt Quelltext 5.15: Transformation von Anfrage 5.14 Sei Projekt eine nicht temporale Tabelle; dadurch gilt bei der Anfrage, dass Projekt nicht durch eine innere Anfrage ersetzt wurden muss. Zu sehen ist auch, wenn einer 72 temporalen Tabelle kein Alias vergeben wurde. Dann ist der Alias der inneren Anfrage dieser temporalen Tabelle der Name eben dieser Relation. Dadurch werden komplizierte Umformungen der SELECT-Klausel umgangen. Sequenced Select 1 SEQUENCED SELECT name 2 FROM ang 3 WHERE Gehalt > 5250 Quelltext 5.16: Historische Anzeige aller Mitarbeiter die über 5250 verdienen 1 SELECT name, ang.vt vt 2 FROM ang 3 WHERE Gehalt > 5250; Quelltext 5.17: Transformation von Anfrage 5.17 Bei einer sequentiellen Anfrage wird garantiert, dass die Ausgabetabelle eine Spalte VT mit der Gültigkeitszeit des Tupels enthält. Dadurch verändert sich bei einem SEQUENCED SELECT an eine temporale Tabelle ohne Angabe eines Zeitrahmens nicht viel. Lediglich die SELECT-Klausel wird um die Gültigkeitszeit erweitert. Dadurch entsteht auch eine Bedingung für SEQUENCED SELECT: Jede in der FROM-Klausel referenzierte Tabelle muss eine Spalte VT für die Gültigkeitszeit besitzen. 1 SEQUENCED SELECT a1.name a1name, a2.name a2name 2 FROM ang a1, ang a2 Quelltext 5.18: Valid Time Join 1 SELECT a1.name AS a1name, a2.name AS a2name, a1.vt ∗ a2.vt vt 2 FROM ang AS a1, ang AS a2 3 WHERE 1 = 1 AND a1.vt && a2.vt Quelltext 5.19: Transformation von Anfrage 5.18 Falls zwei oder mehr Tabellen in der FROM-Klausel auftreten, wird ein temporaler Join eingefügt. Dafür sind zwei Schritte nötig: zunächst muss in der WHERE-Klausel sichergestellt werden, dass nur Tupel angezeigt werden, die gemeinsame Zeitpunkte haben und in der SELECT-Klausel muss der Schnitt der beiden Zeiträume für den Tupel angezeigt werden. Um gemeinsame Zeitpunkte zu garantieren wird der &&-Operator verwendet. Dafür werden alle möglichen Kombinationen aus den angegebenen Tabellen der FROM-Klausel verwendet. Das 1 = 1 am Anfang der Bedingungen soll dabei die Programmierung erleichtern, da dann in der jeder Zeile lediglich AND <Ausdruck> hinzugefügt werden muss, ohne darauf zu achten, ob es sich um den ersten Eintrag handelt. 1 SEQUENCED SELECT a1.name a1name, a2.name a2name, a3.name a3name 2 FROM ang a1, ang a2, ang a3 Quelltext 5.20: Valid Time Join 2 73 1 SELECT a1.name AS a1name, a2.name AS a2name, a3.name AS a3name, a1.vt * a2.vt * a3.vt vt 2 FROM ang AS a1, ang AS a2, ang AS a3 3 WHERE 1 = 1 AND a1.vt && a2.vt AND a1.vt && a3.vt AND a2.vt && a3.vt; Quelltext 5.21: Transformation von Anfrage 5.20 Die Angabe aller möglichen Kombinationen in der WHERE-Klausel ist leichter zu sehen, wenn drei oder mehr Tabellen angefragt werden. Die möglichen Kombinationen sind in diesem Fall a1, a2 & a1, a3 & a2, a3. Wenn alle drei Bedingungen gelten, so gibt es auch keine Möglichkeit, dass die Ausgabe des *-Operators aus der SELECT-Klausel leer ist. 1 SEQUENCED ’[2000-01-01, 2000-09-01)’ SELECT name 2 FROM ang 3 WHERE Gehalt > 5250 Quelltext 5.22: Anfrage mit SEQUENCED und Zeitraumangabe 1 2 3 4 5 6 7 SELECT name, tbl.vt ∗ ’[2000-01-01, 2000-09-01)’ vt FROM ( SELECT name, ang.vt vt FROM ang WHERE Gehalt > 5250 ) tbl WHERE tbl.vt && ’[2000-01-01, 2000-09-01)’ Quelltext 5.23: Transformation von Anfrage 5.22 Falls beim Sequenced noch zusätzlich eine Zeitangabe angegeben wurde, so wird zunächst die Anfrage übersetzt, als würde sie fehlen. Anschließend wird die resultierende Anfrage in der FROM-Klausel als innere Anfrage verpackt, im Quelltext 5.23 in den Zeilen 3-5 zu sehen, und im äußeren Teil wird via der WHERE- und SELECT-Klausel die Angabe auf den angegeben Zeitraum eingeschränkt. Dadurch bleibt die Übersetzung transparent und für weitere Verschachtlungen leichter zu überblicken. Dabei ist anzumerken, dass die in der erweiterten SQL benutzte SELECT-Klausel zweimal benutzt werden muss; in der äußeren und inneren Anfrage. 1 SEQUENCED COALESCED SELECT name 2 FROM ang 3 WHERE Gehalt > 5250 Quelltext 5.24: Anfrage mit Coalesced 1 SELECT ∗ FROM coalesced( 2 ’(SELECT name, ang.vt vt FROM ang WHERE Gehalt > 5250)’, 3 ’name’ 4 ) AS (name text, vt daterange); Quelltext 5.25: Transformation von Anfrage 5.24 74 COALESCED funktioniert ähnlich wie SEQUENCED: es wird zunächst die Anfrage unabhängig vom COALESCED übersetzt und anschließend mit einer inneren Anfrage benutzt. In diesem Fall wird die vorgestellte COALESCED-Funktion verwendet. Es muss beachtet werden, dass beim Einfügen der inneren Anfrage als Zeichenkette die einzelnen Anführungsstriche erneut verdoppelt werden müssen, da sonst die Zeichenkette zu früh abgebrochen wird. Der zweite und dritte Parameter wird via der SPI-Bibliothek ermittelt, indem die innere Anfrage einmal ausgeführt wird, ohne tatsächlich Zeilen aus dem Ergebnis abzufragen, sondern die Spalten und ihren Datentyp zu ermitteln. Für den zweiten Parameter werden die Spaltennamen ohne die VT-Spalte eingefügt. Für die Tabellendefinition darf die Valid Time-Spalte jedoch nicht fehlen, da sie Teil der Ausgabetabelle ist. Durch die Kommunikation mit dem PostgreSQL-Server ist es damit möglich, diese Daten implizit durch den gegebenen erweiterten SQL-Ausdruck weiterzugeben. Das erspart Benutzern eine explizite Angabe, wie es vor der Einführung der Spracherweiterung der Fall war. 1 SEQUENCED ’[2000-01-01, 2000-09-01)’ COALESCED SELECT <-name 2 FROM ang 3 WHERE Gehalt > 5250 Quelltext 5.26: Anfrage mit Coalesced 1 SELECT ∗ FROM coalesced( 2 ’(SELECT name, tbl.vt ∗ ’’[2000-01-01, 2000-09-01)’’ vt 3 FROM ( 4 SELECT name, ang.vt vt 5 FROM ang 6 WHERE Gehalt > 5250 7 ) tbl 8 WHERE tbl.vt && ’’[2000-01-01, 2000-09-01)’’)’, 9 ’name’ 10 ) AS (name text, vt daterange); Quelltext 5.27: Transformation von Anfrage 5.26 Falls sowohl COALESCED als auch eine Zeitangabe existiert, so resultiert dies in einer doppelt verschachtelten Anfrage. Zunächst wird die Anfrage ohne das COALESCED oder die Zeitangabe zu beachten übersetzt. Das Resultat davon wird in die Zeiteinschränkung verpackt und das Ergebnis davon wird anschließend in der COALESCED-Funktion verpackt. Dadurch entsteht eine längliche Anfrage, die jedoch, wenn vernünftig eingerückt, für einen Menschen verständlich aufgebaut ist. Das Verschachteln der Anfragen bei Zeitangaben und COALESCED hat weitere Vorteile. So muss bei der Anpassung der Transformation unabhängig von den beiden Faktoren nicht beachtet werden, ob man die Kompatibilität von ihnen durch das Hinzufügen von speziellen Spalten oder die Änderung der Übersetzungsschritte bricht. Die Übersetzungsschritte sind damit voneinander unabhängig. 75 Nonsequenced Select 1 NONSEQUENCED SELECT ∗ 2 FROM ang a, ang b 3 WHERE a.name = ’Mueller’ AND b.name = ’Meier’ Quelltext 5.28: Anfrage mit Coalesced 1 SELECT ∗ 2 FROM ang AS a, ang AS b 3 WHERE (a.name = ’Mueller’ AND b.name = ’Meier’) Quelltext 5.29: Transformation von Anfrage 5.28 Nichtsequentielle Anfragen werden transformiert, indem das NONSEQUENCEDSchlüsselwort entfernt wird. Falls verschachtelte Anfragen existieren, die komplexere Transformationen benötigen, werden diese vorher behandelt und eingefügt. Verschachtelte Anfragen & Unteranfragen Die bisher vorgestellten Transformationen lassen sich auch mithilfe von regulären Ausdrücken und Suchen/Ersetzen-Operationen realisieren. Somit besteht bis jetzt nur wenig Vorteil, mit dem Parser den AST zu generieren und ihn zu navigieren um Ausdrücke wie die einfache Erweiterbarkeit zu übersetzen. Jedoch ist SQL keine reguläre Sprache. Es existieren kontextfreie Grammatiken um SQL zu erzeugen, damit ist SQL kontextfrei. Das heißt, es existieren auch Ausdrücke, die sich nicht mit Wortersetzungen trivial übersetzen lassen. 1 NONSEQUENCED SELECT x.Nr, x.Name 2 FROM ( 3 SEQUENCED COALESCED SELECT a.Nr, a.Name 4 FROM ang a, ang b 5 WHERE a.ChefNr=b.Nr AND a.Gehalt>b.Gehalt 6 )x Quelltext 5.30: Anfrage mit Coalesced 1 SELECT x.Nr, x.Name 2 FROM ( 3 SELECT ∗ 4 FROM coalesced( 5 ’(SELECT a.Nr, a.Name, a.vt ∗ b.vt vt 6 FROM ang AS a, ang AS b 7 WHERE (a.ChefNr = b.Nr AND a.Gehalt > b.Gehalt) AND a.vt && b.vt)’, 8 ’Nr, Name’ 9 ) AS (nr int4, name text, vt daterange) 10 ) AS x; Quelltext 5.31: Transformation von Anfrage 5.30 76 So existieren in SQL verschachtelte Ausdrücke. In FROM-Klauseln lassen sich beispielsweise SELECT-Anfragen schreiben, die wiederum FROM-Klauseln mit den gleichen Regeln besitzen. Die Rekursionstiefe dieser Ausdrücke ist beliebig. Die Übersetzung umgeht dieses Problem, indem er jeden AST, der eine SELECTAnfrage repräsentiert, auch wie eine SELECT-Anfrage behandelt. Das heißt, jede SELECTAnfrage wird zunächst identisch übersetzt und dementsprechend gespeichert und eingefügt. Im Beispielquelltext 5.30 zu 5.31 ist das zu sehen; dort ist in der FROM-Klausel ein SEQUENCED SELECT anzutreffen. Wenn bei der Navigation des AST die innere Anfrage behandelt wird, ist die Übersetzung identisch zu der äußeren. In diesem Fall ist sie COALESCED, demnach findet dort eine Verschachtelung in die coalesced-Funktion statt. Diese Unteranfrage resultiert in einer temporalen Tabelle. Dadurch ist es möglich, verschachtelte SEQUENCED-Anfragen zu entwerfen, da sie als Voraussetzung haben, dass alle referenzierte Tabellen temporal sind. 1 SELECT name 2 FROM ( 3 SEQUENCED SELECT name, gehalt 4 FROM ( 5 SEQUENCED SELECT nr, name, gehalt 6 FROM ang 7 ) a1 8 ) a2 Quelltext 5.32: Anfrage mit Coalesced 1 SELECT name 2 FROM ( 3 SELECT name, gehalt 4 FROM ( 5 SELECT name, gehalt, a1.vt vt 6 FROM ( 7 SELECT nr, name, gehalt, ang.vt vt 8 FROM ang 9 ) AS a1 10 ) AS a2 11 WHERE ’[now,now]’ <@ vt 12 ) AS a2 Quelltext 5.33: Transformation von Anfrage 5.32 Im Quelltext 5.32 ist eine dreifach verschachtelte Anfrage zu sehen. Dabei ist die äußerste eine abwärtskompatible Schnappschussanfrage. Das Resultat ist dadurch in 5.34 eine vierfach geschachtelte Anfrage, da die äußere Schnappschussanfrage die Tiefe um eins erhöht. Es ist zusätzlich zu beachten, dass bei der Schachtelung der Alias nicht verloren gehen darf, da ansonsten die Spalten aus der SELECT-Klausel ungültig werden. 77 Temporales NOT EXISTS 1 2 3 4 5 6 7 SEQUENCED SELECT a.name, a.gehalt FROM ang a WHERE NOT EXISTS ( SEQUENCED SELECT ∗ FROM ang b WHERE b.gehalt > a.gehalt ) Quelltext 5.34: Temporales NOT EXISTS 1 2 3 4 5 6 7 8 9 10 11 12 13 SELECT nr, name, gehalt, chefnr, a.vt vt FROM ( SELECT nr, name, gehalt, chefnr, relation split(vt, ’ang AS b’) vt FROM ang AS a ) AS a WHERE 1 = 1 AND NOT EXISTS ( SELECT nr, name, gehalt, chefnr FROM ang AS b WHERE 1 = 1 AND a.gehalt > b.gehalt AND a.vt <@ b.vt ); Quelltext 5.35: Transformation von Anfrage 5.34 Das temporale NOT EXISTS bedient sich der relation split-Funktion aus der PostgreSQLExtension um die Tupel der Ausgangsrelation in kleinere Teile aufzuspalten, sodass bei Not Exists nicht darauf geachtet werden muss, wie die Daten innerhalb der inneren Anfrage in temporal in Beziehung stehen. 5.4 PHP-Interface Das Interface für die Bedienung der temporalen Erweiterung, in diesem Fall via PHP und PDO, ist durch die Programmierung trivial. Es muss dafür gesorgt werden, dass eine gegebene SQL-Anweisung in die Funktion der Datenbankerweiterung eingefügt wird und das Ergebnis, welches lediglich eine Zeichenkette oder ein Fehler sein kann, wird von der Sprache entgegengenommen, um anschließend wie üblich ausgeführt zu werden. Dieser Ablauf ist im Sequenzdiagramm 5.7 zu sehen. In PHP heißt das, dass eine neue Klasse erstellt wird, die von PDO erbt. Sei sie in diesem Fall TemporalPDO genannt. Dadurch lässt sich in einer bestehenden Operation jeder Gebrauch von PDO einfach durch TemporalPDO ersetzen, ohne das sich die Funktionalität der Applikation ändert, selbst wenn PHP und PDO ein Update erhalten. Anschließend werden drei neue Funktionen benötigt, welche nun SQL in erweiterter Syntax erwarten; temporalQuery, temporalExec und temporalPrepare. Dabei basieren diese auf den Namensgebern, die bereits in PDO existieren; query, exec und prepare. Wenn 78 :Applikation :TemporalPDO :PostgreSQLServer :Extension :SQLParser RequestData() Prepare(raw tsql) return tsql sql translate(tsql) sql translate(tsql) GenAST(tsql) return ast TranslateLoop WalkAST() opt GetInfo(tbl) return info return part sql return sql return sql Execute(sql) return tbl data return tbl data Abbildung 5.7: Sequenzdiagramm für die Übersetzung der temporalen SQL-Erweiterung 79 eine Zeichenkette mit dem erweiterten SQL an diese Funktionen weitergereicht wird, so wird sie vorbereitet, indem er escaped (das heißt jedes einfache Anführungszeichen wird verdoppelt) und anschließend in die Form SELECT sql translate(<Anweisung>) gebracht wird. Das Ergebnis dieser Query, eine Tabelle mit einer Spalte und lediglich einem Eintrag, dem übersetzten Befehl, wird anschließend an den nicht temporalen PDO Befehl weitergeleitet und das Ergebnis zurückgereicht. Dadurch bleibt die Flexibilität von PDO erhalten, wie beispielsweise das prepare, welches ermöglicht eine Anweisung mit unterschiedlichen Parametern mehrmals auszuführen. Ein Wrapper in dieser Form ist in PHP weniger als 100 Codezeilen und realisiert die Aufgabe vollständig. Ähnliche Software lässt sich trivial auch in anderen Programmiersprachen realisieren. Der Wrapper muss anschließend nicht mehr verändert werden, selbst wenn sich die Transformation selbst verändert. Damit ist es nicht nötig den Wrapper zu aktualisieren, falls es ein Update von der Software gibt. Es sind lediglich Änderungen an der Datenbank nötig, was das Ausliefern der Software erleichtert. 80 Kapitel 6 Beispieldatenbank Modulkatalog Nun soll das entwickelte Projekt praktisch in einem bestehenden temporalen System eingesetzt werden. Dafür sollen die bereits vorhandenen temporalen Elemente durch die neu entwickelten ersetzt werden. Nachdem die Struktur der Datenbank umgewandelt wurde, werden Anfragen an die alte und neue Datenbank gestellt und verglichen, ob die SQL-Erweiterung kürzere und elegantere Anfragen entworfen hat. Dafür wird zunächst ein neuer Datentyp mithilfe einer weiteren PostgreSQL-Extension eingeführt, da die bestehende Datenbank Semester als Granularität verwendet. Diese Granularität ist nicht standardmäßig bei PostgreSQL mitgelierfert und soll als weiterer Datentyp realisiert werden. Abbildung 6.1: ER-Modell für die Studiengänge und Kompetenzbereiche Nachdem der Grundsatz für die Zeitintervalle gelegt wurde, werden zwei Teilmengen der bestehenden Datenbankdefinition mitsamt ihrer Daten entnommen. In der Abbildung 6.1 ist ein Entity-Relationship-Modell für die Studiengänge und Kompetenzbereiche zu erkennen. Dabei besitzen diese beiden Entitäten im Modulkatalog noch wesentlich mehr Attribute, welches im Modell via ... markiert ist, jedoch sind diese zur Übersichtlichkeit halber weggelassen worden. 81 Abbildung 6.2: ER-Modell für die Lehrveranstaltungen In der Abbildung 6.2 sind die Lehrveranstaltungen aus dem Modulkatalog vermerkt. Diese verwendet, wie zuvor für die Kompetenzbereiche, erneut Semester als Granularität. 6.1 Datentyp Semester Der Modulkatalog benutzt für seine Gültigkeitszeittabellen die Granularität Semester, welche nicht in PostgreSQL integriert ist. Um effizient mit dem entwickelten Projekt zu arbeiten, wird ein neuer Datentyp mithilfe einer PostgreSQL-Extension eingeführt und für die neue Modulkatalogs-Datenbank zukünftig verwendet. Wenn diese Datenbank die entsprechenden Operationen unterstützt, die auch beispielsweise DATE oder TIME unterstützt, lässt sich die Erweiterung so benutzen, wie sie schon für DATE benutzt wurde. Um den neuen Datentyp zu realisieren, wird ein Modul in C programmiert, welches zunächst beschreibt, wie der Speicheraufbau aussehen soll und wie der neue Datentyp gelesen und ausgegeben wird. In diesem Fall besteht eine Semesterangabe aus 2 Elementen: Zunächst die Angabe, ob ein Sommer- oder Wintersemester gemeint ist und anschließend, in welchem Jahr dieses Semester stattfindet. Dabei ist zu beachten, dass Wintersemester zwar über zwei Jahre stattfinden, jedoch nur das erste Jahr bei der Angabe zu speichern ist, da das zweite Jahr eine redundante Information ist. Also ist ein Bit für die Kennzeichnung von Winter- oder Sommersemester und ein Integer für die Angabe eines Jahres für die Speicherung notwendig. PostgreSQL definiert die Ausgabe und Eingabe von Daten über Zeichenketten, demnach muss danach definiert werden, wie die Beschaffenheit von Zeichenketten für Semesterangaben ist. In der Entität Semester, welche in den ER-Modellen 6.2 und 6.1 zu sehen ist, steht die Beschreibung im Attribut Lang. Ein Beispiel ist demnach Wintersemester 2012/2013 und Sommersemester 2015. Also müssen diese Zeichenketten gelesen und analysiert werden, ob es sich um ein Sommer- oder Wintersemester handelt und in welchem Jahr es stattfindet. Die Ausgabe findet analog statt. Für die vollständige Definition des Datentyps fehlen jedoch noch die Operatoren, da er sonst nicht innerhalb von PostgreSQL-Ranges eingesetzt werden kann. Ohne die Range-Funktionalität kann der Datentyp nicht als Granularität eingesetzt werden und ist damit für die Datenbank nicht zu gebrauchen. Die Operatoren, die dafür benötigt 82 werden, sind die Vergleichsoperatoren, um eine chronologische Sortierung zu ermöglichen: <, <=, =, >, >=, sowie eine CMP-Funktion, welche zwei Variablen vom erstellten Datentyp entgegennimmt, und entweder -1, 0 oder 1 ausgibt, je nachdem ob die erste übergebene Variable kleiner, identisch oder größer ist als die zweite. Diese definierten Operatoren müssen anschließend noch zu einer Operatorengruppe zusammengefasst werden, die bezeichnet, dass der Datentyp in einem binären Baum nun einsortiert werden kann. Dadurch ist auch gegeben, dass der Datentyp in einem Intervall verwendet werden kann. Der neue Datentyp heißt semester und der neue Range-Datentyp heißt analog semesterrange. Beide werden wie alle anderen Datentypen in PostgreSQL verwendet. Der Quelltext des Datentyps als C- und PL/pgSQL-Modul sind im Anhang A zu finden. 6.2 Studiengänge und Kompetenzbereiche Abbildung 6.3: ER-Modell für die Studiengänge und KB mit neuem Datentyp Semester Der neue Datentyp ermöglicht ein kompakteres ER-Modell, wie in der Abbildung 6.3 zu sehen ist. Statt vier Relationships zu einem Entity wird das nun durch zwei Attribute gelöst, welche die Gültigkeitszeit der Objekte mithilfe von semesterrange als Intervalle darstellt. 1 2 3 4 5 6 7 CREATE TABLE modtemp.studiengang AS SELECT sg.id, sg.name, sg.fachbereich, semesterrange(semester(sem1.lang), semester(sem2.lang), ’[]’) vt FROM modkat.studiengang sg JOIN modkat.semester sem1 ON (sg.gueltig von = sem1.id) JOIN modkat.semester sem2 ON (sg.gueltig bis = sem2.id); Quelltext 6.1: CREATE TABLE Studiengang Im Quelltext 6.1 ist ein CREATE TABLE-Aufruf für den neuen Studiengang angegeben. Dabei wird die AS-Syntax verwendet, welche es ermöglicht aus einem SELECT eine Tabelle zu erstellen. Dadurch wird vermieden, die einzelnen Spalten anzugeben, 83 da diese ja schon durch die ursprüngliche Tabelle gegeben sind. In der Zeile 4 wird die Semesterrange mithilfe des Lang-Attributs der Semester-Tabellen erzeugt. Dafür sind zwei JOINs notwendig, da sonst die Semesterbeschreibungen fehlen würden. Anhand dieses Befehls ist auch bereits zu sehen, wie der neue Datentyp bei den Anfragen hilft. Aus der Anfrage 6.2, welche lediglich die Studiengänge mit der zugehörigen Gültigkeitszeit als lange Semesterbeschreibung anzeigt, wird damit einfach 6.3. 1 SELECT sg.id, sg.name, sg.fachbereich, sem1.lang, sem2.lang 2 FROM modkat.studiengang sg 3 JOIN modkat.semester sem1 ON (sg.gueltig von = sem1.id) 4 JOIN modkat.semester sem2 ON (sg.gueltig bis = sem2.id); Quelltext 6.2: Studiengang Beispielanfrage 1 1 SELECT sg.id, sg.name, sg.fachbereich, sg.vt 2 FROM modtemp.studiengang sg; Quelltext 6.3: Studiengang Beispielanfrage 2 Für Kompetenzbereiche passiert die Transformation wie im Quelltext 6.4. 1 2 3 4 5 6 7 CREATE TABLE modtemp.kb AS SELECT kb.id, kb.name, kb.min lp, kb.max lp, kb.bemerkung, kb.sg, semesterrange(semester(sem1.lang), semester(sem2.lang), ’[]’) vt FROM modkat.kb kb JOIN modkat.semester sem1 ON (kb.gueltig von = sem1.id) JOIN modkat.semester sem2 ON (kb.gueltig bis = sem2.id); Quelltext 6.4: CREATE TABLE Kompetenzbereiche Da die beiden Tabellen nicht mithilfe temporal_create() angelegt wurden, fehlen jedoch noch wichtige Bestandteile. So müssen noch die Primärschlüssel definiert werden und die Trigger an die Relationen gebunden werden. Für die Relation Studiengang ist das im Quelltext 6.5 zu sehen, für die Relation Kompetenzbereich ist das analog möglich. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ALTER TABLE modtemp.studiengang ADD PRIMARY KEY(id, vt); CREATE TRIGGER studiengang temporal unique trigger BEFORE INSERT OR UPDATE OF vt ON modtemp.studiengang FOR EACH ROW EXECUTE PROCEDURE temporal unique trigger(); CREATE TRIGGER studiengang coalesce trigger BEFORE INSERT OR UPDATE ON modtemp.studiengang FOR EACH ROW EXECUTE PROCEDURE temporal coalesce trigger(); CREATE TRIGGER kb referential trigger AFTER INSERT OR UPDATE OR DELETE ON modtemp.kb FOR EACH ROW EXECUTE PROCEDURE temporal referential trigger(’modtemp.kb’, ’sg’, ’modtemp.studiengang’, ’id’); Quelltext 6.5: CREATE TABLE Kompetenzbereiche 84 Zusätzlich zu den Integritätsbedingungen wurde noch ein referentieller temporaler Fremdschlüssel via Trigger in den Zeilen 13-16 eingefügt. Das Attribut sg der Tabelle modtemp.kb ist damit ein Fremdschlüssel auf das Attribut id der Tabelle modtemp.studiengang. Jetzt kann auf den Tabellen die im Kapitel 5 vorgestellten Funktionen sowie die erweiterte SQL-Syntax angewendet werden. So kann nun ein sequentieller Join ausgeführt werden, wie im Quelltext 6.6 und 6.7 beschrieben. 1 SEQUENCED SELECT sg.name, kb.name 2 FROM modtemp.studiengang sg 3 JOIN modtemp.kb kb ON (sg.id = kb.sg) Quelltext 6.6: Sequentieller Join in erweitertem SQL 1 SELECT sg.name, kb.name, sg.vt ∗ kb.vt vt 2 FROM modtemp.studiengang AS sg 3 INNER JOIN modtemp.kb AS kb ON (sg.id = kb.sg) 4 WHERE 1 = 1 AND sg.vt && kb.vt; Quelltext 6.7: Übersetzung von Aufruf in Quelltext 6.6 Wäre der Aufbau wie vorher, wären vier zusätzliche Joins nötig um die lange Semesterbeschreibung zu bekommen. Außerdem wären wesentlich mehr Bedingungen nötig, um zu überprüfen, wie die voneinander abhängigen Gültigskeitszeiten erfüllt sind und die Ausgabe anzupassen. Das zeigt, wie viel komfortabler die neue Syntax tatsächlich ist. 6.3 Lehrveranstaltungen Abbildung 6.4: ER-Modell für die Lehrveranstaltungen mit neuem Datentyp Semester Die Transformation von Lehrveranstaltungen passiert analog zu Studiengang und Kompetenzbereich, wie am ER-Modell 6.4 zu sehen ist. Lehrveranstaltungen wurde als weiteres Beispiel aus dem Modulkatalog entnommen, um ein Problem mit der temporalen Datenhaltung aus dem Modulkatalog zu verdeutlichen. Wie im vorherigen ER-Modell 6.2 zu sehen ist, war dort nur die ID der Primärschlüssel. So ergibt das, dass zusammenhängende Objekte nicht unter einem Zeitinvarianten-Schlüssel gespeichert wurden, sondern bei jeder Änderung ein komplett neues Objekt erschaffen wurde. Das führt dazu, dass zusammenhängende Objekte nicht einfach zueinander zugeordnet werden können. Damit ist die Entwicklung stark eingeschränkt, da ein großer Bestandteil der temporalen Datenhaltung die Verwaltung der Historie von einzelnen Objekten ist. Es muss demnach 85 eine Möglichkeit gefunden werden, zueinander gehörende Objekte zu finden und ihnen einen gemeinsamen zeitinvarianten Schlüssel zu geben. 1 2 3 4 5 6 7 8 9 10 11 UPDATE modtemp.lv lv1 SET id = ( SELECT lv2.id FROM modtemp.lv lv2 WHERE lv1.name = lv2.name AND lv1.id < lv2.id ) WHERE exists ( SELECT lv2.id FROM modtemp.lv lv2 WHERE lv1.name = lv2.name AND lv1.id < lv2.id ); Quelltext 6.8: Zusammengehörende Objekte in Lehrveranstaltungen Eine naive Lösung ist im Quelltext 6.8 zu sehen, wo lediglich alle Veranstaltungen, die den gleichen Namen tragen, zueinander zugeordnet werden. Dafür wird eine UPDATEAnweisung benutzt, welche eine Unteranfrage in der SET-Klausel verwendet. Dieser Ansatz hat sieben der 677 Lehrveranstaltungen zu einem anderen zugeordnet. In den Lehrveranstaltungen hat sich die Anzahl der Semesterwochenstunden geändert. Jedoch finden sich dadurch keine Veranstaltungen, bei denen sich der Name verändert hat. Es gibt bessere Methoden für das Matching von Daten, jedoch ist das eher Thema einer anderen Ausarbeitung. Diese Relation zeigt jedoch die Nachteile einer unzureichenden temporalen Verwaltung und die Vorteile des Ansatzes aus der Ausarbeitung. Der gesamte Modulkatalog ließe sich mithilfe des vorgestellten Projekts nun umsetzen und die Anfragen an diese Datenbank effizienter und lesbarer gestalten. 86 Kapitel 7 Fazit & Ausblick In dieser Ausarbeitung wurden historische und aktuelle Entwicklungen der temporalen Datenhaltung analysiert und anschließend ein eigener Versuch gestartet, temporale Datenhaltung in Form von Gültigkeits- und Transaktionszeit zu realisieren und zu verwalten. Dabei ist klar geworden, dass dieses Unterfangen schon lange ein Thema in den Datenbanken darstellt; frühere Versuche neue Sprachelemente zu SQL hinzuzufügen scheiterten, obwohl sie von führenden Forschern auf dem Gebiet der Datenbankentwicklung geleitet wurden, da einfach keine Akzeptanz entstanden ist. Dabei besteht Bedarf, wenn sogar hauseigene Software im Fachbereich temporale Datenhaltung benötigt, um einfach und effektiv Anfragen im Thema der Lehrveranstaltungen mit ihren Eigenschaften und Modulen über Jahre hinweg zu verwalten. Dabei wurden zunächst die Grundlagen besprochen und dazu reicht es nicht, sich auf die Datenbanken zu beschränken. So sind temporale Daten auch ein Thema, welches Gebiete umfasst, die nicht in die traditionelle Datenbankentwicklung fallen. Es fallen fragen auf wie die Verwaltung von Zeit und ihre Granularität und welche Probleme auftreten, wenn ungewöhnliche Granularitäten verwendet werden. Doch stellen temporale Daten auch die traditionellen Eigenschaften von Tabellen auf den Kopf; so gilt nicht mehr, dass jedes Objekt durch einen Tupel repräsentiert wird. Dadurch sind neue Probleme entstanden, wie die Repräsentation der Tupel bei einer Anfrage und wie die Datenhaltung geregelt werden soll. Wenn Veränderungen an den Daten vorgenommen werden, so entstehen neue Vorgänge, die sich stark von den üblichen UPDATE/DELETE/INSERTAnweisungen unterscheiden. Auch neue Integritätsbedingungen sind entstanden, welche neue Hürden für die Entwicklung geworfen haben; die bekannten Integritätsbedingungen von SQL, wie der Primärschlüssel, eignen sich nicht ausreichend um die neuen Integritätsbedingungen zu entwickeln. Via Trigger mussten neue Mechanismen zu den Tabellen hinzugefügt werden, welches sich als schweres Unterfangen erwiesen hat, da dies abhängig von der Tabelle passieren muss. Der Trigger muss zur Laufzeit wissen, welche Tabelle der Ursprung des Triggers ist und dementsprechend, abhängig von der Beschaffenheit eben dieser, unterschiedliche dynamische Anfragen stellen, um die Integritätsbedingungen, wie den temporalen Schlüssel, zu garantieren. Es entstehen neue Anforderungen an die Tabellen und wie sie verwaltet werden und auch wie sie angefragt werden. Was zum größten Kapitel dieser Ausarbeitung führt; die Analyse bestehender SQL-Erweiterung und 87 die anschließende Entwicklung einer eigenen für die Verwaltung der genannten temporalen Daten. Dabei sind neue Probleme entstanden. Eine Erweiterung für SQL zu beschreiben erfordert Wissen über die bestehende Syntax und für die Konstruktion der Syntax für die Erweiterung zusätzlich noch Wissen über historische Ansätze, um bekannte Fehler zu vermeiden. Im Rahmen der Übersetzung sind dabei verschiedene Probleme entstanden: So war es nötig, zur Laufzeit dynamisch Eigenschaften über die Tabelle zu erfahren. Eine Kommunikation zwischen der Datenbank und der Tabelleneinheit war nötig. Es muss auch darauf geachtet werden, dass nicht zu häufig kommuniziert wird, da es sonst bei der Übersetzung zu einem Engpass kommen kann. Eine andere große Herausforderung war das temporale NOT EXISTS, das heißt die temporale Differenz. Eine saubere Syntax zu entwerfen, welche effizient und ohne Einschränkungen übersetzt werden kann, erforderte einige verworfene Ansätze, bis sich schlussendlich für die Variante mit den aufgespaltenen Tupeln entschieden wurde. Auch gab es Probleme, bei der Übersetzung jedes Detail zu beachten, was auch schlussendlich zum Ausblick führt: was für Möglichkeiten zur Erweiterung existieren? So ist die SQL-Erweiterung nicht vollständig mit dem jetzigen SQL-Standard kompatibel. Es fehlen UNION, INTERSECT und EXCEPT zwischen zwei SELECT-Anweisungen vollständig und werden nicht korrekt gelesen und übersetzt. Im temporalen Kontext könnte auch Funktionalität hinzugefügt werden: Temporale Aggregierungen wie ein SUM, welches die Zeiten gewichtet und ein korrektes Ergebnis im temporalen Kontext anzeigt, fehlen vollkommen, aber könnten nachgerüstet werden. Sie benötigen neue Übersetzungsvorgänge, falls eine solche Funktion in einem SEQUENCED SELECT auftritt. EXISTS und NOT EXISTS in WHERE-Klauseln bei UPDATE/DELETE-Anweisungen sind in der momentanen Implementierung strikt nicht-temporal und beachten nicht den temporalen Zusammenhang zwischen einer SEQUENCED-Unteranfrage und der Tabelle, auf die sich die Anweisung bezieht. Als letzte mögliche Erweiterung wäre es interessant, das Projekt noch einmal zu besuchen, wenn der temporale Teil des SQL:2011-Standards tatsächlich von PostgreSQL implementiert wird. Die jetzige Syntax ist nicht vollständig kompatibel und bestehende Anfragen der Erweiterung müssten leicht korrigiert werden, damit sie gänzlich ohne den selbst konzipierten Übersetzer laufen. So wäre eine weitere Aufgabe, den jetzigen Übersetzer vollständig kompatibel zum Standard zu gestalten, mitsamt der Syntax für DDL-Statements und der Möglichkeit, die Attribute für die Gültigkeits- und Transaktionszeit selbst zu benennen. Das würde eine starke Veränderung der Software zugrunde haben, würde aber stärkere Flexibilität im System ermöglichen. So gilt es, die bestehenden Erfahrungswerte über die temporale Datenhaltung weiterhin zu bewahren und zu erweitern. Temporale Daten werden nicht auf einmal verschwinden; die Frage nach der Vergangenheit, Gegenwart und Zukunft wird im Zeitalter der Big Data nur noch bedeutender. 88 Anhang A PostgreSQL-Extension: Datentyp Semester 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include #include #include #include <string.h> ”postgres.h” ”utils/rangetypes.h” ”fmgr.h” PG MODULE MAGIC; Datum Datum Datum Datum Datum Datum Datum Datum Datum semester in(PG FUNCTION ARGS); semester out(PG FUNCTION ARGS); semester abs lt(PG FUNCTION ARGS); semester abs le(PG FUNCTION ARGS); semester abs eq(PG FUNCTION ARGS); semester abs ge(PG FUNCTION ARGS); semester abs gt(PG FUNCTION ARGS); semester abs cmp(PG FUNCTION ARGS); semesterrange canonical(PG FUNCTION ARGS); typedef enum { winter, summer } SemesterSeason; typedef struct Semester { int year; SemesterSeason season; } Semester; PG FUNCTION INFO V1(semester in); Datum semester in(PG FUNCTION ARGS) { char ∗str = PG GETARG CSTRING(0); char season str[128]; 89 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 int year; Semester ∗semester; semester = (Semester ∗) palloc(sizeof(Semester)); if (strcmp(str, ”now”) == 0) { semester−>season = summer; semester−>year = 2015; PG RETURN POINTER(semester); } if (sscanf(str, ”%s %d”, season str, &year) != 2) { ereport(ERROR, (errcode(ERRCODE INVALID TEXT REPRESENTATION), errmsg(”invalid input syntax for semester: \”%s\””, str))); } if (strcmp(season str, ”Wintersemester”) == 0) { semester−>season = winter; } else if (strcmp(season str, ”Sommersemester”) == 0) { semester−>season = summer; } else { ereport(ERROR, (errcode(ERRCODE INVALID TEXT REPRESENTATION), errmsg(”invalid input syntax for semester: \”%s\””, str))); } semester−>year = year; PG RETURN POINTER(semester); } PG FUNCTION INFO V1(semester out); Datum semester out(PG FUNCTION ARGS) { Semester ∗semester = (Semester ∗) PG GETARG POINTER(0); char ∗result; result = (char∗)palloc(128); if (semester−>season == summer) { sprintf(result, ”Sommersemester %d”, semester−>year); } else { sprintf(result, ”Wintersemester %d/%d”, semester−>year, semester−>year+1); } PG RETURN CSTRING(result); } static int semester abs cmp internal(Semester ∗ a, Semester ∗ b) { if (a−>year < b−>year || (a−>year == b−>year && a−>season == summer && b−>season == winter)) return −1; if (a−>year > b−>year || (a−>year == b−>year && a−>season == winter && b−>season == summer)) 90 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 return 1; return 0; } PG FUNCTION INFO V1(semester abs lt); Datum semester abs lt(PG FUNCTION ARGS) { Semester ∗a = (Semester ∗) PG GETARG POINTER(0); Semester ∗b = (Semester ∗) PG GETARG POINTER(1); PG RETURN BOOL(semester abs cmp internal(a, b) < 0); } PG FUNCTION INFO V1(semester abs le); Datum semester abs le(PG FUNCTION ARGS) { Semester ∗a = (Semester ∗) PG GETARG POINTER(0); Semester ∗b = (Semester ∗) PG GETARG POINTER(1); PG RETURN BOOL(semester abs cmp internal(a, b) <= 0); } PG FUNCTION INFO V1(semester abs eq); Datum semester abs eq(PG FUNCTION ARGS) { Semester ∗a = (Semester ∗) PG GETARG POINTER(0); Semester ∗b = (Semester ∗) PG GETARG POINTER(1); PG RETURN BOOL(semester abs cmp internal(a, b) == 0); } PG FUNCTION INFO V1(semester abs ge); Datum semester abs ge(PG FUNCTION ARGS) { Semester ∗a = (Semester ∗) PG GETARG POINTER(0); Semester ∗b = (Semester ∗) PG GETARG POINTER(1); PG RETURN BOOL(semester abs cmp internal(a, b) >= 0); } PG FUNCTION INFO V1(semester abs gt); Datum semester abs gt(PG FUNCTION ARGS) 91 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 { Semester ∗a = (Semester ∗) PG GETARG POINTER(0); Semester ∗b = (Semester ∗) PG GETARG POINTER(1); PG RETURN BOOL(semester abs cmp internal(a, b) > 0); } PG FUNCTION INFO V1(semester abs cmp); Datum semester abs cmp(PG FUNCTION ARGS) { Semester ∗a = (Semester ∗) PG GETARG POINTER(0); Semester ∗b = (Semester ∗) PG GETARG POINTER(1); PG RETURN INT32(semester abs cmp internal(a, b)); } PG FUNCTION INFO V1(semesterrange canonical); Datum semesterrange canonical(PG FUNCTION ARGS) { RangeType ∗r = PG GETARG RANGE(0); TypeCacheEntry ∗typcache; RangeBound lower; RangeBound upper; bool empty; Semester ∗semester; typcache = range get typcache(fcinfo, RangeTypeGetOid(r)); range deserialize(typcache, r, &lower, &upper, &empty); if (empty) PG RETURN RANGE(r); if (!lower.infinite && !lower.inclusive) { semester = (Semester ∗)lower.val; if (semester−>season == summer) { semester−>season = winter; } else { semester−>season = summer; semester−>year++; } lower.inclusive = true; } if (!upper.infinite && upper.inclusive) { semester = (Semester ∗)upper.val; if (semester−>season == summer) { semester−>season = winter; 92 191 192 193 194 195 196 197 198 199 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 } else { semester−>season = summer; semester−>year++; } upper.inclusive = false; } PG RETURN RANGE(range serialize(typcache, &lower, &upper, false)); CREATE FUNCTION semester in(cstring) RETURNS semester AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION semester out(semester) RETURNS cstring AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE TYPE semester ( internallength = 16, input = semester in, output = semester out, alignment = int ); CREATE FUNCTION semester abs lt(semester, semester) RETURNS bool AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION semester abs le(semester, semester) RETURNS bool AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION semester abs eq(semester, semester) RETURNS bool AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION semester abs ge(semester, semester) RETURNS bool AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION semester abs gt(semester, semester) RETURNS bool AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE OPERATOR < ( leftarg = semester, rightarg = semester, procedure = semester abs lt, commutator = > , negator = >= , restrict = scalarltsel, join = scalarltjoinsel ); CREATE OPERATOR <= ( leftarg = semester, rightarg = semester, procedure = semester abs le, commutator = >= , negator = > , restrict = scalarltsel, join = scalarltjoinsel ); CREATE OPERATOR = ( leftarg = semester, rightarg = semester, procedure = semester abs eq, commutator = = , restrict = eqsel, join = eqjoinsel 93 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 ); CREATE OPERATOR >= ( leftarg = semester, rightarg = semester, procedure = semester abs ge, commutator = <= , negator = < , restrict = scalargtsel, join = scalargtjoinsel ); CREATE OPERATOR > ( leftarg = semester, rightarg = semester, procedure = semester abs gt, commutator = < , negator = <= , restrict = scalargtsel, join = scalargtjoinsel ); CREATE FUNCTION semester abs cmp(semester, semester) RETURNS int4 AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE OPERATOR CLASS semester abs ops DEFAULT FOR TYPE semester USING btree AS OPERATOR 1 < , OPERATOR 2 <= , OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , FUNCTION 1 semester abs cmp(semester, semester); CREATE FUNCTION semester hash(s semester) RETURNS int AS $$ BEGIN RETURN hashtext(text(s)); END; $$ LANGUAGE plpgsql; CREATE OPERATOR CLASS semester abs hash op DEFAULT FOR TYPE semester USING hash AS OPERATOR 1 = , FUNCTION 1 semester hash(semester); CREATE TYPE semesterrange; CREATE FUNCTION semesterrange canonical(semesterrange) RETURNS semesterrange AS ’semester4psql’ LANGUAGE C IMMUTABLE STRICT; CREATE TYPE semesterrange AS RANGE ( subtype = semester, canonical = semesterrange canonical ); 94 Abbildungsverzeichnis 2.1 2.2 2.3 2.4 Darstellung der Relationen in Tabelle 2.1 (Grafik entnommen aus [TYT11]) Taxonomie der Allen-Relationships (Grafik entnommen aus [JW10]) . . . Darstellung der Relationen in Tabelle 2.2 (Grafik entnommen aus [TYT11]) Darstellung der Relationen in Tabelle 2.3 (Grafik entnommen aus [TYT11]) 9 10 11 11 5.1 5.2 5.3 5.4 5.5 5.6 5.7 Architektur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Syntax-Diagramm für CREATE TABLE . . . . . . . . . . . . . . . . . . . Syntax-Diagramm für UPDATE . . . . . . . . . . . . . . . . . . . . . . . Syntax-Diagramm für DELETE . . . . . . . . . . . . . . . . . . . . . . . Syntax-Diagramm für SELECT-Anfragen . . . . . . . . . . . . . . . . . . Beispiel AST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sequenzdiagramm für die Übersetzung der temporalen SQL-Erweiterung 54 56 57 58 58 69 79 6.1 6.2 6.3 6.4 ER-Modell ER-Modell ER-Modell ER-Modell 81 82 83 85 für für für für die die die die Studiengänge und Kompetenzbereiche . . . . . . . . Lehrveranstaltungen . . . . . . . . . . . . . . . . . . Studiengänge und KB mit neuem Datentyp Semester Lehrveranstaltungen mit neuem Datentyp Semester . 95 . . . . Tabellenverzeichnis 2.1 2.2 2.3 2.4 2.5 Die 13 Relationen zwischen Zeitintervallen nach James F. Allen Temporale Relationen zwischen Zeitpunkt und Zeitintervall . . . Temporale Relationen zwischen zwei Zeitpunkten . . . . . . . . Range-Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . Range-Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 10 11 19 20 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 24 25 26 26 27 27 27 28 28 29 29 30 31 31 32 32 32 33 3.21 3.22 3.23 Beispielausprägungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Relation ARBEITET mit Valid Time . . . . . . . . . . . . . . . . . . . . Ausprägung, die den temporalen Schlüssel verletzt . . . . . . . . . . . . . Relation ANGESTELLTE mit Valid Time . . . . . . . . . . . . . . . . . Mitarbeiter 2 wurde versetzt . . . . . . . . . . . . . . . . . . . . . . . . . Relation ANGESTELLTE mit Valid Time . . . . . . . . . . . . . . . . . Relation ANGESTELLTE mit Valid Time . . . . . . . . . . . . . . . . . Relation ANGESTELLTE mit Valid Time . . . . . . . . . . . . . . . . . Relation ANGESTELLTE mit Valid Time . . . . . . . . . . . . . . . . . Zweite Gehaltserhöhung von Max . . . . . . . . . . . . . . . . . . . . . . Temporaler Join zwischen ANG und ARBEITET . . . . . . . . . . . . . Projekte, in denen Bob nicht mitwirkt. . . . . . . . . . . . . . . . . . . . Ausgangsausprägung für das temporale Löschen . . . . . . . . . . . . . . Bob im Zeitraum [2010-01-01,2014-01-01) wird gelöscht . . . . . . . . . . Max im Zeitraum [2010-01-01,2011-01-01) wird gelöscht . . . . . . . . . . Max im Zeitraum [2012-01-01,2013-01-01) wird gelöscht . . . . . . . . . . Max im Zeitraum [2014-01-01,2015-01-01) wird gelöscht (Schritt 2) . . . . Max im Zeitraum [2012-01-01,2013-01-01) wird gelöscht (Schritt 3) . . . . Update Gehalt von Bob auf 3400 im Zeitraum [2014-01-01,2015-01-01) (Schritt 1) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Update Gehalt von Bob auf 3400 im Zeitraum [2014-01-01,2015-01-01) (Schritt 2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Transaction Time-Tabelle mit Zeitstempeln . . . . . . . . . . . . . . . . Transaction Time-Tabelle mit Zeiträumen . . . . . . . . . . . . . . . . . Bitemporale Tabelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1 4.2 4.3 Valid Time-Tabelle nach dem INSERT in 4.8 . . . . . . . . . . . . . . . . Valid Time-Tabelle nach dem UPDATE in 4.9 . . . . . . . . . . . . . . . Valid Time-Tabelle nach dem DELETE in 4.10 . . . . . . . . . . . . . . 43 44 45 3.20 96 . . . . . . . . . . . . . . . . . . . . 33 34 35 35 36 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13 Transaction Time-Tabelle nach dem INSERT in 4.13 . . . . . . . . . . . Transaction Time-Tabelle nach dem UPDATE in 4.14 . . . . . . . . . . . Transaction Time-Tabelle nach dem DELETE in 4.15 . . . . . . . . . . . Ergebnis nach Anfrage 4.16 an Transaction Time-Tabelle . . . . . . . . . Beispielausprägung einer temporalen Tabelle inVTSQL2+ . . . . . . . . . Ergebnis von Anfrage 4.19 . . . . . . . . . . . . . . . . . . . . . . . . . . Ergebnis von Anfrage 4.20 . . . . . . . . . . . . . . . . . . . . . . . . . . Ergebnis von Anfrage 4.21 . . . . . . . . . . . . . . . . . . . . . . . . . . Ergebnis von Anfrage 4.22 . . . . . . . . . . . . . . . . . . . . . . . . . . Ergebnis von Anfrage 4.23 (Namen der Übersichtlichkeit halber gekürzt) 46 46 47 47 49 50 50 51 51 52 5.1 Übersetzung der 13 Relationships-Arten von Allen zu Operationen auf den Range-Datentyp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übersetzung der Relationen zwischen Zeitpunkt und Zeitintervall . . . . Übersetzung der Relationen zwischen Zeitpunkt und Zeitintervall . . . . Ausgangsausprägung für das temporale Löschen . . . . . . . . . . . . . . Ausgangsausprägung für das temporale Löschen . . . . . . . . . . . . . . 60 60 61 67 67 5.2 5.3 5.4 5.5 97 Literaturverzeichnis [All83] J. F. Allen. Maintaining Knowledge About Temporal Intervals. Commun. ACM, 26(11), 1983, 832–843. ISSN 0001-0782. URL http://doi.acm. org/10.1145/182.358434. [Bei01] F. Beier. Objekt-relationale Realisierung einer temporalen Datenbanksprache. Diplomarbeit, Leibniz Universität Hannover, 2001. [Bet10] C. Bettini. Time Granularities in Databases, Data Mining, and Temporal Reasoning. Springer-Verlag, Berlin, Heidelberg, 2010. ISBN 3642086349, 9783642086342. [JW10] T. Johnston, R. Weis. Managing Time in Relational Databases: How to Design, Update and Query Temporal Data. Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 2010. ISBN 0123750415, 9780123750419. [KM12] K. Kulkarni, J.-E. Michels. Temporal Features in SQL:2011. SIGMOD Rec., 41(3), 2012, 34–43. ISSN 0163-5808. URL http://doi.acm.org/10. 1145/2380776.2380786. [Lev09] J. Levine. flex & bison: Text Processing Tools. O’Reilly Media, 2009. ISBN 0596155972. [Mal09] E. Malinowski. Advanced Data Warehouse Design: From Conventional to Spatial and Temporal Applications. Springer-Verlag, Berlin, Heidelberg, 2009. ISBN 3642093833, 9783642093838. [SA86] R. Snodgrass, I. Ahn. Temporal Databases. Computer, 19(9), 1986, 35– 42. ISSN 0018-9162. URL http://dx.doi.org/10.1109/MC.1986. 1663327. [SAA+ 94a] R. T. Snodgrass, I. Ahn, G. Ariav, D. Batory, J. Clifford, C. E. Dyreson, R. Elmasri, F. Grandi, C. S. Jensen, W. Käfer, N. Kline, K. Kulkarni, T. Y. C. Leung, N. Lorentzos, J. F. Roddick, A. Segev, M. D. Soo, S. M. Sripada. TSQL2 Language Specification. SIGMOD Rec., 23(1), 1994, 65–86. ISSN 0163-5808. URL http://doi.acm.org/10.1145/181550.181562. 98 [SAA+ 94b] R. T. Snodgrass, I. Ahn, G. Ariav, D. Batory, J. Clifford, C. E. Dyreson, R. Elmasri, F. Grandi, C. S. Jensen, W. Käfer, N. Kline, K. Kulkarni, T. Y. C. Leung, N. Lorentzos, J. F. Roddick, A. Segev, M. D. Soo, S. M. Sripada. A TSQL2 Tutorial. SIGMOD Rec., 23(3), 1994, 27–33. ISSN 0163-5808. URL http://doi.acm.org/10.1145/187436.187449. [Sno95] R. T. Snodgrass. The TSQL2 Temporal Query Language. Kluwer Academic Publishers, Norwell, MA, USA, 1995. ISBN 0792396146. [Sno00] R. T. Snodgrass. Developing Time-oriented Database Applications in SQL. Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 2000. ISBN 1-55860-436-7. [TCG+ 93] A. U. Tansel, J. Clifford, S. Gadia, S. Jajodia, A. Segev, R. Snodgrass, Hg. Temporal Databases: Theory, Design, and Implementation. BenjaminCummings Publishing Co., Inc., Redwood City, CA, USA, 1993. ISBN 0-8053-2413-5. [TYT11] Y. Tang, X. Ye, N. Tang. Temporal Information Processing Technology and Its Applications. Springer Publishing Company, Incorporated, 2011. ISBN 3642149588, 9783642149580. 99 Erklärung Hiermit versichere ich, dass ich die vorliegende Arbeit und die zugehörige Implementierung selbstständig verfasst und dabei nur die angegebeben Quellen und Hilfsmittel verwendet habe. Hannover, 20.04.2015 Marcell Salvage 100