Schritt 2: Erzeugen einer Tabelle

Werbung
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
Herunterladen