Aufgabe 1 (20 Punkte) Wie können SQL-Anweisungen in einen Quelltext (z.B. PL/I, COBOL, Pascal usw.) eingebettet werden? Wie arbeitet der dazu benötigte Precompiler? - - - Einbettung mit "exec SQL" und ";" Precompiler übersetzt erst den eingebetteten SQL-Text und macht ihn für den Compiler lesbar, indem er entsprechenden Code generiert, der anschließend vom Compiler verarbeitet werden kann Um korrekte Datenübertragungen zwischen Datenbank und Hostvariablen vorzunehmen, muss der Precompiler die Datentypen der Hostvariablen kennen. Das ist der Grund für denjenigen Programmabschnitt, der durch BEGIN DECLARE SECTION eingeleitet und durch END DECLARE SECTION beendet wird. Hierbei handelt es sich nicht um SQL-Anweisungen, sondern um Anweisungen an den Precompiler. Da Hostvariablen im Programm wie normale Variablen der jeweiligen Sprache behandelt werden, hängen die für Hostvariablen einsetzbaren Datentypen natürlich von der gewählten Programmiersprache ab. Der Precompiler bearbeitet die Programmdatei mit den eingebetteten SQLAnweisungen sequentiell. Trifft er auf eine SQL-Anweisung, die er aufgrund der Einleitung erkennt, so liest er zunächst die gesamte SQL-Anweisungen bis zum Endekennzeichen ein und überprüft sie in Zusammenarbeit mit dem Datenbanksystem. Zur Überprüfung gehören üblicherweise folgende Punkte: o Ist die Anweisung syntaktisch korrekt? o Sind alle referenzierten Objekte in der Datenbank vorhanden? o Sind die Zugriffsrechte gegeben? Nach erfolgreicher Überprüfung ersetzt der Precompiler die eingebettete SQLAnweisung durch die benötigten Datenstrukturen sowie Prozeduraufrufe in der Sprache des verwendeten Compilers. Die Prozeduren befinden sich in einer Bibliothek, die als Schnittstelle zu Kommunikation mit dem DBMS dient. Wesentliche Aufgabe dieser Bibliothek ist neben der Kommunikation mit dem DBMS darin zu sehen, dass die Umwandlung der Daten der Hostvariablen zwischen Datenbank und Programm unterstützt wird. Eine weitere Aufgabe ist die Ermittlung des optimalen Zugriffspfades. Aufgabe 2 (25 Punkte) Beschreiben Sie den Zugriff auf eine Datenbank mit JDBC. Insbesondere soll wiederholt mit wechselnden Parametern eine Abfrage SELECT * FROM tab WHERE regal = ? auf eine Tabelle mit den Character-Spalten nr, fach, regal, ware durchgeführt werden. Schritt 1: Aufbau einer Datenbankverbindung Um mit einer Datenbank kommunizieren zu können, muss zuerst eine Verbindung hergestellt werden. Dazu ist ein Treiber erforderlich, der die Schnittstelle zwischen JDBCAPI und dem konkreten Datenbanksystem implementiert. Mit dem folgenden Kommando wird der JDBC-ODBC-Brückentreiber von Sun und Intersolv geladen: Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Über die URL der Datenbank findet der DriverManager, den passenden JDBC-Treiber. Der DriverManager ist eine Fabrikklasse die alle geladenen Treiber verwaltet. Er erzeugt ein Connection-Objekt, das die Datenbankverbindung repräsentiert. Alle SQL-Befehle werden über dieses Objekt gegen die Datenbank abgesetzt. Im Quelltext wird das Programm mit der Datenbank pizzaservice verbunden, das Connection-Objekt conn repräsentiert die Verbindung zu pizzaservice. Connection conn = DriverManager.getConnection("jdbc:odbc:pizzaservice"); Schritt 2: Erzeugen einer Tabelle SQL-Befehle werden mit Statement-Objekten gegen die Datenbanken abgesetzt. Für Anfragen und Änderungen stellt das Statement-Interface die Methoden executeQuery und executeUpdate zu Verfügung. Die Tabelle wird eigentlich mit dem Befehl CREATE TABLE erzeugt. Das Statement-Objekt übernimmt die Versendung. Statement stmt = conn.createStatement(); stmt.executeUpdate("CREATE TABLE PizzaTabelle (" + " id INTEGER, " + " name CHAR(20), " + " preis FLOAT )" ); Schritt 3: Füllen der Tabelle Die Pizza Margherita wird mit INSERT in die PizzaTabelle geschrieben. Wie in Schritt 2 setzt das Statement-Objekt den Befehl ab. Die eigentliche Arbeit wird von dem SQL-Befehl erledigt. stmt.executeUpdate ("INSERT INTO PizzaTabelle" + "(id, name, preis) VALUES(12, 'Margherita', 7.20)"); Schritt 4: Anfragen Anfragen werden mit SELECT formuliert. Im Beispiel sollen aus der Tabelle alle Pizzen gelesen werden, die 9.80 DM kosten. Von diesen sollen alle verfügbaren Daten angezeigten werden. SELECT * FROM PizzaTabelle WHERE preis = 9.80 Für Anfragen wird ein Statement-Objekt verwendet, wie schon in den vorangegangenen Schritten. Die executeQuery Methode setzt den SELECT-Befehl ab und liefert ein ResultSet-Objekt als Ergebnis zurück. Schritt 5: Analyse des Ergebnisses Das Datenbanksystem erzeugt eine Tabelle als Ergebnis der Anfrage. Die Tabelle wird als ResultSet zur Verfügung gestellt. Dieses wird Element für Element durchlaufen. Jedem Element des ResultSets entspricht eine Zeile in der Ergebnistabelle. ResultSet rs = stmt.executeQuery("SELECT * FROM "+"PizzaTabelle WHERE preis = 9.80"); Die next-Methode steuert den Lesezeiger (Cursor) des ResultSets. Der erste Aufruf stellt den Lesezeiger an den Anfang, jeder weitere Aufruf verschiebt den Zeiger um jeweils ein Element. Der Rückgabewert der next-Methode ist vom Typ boolean. Solange der Lesezeiger auf ein Element der Treffermenge verschoben wird, liefert next als Rückgabewert true. Der Rückgabewert ist false, wenn das Ende der Treffermenge erreicht ist. while (rs.next()){ int id = rs.getInt("id"); String name = rs.getString("name"); float preis = rs.getFloat("preis"); System.out.println ("Treffer: "+name+" , "+preis);} Jeder Datentyp XXX (z.B. String oder int) wird mit entsprechenden getXXX-Methoden aus dem ResultSet gelesen. Strings werden beispielsweise mit getString() gelesen, Integer mit getInt(). JDBC führt darüber eine Abbildung zwischen den SQL-Datentypen der Datenbank und den Java-Datentypen durch. Schritt 6: Schließen der Verbindung und freigeben der Ressourcen Bei Beendigung des Programms müssen die belegten Ressourcen freigegeben werden, und dazu gehört auch eine Datenbankverbindung. Findet die Freigabe nicht statt, gehen begrenzte Datenbankressourcen wie Cursor oder Verbindungen aus, im schlimmsten Fall kann auf die Datenbank nicht mehr zugegriffen werden. rs.close(); stmt.close(); conn.close(); Querschnittsfunktion: Fehlerbehandlung Die Korrektheit der SQL-Strings kann erst zur Laufzeit geprüft werden. Zusätzliche Fehler können während des Zugriffs auf eine Datenbank auftreten. Eine geworfene SQLException repräsentiert einen Fehler. Dieser muss vom Programm behandelt werden. Im Beispiel wird nur der Fehlertext ausgegeben. try{ \\ der oben behandelte Code } catch (SQLException ex) { System.err.println(ex.getMessage()); } Aufgabe 3 (15 Punkte) Warum sind in relationalen DBMS Indexe in der Regel als Bäume und nicht als Hash-Verfahren implementiert? Gibt es ggf. auch bei Bäumen gewisse Probleme? Der Baum, insbesondere der augeglichene, sortierte B-Tree ist ein Spezialfall von indexsequentiellen Verfahren. Der Weg von der Wurzel zu jedem Blatt ist gleich lang. Wird mit k die für den Baum vereinbarte Schlüsselanzahl bezeichnet, so gilt, dass jeder Knoten mindestens k und max. 2k Schlüssel enthält. k beschreibt die Breite des Baumes. k = 100 bedeutet z.B. dass man mit 2 Zugriffen 100 * 100 = 10000 Tupel-Identifier auffinden kann und dass man mit 3 Zugriffen einen Datensatz in 10000 Datensätzen lesen kann. Der Zugriff ist schnell; es sind Bereichsabfragen möglich, z.B. Intervall von a bis b; es wird sortiert zugegriffen. Hashing ist eine gut bekannte Technik für Direkt-Zugriffs-Dateien. Die Methode ist sehr einfach und die Zeit zum Wiederauffinden eines Datensatzes ist sehr gering. Grundoperationen, wie Einfügen und Löschen, sind keine aufwendigen und komplizierten Verfahren und können zur Laufzeit im Normalfall auch sehr schnell durchgeführt werden. Einer der großen Nachteile der statischen Hashverfahren ist die statische Speicherplatzverwaltung. Die Größe der Datei muss im Voraus bekannt sein oder beurteilt werden können, um einen physikalischen Speicher für die Datei zu reservieren. Die Größe des bereitgestellten Speichers ist von einer festen Länge und kann nicht ohne Reorganisation des gesamten schon bestehenden Files verändert werden. Wenn die Speichernachfrage eines Files bei der Erstellung zu gering eingeschätzt wurde, dann gibt es zwangsläufig eine hohe Zahl von Overflow-Einträgen, was die Suche nach einem Record oder das Einfügen eines Records verlangsamt. Bei zu geringer Ausnutzung des Speichers wird andererseits Speicherplatz verschwendet. So kommen wir zu dem Schluss, dass es sehr schwer ist, eine angemessene Größe für ein File zu bestimmen, wenn die Größe dieses Files großen Schwankungen in der Anzahl der Records unterlegen ist, d.h. die Größe sehr dynamisch ist. Beim Hash-Verfahren ist keine Bereichsanfrage möglich. Oft werden gleichartige Datensätze an unterschiedlichen Orten gespeichert. Desweiteren ist kein Zugriff in sortierter Reihenfolge möglich. Deshalb wird für RDBMS typischerweise der B-Tree benutzt. Ein Problem des Baumes ist jedoch die Reorganisation. Diese ist sehr performanceaufwändig, deshalb sollte sie nicht nach jedem Ändern eines Datensatzes durchgeführt werden. Aufgabe 4 (25 Punkte) Begründen Sie, warum man die von Oracle vorgenommenen Erweiterungen des RDBMS als ,,objekt-relational`` und nicht als ,,objekt-orientiert`` bezeichnen muss. Oracle weist schon einige Merkmale des objektorientierten Ansatzes auf, ist aber immer noch kompatibel zu älteren SQL-Versionen. Die Erweiterung von Oracle hinsichtlich des objektorientierten Ansatzes sind: Konzept benutzerdefinierter Datentypen (ADT) Funktionen der ADT werden analog zu Methoden durch das DBMS verwaltet Tupeltypen (row) auf ADT ist Subtypkonzept mit Vererbung realisiert Redefinition von Attributen und Funktionen ist erlaubt Typkonstruktoren list, set, multiset nicht-atomare Wertebereiche für Attribute, können zusammen mit row verwendet werden Identifikatoren können für Tupel vergeben werden und können als Referenzen verwendet werden Allerdings besitzt Oracle keine Möglichkeit Attribute zu vererben und ist auf einen Kollektionstyp (Tabelle) beschränkt. Das eigentliche Konstrukt ist trotz vieler objektorientierter Konzepte im Datenbankmodell (z.B. Tabellenhierarchie, Methoden, Vererbung von Strukturen und Methoden) immer noch die Relation oder Tabelle. ... "Unterstützt ein System komplexe Daten und Anfragen, so ist es objektrelational. Unterstützt ein System komplexe Daten, aber keine Anfragen, so ist es objektorientiert. " nach Stonebraker Muss-Kriterien für objektorientierte Datenbanksysteme sind: Typkonstruktoren Objektidentität Strukturvererbung generischer Operationenteil Methoden Vererbung Overriding Aufgabe 5 (20 Punkte) Lotus Notes ist sicher kein RDBMS. Kann es überhaupt als DBMS angesehen werden? Welcher Typ von Objekt kann damit verwaltet werden? Wo sehen Sie besondere Stärken dieses Produktes? --- Aufgabe 6 (25 Punkte) Skizzieren Sie die Möglichkeiten, die Performance eines Datenbank-Servers hinsichtlich einer bestehenden DBAnwendung zu verbessern. Anmerkung: eine reine Stichwort-Sammlung ist nicht erwünscht, diskutieren Sie jeden Vorschlag mit einem/ein paar kurzen Sätzen. Verteilung Für die Installation eines Datenbanksystems ist es bzgl. Performance eher günstig, viele kleinere Platten zu haben, und die verschiedenen Dateien darauf zu verteilen, als wenige große Platten, da Datenbanksysteme „Ressourcenfresser“ sind! Insbesondere sollten Index-, Daten und Logdateien immer auf verschiedenen Platten liegen. Data Dictionary Das DBMS muss das komplette DD jederzeit in unmittelbarem Zugriff haben, wenn nicht die Abfragebearbeitung stocken soll, weil auf Informationen gewartet werden muss. Je aktiver und größer eine Datenbank ist, desto umfangreicher ist das DD. Ziel muss sein, das DD komplett im Hauptspeicher des Datenbank-Servers zu halten! Index Es ist vor allem aus Gründen der Performanceverbesserung sinnvoll, Indizes zu benutzen, die den Zugriff auf die Daten schneller machen. Sinnvollerweise ist hier darauf zu achten, dass Daten-, Index- und Logdateien auf physikalisch verschiedenen Platten liegen, damit parallel gesucht und geschrieben werden kann. Liegen die drei Arten von Dateien auf einer Platte, muss z. B. beim Lesevorgang der Schreib/Lesekopf der Platte beim Wechsel zwischen Index- und Datendatei jedes Mal neu positioniert werden. Das kostet Zeit, die nicht notwendig ist! Diejenigen Attribute, die sehr oft in where-Bedingungen vorkommen, sollten indiziert werden. Entweder in einem oder verschiedenen Indexen. bei kleinen Datenvolumen der Tabellen (wenige Pages) kostet ein Zugriff über einen Index mehr als ein Table-Scan (sequentielles Lesen einer Tabelle), bei jeder Änderung der Tabelle muss das DBMS alle Indexe auf die Tabelle mitpflegen. Indexe sollten deshalb bei Tabellen, in die häufig geschrieben wird, nur auf diejenigen Spalten bzw. Spaltenkombinationen erstellt werden, die einen "Gewinn" bringen: Schlüsselspalten, sofern das DBMS dies nicht automatisch selbst vornimmt, Spalten, die in Join-, Group-, Having- und Sort-Operationen benötigt werden, je nach DBMS: wenn bereits der Index die gesamte benötigte Information enthält, so dass nicht mehr auf die Tabellenzeile selbst zugegriffen werden muss. Denormalisierung Denormalisierung kann bedeuten, dass Verdichtungen der Daten in regelmäßigen Abständen berechnet und physisch gespeichert werden, um I/O-Operationen und Rechenzeit zu sparen. Anstatt jedes Mal auf die Originaldaten zuzugreifen, erfolgt nur ein kurzer Zugriff auf die redundant gespeicherten Daten. Denormalisierung kann auch bedeuten, dass für einige Daten die Normalisierung schon frühzeitig beendet wird, um Join-Operationen zu sparen. Denormalisierung kann auch einfach eine Verdopplung von Tabellen oder Spalten darstellen, um damit mehr Zugriffsmöglichkeiten für den lesenden Zugriff bereitzustellen. Besondere Probleme ergeben sich daraus für ändernde Zugriffe. Weitere Formen der Denormalisierung werden bei der Verteilung und Replikation angesprochen. In jedem Falle werden flankierende Maßnahmen benötigt, um nicht durch die auftretende Redundanz Probleme zu bekommen. Datenbankdesign Generelle Faustregel: je kleiner das Datenbankschema (d.h. .je weniger Relationen), desto performanter die Datenbank. Natürlich muss man wegen gewisser Eigenheiten des Relationalen Modells viele Relationen in Kauf nehmen, die ursprünglich zu einem Objekt gehörten! D. h. bei jeder Normalisierung ist abzuwiegen und zu testen, ob die Performance darunter leidet und wie stark! Je mehr joins notwendig sind um eine Information zu gewinnen, desto langsamer ist die Verarbeitung! Partitionierung von Tabellen Aufteilung von Tabellen dient dazu, bei den Anfragen im Mittel mit weniger physischen I/O-Vorgängen auszukommen. Zudem können Partitionen, auf die häufig zugegriffen werden muss, auf möglichst schnellen Platten vorgehalten werden. Es wird zwischen horizontaler und vertikaler Partitionierung unterschieden. Schneller werden aufgrund des geringern IOs diejenigen Zugriffe, die nur eine Partition betreffen. Partitionsübergreifende Zugriffe setzen jedoch einen Join voraus und werden dadurch erheblich teurer. Diese Art der Zerlegung ist deshalb sehr sorgfältig zu planen. Clustern Die Tabellenzeilen werden physisch anhand eines Sortierkriteriums - in der Regel eines Indexes - so abgelegt, dass "nahe benachbarte" Zeilen auch physisch nahe benachbart gespeichert werden. Damit werden insbesondere solche Zugriffe beschleunigt, bei denen anhand des benutzten Sortierkriteriums zugegriffen wird. des weiteren Einsatz eines Optimizers Aufgabe 7 (20 Punkte) Warum werden Trigger unterstützt? Beschreiben Sie Einsatz sowie die Ihrer Meinung nach wichtigsten Grundlagen. Trigger sind für eine Reihe von Aufgaben wichtig: zum Replizieren von Änderungen, d.h. eine Änderung in einer Tabelle wird – mit einem gewissen Zeitverzug – in einer anderen Tabelle, die sich in der Regel in einer anderen Datenbank auf einem anderen Rechner befindet, nachvollzogen, um komplexe Geschäftsregeln nachzubilden, die über referentielle Integrität hinausgehen, um Änderungen zu protokollieren, zum automatischen Erzeugen redundanter Informationen, z.B. um Spaltensummen in einer anderen Tabelle zum schnellen Zugriff bereitzustellen und für den Datenschutz. Als Beispiel sollen die Aktionen, die den Lagerbestand verändern, in einer Tabelle lagerprotokoll mitgeführt werden. Wird eine geeignete Struktur der protokollierenden Tabelle lagerprotokoll vorausgesetzt, könnte das Kommando zur Erzeugung eines Triggers, der alle Änderungen in der Tabelle lager mithält, lauten: CREATE TRIGGER lagerprotokoll AFTER UPDATE ON lager FOR EACH ROW BEGIN ATOMIC INSERT INTO lagerprotokoll VALUES (USER, CURRENT TIMESTAMP, lager.tnummer, lager.anzahl) END Dieser Trigger wird ausgelöst, wenn eine UPDATE-Anweisung auf die Tabelle lager erfolgt. Die im Trigger vereinbarte INSERT-Anweisung protokolliert dann, wer die Änderung in der Datenbank vornimmt, zu welchem Zeitpunkt die Änderung erfolgt und welche Ware in welcher Menge betroffen ist. Entsprechende Trigger müssten noch für INSERT- und DELETE-Operationen auf der Tabelle lager angelegt werden. Probleme können bei Triggern dadurch entstehen, dass ein Trigger weitere Trigger auslösen kann: die Operationen werden bei unvorsichtigem Gebrauch kaum noch nachvollziehbar, die Zahl der Änderungen innerhalb einer Transaktion kann "explosionsartig" anwachsen oder es gar zu Deadlocks kommen. Beispiel für eine allgemeine Regel, die vom System automatisch überprüft werden und bestimmte Aktionen auslösen soll, könnte lauten: Gib eine Meldung aus, sobald bei einer Entnahme der Ware A0110 die im Lager befindliche Menge unter einen vorgegebenen Wert sinkt. Aufgabe 8 (15 Punkte) Was ist ein Deadlock? Was sieht der SQL-Standard zur Vermeidung von Deadlocks vor, welche Mittel stellen die Hersteller von relationalen DBMS zur Verfügung? Wird in einer Transaktion ein Objekt verändert und damit gesperrt, so kann eine andere Transaktion auf dieses Objekt erst nach dem Ende der ersten Transaktion zugreifen. Was passiert aber, wenn eine andere Transaktion auch ein Objekt ändert und somit sperrt und wenn die Transaktionen wechselseitig auf die gesperrten Objekte der jeweils anderen Transaktionen zugreifen wollen? Dies ist die typische Verklemmung, auch Deadlock genannt. Erst das Abbrechen einer der beteiligten Transaktionen mit der ROLLBACK-Anweisung gibt die vormals gesperrten Objekte wieder frei, die verbleibenden Transaktionen können nun weiterarbeiten. Die abgebrochene Transaktion muss zu einem späteren Zeitpunkt wiederholt werden. Will der Benutzer selbst Sperren anfordern, so muss er sich der LOCK-Anweisung bedienen. Soll z.B. die Tabelle lager zum Schreiben gesperrt werden – weil viele Einfügungen und Änderungen vorzunehmen sind -, so lautet die Anweisung LOCK TABLE lager IN EXCLUSIVE MODE und lager ist bis zur nächsten COMMIT- bzw. ROLLBACK-Anweisung für alle anderen Benutzer gesperrt. Soll die Tabelle hingegen nur zum Lesen gesperrt werden, so wird das bewirkt durch LOCK TABLE lager IN SHARE MODE Da SQL automatisch Sperren anfordert, liegt der Vorteil einer expliziten Anforderung von Sperren darin, dass in besonders gelagerten Fällen der Aufwand des Datenbanksystems, viele Sperren aufzubauen, durch eine übergreifende Sperren-Anforderung verringert werden kann. Durch das Anfordern der nötigen Sperren kann ein Deadlock vermieden werden; es kann garantiert werden, dass Batch-Arbeiten ungestört zum Abschluss kommen. Zum Schutz gegen Deadlocks bieten die DBMS'e weiterhin das optimistische Synchronisationsverfahren an. Diese Systeme gehen von der Annahme aus, dass Konflikte zwischen Transaktionen seltene Ereignisse darstellen und somit das präventive Sperren der Objekte unnötigen Aufwand verursacht. Daher greifen diese Verfahren zunächst nicht in den Ablauf einer Transaktion ein, sondern erlauben ein nahezu beliebig paralleles Arbeiten auf der Datenbank. Erst bei Transaktionsende wird geprüft, ob Konflikte mit anderen Transaktionen aufgetreten sind. Gemäß dieser Vorgehensweise unterteilt man die Ausführung einer Transaktion in 3 Phasen: 1. Lesephase 2. 3. Validierungsphase Schreibphase Die Verwendung von Sperr-Protokollen führt zu einigen Problemen etwa in der DeadlockErkennung. Dies tritt verschärft in verteilten Datenbanken auf. Insbesondere dort werden weitere Verfahren verwendet, so z.B. das Zeitstempelverfahren. Jede Transaktion erhält einen eindeutigen Zeitstempel. Dieser Zeitstempel wird bei Zugriff auf ein Objekt in eine dafür vorgesehenes zusätzliches Feld des Objektes eingetragen. Die Datenstruktur muss also für jedes Datenobjekt um ein oder mehrere Zeitstempelfelder erweitert werden. Es entsteht so gesehen eine Reihenfolge an Transaktionen, die erst abgearbeitet werden Vorteil des Zeitstempelverfahrens ist die garantierte Deadlock-Freiheit. Zumindest zwei Probleme sind jedoch mit dem Zeitstempelverfahren verbunden: die muss. Der Erweiterung der Datenstrukturen, die Benachteiligung länger laufender Transaktionen; diese werden durch die Ankunft neuerer Transaktionen zurückgesetzt, sobald sie auf Daten zugreifen müssen, die bereits von der jüngeren Transaktion berührt wurden. Albrecht Achilles 2002-06-07