Sql-Tutorial: Lernen Sie
Sql und üben Sie die entsprechenden Befehle interaktiv anhand des Sql-interaktiv-lernen
Auf den folgenden Seiten finden Sie ein Sql-Tutorial, das anhand einer einfachen Datenbank die zentralen Prinzipien der beiden
Teile von SQL, der Data Manipulation Language (DML) und der Data Definition Language (DDL) verdeutlicht.
Die Beispieldatenbank, bestehend aus drei kleinen Tabellen (
Artikel.txt,
Vertreter.txt,
Umsatz.txt)
können Sie sich entweder direkt herunterladen und in ein eigenes Datenbank-System einspielen oder Sie nutzen das
Sql-interaktiv-lernen aus den
Freeware-Tools. In diesem ist eine Access-Datenbank mit den drei Beispieltabellen bereits enthalten. Eine
Access-Installation ist zur Nutzung nicht notwendig. Es genügt die .NET1.1-Laufzeitumgebung, der Zugriff auf die
Datenbank erfolgt über das Programm.
Falls Sie das Beispiel auf einem Ms-SqlServer oder einem anderen vergleichbaren Datenbank-System verwenden möchten, können Sie das
Script
create-sql-interaktiv.sql verwenden. Mit dem OSQL.exe können
Sie dieses Script ausführen, ein Beispiel finden Sie in der Datei.
Neue Lösungen für Ihre DatenServer-Daten: Die Web - Datenbank als CRM - Lösung ist:
- die Single-Data-Solution: Alles in einer Datenbank, ein Login.
- Sie entscheiden, was Ihre Datenbank macht. Diese funktioniert.
- Einfach für alle Generationen - Internetzugang genügt.
- Erste Formulare nach ein bis zwei Tagen.
- Kontakt per Telefon (+49(0)30 420 200 60) oder per Mail
- Zahlungsmöglichkeiten: Einmalige Einrichtung plus Miete (Preisliste)
oder Einrichtung+Miete verteilt auf 1 bis 2 Jahre monatlich
- Jetzt anmelden!
Ein Hinweis zum Aufbau dieses Tutorials: Bevor Sie sich zu viel mit der Theorie beschäftigen - erarbeiten Sie
sich zunächst die praktischen Beispiele zum SELECT-Befehl (DML-I). Sql arbeitet mit Mengen. Sind Ihnen
Auswertungen anhand eines überschaubaren, absichtlich kleinen Beispiels geläufig, so lernen Sie die zugrundeliegende
relationale Theorie beiläufig.
Auswahl und Aggregation vorhandener Daten (Data Manipulation Language - DML-I)
- SELECT - der Grundbegriff zur Auswahl von Daten: Sobald Sie Daten auswählen,
sei es, um diese direkt anzeigen zu lassen oder sie weiterzuverarbeiten, verwenden Sie
den SELECT-Befehl. In der einfachsten Version werden aus einer Tabelle alle oder einige
Spalten ausgewählt.
- Mit WHERE wenige Zeilen auswählen: Sollen von einer Tabelle nicht sämtliche, sondern
nur wenige Zeilen zurückgegeben werden, so reduzieren Sie die Zahl der ausgegebenen Zeilen
mit einer WHERE-Klausel.
- Mit JOIN Tabellen kombinieren: Wurden die Regeln für die Normalisierung der Daten
eingehalten, so sind die zu einem Datensatz gehörenden Zeilen oftmals über mehrere Tabellen
verteilt. Mit einer JOIN - Klausel lassen sich zwei Tabellen zu einer neuen, virtuellen Tabelle
zusammenfügen, so daß bsp. Zellen aus verschiedenen Tabellen miteinander multipliziert oder nach fehlenden
Einträgen in einer der Tabellen gesucht werden kann.
- Mit GROUP BY Daten aggregieren und auswerten: Alle bisherigen Techniken wählen aus,
verändern die Ausgabezeilen jedoch nicht. Möchten Sie Werte einer Spalte aus mehreren Zeilen addieren, das
Maximum in einer Spalte bestimmen oder Umsätze pro Tag und pro Artikel erhalten, so verwenden
Sie Aggregatfunktionen und die GROUP-BY - Klausel.
- Subqueries / Unterabfragen: Unterabfragen, meist Subqueries genannt, werden verwendet,
um das Ergebnis einer Abfrage sofort als zusätzlichen Input für eine weitere Abfrage heranzuziehen.
Damit lassen sich tieferliegende Strukturen ermitteln, etwa die Frage, welche Artikel oder Vertreter
überdurchschnittlich viel zum Umsatz beigetragen haben.
Grundlagen für relationale Datenbank-Systeme und Entwurf der Speicherstruktur
- Datenbank-Grundbegriffe: Im Bereich der Datenbank-Entwicklung werden grundlegende Unterscheidungen
verwendet: Datenbank versus Datenbank-Management-System, die Forderung nach Redundanzfreiheit sowie das
Erzwingen der Datenintegrität, der Unterschied zwischen desktop- und serverbasierten DBMS.
- Organisation der Daten: Tabelle, Zeile und Spalte: Bei der Entwicklung einer relationalen Datenbank
kann zunächst von einer einzigen, großen Tabelle ausgegangen werden. An dieser lassen sich diverse
Probleme wie Redundanz und Insert-, Update- und Delete-Anomalien beobachten.
- Primär- und Fremd- Schlüssel, Tabellen- Beziehungen: Ziel der Normalisierung ist, die denormalisierte
Ausgangs-Tabelle in kleine Tabellen zu zerlegen, die miteinander verknüpft sind. Dies erfolgt mit Konzepten
wie Schlüssel, Nicht-Schlüssel, Primär- und Fremdschlüssel sowie zusätzlichen Festlegungen für einzelne
Spalten.
- Erste Normalform - atomare Zellen und Datentypen: Die erste Normalform fordert atomare Attribute sowie das Auslagern
von Gruppen. Um auf den Daten später spezielle Operationen durchführen zu können, werden für Spalten
Datentypen festgelegt.
- Zweite Normalform - funktional abhängige Attribute: Die zweite Normalform verlangt, daß alle
Attribute in einer Tabelle voll funktional vom Primärschlüssel abhängen. Wird ein Attribut bereits durch
eine Teilmenge des Primärschlüssels festgelegt, so ist dieses mit der Primärschlüssel-Teilmenge in eine
eigene Tabelle auszulagern.
- Dritte Normalform - keine transitive Abhängigkeit: Die dritte Normalform ist erfüllt, falls kein
Attribut transitiv abhängig ist, daß es kein Nichtschlüssel-Attribut gibt, welches dieses Attribut bereits
festlegt.
- Sql als Sprache der vierten Generation: Sql ist als Programmiersprache durch einige Besonderheiten
charakterisiert. Das DBMS muß Schnittstellen zum Austausch von Sql-Befehlen zur Verfügung stellen, die
Sprache läßt sich in eine Wirtssprache einbetten und ist mengenorientiert. Die Abgrenzung zu anderen
Programmiersprachen läßt sich durch die fünf Sprachgenerationen skizzieren.
- Die historische Entwicklung von SQL: Einige Hinweise zur historischen Entwicklung von
Sql, beginnend mit der Arbeit von Codd über den Vorläufer SEQUEL, die ersten Implementierungen bis
zu den heutigen Standards Sql-92 und Sql-99.
Eintragen, Ändern und Löschen von Daten (Data Manipulation Language - DML-II)
- INSERT - Hinzufügen neuer Zeilen zur Tabelle: Mit dem INSERT-Befehl werden Daten neu in vorhandene
Tabellen eingefügt. Als Quelle kann eine Zeile mit VALUES direkt angegeben werden oder die Werte stammen aus dem
Ergebnis einer SELECT-Abfrage.
- Mit UPDATE vorhandene Zeilen aktualisieren: Die UPDATE-Anweisung ermöglicht die Aktualisierung vorhandener
Zeilen einer Tabelle. Das zu verändernde Rowset kann sowohl mit WHERE-Klauseln als auch über eine Verknüpfung
mit anderen Tabellen ermittelt werden, ebenso können die neuen Werte aus anderen Tabellen stammen.
- DELETE - Zeilen löschen: DELETE entfernt Zeilen. Analog zu Update können die zu löschenden Zeilen
durch WHERE eingeschränkt oder per JOIN-Verknüpfung über den Umweg anderer Tabellen spezifiziert werden.
Weitere Seiten zum Erstellen von Datenbank-Objekten (DDL) werden folgen. DLL-Befehle können allerdings nicht von Nutzern
beim Hauptprojekt verwendet werden, insofern sind diese Themen derzeit nachrangig.
Die aktuellen Texte stehen bereits zur Verfügung, damit Beispiele für das Sql-interaktiv-lernen aus den
Freeware-Tools existieren und genutzt werden können.
Falls Sie diesen Text interessant finden, können Sie gerne einen Verweis auf Ihrer Site einfügen. Verlinken Sie
bitte ausschließlich das Html-Angebot, die Xml-Version ist nur für Leser mit dem IE6 verwendbar.
Kopieren Sie die folgende Html-Zeile in den Quellcode Ihrer Datei oder passen Sie die Darstellung Ihren eigenen Bedürfnissen an:
<a href="http://www.sql-und-xml.de/sql-tutorial/">Sql-Tutorial</a>
TOP
Daten auswählen mit dem SELECT-Befehl
Die SELECT-Anweisung ist fundamental für jedes Auswählen von Daten und stellt diese in Form einer
virtuellen Tabelle
zur Verfügung. Diese virtuelle Tabelle, auch
Recordset genannt, also eine Menge (= Set) von Records (= Datenzeilen, Datensätzen),
existiert zunächst nur temporär im Arbeitsspeicher und wird nach dem Ende der Befehlsausführung verworfen.
Wird SELECT ohne weitere Ergänzungen verwendet, so werden die Daten angezeigt. Die Ausgabe kann auch mit SELECT ... INTO ... FROM in
eine neue Tabelle kopiert oder mit INSERT INTO ... SELECT ... zu einer bestehenden Tabelle hinzugefügt werden.
Syntax
-
SELECT [DISTINCT]
<Name einer Spalte>
<Konstante>
<Berechnung>
<einer der obigen Ausdrücke> As Spaltenalias
[, weitere der obigen Ausdrücke]
FROM <Ausdruck, der eine Tabelle zurückgibt> As Tabellenalias
[WHERE ...]
[GROUP BY ...]
[HAVING ...]
[UNION [ALL]]
[Weitere SELECT-Anweisung, welche dieselbe Zahl von
Spalten und Datentypen liefert]
[ORDER BY [Order-By-Ausdruck] ASC | DESC]]
[, weitere Sortierungen]
Dies ist die grundlegende Syntax des SELECT-Befehls. Details zu den Ausdrücken nach FROM, WHERE,
GROUP BY und HAVING finden Sie in den Abschnitten über JOIN, WHERE und GROUP BY. Die hiesigen Beispiele behandeln nur
den Abschnitt zwischen SELECT und FROM.
SELECT <Name einer Spalte - wie oben >
INTO <neue Tabelle>
FROM <Ausdruck, der eine Tabelle zurückgibt> As Tabellenalias
Beispiele
SELECT A_NR,
A_NAME,
A_PREIS
FROM ARTIKEL
Einfache, kommagetrennte Auflistung der gewünschten Spalten, die im Tabellen-Ausdruck vorkommen. Ohne
Alias für die Tabelle.
SELECT A.A_NR,
A.A_NAME,
A.A_PREIS
FROM ARTIKEL As A
ORDER BY A.A_PREIS
Dasselbe wie im ersten Beispiel, aber mit Aliasname A für die Tabelle und aufsteigender Sortierung nach
der Spalte A_PREIS.
SELECT A_NR,
A_PREIS As Netto,
0.19 As MwSt,
A_PREIS * 1.19 As Brutto,
FROM ARTIKEL
ORDER BY A_PREIS DESC
Hier werden Alias-Ausdrücke für die Spalten verwendet - das Ergebnis kennt die drei Spalten
Netto, MwSt und Brutto. MwSt ist ein konstanter Wert, in der Spalte Brutto wird der Inhalt
von A_PREIS multipliziert mit einer Konstanten. Da ein
solches Ergebnis keinen Spaltennamen hat, sollte dieser anschließend festgelegt werden. Das Ergebnis
wird nach A_PREIS absteigend sortiert.
SELECT A.* FROM ARTIKEL As A
ORDER BY A.A_NAME ASC,
A.A_PREIS DESC
Hier wird für die Tabelle ein Alias A verwendet und mit * sämtliche Spalten ausgewählt. Das Ergebnis
wird aufsteigend nach den Artikel-Namen, absteigend nach den Artikel-Preisen sortiert.
SELECT DISTINCT A.A_NAME
FROM ARTIKEL As A
Das Schlüsselwort DISTINCT entfernt alle mehrfach vorkommenden Zeilen mit Ausnahme einer. Diese Abfrage
liefert deshalb nicht vier Zeilen mit doppeltem 'Oberhemd', sondern nur drei Zeilen zurück, eine Zelle
mit dem Wert 'Oberhemd' wurde entfernt.
SELECT A.A_NAME
FROM ARTIKEL As A
UNION
SELECT B.V_NAME
FROM VERTRETER As B
Diese inhaltlich merkwürdige Abfrage liefert alle Artikel- und alle Vertreter-Namen in einer
einzigen Liste aus. Da ALL fehlt, werden sechs Zeilen ausgegeben, das doppelte 'Oberhemd' wird nur
einfach in das Resultset übernommen. Beachten Sie, daß die Datentypen übereinstimmen müssen und daß
jede einzelne SELECT-Abfrage dieselbe Zahl von Spalten zurückliefern muß. Die Spaltennamen müssen
allerdings nicht übereinstimmen, bei den SELECT-Anweisungen ab der zweiten Abfrage kann auf ALIAS-Namen
verzichtet werden.
SELECT V.*
INTO [Kopie-von-Vertreter]
FROM VERTRETER As V
Dies transferiert die virtuelle Tabelle in ein reales neues Tabellenobjekt. Zunächst wird die neue Tabelle 'Kopie-von-Vertreter'
erstellt und anschließend mit den Zeilen gefüllt, die von der SELECT-Anweisung zurückgegeben wurden. Ergänzt man diese
Abfrage um eine WHERE-Klausel 0 = 1, so werden keine Zeilen kopiert, es wird jedoch eine neue leere Tabelle erstellt.
Bemerkungen
- Machen Sie es dem Sql-Parser möglichst einfach. Verwenden Sie Alias-Namen für Tabellen,
dann können die Spalten rascher identifiziert werden, es muß nicht erst ermittelt werden, von welcher
Tabelle diese Spalte stammt.
- Wenn der Spalten-, Alias- oder Tabellenname Sonderzeichen, etwa ein Leerzeichen oder Minus (-) enthält, so
schließen Sie den Ausdruck in eckige Klammern ein. Beispiel:
Select A.A_PREIS * 1.19 As [Aktueller Bruttopreis]
FROM ARTIKEL
- Soll ein konstanter Text angegeben werden, so setzen Sie diesen in einfache Hochkommata. Beispiel
SELECT A.A_NAME, 'aktuell' As Info
FROM ARTIKEL As A
Dies liefert zwei Spalten, die erste Spalte heißt 'A_NAME', die zweite 'Info'. Das Ergebnis enthält
vier Zeilen, in der ersten Spalte stehen die Werte aus der Tabelle, die zweite Spalte enthält viermal
den Text 'aktuell'.
- Benötigen Sie eine zusätzliche Zahl, etwa beim Vereinigen zweier Tabellen mit UNION, so können
Sie diese einfach notieren:
SELECT A.A_NAME, 1 As [Index]
FROM ARTIKEL As A
UNION
SELECT B.V_NAME, 2
FROM VERTRETER As B
Eine solche Technik kann nützlich sein, wenn mit
UNION die Ausgaben mehrerer SELECT-Anweisungen zusammengefügt werden und eine Information benötigt wird, aus welchem
SELECT-Abschnitt die einzelne Zeile stammt.
- Hinter FROM folgt ein Ausdruck, der eine Tabelle zurückgibt. Dies kann, wie in den obigen Beispielen,
eine einfache Tabelle sein, das Ergebnis einer JOIN-Verknüpfung oder einer Unterabfrage. Ferner lassen es
manche Datenbanksysteme zu, daß der FROM-Abschnitt fehlt. In diesem Fall wird genau eine Zeile ausgegeben.
- Beim Sortieren auf dem MS-SQLServer kann der Aliasname in der ORDER-BY-Klausel angegeben werden.
Access dagegen versteht diesen Namen nicht, so daß bei der Sortierung nach dem Ergebnis einer Rechenoperation
diese erneut notiert werden muß.
- Verwendet man die Technik 'SELECT ... INTO ... FROM ...', um eine neue Tabelle zu erzeugen und enthält die
ursprüngliche Tabelle eine automatisch hochzählende ID (Identity(1,1)), so wird dieser spezielle Spaltentyp auch in der
Zieltabelle erzeugt. Soll dies vermieden werden, kann zur Identitätsspalte 0 hinzugefügt und der Spaltenname als Alias
notiert werden:
SELECT U.Umsatz_Nr + 0 As [Umsatz_Nr], U.A_Nr
Into [Umsatz-Kopie]
FROM UMSATZ As U
Nun wird die Spalte Umsatz_Nr der neuen Tabelle als Integer definiert.
TOP
Mit WHERE wenige Zeilen auswählen
Die SELECT-Anweisung liefert eine virtuelle Tabelle, bestehend aus Zeilen und Spalten zurück. Der nach FROM folgende
WHERE-Abschnitt kann Spaltennamen verwenden, um Bedingungen festzulegen. Für jede Zeile wird geprüft, ob die durch den Spaltennamen
festgelegte Zelle die Bedingung erfüllt. Falls dies der Fall ist, wird die Zeile zur Ausgabe hinzugefügt, ansonsten wird diese Zeile
nicht zum Resultset ergänzt.
Syntax
Beispiele:
Select A.*
FROM ARTIKEL As A
WHERE A.A_NR = 11
Wählt die Zeile aus, bei welcher die Zelle A_NR den Wert 11 hat. Hier ist eine Bedingung und kein
logischer Operator angegeben.
WHERE NOT A.A_NR = 11
WHERE A.A_NR <> 11
Wählt alle Zeilen aus, deren A_NR verschieden von 11 und nicht leer (Not Null) ist.
WHERE A_PREIS IS NULL
Wählt jene Zeilen aus, bei welchen derzeit kein Preis definiert ist, bei denen die Zelle A_PREIS also
leer ist. Beachten Sie, daß eine Zelle mit dem Wert 0 nicht leer ist, sondern den Wert 0 enthält.
Ebenso ist bei Zellen mit Text die Belegung mit einer leeren Zeichenfolge (A_NAME = "" bzw. '') verschieden
von der Zuweisung A_NAME = NULL bzw. der Abfrage A_NAME IS NULL.
WHERE A.A_NAME = 'Oberhemd' And A.A_PREIS < 40.00
Dies wählt nur jene Zeilen aus, bei denen sowohl der Wert in A_NAME gleich 'Oberhemd' und der Wert
in A_PREIS kleiner als 40.00 ist. Es wird nur die Zeile mit A_NR = 12 ausgewählt.
WHERE A.A_NAME = 'Oberhemd' Or A.A_PREIS > 40.00
Dies wählt jene Zeilen aus, bei denen der Wert in A_NAME gleich 'Oberhemd' oder der Wert
in A_PREIS größer als 40.00 ist. Die einzigste Zeile, die einen Preis < 40.00 hat, beschreibt ein
'Oberhemd', so daß von dieser Abfrage alle vier Zeilen zurückgegeben werden.
WHERE A.A_PREIS BETWEEN 39.8 AND 100.00
Listet die beiden Zeilen auf, deren Preis zwischen 39.8 und 100.00 liegt. Die Randwerte werden mitgezählt,
deshalb ist die Zeile A_NR = 12 im Ergebnis enthalten. Beachten Sie, daß das AND zum BETWEEN gehört und
hier keine logische Bedeutung hat.
WHERE 39.8 <= A.A_PREIS And A.A_PREIS <= 100
Diese Version ist gleichwertig zur vorherigen Version.
WHERE A.A_NAME IN ('Hose', 'Mantel', 'Strümpfe')
Vergleicht den Wert von A_NAME mit jedem der in der Klammer angegebenen Werten. Stimmt er mit einem dieser
überein, so wird die betreffende Zeile ausgegeben. Der Ausdruck ist gleichwertig zu
WHERE A.A_NAME = 'Hose' OR
A.A_NAME = 'Mantel' OR
A.A_NAME = 'Strümpfe'
Rechts kann, wie hier, eine Liste von Konstanten,
berechneten Werten oder eine Unterabfrage notiert werden. Die Unterabfrage muß eine Spalte zurückliefern.
WHERE 0 = 1
oder
WHERE FALSE
Dieser Ausdruck liefert immer False zurück, es werden also keine Zeilen ausgewählt. Dies kann verwendet
werden, falls nur die Spaltennamen gewünscht sind. Analog liefert 0 = 0 oder TRUE alle Zeilen zurück, so daß eine solche
WHERE-Klausel redundant ist.
- Textvergleiche mit LIKE
SELECT V.* FROM VERTRETER
WHERE V.V_NAME LIKE 'Me_er, Franz'
Mit LIKE können Zellen gegen Textmuster geprüft werden, ohne daß eine vollständige Übereinstimmung
notwendig ist. Der Unterstrich (_) fungiert als Platzhalter für ein Zeichen, so daß der obige Ausdruck
sowohl 'Meyer, Franz' als auch 'Meier, Franz' findet.
WHERE A.V_NAME LIKE 'Me%'
Das Prozentzeichen (%) schließt 0 bis mehrere Zeichen ein, so daß aus der Beispieldatenbank sowohl
'Meier, Franz' als auch 'Meyer, Emil' gefunden wird.
- Suche nach den Sonderzeichen '_' und '%': Wenn Sie nach diesen Sonderzeichen selbst suchen möchten,
dann setzen Sie diese in eckige Klammern ([]). Damit wird die eckige Klammer selbst zum Sonderzeichen, so daß auch eine Suche
nach einer eckigen Klammer den Einschluß erfordert. Ansonsten erlauben eckige Klammern das Angeben eines Bereiches. Beispiel:
WHERE <Spaltenname> LIKE '%Tulpen[_]und[_]Zwiebeln%'
findet 'Hier gibt es Tulpen_und_Zwiebeln', nicht jedoch 'Hier gibt es Tulpen-und-Zwiebeln'. Entfernt man die Eckklammern, werden
beide Einträge ausgegeben.
WHERE <Spaltenname> LIKE 'Mayer [5-7]'
WHERE <Spaltenname> LIKE 'Mayer [567]'
findet 'Mayer 5', 'Mayer 6', 'Mayer 7', nicht jedoch 'Mayer 8'
WHERE <Spaltenname> LIKE 'Mayer [[]5]'
findet 'Mayer [5]'
- Mit Exists prüfen, ob eine Unterabfrage Werte enthält:
SELECT A.A_NR
FROM ARTIKEL As A
WHERE EXISTS
(SELECT B.UMSATZ_NR
FROM UMSATZ As B
WHERE B.A_NR = A.A_NR)
Diese Abfrage liefert jene Artikel einmal (!) zurück, für die es Einträge in der Tabelle 'Umsatz' gibt. Diese Abfrage
erzeugt dasselbe Ergebnis wie:
SELECT DISTINCT A.A_NR
FROM ARTIKEL As A INNER JOIN UMSATZ As U
ON A.A_NR = U.A_NR
Nur wird die letztere Abfrage in der Regel teurer sein, da zunächst alle passenden Zeilen gesucht und mehrfache Einträge anschließend
mit DISTINCT entfernt werden. EXISTS bricht dagegen ab, falls bereits eine einzige Zeile gefunden wurde.
Bemerkungen
- Zur Suche nach ganzen Zahlen werden diese direkt notiert. Für Zahlen mit Nachkommastellen ist der Punkt
das gültige Trennzeichen (A_Preis > 99.99). Textkonstanten werden in einfache Hochkommata (') gesetzt.
Soll nach einem einfachen Hochkomma gesucht werden, so genügt es, dieses zu verdoppeln:
WHERE V.V_Name = 'O''Neil'
findet
O'Neil
- Grundsätzlich gilt, daß jeder Vergleich mit einem Operator (NOT, =, <, >, IN) auf NULL
ausgewertet wird, falls einer der beiden Ausdrücke NULL ist. Damit liefert auch die Verneinung
eines solchen Ausdrucks NULL. Damit wird eine gesamte Menge von Zeilen mit NULL-Zellen nicht
vollständig durch einen Vergleich und dessen Verneinung ausgeschöpft.
WHERE A.A_PREIS = 10.00
...
UNION
...
WHERE A.A_PREIS <> 10.00
liefert alle Zeilen mit Preis, jedoch nicht jene Artikel,
für die kein Preis definiert ist, bei welchen die A_PREIS-Zelle leer ist. Denn bei einer leeren Zelle kann keine der obigen Bedingungen
positiv geprüft werden.
- Ein Beispiel mit einem logischen Operator:
SELECT A.* FROM ARTIKEL AS A
WHERE A.A_NAME = 'Hose'
UNION
SELECT A.* FROM ARTIKEL AS A
WHERE NOT A.A_NAME = 'Hose'
Im Rahmen von jedem Logik-Kurs wird Ihnen mitgeteilt, daß diese Abfrage sämtliche Zeilen liefert,
hier also 4 Zeilen ausgibt. Löschen Sie anschließend testweise eine Zelle der Spalte A_NAME,
etwa eine Zelle mit dem Wert 'Oberhemd'. Löschen Sie nur die Zelle, nicht die Zeile.
Führen Sie anschließend die Abfrage erneut aus, so werden Sie nur drei Ergebniszeilen erhalten.
- Operatorreihenfolge: Ein Vergleich mit = bindet am stärksten, so daß ein Ausdruck
A_NAME = 11 Or A_PREIS > 100
wie gewünscht interpretiert wird:
(A_NAME = 11) Or (A_PREIS > 100)
Verwenden Sie verschiedene logische Operatoren in einem WHERE-Ausdruck, so nutzen Sie am besten Klammern.
Dies ist für jeden, der den Code später bearbeitet und die genaue Operator-Reihenfolge nicht weiß, die
sicherste Lösung, damit sich nicht unbeabsichtigte Fehler einschleichen.
Betrachten Sie den folgenden Ausdruck:
WHERE A.A_NR BETWEEN 11 AND 10 OR 15
Der erste Ausdruck ist für A_NR = 11 zumindest für Access in Sql-Interaktiv-lernen erfüllt, so daß mit
einem Datensatz zu rechnen wäre. Da AND stärker bindet als OR und AND zu BETWEEN gehört, wird der Ausdruck
tatsächlich jedoch so ausgewertet:
WHERE (A.A_NR BETWEEN 11 AND 10) OR (15)
Der Ausdruck (15) ist verschieden von 0, damit in den meisten Datenbank-Systemen wahr. Also handelt
es sich um eine Konstante, so daß alle Zeilen ausgegeben werden.
- Platzhaltersuche bei Access: Werden in Access über die gewöhnliche Programmierumgebung Abfragen erstellt, so müssen
entgegen den oben genannten Konventionen für die Suche nach einem einzelnen Zeichen das '?', für die Suche nach mehreren Zeichen
'*' verwendet werden. In diesem Fall wird das DAO-Modell (Data Access Objects) genutzt, welches einen Non-Standard - Sql-Dialekt implementiert.
Wird dagegen auf eine Access-Datenbank über eine der Programmierschnittstellen ADO oder .NET zugegriffen, so sind die Standard-Zeichen
'_' und '%' zu verwenden. Das Sql-Interaktiv-lernen verwendet .NET-Zugriffstechniken, so daß hier der Standard einzusetzen ist.
- Anstatt eines Ausdrucks der Form
<Spaltenname> = <Wert>
kann rechts auch entweder ein anderer Spaltenname oder eine eigenständige in Klammern stehende
Select-Abfrage angegeben werden. Diese muß genau eine Zelle zurückliefern. Ebenso kann bei einem Ausdruck
der Form
<Spaltenname> IN (<Unterabfrage mit einer Spalte>)
eine Unterabfrage eingesetzt werden, die genau eine Spalte zurückliefert. Beispiele hierzu finden
Sie im Abschnitt zu Unterabfragen.
- EXISTS kann beim Sql-Server ressourcenschonend bsp. am Anfang eines Scripts dazu verwendet werden, um zu prüfen, ob ein
Objekt existiert, um es gegebenenfalls zu löschen:
If (Exists
(Select A.Table_Name
From Information_Schema.Tables As A
Where A.Table_Name = 'Artikel')
)
Drop Table Artikel
Diese Technik ist jedem Group By bzw. Count(*) vorzuziehen, etwa dem folgenden Code:
If ((Select Count(*) From Information_Schema.Tables As A
Where A.Table_Name = 'Artikel') > 0)
Drop Table Artikel
TOP
JOIN - Normalisierte Tabellen für eine Abfrage wieder zusammenfassen
Vorbemerkung
Aufgabe der JOIN-Verknüpfung: Die Daten wurden normalisiert, also auf verschiedene Tabellen aufgeteilt.
So sind die Daten zu einer Bestellung (Artikel, Datum, Menge, Vertreter) in der Beispiel-Datenbank nicht
in einer Tabelle abgelegt, sondern wurden auf drei Tabellen verteilt: Artikel und Vertreter umfassen die
Stammdaten, die Tabelle Umsatz enthält die eigentlichen Bewegungsdaten. Pro Zeile wird lediglich ein
Verweis auf die zur einzelnen Bestellung gehörenden Datensätze abgelegt. Sollen alle Daten einer
Bestellung als ein Datensatz ausgegeben werden, so müssen die Einträge aus den verschiedenen Tabellen
einander zugeordnet werden. Dies leistet die JOIN-Verknüpfung. Ein JOIN fügt zwei Tabellen zu einer
neuen, virtuellen Tabelle zusammen, die anschließend mit einem weiteren JOIN sowie der nächsten Tabelle
verknüpft werden kann.
Folglich genügt es, einen JOIN zwischen zwei Tabellen zu behandeln.
Syntax
- Komma-Version:
FROM <Tabelle oder Abfrage>, <Tabelle oder Abfrage>
- Einfachste Form des INNER JOIN
FROM <Tabelle oder Abfrage> INNER JOIN <Tabelle oder Abfrage>
ON <Spalte1> OPERATOR <Spalte2>
- Mehrfache Verknüpfung:
FROM <Tabelle oder Abfrage> INNER JOIN <Tabelle oder Abfrage>
ON <Spalte1> OPERATOR <Spalte2>
AND
<Spalte3> OPERATOR <Spalte4>
-
Statt INNER kann auch eines der folgenden Schlüsselwörter verwendet werden:
LEFT, RIGHT, OUTER
-
Statt dem AND-Operator kann auch OR verwendet werden, ferner können mehr als zwei Vergleichsausdrücke
angegeben sowie AND und OR kombiniert werden.
- Eine Tabelle kann auch zweimal angegeben werden, man spricht von einer Selbstverknüpfung,
da Zeilen der Tabelle zu anderen Zeilen der Tabelle in Beziehung gesetzt werden. Abgesehen von der Komma-Version ist mindestens
ein Alias-Name für eine Tabelle Pflicht, um die Spalten eindeutig zu beschreiben.
Beispiele
SELECT A.*, U.*
FROM ARTIKEL As A, UMSATZ As U
Diese Komma-Version kombiniert jede Zeile der Tabelle ARTIKEL mit jeder Zeile der Tabelle UMSATZ und
gibt das Ergebnis aus 4 * 9 = 36 Zeilen vollständig aus. Vergleicht man die beiden Spalten
A.A_NR und U.A_NR, so fällt auf, daß den Umsätzen in den meisten Fällen Artikel zugeordnet sind, welche
von dem im Umsatz erwähnten Artikel verschieden sind. Diese (36 - 9) = 27 Ergebnisse sind also sinnlos,
Durch die in der ON-Klausel festgelegte Filterbedingung in der folgenden JOIN-Klausel werden diese
Datensätze entfernt.
-
SELECT A.A_NR,
A.A_NAME,
A.A_PREIS,
U.DATUM,
U.A_STUECK,
A.A_PREIS * U.A_STUECK AS Preis
FROM UMSATZ AS U INNER JOIN ARTIKEL AS A
ON U.A_NR = A.A_NR
Dies ist die einfachste und am häufigsten genutzte Form des JOIN. Die Spalte A.A_NR
ist gleichzeitig der Primärschlüssel der Tabelle ARTIKEL, zu einem
Artikel kann es mehrere Bestellungen, also Zeilen in der Tabelle UMSATZ
geben. Aus der oben angegebenen Komma-Version werden all jene Zeilen entfernt, bei welchen die beiden Spalten
A.A_NR und U.A_NR nicht übereinstimmen.
Ersetzt man in diesem Ausdruck den INNER JOIN durch die Komma-Version, so wird deutlich: Hier ist die
Komma-Version unsinnig. Denn eine Mengenangabe, die sich auf einen Artikel bezieht, wird kombiniert
mit dem Preis eines anderen Artikels.
SELECT A.A_NR,
A.A_NAME,
A.A_PREIS,
V.V_NR,
V.V_NAME,
U.DATUM,
A.A_PREIS * U.A_STUECK AS Preis
FROM (ARTIKEL AS A INNER JOIN UMSATZ AS U
ON A.A_NR = U.A_NR) INNER JOIN VERTRETER AS V
ON U.V_NR = V.V_NR
Dies ist die Version für die Verknüpfung von drei Tabellen. In der ersten Klammer werden zunächst jene neun
Ergebniszeilen gefiltert, bei denen der Umsatz zum Artikel gehört. Zu dieser neuen Tabelle werden jene
Zeilen hinzugenommen, bei denen die Vertreter-Id übereinstimmt. Die Komma-Version würde (4 * 9) * 3 = 108
Zeilen liefern, der doppelte Join liefert korrekt die neun Zeilen aus der Tabelle UMSATZ, ergänzt um die
Informationen aus den beiden anderen Tabellen.
Manche Datenbank-Systeme, etwa Access, fordern, daß bei einem solchen Ausdruck ein Join geklammert wird.
Beim MS-SqlServer kann auf die Klammern verzichtet werden.
-
Ausdrücke mit Operatoren werden beim INNER JOIN nur dann ausgewertet, wenn die einzelnen Ausdrücke nicht
Null sind. Ist ein Ausdruck ein Spaltenname, so werden bsp. bei einer Prüfung auf Gleichheit alle Zeilen
ignoriert, in welchen die zugehörige Zelle leer ist. Damit liefert der INNER JOIN immer nur Informationen
über tatsächliche Vorgänge, etwa über Verkäufe von Artikeln. Es sind damit jedoch zunächst keine Informationen
über nicht verkaufte Artikel ermittelbar, der normale JOIN erzeugt keine Informationen zu einem
Nicht-Ereignis. Sollen hierüber Aussagen getroffen werden, gelingt
dies mit LEFT bzw. RIGHT JOIN:
SELECT A.A_NR,
U.DATUM
FROM ARTIKEL AS A LEFT JOIN UMSATZ AS U
ON A.A_NR = U.A_NR
Fügen Sie Ihrer Tabelle ARTIKEL einen neuen Artikel hinzu. Als Befehl:
INSERT INTO ARTIKEL(A_NR, A_NAME, A_PREIS)
VALUES (25, 'Mantel', 149.00)
Da es für diesen Artikel keine passende Zeile in der Tabelle UMSATZ gibt, wird dieser Artikel bei einer
INNER-JOIN-Verknüpfung ignoriert. Der obige LEFT JOIN gibt zunächst dasselbe wie ein INNER JOIN aus.
Zusätzlich fügt er alle Datensätze aus der links (left) stehenden Tabelle hinzu, die im Ergebnis bislang
noch fehlen. Werden Spalten aus der rechts stehenden Tabelle mit ausgegeben, so werden deren Werte als
NULL-Werte ausgegeben. In Kombination mit einer WHERE-Abfrage lassen sich die niemals verkauften Artikel
ermitteln:
SELECT A.A_NR,
A.A_NAME
FROM ARTIKEL AS A LEFT JOIN UMSATZ AS U
ON A.A_NR = U.A_NR
WHERE U.UMSATZ_NR IS NULL
Beachten Sie, daß Sie mit dieser Abfrage nicht ermitteln, welcher Artikel bloß an bestimmten Tagen nicht
verkauft wurde. Wurde ein Artikel ein einziges Mal verkauft, so erscheint er nicht mehr in dieser Liste.
- Ermittlung der an einzelnen Tagen nicht verkauften Artikel. Betrachten Sie zunächst die folgende Abfrage:
SELECT C.DATUM, D.A_NR
FROM
(SELECT Distinct A.DATUM
FROM UMSATZ As A
Where A.DATUM Is Not Null) As C,
(SELECT B.A_NR
FROM ARTIKEL As B) As D
Die beiden inneren Abfragen liefern Ihnen eine Liste aller Datumsangaben, an welchen etwas verkauft wurde
sowie eine Liste aller Artikel-Nummern. Beide Tabellen werden durch die Komma-Version kombiniert, so
daß die Ergebnistabelle jede Kombination aus Datum und Artikel umfaßt. Kombinieren Sie diese neue, nur
temporär existierende Tabelle per LEFT JOIN mit den tatsächlichen Umsätzen:
SELECT E.DATUM, E.A_NR,
F.DATUM, F.A_NR
FROM
(SELECT C.DATUM, D.A_NR
FROM
(SELECT Distinct A.DATUM
FROM UMSATZ As A
Where A.DATUM Is Not Null) As C,
(SELECT B.A_NR
FROM ARTIKEL As B) As D) As E
LEFT JOIN UMSATZ AS F
ON E.DATUM = F.DATUM AND E.A_NR = F.A_NR
Die Ausgabe ergibt, daß am 25.6 bei den Artikeln 12 und 13 die Spalten F.DATUM und F.A_NR leer sind, da
für beide Artikel an diesem Tag kein Umsatz vorliegt. Entfernen Sie F.DATUM und F.A_NR in der Liste der
Ausgabespalten und ergänzen Sie die Abfrage um die folgende Bedingung:
WHERE F.DATUM IS NULL
Dann liefert Ihnen diese Abfrage sämtliche Kombinationen von Tagen mit den an diesen Tagen nicht verkauften
Artikeln.
- Selbstverknüpfung einer Tabelle:
SELECT DISTINCT U1.DATUM, U1.A_NR
FROM UMSATZ As U1 INNER JOIN UMSATZ As U2
On (U1.DATUM = U2.DATUM) And
(U1.A_NR = U2.A_NR) And
(U1.A_STUECK <> U2.A_STUECK)
| Datum | A_NR |
| 24.06.1999 | 12 |
| 24.06.1999 | 13 |
Diese Abfrage ordnet zunächst all jene Zeilen paarweise einander zu, die sich auf dasselbe Datum und denselben
Artikel beziehen. Da als dritte Bedingung die Verschiedenartigkeit der A_STUECK-Spalte geprüft wird, werden zunächst alle
Kombinationen aus Datum und Artikel entfernt, die sich auf dieselbe Zeile beziehen. Dasselbe würde jedoch die Prüfung
U1.UMSATZ_NR <> U1.UMSATZ_NR erfüllen. Die tatsächlich verwendete Prüfung entfernt jedoch zusätzlich alle Kombinationen aus
Datum und Artikel, bei welchen der Artikel mehrfach mit derselben Stückzahl verkauft worden ist, nur jene Kombinationen
werden ausgegeben, bei welchen der Artikel an einem Tag zu mindestens zwei verschiedenen Stückzahlen verkauft worden ist. Da
jede diese Bedingungen erfüllende Kombination von Zeilen auch erfüllt ist, wenn die beiden Zeilen vertauscht sind, kann man die
Bedingung <> auch ersetzen durch < und mit DISTINCT alle weiteren Zeilen dieser Kombination entfernen.
Sollen dagegen jene Kombinationen aus Tagen und Artikeln bestimmt werden, für die an einem Tag nur eine Stückzahl, jedoch mehrere
Verkäufe durchgeführt wurden, so genügt es nicht, nur den Unterschieds-Operator durch den Gleichheits-Operator zu ersetzen. Denn
in diesem Fall werden auch jene Tage/Artikel ausgegeben, die genau einmal verkauft worden sind. Wird diese Abfrage ergänzt um
die Bedingung U1.Umsatz_Nr <> U2.Umsatz_Nr, so wird nur der Artikel 11 am 25.06.1999 ausgegeben, der zweimal zwanzigmal
verkauft wurde. Insgesamt:
SELECT DISTINCT U1.DATUM, U1.A_NR
FROM UMSATZ As U1 INNER JOIN UMSATZ As U2
On (U1.DATUM = U2.DATUM) And
(U1.A_NR = U2.A_NR) And
(U1.A_STUECK = U2.A_STUECK) And
(U1.UMSATZ_NR <> U2.UMSATZ_NR)
Bemerkungen
- Falls Sie Tabellen verknüpfen, so sollten Sie im Regelfall eine der JOIN-Methoden verwenden. Insbesondere ist die
'alte' Technik, welche die Klammerversion nutzt und die ON-Klausel in die WHERE-Bedingung verschiebt, zu vermeiden.
Vergleichen Sie die beiden folgenden Darstellungen:
SELECT A.*, U.*
FROM ARTIKEL As A, UMSATZ As U
WHERE A.A_NR = U.A_NR
AND A.A_NR < 13
mit
SELECT A.*, U.*
FROM ARTIKEL As A INNER JOIN UMSATZ As U
On A.A_NR = U.A_NR
WHERE A.A_NR < 13
Bei der ersten, nicht zu verwendenden Version ist die Bedingung zur Verknüpfung der beiden Tabellen gemischt mit der Bedingung,
welche wenige Zeilen auswählt. Die zweite Version trennt dagegen die Verknüpfung der Tabellen von der Reduktion der Spalten.
In der Regel werden zunächst alle WHERE-Bedingungen ausgeführt, welche die Zahl der Zeilen reduzieren, erst über die verbleibende
Restmenge der Zeilen wird der JOIN gebildet.
- LEFT und RIGHT Join sind zueinander symmetrisch. Die beiden folgenden Anweisungen zur Ermittlung der niemals
verkauften Artikel sollten deshalb denselben Ablaufplan erzeugen (fügen Sie wie oben einen niemals verkauften Artikel hinzu):
SELECT A.*
FROM ARTIKEL As A LEFT JOIN UMSATZ As U
ON A.A_NR = U.A_NR
WHERE U.A_NR Is null
sowie
SELECT A.*
FROM UMSATZ As U RIGHT JOIN ARTIKEL As A
ON U.A_NR = A.A_NR
WHERE U.A_NR Is null
OUTER JOIN kombiniert LEFT/RIGHT und akzeptiert sowohl übereinstimmende Zeilen als auch Nullwerte beider Tabellen.
- Left- bzw. Right-Join-Konstrukte sind zwingend, falls eine Tabelle eine Verknüpfung zu einer optionalen Randtabelle enthält.
Ein klassisches Beispiel ordnet Personen Titel zu, die Titel (Dr., Dr.med, Prof.Dr.) sind in einer Randtabelle gespeichert, die
Personentabelle enthält eine Spalte mit der ID der Titel-Tabelle. Gibt es Personen ohne Titel, so kann entweder in der Randtabelle
ein Eintrag ohne Text erzeugt und ein INNER JOIN zur Verknüpfung genutzt werden oder es fehlt ein solcher Eintrag. Damit ist
die entsprechende Zelle in der Personentabelle leer, so daß ein Left/Right-Join von der Haupt- auf die Randtabelle gebildet werden
muß, falls alle Personen gefunden werden sollen.
Ein Beispiel für den MS-SqlServer:
SELECT A.Nachname + ', ' +
A.Vorname + CoalEsce(' ' + B.Titel, '')
As Name
From Personen As A Left Join PersonenTitel As B
On A.TitelId = B.TitelIdCoalEsce liefert den ersten Ausdruck in der Liste, der von Null verschieden ist. Existiert
ein Titel, so wird dieser mit einem Leerzeichen als Trenner ausgegeben, ansonsten wird ein Leerstring zur bisherigen Ausgabe
aus Nachname, Komma und Vorname hinzugefügt.
TOP
Mit Aggregat-Funktionen und GROUP BY Daten auswerten und aggregieren
Vorbemerkung
-
Mit SELECT, FROM und WHERE erhalten Sie einzelne Zellen der gewählten Tabellen zurück. Es wird jedoch
keine Aggregation des Ergebnisses durchgeführt, die Daten werden nicht zusammengefaßt. Liefert Ihre
JOIN-Anweisung 50.000 Zeilen und entfernt die WHERE-Klausel hiervon 30.000, so enthält Ihre Ergebnistabelle
20.000 Zeilen. Alle Techniken mit Aggregatfunktionen fassen dagegen mehrere Zeilen zu einer Ergebniszeile
zusammen und geben nur noch diese aus.
- Eine solche Aggregation / Zusammenfassung / Ermittlung von Kennziffern / Konsolidierung kann
sich über die gesamte Tabelle erstrecken. In diesem Fall werden nur Aggregatfunktionen, jedoch
keine GROUP BY - Klausel benötigt. Oder die Kennziffern sollen aufgesplittet nach Artikeln, Verkäufern,
Datum, Regionen oder anderen Kriterien bestimmt werden. In diesem Fall wird die Tabelle zunächst gemäß
dieser Kriterien mit der GROUP BY - Klausel in Teiltabellen zerlegt und für jede Teiltabelle die Kennziffern berechnet. Damit liegt
pro Teiltabelle eine Ergebniszeile vor, eventuell sind die konkreten Werte, die über die Zugehörigkeit
zu dieser Teiltabelle entscheiden, ebenfalls mit ausgegeben. Aus all diesen Einzelzeilen wird das
Gesamtergebnis zusammengefügt.
- Bildlich gesprochen: Die bisherigen Techniken liefern Ihnen einen Stapel von Zeilen zurück.
Verwenden Sie eine Aggregat-Funktion, so wird der Zeilenstapel auf eine Zelle, einen einzigen Wert
gestaucht. Verwenden Sie mehrere Aggregat-Funktionen, so wird der Zeilenstapel auf mehrere Zellen komprimiert,
diese werden als eine Zeile ausgegeben. Gruppieren Sie das Ergebnis zusätzlich mit GROUP BY, so wird der Gesamtstapel zunächst gemäß
der GROUP BY - Kriterien in Teilstapel zerlegt, über jeden dieser Teilstapel wird aggregiert, dieser also
in einer Zeile zusammengefaßt, so daß am Ende alle Teilstapel-Ergebniszeilen zum Gesamtergebnis vereinigt
werden.
- Sollen Ergebnisse, die mittels der Aggregatfunktionen ermittelt wurden, als zusätzliches
Filterkriterium herangezogen werden, so können solche Ausdrücke in der HAVING-Klausel angegeben werden.
Syntax
- Rückgabe einer einzelnen Zelle - einfache Aggregation
SELECT <Aggregatfunktion(Spaltenname) As [Spaltenname]>
FROM ...
[WHERE ...]
- Rückgabe einer Zeile mit mehreren Zellen - mehrere Kennziffern gleichzeitig ermitteln
SELECT <Aggregatfunktion-1(Spaltenname-A1) As Ausgabename-1,>
<Aggregatfunktion-2(Spaltenname-A2) As Ausgabename-2,>
...
<Aggregatfunktion-n(Spaltenname-An) As Ausgabename-n>
FROM ...
[WHERE ...]
[HAVING ...]
- Gruppierung in Teilgruppen, Aggregation über die Teilgruppen und Zusammenfassen der Einzelergebnisse
SELECT <Aggregatfunktion-1(Spaltenname-A1) As Ausgabename-1,>
<Aggregatfunktion-2(Spaltenname-A2) As Ausgabename-2,>
...
<Aggregatfunktion-n(Spaltenname-An) As Ausgabename-n>,
Spaltenname-1,
Spaltenname-2,
...
Spaltenname-m
FROM ...
[WHERE ...]
GROUP BY Spaltenname-1,
Spaltenname-2,
...
Spaltenname-m
[HAVING ...]
Beispiele
-
SELECT COUNT(A.DATUM) As [Zahl-der-Umsätze]
FROM UMSATZ As A
Beachten Sie, daß Ihnen diese Abfrage 9 als Ergebnis zurückliefert, also die Zahl der nichtleeren
Zellen. Fügen Sie einen weiteren Datensatz ein und lassen bei diesem das Feld DATUM leer, so erhalten Sie
mit der obigen Abfrage weiterhin das Ergebnis 9.
Es wird nicht 2 (die Zahl der verschiedenen Datumsangaben) ausgegeben. Sind alle Zellen einer
Tabelle belegt, so ist es für diese Abfrage mit COUNT unerheblich, welche Zelle Sie verwenden.
SELECT A.DATUM, COUNT(A.DATUM) As [Zahl-der-Umsätze-pro-Tag]
FROM UMSATZ As A
GROUP BY A.DATUM
Sie erhalten als Ergebnis zwei Zeilen, für jede der unterschiedlichen Datumsangaben eine Zeile. Jede Zeile
enthält die zusätzliche Zelle 'Zahl-der-Umsätze-pro-Tag' mit den Werten 6 für den 24.06 und 3 für den 25.06.99.
Verzichten Sie auf die Ausgabespalte A.DATUM, so können Sie die beiden Zellen mit den Werten 6 und 3 nicht
mehr interpretieren.
SELECT MAX(A.A_PREIS) As [teuerster-Artikel]
FROM ARTIKEL As A
Dies liefert Ihnen den Preis des teuersten Artikels (360.00). Beachten Sie, daß Sie mit dieser Abfrage noch nicht den Namen
oder die Details dieses Artikels erfahren. Dies gelingt nur, wenn Sie das obige Ergebnis selbst als
Kriterium in einer Where-Abfrage verwenden. Näheres finden Sie im Abschnitt über Unterabfragen. Hier
eine Möglichkeit:
SELECT B.*
FROM ARTIKEL As B
WHERE B.A_PREIS = (SELECT MAX(A.A_PREIS) As [teuerster-Artikel]
FROM ARTIKEL As A)
| Ergebnis: | |
| A_NR | A_NAME | A_PREIS |
| 22 | Mantel | 360.00 |
|
-
SELECT SUM(A.A_PREIS * B.A_STUECK) As [Gesamtumsatz]
FROM ARTIKEL As A INNER JOIN UMSATZ As B
ON A.A_NR = B.A_NR
Diese Abfrage kombiniert jede Umsatz-Zeile mit der zu ihr gehörenden Artikel-Zeile. Damit sind
die Spalten A_PREIS und A_STUECK bekannt und können miteinander multipliziert werden. Ohne die Aggregatfunktion
SUM würden neun Zeilen mit dem tatsächlichen Umsatz dieser einzelnen Verkaufshandlung ausgegeben. So wird
über all diese Einzelergebnisse summiert und der Gesamtumsatz von 27.472,00 ermittelt.
-
SELECT A.A_NR, SUM(A.A_PREIS * B.A_STUECK) As [Umsatz-dieses-Artikels]
FROM ARTIKEL As A INNER JOIN UMSATZ As B
ON A.A_NR = B.A_NR
GROUP BY A.A_NR
| Ergebnis: | |
| A_NR | Umsatz-dieses-Artikels |
| 11 | 4.862,00 |
| 12 | 1.990,00 |
| 13 | 4.420,00 |
| 22 | 16.200,00 |
|
Diese Aggregation zerlegt den Gesamtstapel der neun Umsätze zunächst in Teilstapel anhand der
Artikelnummern. Für jeden Teilstapel wird multipliziert und die Summe ermittelt. Das Ergebnis wird,
ergänzt um die Information, zu welchem Artikel die Summation gehört, ausgegeben.
-
SELECT B.DATUM,
MAX(A.A_PREIS * B.A_STUECK) As [Max-Tages-Einzelumsatz]
FROM ARTIKEL As A INNER JOIN UMSATZ As B
ON A.A_NR = B.A_NR
GROUP BY B.DATUM
| Ergebnis: | |
| DATUM | Max-Tages-Einzelumsatz |
| 24.06.1999 | 3.867,50 |
| 25.06.1999 | 12.600,00 |
|
Anstelle der obigen Summation wird das Maximum über alle Einzelumsätze für jeden Tag ermittelt. Beachten Sie,
daß Sie auch mit dieser Abfrage nicht ermitteln können, welcher Artikel für diesen höchsten Umsatz
verantwortlich ist. Denn sobald Sie zur Gruppierung die Artikel-Nummer hinzunehmen, erhalten Sie für jede
Kombination aus Artikel und Tag eine Zeile, so daß das Maximum über solche einzeiligen Teilmengen gleich
dem Wert in dieser Zelle ist. Möchten Sie zusätzlich wissen, welcher Artikel für diesen höchsten Einzelumsatz
zuständig ist, so kombinieren Sie diese Abfrage als JOIN mit einer direkten Multiplikation:
SELECT A.DATUM, A.A_NR, A.Einzelumsatz
FROM
(SELECT C.DATUM, B.A_NR,
B.A_PREIS * C.A_STUECK As [Einzelumsatz]
FROM ARTIKEL As B INNER JOIN UMSATZ As C
ON B.A_NR = C.A_NR) AS A
INNER JOIN (
SELECT Y.DATUM,
MAX(X.A_PREIS * Y.A_STUECK) As [Max-Tages-Einzelumsatz]
FROM ARTIKEL As X INNER JOIN UMSATZ As Y
ON X.A_NR = Y.A_NR
GROUP BY Y.DATUM) As D
ON (A.DATUM = D.DATUM) AND
(A.Einzelumsatz = D.[Max-Tages-Einzelumsatz])
| Ergebnis: | |
| DATUM | A_NR | Einzelumsatz |
| 24.06.1999 | 13 | 3.867,50 |
| 25.06.1999 | 22 | 12.600,00 |
|
Unterstützt das Datenbanksystem - im Gegensatz zu Access - eine direkte Multiplikation im JOIN-Ausdruck,
so können Sie den ersten geklammerten JOIN-Ausdruck herausziehen und die Multiplikation direkt in der
JOIN-Klausel durchführen.
SELECT B.DATUM, B.A_NR,
SUM(A.A_PREIS * B.A_STUECK) As Tagesumsatz
FROM ARTIKEL As A INNER JOIN UMSATZ As B
ON A.A_NR = B.A_NR
GROUP BY B.DATUM, B.A_NR
| Ergebnis: | |
| DATUM | A_NR | Tagesumsatz |
| 24.06.1999 | 11 | 3.094,00 |
| 24.06.1999 | 12 | 1.990,00 |
| 24.06.1999 | 13 | 4.420,00 |
| 24.06.1999 | 22 | 3.600,00 |
| 25.06.1999 | 11 | 1.768,00 |
| 25.06.1999 | 22 | 12.600,00 |
|
Das ist nun die klassische ausdifferenzierte Version: Die tatsächlichen Einzelumsätze werden pro Tag und
pro Artikel aggregiert, das Ergebnis wird mit den beiden gruppierenden Spalten ausgegeben.
-
SELECT A.DATUM, A.A_NR, COUNT(*) As [Umsatz-Anzahl]
FROM UMSATZ As A
GROUP BY A.DATUM, A.A_NR
HAVING COUNT(*) > 1
| Ergebnis: | |
| DATUM | A_NR | Umsatz-Anzahl |
| 24.06.1999 | 12 | 2 |
| 24.06.1999 | 13 | 2 |
| 25.06.1999 | 11 | 2 |
|
Ohne HAVING-Klausel würde diese Abfrage sechs Zeilen zurückliefern - vier für den 24.06, an dem vier Artikel
verkauft wurden, 2 für den 25.06. Die HAVING-Klausel definiert eine zusätzliche Einschränkung und beschränkt
die Ausgabe auf jene Artikel, die mindestens zwei Mal verkauft worden sind.
-
SELECT A.A_NR, SUM(A.A_PREIS * B.A_STUECK) As [Umsatz-dieses-Artikels]
FROM ARTIKEL As A INNER JOIN UMSATZ As B
ON A.A_NR = B.A_NR
GROUP BY A.A_NR
HAVING SUM(A.A_PREIS * B.A_STUECK) > 10000
Dies ermittelt jene Artikel, mit welchen ein Umsatz von über 10.000 gemacht wurde. Die SQL-Abfrage
ist dieselbe wie unter Beispiel 5, es wurde nur die HAVING-Klausel hinzugefügt. Artikel 22 wird ausgegeben.
Aggregatfunktionen
- COUNT: Ermittlung der Zeilen bzw. Zellen. Diese Funktion kann in zwei verschiedenen Versionen
genutzt werden: COUNT(*) gibt die Anzahl aller Zeilen gemäß dem GROUP-BY-Abschnitt zurück.
COUNT(<Spaltenname>) liefert die Zahl der nichtleeren Zellen in der angegebenen Spalte.
- MIN(<Spaltenname>) / MAX(<Spaltenname>): Ermittelt das Minimum / Maximum der
angegebenen Spalte.
- SUM(<Spaltenname>): Summation über alle Zellwerte
- AVG(<Spaltenname>): Mittelwert aller Zellwerte, NULL-Werte werden ignoriert.
Dies sind die Funktionen, die jedes Datenbanksystem mit Sql-Schnittstelle anbieten dürfte. Hinweise
für weitere Funktionen Ihres Systems finden Sie in der zugehörigen Dokumentation. Die Aggregatfunktionen mit
numerischem Bezug (SUM, AVG) können nur für Spalten verwendet werden, welche numerische Datentypen enthalten.
Bemerkungen
- Wenn Sie eine Spalte ausgeben, über diese jedoch weder aggregieren noch sie in der GROUP-BY-Klausel
erwähnen, so erhalten Sie eine typische Fehlermeldung: 'Sie wollten eine Abfrage ausführen, die den
angegebenen Ausdruck 'A_NR' nicht als Teil der Aggregatfunktion einschließt' (Beispiel Access). Diese
Fehlermeldung ist insofern irreführend, da es drei Möglichkeiten gibt: Sie entfernen die Spalte aus der
Liste der Ausgabespalten, Sie fügen die Spalte zum GROUP-BY-Abschnitt hinzu oder Sie aggregieren über
diese Spalte. Beim MS-SQLServer heißt die analoge Fehlermeldung: 'Die U.A_NR-Spalte ist in der Auswahlliste
ungültig, da sie nicht in einer Aggregatfunktion enthalten und keine GROUP BY-Klausel vorhanden ist'.
Wurde eine GROUP-BY-Klausel angegeben, so wird Ihnen mitgeteilt: 'und nicht in der GROUP BY-Klausel
enthalten ist'. Beim MS-SqlServer ist die Fehlermeldung also weitaus spezifischer als bei Access.
- Ist zusätzlich ein WHERE-Abschnitt angegeben, so wird dieser zuerst kostengünstig ausgewertet. Die Aggregation
mit eventueller vorheriger Gruppierung wird erst anschließend ausgeführt. In den HAVING-Abschnitt gehören deshalb
nur jene Ausdrücke, welche sich auf Aggregationen beziehen.
- Access neigt bei der visuellen Codegenerierung dazu, bei vorhandenen Aggregationen alle Kriterien von
der WHERE-Bedingung in die HAVING-Klausel zu verschieben. Dies ist jedoch unnötig teuer und sollte gegebenenfalls
per Hand bzw. durch den Verzicht auf die visuelle Codierung vermieden werden.
- Wenn Sie nur wissen möchten, welche verschiedenen Zeilen existieren, so liefern die beiden
Alternativen DISTINCT sowie das Gruppieren nach allen Ausgabespalten dieselben Ergebnisse. DISTINCT ist
jedoch immer ressourcenschonender als GROUP BY und deshalb in solchen Fällen erste Wahl.
- In PHP-Foren findet sich wiederholt Code, mit welchem die Zahl der Zeilen in einer Tabelle ermittelt werden
soll. Dieser Code ist wie folgt aufgebaut:
$select = mysql_query("Select * From Artikel");
$reihen = mysql_num_rows($select);Eine solche Verarbeitung widerspricht sämtlichen Prinzipien ressourcenschonender
Programmierung. Akzeptabel ist eine Lösung der Form:
$select = mysql_query("Select Count(*) From Artikel");
$zahl_der_zeilen = mysql_fetch_row($select);
echo $zahl_der_zeilen[0];
Ein DBMS sollte nur jene Zeilen zurückgeben, die tatsächlich benötigt werden. Das Zählen von Datensätzen wird von jedem
DBMS rascher erledigt als im Rahmen eines externen Aufrufs von der umgebenden Wirtssprache. Ferner wird nur eine einzige Zelle, also
ein Gebilde aus einer Spalte und einer Zeile, zurückgegeben. Bei gespeicherten Prozeduren läßt sich der Code weiter optimieren:
Create Procedure _up_count_Artikel
@num_rows int Output
As
Select @num_rows = Count(*)
From Artikel
In diesem Fall muß keine DataTable oder ein RecordSet mit Informationen über die zurückgegebenen Datentypen
erstellt werden, da der Rückgabewert von vornherein bekannt ist. Der Aufruf kann nun (VB.NET) so erfolgen:
Dim oCmd As New SqlCommand("_up_count_Artikel", oConn), _
i_num_rows As Integer
oCmd.Parameters.Add("@num_rows", SqlDbType.Int).Direction = _
ParameterDirection.Output
Try
oCmd.ExecuteNonQuery()
i_num_rows = CType(oCmd.Parameters("@num_rows").value, Integer)
Catch _e As Exception
'Fehlerbehandlung
End Try
TOP
Unterabfragen bzw. Subqueries - einfach, korreliert, anstelle einer Tabelle
Vorbemerkung
Die folgenden Techniken verwenden kein neues Schlüsselwort, sondern ausschließlich die bisher bekannten
grundlegenden Techniken mit SELECT, JOIN, WHERE und GROUP BY. Unterabfragen, für die meistens das Wort Subqueries
genutzt wird, werden dann verwendet, falls das Ergebnis einer Abfrage / Query herangezogen wird, um eine
neue Abfrage zu erstellen, mit welcher ein genauerer Einblick in die Datenstruktur gewonnen werden kann.
Unterabfragen werden deshalb in WHERE-Klauseln und als Tabellen-Ersatz in JOIN-Klauseln genutzt.
Die einfachste Form - eine zurückgegebene Zelle
In der elementarsten Version einer Unterabfrage liefert diese eine Zelle zurück und ist unabhängig
von der sie aufrufenden Abfrage. Sie wird verwendet, um Details zu einem Datensatz zu ermitteln.
Beispiel:
SELECT A.*
FROM ARTIKEL As A
WHERE A.A_PREIS =
(SELECT MAX(A.A_PREIS)
FROM ARTIKEL As A)
Die Unterabfrage ermittelt durch Maximumsbildung den teuersten Artikel. Dieser Wert wird zurückgegeben, die
übergeordnete Abfrage sucht nach diesem Wert, den es mindestens einmal geben muß und gibt die zugehörigen
Zeilen aus.
Die ganze Konstruktion mag für jemanden, der dies zum ersten Mal macht, verdoppelt aussehen: 'Suche die
Zeilen, deren Wert in A_PREIS gleich dem Maximum aller Preise ist'. Erst muß dieses Maximum einmal ermittelt
werden, dann ist ein zweiter Durchlauf notwendig, um die zu diesem Preis gehörenden Zeilen zu bestimmen.
Wird die Abfrage allerdings auf dem MS-Sql-Server durchgeführt und zuvor mit SET SHOWPLAN_TEXT ON die
Analyseoption eingeschaltet, so wird sichtbar, daß der Optimierer einen veränderten Abfragetext vorgeschlagen
hat:
Top(1)
Filter(WHERE:(Artikel.A_Preis <> NULL)
Sort(Order By:(Artikel.A_Preis DESC)
Clustered Index Scan(Object:Artikel.pk_Artikel)
Es wird also eine Abfrage ausgeführt, die in etwa dem folgenden Sql-Befehl entspricht:
SELECT TOP 1 A.*
FROM ARTIKEL As A
ORDER BY A.A_PREIS DESC
Das heißt, daß der Optimierer eine Unterabfrage gegebenenfalls in eine andere Sql-Befehlsfolge
auflöst und diese ausführt. Verwenden Sie die obige, umgeschriebene Abfrage jedoch nicht. Denn setzen Sie
alle Artikel auf denselben Preis, so wird Ihnen die Version mit der Unterabfrage korrekt alle vier Datensätze
ausgeben, die umgeschriebene Version liefert jedoch nur einen Datensatz.
Eine solche Unterabfrage wird also in Klammern gesetzt, muß genau eine Zelle zurückliefern und darf überall
dort verwendet werden, wo Konstanten, etwa Zahlen, oder Spalten eingesetzt werden dürfen.
Unterabfragen, die eine Spalte zurückliefern
Jede Zeile in der Tabelle Umsatz stellt einen einzelnen Geschäftsvorfall dar, an dem ein Artikel und ein
Verkäufer beteiligt ist, jeder Geschäftsvorfall liefert einen gewissen Brutto-Umsatz. Der durchschnittlichen
Brutto-Umsatz kann mit der AVG-Funktion ermittelt werden. Dieses Kriterium läßt sich verwenden, um jene
Geschäftsvorfälle zu ermitteln, welche hieran beteiligt waren. Interessant ist nun: Welche Artikel
und welche Verkäufer sorgen für diese überdurchschnittlichen Geschäftsvorfälle? Offenbar ist nicht bekannt,
wieviele Artikel und wieviele Verkäufer dies sind, also wird eine Liste zurückgegeben, wobei sich
der Auswertende für die Klartext-Namen, nicht für die internen Primärschlüssel interessiert.
Betrachten Sie zunächst die folgende Abfrage:
SELECT U.A_NR, U.DATUM, U.V_NR
FROM ARTIKEL As A INNER JOIN UMSATZ As U
ON A.A_NR = U.A_NR
WHERE A.A_PREIS * U.A_STUECK >
(SELECT AVG(A.A_PREIS * U.A_STUECK)
FROM ARTIKEL As A INNER JOIN UMSATZ As U
ON A.A_NR = U.A_NR)
| Ergebnis: | |
| A_NR | DATUM | V_NR |
| 22 | 24.06.1999 | 5016 |
| 11 | 24.06.1999 | 8413 |
| 22 | 25.06.1999 | 5016 |
| 13 | 24.06.1999 | 8413 |
|
An den überdurchschnittlichen Umsätzen sind drei von vier Artikeln sowie zwei von drei Verkäufern beteiligt.
Werden deren Klartext-Informationen benötigt, so reduziert man die Ausgabespalten auf A_Nr bzw. V_Nr und
verwendet den ganzen Ausdruck als Unterabfrage für die Tabelle mit den zugeordneten Stammdaten. Am Beispiel
der Artikel:
SELECT A.*
FROM ARTIKEL As A
WHERE A.A_NR IN
(SELECT U.A_NR
FROM ARTIKEL As A INNER JOIN UMSATZ As U
ON A.A_NR = U.A_NR
WHERE A.A_PREIS * U.A_STUECK >
(SELECT AVG(A.A_PREIS * U.A_STUECK)
FROM ARTIKEL As A INNER JOIN UMSATZ As U
ON A.A_NR = U.A_NR)
)
Korrelierte Unterabfragen
Bislang waren die Unterabfragen immer unabhängig von der übergeordneten Abfrage. Man könnte sie eigenständig
ausführen, ihr Ergebnis in eine neue Tabelle eintragen (Select ... Into) und die übergeordnete Abfrage mit dieser
verknüpfen. Nun soll der Fall betrachtet werden, bei dem zu jeder Zeile der übergeordneten Tabelle eine
Anweisung ausgeführt werden soll: Es soll ermittelt werden, mit welchem Artikel und an welchem Tag
jeder Vertreter seinen höchsten Einzelumsatz gemacht hat. Bei wenigen Vertretern und vielen Einzelumsätzen
liegt es nahe, diesen Befehl für jeden Vertreter getrennt auszuführen, also V_NR als Parameter @V_NR zu übergeben:
SELECT U.DATUM, U.A_NR, MAX(A.A_PREIS * U.A_STUECK) As Maximum
FROM UMSATZ As U INNER JOIN ARTIKEL As A
ON U.A_NR = A.A_NR
WHERE U.V_NR = @V_NR
Diese Abfrage funktioniert auch dann nicht, wenn man @V_NR durch eine Konstante, etwa 1215 ersetzt. Da eine
Aggregatfunktion verwendet wird, muß nach den anderen Spalten gruppiert werden, dann erhält man jedoch
zu jedem Vertreter so viele Zeilen, wie dieser verschiedene Artikel verkauft hat, anstatt daß man nur eine
Zeile für einen Vertreter erhält.
Ein Umschreiben ergibt zunächst soviele Zeilen aus, wie dieser Vertreter Umsätze gemacht hat:
SELECT U.DATUM, U.A_NR, A.A_PREIS * U.A_STUECK As Maximum
FROM UMSATZ As U INNER JOIN ARTIKEL As A
ON U.A_NR = A.A_NR
WHERE U.V_NR = @V_NR
Also muß dieser Ausdruck noch ergänzt werden um eine filternde Unterabfrage, welche das Maximum für
diesen Vertreter ermittelt:
SELECT U.DATUM, U.A_NR, A.A_PREIS * U.A_STUECK As Maximum
FROM UMSATZ As U INNER JOIN ARTIKEL As A
ON U.A_NR = A.A_NR
WHERE (U.V_NR = @V_NR) AND
(A.A_PREIS * U.A_STUECK) =
(SELECT MAX(A.A_PREIS * U.A_STUECK)
FROM ARTIKEL As X INNER JOIN UMSATZ As Y
ON X.A_NR = Y.A_NR
WHERE Y.V_NR = @V_NR)
Diesen Ausdruck müßte man für jeden Vertreter aufrufen und die Ergebnisse anschließend vereinigen.
Tatsächlich kann man stattdessen auch eine einzige Anweisung verwenden. Denn sowohl die äußere als auch die
innere Abfrage verwenden denselben Parameter @V_NR, dieser entspricht einer Zelle in der äußeren Abfrage und
kann von dieser auf die innere Abfrage 'durchgereicht' werden. Die Unterabfrage erhält von der übergeordneten
Abfrage die V_NR und ermittelt nur für diese den maximalen Umsatz.
SELECT V.V_NR, V.V_NAME, U.DATUM,
A.A_NR, A.A_PREIS * U.A_STUECK As [Höchster Umsatz]
FROM (VERTRETER AS V INNER JOIN UMSATZ AS U
ON V.V_NR = U.V_NR) INNER JOIN ARTIKEL As A
ON U.A_NR = A.A_NR
WHERE (A.A_PREIS * U.A_STUECK) =
(SELECT MAX(X.A_PREIS * Y.A_STUECK)
FROM ARTIKEL As X INNER JOIN UMSATZ As Y
ON X.A_NR = Y.A_NR
WHERE Y.V_NR = V.V_NR)
Die äußere Abfrage ruft für jede V_NR einmal die Unterabfrage auf und belegt den Wert von V_NR mit dem aktuellen
Wert. Das Ergebnis wird genutzt, um von den Zeilen, welche die äußere Abfrage liefert und welche diesen Vertreter
betreffen, die Zeilen zu entfernen, deren Produkt aus A_PREIS und A_STUECK nicht dem Maximum entspricht.
Das Interessante an dieser Anweisung ist, daß sie auch dann die korrekten Ergebnisse liefert, falls man
einen Artikel im Wert von 12.600,00, einen neuen Vertreter und einen Umsatz dieses Vertreters mit diesem
Artikel (A_STUECK = 1) hinzufügt. Obwohl es bei dieser Datenstruktur zwei maximale Umsätze mit demselben Betrag
gibt, die Abfrage auf die Gleichheit von Produkt und Maximum also zwei Werte zurückliefern müßte, wird pro
Vertreter nur eine Zeile mit den ansonsten korrekten Daten ausgegeben. Die äußere Abfrage wird also, wie
bei einer mit GROUP BY gruppierten Abfrage, nach V_NR gruppiert, für jede Gruppe wird einmal die Unterabfrage
aufgerufen. Und nur auf diese Teilmenge der äußeren Abfrage wird das zusätzliche, durch die Unterabfrage
bestimmte Kriterium angewandt.
Beim Entwickeln solcher Abfragen ist zu beachten, daß die Alias-Namen für die Tabelle in der untergeordneten
Abfrage anders gewählt werden müssen als in der übergeordneten Abfrage. Enthält die Unterabfrage Ausdrücke,
die nicht aufgelöst werden können, so wird in der übergeordneten Abfrage nach einem entsprechenden Ausdruck
gesucht. Wird ein solcher gefunden, handelt es sich um eine korrelierende Unterabfrage.
Unterabfragen anstelle von Tabellen in einer JOIN-Klausel
Möchte man zu jedem Vertreter mit seinem Klarnamen den maximalen Umsatz ermitteln, so wurde dies bislang
mit einer Doppelberechnung erledigt. Zunächst wurde für diesen Vertreter der Maximalumsatz ermittelt,
anschließend wurden die Produkte A_PREIS * A_STUECK für diesen Vertreter mit dem errechneten Maximum
verglichen, um die anderen Daten DATUM und A_NR zu bestimmen. Ist man nur an dem Maximalwert interessiert,
erhält man diesen für alle Vertreter gemeinsam mit einer Abfrage, die mit MAX aggregiert und nach V_NR
gruppiert. Diese kann in einem JOIN verwendet werden, um die Daten direkt auszugeben.
SELECT V.V_NR, V.V_NAME, X.MAXIMUM
FROM VERTRETER AS V INNER JOIN
(SELECT U.V_NR, MAX(A.A_PREIS * U.A_STUECK) As MAXIMUM
FROM ARTIKEL As A INNER JOIN UMSATZ As U
ON A.A_NR = U.A_NR
GROUP BY U.V_NR) AS X
ON V.V_NR = X.V_NR
Anstelle einer Tabelle wird eine eigenständige Abfrage notiert, geklammert und mit einem Alias identifiziert.
Beachten Sie, daß in diesem Fall ein Alias für die Tabelle, die aus der Unterabfrage gebildet wird, Pflicht
ist.
TOP
Grundbegriffe und Konzepte von Datenbank-Systemen
In diesem Abschnitt werden die folgenden Konzepte thematisiert: Der Unterschied zwischen
einer Datenbank und einem Datenbank-Management-System, zwei fundamentale Anforderungen, die jedes computerbasierte
Datenbanksystem erfüllen sollte, der Unterschied zwischen desktop- und serverbasierten Systemen sowie eine
kurze Erläuterung zu drei Typen von Datenbanken.
Datenbank versus Datenbank-Management-System
- Eine Datenbank ist eine logische Einheit zusammengehörender Daten, ihrer
Meta-Beschreibungen sowie zusätzlicher Informationen, die zur Verarbeitung und Bereitstellung dieser Daten
benötigt werden. Physikalisch kann eine Datenbank in mehrere Einzelteile zerfallen, die getrennt werden können,
jedoch nur gemeinsam eine Datenbank bilden.
-
Datenbank-Beispiele in diesem logischen Sinne sind:
- Eine Access-MDB-Datei
- Eine mySql-Datenbank, zerfallend in viele Dateien, die in einem gemeinsamen Ordner liegen
- Eine MS-SqlServer-Datenbank, verteilt auf diverse Dateien, die auf verschiedenen Festplatten mit
unterschiedlicher RAID-Stufe untergebracht sind
- Ein Archiv mit tausenden von Regalmetern voller Aktenordnern sowie einem zusätzlichen Klassifikationssystem
in Form weiterer Ordner
- Ein Zettelkasten mit Personennamen und zugeordneten Telefonnummern
- Eine Sql-Datenbank zum Mieten, deren konkrete Implementierung nach außen hin unsichtbar ist
- Eine Datenbank kann in den wenigsten Fällen direkt verwendet werden. Zwar mögen die Daten in Form von
Dateien vorliegen, ein direktes Lesen mit einem Editor liefert jedoch keine verwertbaren Informationen. Oder es
bestehen Zugriffsbeschränkungen verschiedenster Art, so daß ein gewöhnlicher Benutzer keine Informationen darüber
hat, in welcher Form und an welchem Ort die Dateien abgelegt sind.
- Ein Datenbank-Management-System (DBMS) stellt
unterschiedliche Werkzeuge bereit, mit welchen eine oder mehrere Datenbanken erstellt, mit Daten gefüllt und
verwaltet werden können. Ein DBMS verfügt in der Regel über mindestens eine Benutzerschnittstelle und kann
eine oder mehrere Datenbanken nur aufeinanderfolgend oder parallel verwalten. Ferner gibt es - explizit oder
implizit - Regeln, wer zu welchem Zeitpunkt Zugriff auf und Änderungsrecht an den Daten hat. Schließlich können
Sicherungsstrategien / Backup-Verfahren definiert und regelmäßig ausgeführt werden.
- Jede Benutzerschnittstelle besteht entweder aus verschiedenen Masken (Sql-Enterprise-Manager) oder
bietet dem Nutzer eine Möglichkeit, direkt Befehle entgegenzunehmen. Diese werden an das DBMS gesandt und
von diesem gegen die aktuelle Datenbank ausgeführt. Die hierfür verwendete Sprache könnte vom Hersteller des
DBMS frei entwickelt worden sein. Tatsächlich wird von den meisten relationalen DBMS Structured Query Language (SQL) mit
eventuellen eigenen Erweiterungen unterstützt.
- Datenbank-Management-Systeme in diesem Sinne sind:
- Die Installation einer geeigneten Ms-Access-Version zur Verwaltung von jeweils einer
Datenbank.
Oder: Die Installation der NET-Laufzeitumgebung sowie des Sql-Interaktiv-lernen. Mit diesem können
Datenbank-Befehle direkt auf der Access-Datenbank ausgeführt werden.
- Eine mySql-Installation, die vielleicht eine Datenbank verwaltet, deren Ordner auf einem
anderen Rechner liegt.
- Eine MS-SqlServer-Standard-Installation, die einige Werkzeuge (Enterprise-Manager, OSQL) bereitstellt.
Oder: Ein installiertes MS-Access, in dem ein Access-Projekt erstellt wird, das auf eine Datenbank auf
einem MS-SqlServer zugreift.
- Regeln für den Zutritt zum Archiv (Ausweispflicht, zeitliche Einschränkungen, nicht in der Urlaubszeit).
Gespräch mit den Archivverwaltern, um bestimmte Informationen zu finden bzw. um das Klassifikationssystem
erst einmal zu verstehen, Verbot der Archivbenutzung bei zu vielen Anfragen oder bei ungebührlichem Benehmen.
Implizite oder explizite Verhaltensregeln innerhalb des Archivs (kein Essen / Trinken / Rauchen),
da das Archiv einmalig ist und keine Sicherung zur Verfügung steht.
- Der Zettelkasten: Diese Datenbank ist einmalig und kann nicht mit einem vertretbaren Aufwand kopiert / gesichert
werden. Sie könnte höchstens, Karte für Karte, kopiert bzw. abgeschrieben werden. Für neue Datensätze genügt
einfaches kartoniertes Papier. Spezielle Kenntnisse zur Erstellung neuer Datensätze sind zunächst nicht
notwendig. Jeder mit Zutritt zum Zimmer kann auf diese Daten zugreifen und die Sortierung zerstören.
Es kann jedoch einen zweiten Zettelkasten geben, der nach vergleichbaren Prinzipien aufgebaut und
von derselben Person verwaltet wird.
- Zu einer Sql-Datenbank zum Mieten wird ein Web-Interface mit üblichen Masken angeboten, wie sie
bsp. von Access her bekannt sind. Im Hintergrund werden aus den Nutzereingaben Sql-Befehle generiert und
diese auf der Kunden-Datenbank ausgeführt. Verschiedene Nutzer verwenden zwar dasselbe Interface, haben
jedoch keinen Zugriff auf die Datenbanken der anderen Nutzer.
- Zusammengefaßt: Eine Datenbank als logische Einheit umfaßt alle Daten mit den notwendigen Meta-Informationen,
diese sind ohne das zugehörige Datenbank-Management-System unverständlich und nicht verwendbar. Letzteres stellt die eigentliche
Rahmenarchitektur zur Erstellung von Datenbanken dieses Typs mit ihren speziellen Inhalten dar und bietet datenbankübergreifend
zusätzliche Service-Leistungen an.
Grundlegende Anforderungen an computergestützte Datenbank-Systeme
An Datenbank-Systeme, die edv-basiert sind, werden üblicherweise zwei fundamentale Forderungen gestellt:
Vermeiden von Redundanz sowie die Sicherstellung einer maximalen
Integrität der Daten.
- Redundanzfreiheit: Informationen sollten nicht mehrfach abgelegt werden.
Wurden Daten zu einer Einheit, einem Objekt, einem Vorgang abgelegt, so sollen diese Daten nur einmalig in
der Datenbank vorhanden sein, so daß beim lesenden Zugriff immer nur diese eine Position abgefragt, beim
schreibenden Zugriff nur diese Position geändert wird. Enthält die Datenbank bsp. Informationen zur Person
'Max Mustermann, Tel. 773355', so sollen die Eigenschaften 'Vorname', 'Nachname' und 'Telefon' nur einmalig
existieren. Ändert sich etwa die Telefonnummer, so muß sie nur an einer einzigen Stelle geändert werden. Alle
anderen Stellen, welche diese Information benötigen, lesen sie von dort aus und erhalten ab der Änderung
unverzüglich den neuen Wert.
Papierversionen von Datenbanken sind oft redundant, so werden Personennamen ausgeschrieben, anstatt daß nur
eine Nummer eingesetzt wird. Damit steht bsp. auf Karteikarten der Name des Erstellers, gleichzeitig ist es
nur mit einem unvertretbar hohen Aufwand möglich, alle von diesem erstellten Karten herauszufinden.
- Datenintegrität erzwingen: Alle abzulegenden Informationen zerfallen
in kleinste Einheiten, die vielfachen Einschränkungen unterliegen können. Ein Geburtsdatum darf nur gültige
Datumswerte enthalten, bei Gewichts- oder Mengenangaben sollen nur ganze Zahlen größer Null zulässig sein.
Bei Namen sind vielleicht Sonderzeichen verboten, bei Benutzernamen für ein Forum mögen keine Leerzeichen
(... van der ...) gewünscht und die Länge beschränkt sein. Telefonnummern erlauben lediglich die Eingabe von Zahlen
und Leerzeichen einschließlich führender Nullen, die nicht abgeschnitten werden sollen. Oder ein Feld
erfordert die Auswahl aus einer Reihe von Werten, etwa Währungen, Personen oder Kategorien. Hier sollte
eine Pulldown-Liste versehentliche oder absichtliche Fehleingaben ausschließen. Schließlich gibt es
Pflichtfelder, die zwingend zu belegen sind, Werte müssen eindeutig sein - etwa Personalnummern - oder
sollen zusätzliche Bedingungen erfüllen (10 < Wert < 100). Ferner kann es wünschenswert sein, Lese- und Schreibrechte
fein granuliert zu verteilen, so daß nur berechtigte Personen Zugriff auf den jeweiligen Ausschnitt der
Daten erhalten.
Zur Forderung der Datenintegrität zählt ferner, daß das DBMS parallele Benutzerzugriffe gestattet, so daß parallele
Lese- und Schreibzugriffe sich nicht gegenseitig behindern. Bei schwerwiegenden Störungen wie einem Verbindungsabbruch
innerhalb der Netzkommunikation oder einem Stromausfall muß es das DBMS gewährleisten, daß die Daten konsistent bleiben
bzw. daß das System auf einen Zustand kurz vor dem Abbruch zurückgesetzt werden kann.
Beachten Sie, daß das vielleicht erwartete, üblicherweise edv-typische Kriterium des 'raschen Zugriffs' hier
ausdrücklich nicht thematisiert worden ist. Dieses Kriterium stellt zwar einen der entscheidenden Unterschiede
zu jeder Form eines papiergebundenen Archivs dar, für das mühsam vielleicht einzelne Schlagworte mit Seitenangaben
katalogisiert wurden. Es wird jedoch im Laufe der weiteren Ausführungen deutlich werden, daß der rasche Suchzugriff
höchstens eine Folge der obigen Anforderungen darstellt. Sind die Daten nicht redundanzfrei abgelegt oder ist
die Datenintegrität nicht bei jeder Verarbeitung sichergestellt, so liefert jede nachfolgende Auswertung ebenfalls
ungenügende oder falsche Ergebnisse.
Desktop- versus serverbasierte Datenbank-Management-Systeme
- Ein desktop-basiertes DBMS ist dadurch gekennzeichnet, daß der Betriebssystem-Prozeß,
der auf die Datenbank zugreift, nur in der Zeit aktiv sein muß, in welcher die Datenbank tatsächlich benutzt wird.
Damit können verschiedene Instanzen desselben DBMS auf verschiedenen Rechnern gestartet werden und versuchen, auf
dieselbe logische Datenbank zuzugreifen, deren Datei auf einem Netzlaufwerk freigegeben worden ist. Jede dieser
Instanzen muß freiwillig Zugriffe anderer Instanzen respektieren und eventuell gesetzte Schreibsperren
beachten. Ein böswillig geschriebenes oder einfach fehlerhaftes Programm kann ebenfalls auf die Datenbank zugreifen
und deren Inhalt beliebig verändern, so daß die Datenbank zerstört wird. Der Rechner, auf dem die Datenbank-Datei liegt,
wird nur als Fileserver verwendet. Alle Filter- und Sortiervorgänge werden ausschließlich von den Benutzerprozessen
erledigt, so daß sich eine solche Instanz alle Daten über das Netzwerk holt, erst lokal wenige Zeilen aussondert und
diese dem Nutzer anzeigt. Fordert ein anderer Nutzer dieselbe Teilmenge von Daten an, führt der Betriebssystem-Prozeß
dieses Nutzers dieselbe Aktivität durch. Sollen Sicherungen erstellt werden, so gelingt dies nur durch ein physikalisches
Kopieren der Datei. Wird die Datei so beschädigt, daß sie nicht mehr verwendbar ist, kann nur auf die eventuell
mehrere Stunden alte Sicherung zurückgegriffen werden, so daß alle in der Zwischenzeit erfolgten Änderungen verloren
sind.
Beispiel: Eine Access-Datenbank auf einem freigegebenen Laufwerk. Mehrere Benutzer starten auf ihrem Rechner
Access und öffnen die Datenbank. Zusätzlich schreibt jemand ein Programm, welches direkt, unter Umgehung der
üblichen Treiber, auf die Datenbank zugreift und diese als Textdatei anspricht.
- Ein serverbasiertes DBMS ist durch mindestens einen kontinuierlich laufenden
Betriebssystem-Prozeß charakterisiert, welcher vom Server-Betriebssystem verwaltet und geschützt wird. Nur dieser
weiß, wo die physikalischen Dateien tatsächlich liegen und verbietet jedem anderen Prozeß den Zugriff auf diese. Dieser
Prozeß stellt verschiedene Kommunikations-Schnittstellen zur Verfügung, die von Nutzern angesprochen werden können.
Alle über die Schnittstellen mitgeteilten Wünsche werden zunächst vom Server-Prozeß analysiert. Nur wenn die Befehle syntaktisch
korrekt sind und die Berechtigung für diesen Zugriff vorliegt, wird die Befehlsfolge ausgeführt und gegebenenfalls Daten zurückgesandt.
Fordert ein Nutzer nur wenige Datensätze an, so wird die Filterung sofort auf dem Server durchgeführt, für das Senden
der Daten sind nur geringe Netzwerk-Ressourcen notwendig. Fordern mehrere Nutzer dieselben Informationen ab, können
spätere Anfragen direkt aus dem Arbeitsspeicher heraus beantwortet werden, so daß die Antwortzeit entsprechend verkürzt
wird. Sicherungen können während des laufenden Betriebs vom Serverprozeß erstellt werden.
- Vergleicht man diese beiden Typen mit einem klassischen Archiv, so entspricht einem Desktop-System ein
Archiv, das frei zugänglich ist, bei dem jeder Archiv-Nutzer selbst zu den Regalen geht, in den Akten blättert
oder einen Ordner aus dem Regal nimmt und an einer anderen Stelle wieder einstellt. Die Integrität und Konsistenz
des Archivs ist also vollständig vom Verhalten der Nutzer abhängig, die Logik der Archivierung muß jedem Nutzer
neu erläutert werden.
Eine serverbasierte Datenbank-Lösung läßt sich dagegen beschreiben als eine Art Hochsicherheitstrakt mit frei zugänglichem
Eingangsbereich. Die Nutzer haben keinen Zutritt zu den Stockwerken und Regalen, sie dürfen im Erdgeschoß ihre Wünsche
interaktiv oder telefonisch mitteilen. Nur, wenn die Wünsche syntaktisch korrekt formuliert und die notwendige
Berechtigung vorliegt, wird ausschließlich das erlaubte Datenpaket zugesandt.
Drei Typen von Datenbank-Systemen
Bei Datenbank-Management-Systemen werden drei verschiedene Typen unterschieden:
- Hierarchische Datenbank: Dies ist der historisch früheste Typ.
Die Daten werden in Form einer Baumstruktur abgebildet, so daß Parent-Child-Beziehungen maßgeblich sind.
- Relationale Datenbank: 1970 veröffentlichte Edgar Frank Codd
den Text A Relational Model of Data for Large Shared Data Banks (früher zugänglich unter
http://www.acm.org/classics/nov95/toc.html),
mit dem er die Grundlage für relationale Datenbank-Systeme legte. Die Daten werden in flache Tabellen (= Relationen)
abgelegt, die durch Fremdschlüssel-Beziehungen beliebig miteinander verknüpft werden können. Die fünf Normalformen
stellen Regeln dar, entlang derer ein Tabellenschema so entworfen wird, daß die Daten ohne Informationsverlust
und redundanzfrei gespeichert werden. Charakteristisch für das relationale Modell ist, daß die
Informationen zu einem Geschäftsvorfall auf viele Tabellen verteilt und bei Bedarf zusammengesetzt werden.
Das relationale Modell ist bis heute das vorherrschende Modell für Datenbanken.
- Objektorientierte Datenbank: Ein objektorientiertes DBMS erlaubt es,
zusammengehörende Einheiten auch zusammen abzuspeichern. Dabei lehnt es sich an die objektorientierte
Programmierung an und vermeidet damit das wiederholte Zusammensuchen zusammengehörender Daten.
TOP
Daten verwalten - die denormalisierte Tabelle als Ausgangspunkt für die
Definition der Relationen / des Tabellenschemas
- Aufgabe einer Datenbank ist es, alle Informationen zu einer Thematik zusammenzutragen. Hierbei kann
es sich um sämtliche Geschäftsvorfälle einer Firma oder um ein privates Werkzeug-Archiv handeln. Bei der Firma
sollen alle Mitarbeiter-, Lieferanten- und Kundendaten sowie jede einzelne Bestellung erfaßt werden. Das private
Werkzeug-Archiv bestehe aus tausenden von Gegenständen, gegliedert in diverse Kategorien, verteilt auf verschiedene
Kästen und Regale.
- Sollen all diese Informationen erfaßt werden, so kann dies nach einem einfachen Schema realisiert werden:
Zu jedem Detail gibt es eine Spalte. Findet irgendeine Aktivität statt, so wird eine Zeile hinzugefügt und
die bei dieser Aktivität ermittelten Informationen in die zur Zeile gehörenden Zellen eingetragen. Alle bei diesem
Geschäftsvorfall nicht benötigten Zellen bleiben leer. Eine solche Tabelle kann auch als
vollständig denormalisierte Tabelle bezeichnet werden.
- Beispiel 1:
| U_NR | A_NAME | A_PREIS | A_STUECK | DATUM | V_NAME | V_ANSCH |
| 1 | Oberhemd | 39,80 | 40 | 24.06.1999 | Meyer, Emil | Wendeweg 10, 2800 Bremen |
| 2 | Mantel | 360,00 | 10 | 24.06.1999 | Meier, Franz | Kohlstr. 1, 2800 Bremen |
| 3 | Oberhemd | 44,20 | 70 | 24.06.1999 | Meyer, Emil | Wendeweg 10, 2800 Bremen |
| 4 | Oberhemd | 44,20 | 20 | 25.06.1999 | Schulze, Fritz | Gemüseweg 3, 2800 Bremen |
| 5 | Mantel | 360,00 | 35 | 25.06.1999 | Meier, Franz | Kohlstr. 1, 2800 Bremen |
| 6 | Hose | 110,50 | 35 | 24.06.1999 | Meyer, Emil | Wendeweg 10, 2800 Bremen |
| 7 | Hose | 110,50 | 5 | 24.06.1999 | Schulze, Fritz | Gemüseweg 3, 2800 Bremen |
| 8 | Oberhemd | 39,80 | 10 | 24.06.1999 | Schulze, Fritz | Gemüseweg 3, 2800 Bremen |
| 9 | Oberhemd | 44,20 | 20 | 25.06.1999 | Meyer, Emil | Wendeweg 10, 2800 Bremen |
Dieses Beispiel scheint relativ klar strukturiert zu sein, so daß man eine solche denormalisierte Tabelle
rasch in drei Tabellen zerlegen könnte. Werden allerdings tagesaktuelle Preise berücksichtigt, so stellt sich die
Frage, ob der Preis in die Artikel- oder in die Umsatztabelle eingetragen wird.
- Beispiel 2:
| lfNr | Liefer-Nr | Datum |
Artikel (E) | Lieferant | EP | Zahl |
Artikel (V) | Empfänger | EP | Zahl |
| 1 | 24 | 15.2.2003 |
Hosen, blau | FA Muster-Liefer GbR, Nürnberg | 39.90 | 50 |
| | | |
| 2 | 24 | 15.2.2003 |
Hose, braun | FA Muster-Liefer GbR, Nürnberg | 39.90 | 50 |
| | | |
| 3 | 27 | 16.2.2003 |
| | | |
Hosen | Max Müller, Berlin | 49.90 | 10 |
| 4 | 28 | 17.2.2003 |
Hemden | FA Groß-Handel AG, Hamburg | 18.40 | 30 |
| | | |
| 5 | 28 | 17.2.2003 |
Hemden | FA Groß-Handel AG, Hamburg | 18.40 | 40 |
| | | |
| 6 | 35 | 18.2.2003 |
| | | |
Hemden | Lisa Mayer, München | 23.90 | 5 |
Merkmale und Nachteile einer vollständig denormalisierten Tabelle
- Einerseits läßt sich offenkundig jede Information in eine solcherart strukturierte, sehr viele Spalten
umfassende Tabelle einsortieren. Wird die Archivierung zusätzlicher Informationen gewünscht, so genügt es, weitere
Zeilen hinzuzufügen und diese gegebenenfalls leer zu lassen. Jede Datenbank könnte sich theoretisch damit begnügen,
eine einzige, nach diesem Muster strukturierte Tabelle zu enthalten. Im extremsten Fall würde die Tabelle eine
einzige große Textspalte enthalten, in welcher sämtliche Informationen der Reihe nach aufgeführt wären. Andererseits
weist diese Form der internen Darstellung offensichtliche Nachteile auf, die im folgenden zu diskutieren sind.
- Die Redundanz dieser Beispiele ist offenkundig. So werden im ersten Beispiel dieselben Adressdaten mehrfach
aufgeführt, so daß bei einer Änderung einer Adresse mehrere gleichlautende Zellen gesucht und umgeschrieben
werden müssten. In solch einem Fall spricht man von einer Update-Anomalie.
Dann könnte es zwei verschiedene Personen mit demselben Vor- und Nachnamen geben, so daß bei der dem Update vorgeschalteten
Suche mehrere Adressen gefunden und anschließend geändert werden. Eindeutige Personen sollten eindeutig charakterisierbar
sein.
- Werden im Beispiel 2 alle Hosen gesucht, so muß mit einer Platzhaltersuche gearbeitet werden, die nach 'Hose*' oder
'*Hose*' sucht und den '*' als Platzhalter nutzt. Denn die Zelle 'Einkauf: Artikel' enthält mehrere Informationen
gleichzeitig. Interessiert nur eine Teilinformation, so ist unbekannt, wo diese steht (am Anfang oder in der
Zellenmitte), so daß der zu suchende Ausdruck von beiden Seiten her durch einen Platzhalter ergänzt werden muß.
Ein vergleichbares Problem existiert bei der Suche nach Ortschaften. Eine Person, die als Nachname ein Wort hat,
das gleichzeitig als Ort eingetragen ist, würde bei der Suche nach dem Ort gefunden werden.
- Eine solche Tabelle scheint nur Text zu enthalten. Damit könnte in die Datumszelle auch Text eingetragen
oder bei den Preisangaben anstelle des Punktes ein Komma als Trennzeichen verwendet werden, so daß der Wert nicht
mehr korrekt für Rechenoperationen verwendet werden kann.
- Das Beispiel 2 umfaßt derzeit nur Bestellungen und Lieferungen. Würde die Lieferung an 'Lisa Mayer' gelöscht oder
storniert werden, so würden auch die Adressangaben verschwinden, obwohl diese vielleicht noch notwendig sind.
Dies wird als Delete-Anomalie benannt. Eine Alternative bestünde darin, 'Lisa Mayer' ohne
eine Lieferung einzutragen. Damit dürfen die für eine Lieferung zwingend notwendigen Felder nicht mit NOT NULL
deklariert sein, das DBMS kann nicht die Konsistenz der Eingabe sicherstellen. Wäre dagegen eine Bestellung zwingend, so wäre dies ein Beispiel für eine Insert-Anomalie.
Selbst bei der Zulässigkeit einer Person ohne Bestellung ist unklar, ob die Daten in die Spalte 'Lieferant' oder 'Empfänger'
einzutragen sind. Schließlich ist der Fall denkbar, daß eine Firma sowohl Lieferant als auch Empfänger ist oder
an eine als Mitarbeiter eingetragene Person Waren gesandt werden.
- Liefert die Firma 'Muster-Liefer' verschiedenfarbige Hosen, so kann diese Information ignoriert werden.
Damit ist es später unmöglich, die Einkaufs- und Verkaufszahlen nach den Farben der Hosen aufzuschlüsseln. Wird
diese Information zusätzlich abgelegt, so müssen auch sämtliche Adressdaten erneut eingegeben werden, so
daß die Wahrscheinlichkeit von Eingabefehlern steigt. Ein Verschreiber 'FI Muster-Liefer' würde bei einer Textsuche
nach 'FA Muster-Liefer*' zum Verschwinden dieses Datensatzes führen.
- Die beiden Lieferungen am 15.02.2003 betreffen verschiedene Artikel. Die zwei Lieferungen vom 17.02.2003
dagegen könnten zu einer Lieferung mit 70 Stück zusammengefaßt werden, da die Zellwerte, mit Ausnahme der Spalte
'Stückzahl', identisch sind und bei der Stückzahl die Addition zweier Einzelinformationen sinnvoll ist.
Offenkundig produziert ein solches Design diverse Probleme und fehlerhafte Auswertungen.
Zwar wäre im Gegensatz zu jedem Archiv in Papierform eine reine Textsuche möglich. Jedoch müßten die Ergebnisse
erneut per Hand geprüft werden, jede fehlerhafte Eingabe läßt sich nur durch die Kontrolle der Originalzelle
ermitteln. Deshalb würde ein solches, denormalisiertes Design all jene Vorzüge einer edv-gestützten Datenarchivierung
zunichte machen.
Die folgenden Seiten beschäftigen sich deshalb mit den datenbank-typischen Techniken, eine solche denormalisierte
Tabelle in mehrere kleinere Tabellen zu zerlegen.
TOP
Tabellen als Relationen mit Attributen
Eine denormalisierte Tabelle enthält pro Geschäftsvorfall eine Zeile und ist insofern mit einer freien schriftlichen
Aufzeichnung vergleichbar. Wird diese Zeile auf einzelne Zellen aufgeteilt und werden den Spalten Namen zugewiesen,
so spricht man davon, daß der Tabelle
Attribute zugeordnet sind. Stimmen zwei Zeilen
in allen Attributen überein, so kann eine Zeile entfernt werden, da in der
Menge der
Zeilen kein
Element (= Zeile) doppelt vorkommen kann: Entweder handelt es sich um einen
Eingabefehler oder es gibt bsp. tatsächlich zwei verschiedene Personen mit demselben Vor- und Nachnamen, dann muß
ein zusätzliches Attribut eingeführt werden, welches die Eindeutigkeit jeder Zeile sicherstellt. Statt
Tabelle
wird in der eher mathematisch orientierten Literatur auch von
Relation gesprochen: Ein
Geschäftsvorfall stellt eine individuelle Relation zwischen verschiedenen Attribut-Werten dar.
Ein
Schlüssel ist eine Menge von Attributen, mit dem eine Datenzeile eindeutig identifiziert
werden kann. Ein
Schlüssel-Kandidat ist ein Schlüssel mit minimaler Attribut-Anzahl.
Eine Tabelle / Relation kann mehrere Schlüssel-Kandidaten haben. Ein
Primär-Schlüssel ist
ein beliebig ausgewählter Schlüsselkandidat. Besteht dieser aus mehreren Attributen, so wird er als
zusammengesetzter Primärschlüssel bezeichnet. Ein
Schlüssel-Attribut
ist schließlich ein Attribut, das zu mindestens einem Schlüssel gehört, ansonsten handelt es sich um ein
Nicht-Schlüssel-Attribut.
Ein Attribut B heißt
funktional abhängig vom Attribut A, falls zu einem Wert von Attribut
A höchstens ein Wert von B gehört. So sind Name (B) und Vorname (C) einer Person funktional abhängig von der Personalnummer
dieser Person. Ein Attribut B heißt
voll funktional abhängig vom Schlüssel A, falls
B funktional abhängig ist von A, jedoch nicht schon funktional abhängig von einer Teilmenge von A ist. Besteht
der Schlüssel A nur aus einem Attribut und ist B funktional abhängig von A, so ist B bereits voll funktional abhängig.
Das Attribut C heißt
transitiv abhängig von A, falls es ein Nicht-Schlüssel-Attribut B gibt,
das funktional abhängig ist von A und von dem C funktional abhängt. Ein Attribut B kann, im Gegensatz zur
funktionalen Abhängigkeit von A, auch
mehrwertig abhängig von A sein. In diesem
Fall gibt es mehrere Attribut-Werte B, die A zugeordnet sein können (Bsp.: mehrere Mailadressen einer Person).
Ziel der
Normalisierung über die fünf
Normalformen ist es, zunächst
atomare Attribute einzuführen und anschließend die
Mengen von Schlüsseln und Nichtschlüssel-Attributen so der Reihe nach zu identifizieren, daß alle redundanten Beziehungen
herausgezogen und in einzelne Tabellen ausgelagert werden. Die oben definierten Begriffe der 'vollen funktionalen
Abhängigkeit' sowie der Transitivität dienen dazu, Abhängigkeiten zwischen den Attributen aufzuspüren und solche
Wiederholungen zwischen verschiedenen Attributen in neue Tabellen auszulagern. Das Ergebnis ist nicht mehr eine große, redundante Tabelle,
sondern viele kleine und schmale Tabellen, die durch verschiedenste Beziehungen miteinander verknüpft sind. Für
diese Beziehungen wird - leider - oftmals ebenfalls der Begriff
Relation verwendet,
so daß dieser Begriff sowohl für Tabellen als auch für Beziehungen zwischen Tabellen genutzt wird. Jede dieser Einzel-Tabellen
kann um einen eigenen zusätzlichen Primärschlüssel ergänzt werden, der als ganze Zahl implementiert, dessen Wert
vom Datenbanksystem festgelegt und beim Einfügen neuer Datensätze automatisch hochgezählt wird. Wird in einer anderen
Tabelle auf diese Tabelle bezug genommen, so genügt es, eine zusätzliche Spalte einzufügen und diese als
Fremdschlüssel (
foreign key) zu deklarieren.
Einschränkungen auf der Ebene von Spalten
Für eine einzelne Spalte können - unabhängig voneinander - zwei Einschränkungen deklariert werden:
- Nicht Null / Not Null: Ein solches Feld
darf nicht leer sein, es muß auf jeden Fall einen Wert erhalten.
- Eindeutigkeit / UNIQUE: Jeder Wert in dieser
Spalte muß innerhalb der Spalte eindeutig sein.
In beiden Fällen genügt es, das Feld bei der Erstellung mit diesen Eigenschaften zu deklarieren bzw. bei einer
bereits definierten Tabelle die Felddefinition zu ändern. Ab dann überprüft das DBMS bei jedem Einfügungs- und
Aktualisierungsversuch, ob die gesetzten Bedingungen durch den neu einzufügenden oder zu ändernden Datensatz
mißachtet werden und verhindert die auslösende Operation gegebenenfalls.
Ist die Spalte als Primärschlüssel deklariert, so sind implizit beide Sondereigenschaften gesetzt: Ein
Primärschlüssel darf nicht leer sein und er muß eindeutig sein. Denn der Wert einer Primärschlüssel-Zelle ist
für diese Zeile eindeutig, damit sind mehrere leere oder doppelte Werte ausgeschlossen.
Beziehungen zwischen Tabellen
Stehen mehrere Tabellen zueinander in Beziehung, so sind verschiedene Szenarien denkbar:
- Basis- und Detail-Tabelle in einer 1:n-Beziehung:
Zu einem Datensatz in der Basistabelle kann es keinen, einen oder mehrere Datensätze in der Detailtabelle geben.
Die Detailtabelle enthält mehrwertige Attribute zur Basistabelle.
So sollen zu Mitarbeitern Mailadressen hinzugefügt werden. Die Tabelle der Mitarbeiter verwendet einen eigenen,
zahlbasierten Primärschlüssel, da weder Nachname noch Nachname + Vorname eindeutig sind. Es soll jedoch möglich
sein, einem Mitarbeiter keine, eine oder mehrere Mailadressen zuzuweisen. Würde für jeden Mitarbeiter höchstens
eine Mailadresse erfaßt, so würde es genügen, der Tabelle 'Mitarbeiter' eine weitere Spalte hinzuzufügen.
Da jedoch beliebig viele Adressen zulässig sein sollen, wird eine Detailtabelle benötigt.
Basistabelle tbl_Mitarbeiter:
| Mitarbeiter-ID | Name | Vorname |
| 15 | Maier | Horst |
| 16 | Schmidt | Susanne |
| 18 | Schmidt | Frank |
Detailtabelle tbl_MailAdressen mit Fremdschlüssel von Mitarbeiter-Id auf die gleichnamige Primärschlüsselspalte
in tbl_Mitarbeiter:
| ID | Mitarbeiter-Id | eMail |
| 2 | 15 | horst.maier@mustermann-ag.de |
| 3 | 15 | abt-1@mustermann-ag.de |
| 4 | 15 | maier@privatadressen-erdbewohner.de |
| 5 | 18 | frank.schmidt@mustermann-ag.de |
Damit wurden Horst Maier drei und Frank Schmidt eine Mailadresse zugewiesen, für Susanne Schmidt wurde keine
Mailadresse aufgenommen.
- Zwei Basis-Tabellen werden in einer Detail-Tabelle durch eine n:m-Beziehung verknüpft:
Es gibt zwei voneinander unabhängige Basis-Tabellen und es gibt Geschäftsvorfälle, bei welchen genau einem Datensatz der ersten
Tabelle genau ein Datensatz der zweiten Tabelle zugewiesen wird. Jeder Datensatz aus jeder Basistabelle kann
beliebig oft an Geschäftsvorfällen teilnehmen. Die n:m - Beziehung wird also aufgelöst in eine 1:m sowie in eine
n:1 - Beziehung.
Die Mitarbeiter der obigen Tabelle pflegen Kundenkontakte, jeder Kundenkontakt soll mit Datum und Notiz aufgezeichnet
werden. Ein Mitarbeiter kann viele Kunden beraten, ein Kunde kann mit vielen Mitarbeitern Kontakt haben.
Basistabelle tbl_Kunden:
| Kunden-ID | Name | Vorname |
| 30 | Müller | Silvia |
| 35 | Becker | Karl |
Detailtabelle tbl_Kundenkontakte mit Fremdschlüssel-Einschränkungen von Mitarbeiter-Id auf tbl_Mitarbeiter sowie
von Kunden-Id auf tbl_Kunden:
| ID | Mitarbeiter-Id | Kunden-Id | Datum | Notiz |
| 2 | 16 | 35 | 20.04.2004 | Erstgespräch |
| 3 | 15 | 35 | 22.04.2004 | Tel. Verschiebung |
| 4 | 16 | 30 | 25.04.2004 | Besichtigung |
| 5 | 16 | 35 | 28.04.2004 | Vertragsunterzeichnung |
Hier hat der Kunde Karl Becker drei Kontakte, zwei mit der Mitarbeiterin Susanne Schmidt sowie ein Telefonat mit
Horst Mayer. Mitarbeiterin Susanne Schmidt führt ferner eine Besichtigung mit Kundin Silvia Müller durch.
- Eine Basis-Tabelle muß um diverse Spalten ergänzt werden, die Zellen bleiben jedoch für viele Zeilen leer.
In diesem Fall ist es besser, eine zweite Tabelle zu definieren und diese durch eine 1:1-Beziehung
mit der ersten Tabelle zu verknüpfen. Nun werden in der Randtabelle nur zu jenen Zeilen der Basis-Tabelle neue
Zeilen eingefügt, falls zu dem Basis-Datensatz jene speziellen Informationen aufzuzeichnen sind:
Es gibt die Tabelle tbl_Mitarbeiter mit sämtlichen Mitarbeitern (festangestellte und freie) und ihren Grunddaten. Zu den festangestellten
Mitarbeitern sollen zusätzlich das Eintrittsdatum sowie Gehaltsinformationen abgelegt werden.
Randtabelle tbl_MitarbeiterInfos:
| Mitarbeiter-ID | Eintritt | Stufe |
| 15 | 01.01.2000 | XI-2 |
| 16 | 01.05.2001 | X-3 |
Hier wird die Spalte Mitarbeiter-Id zusätzlich (1) als Primärschlüssel sowie (2) als Fremdschlüssel auf die
Tabelle tbl_Mitarbeiter deklariert, es existiert keine Spalte vom Typ Auto-Wert. Durch die Auszeichnung als
Primärschlüssel ist gewährleistet, daß zu jedem Mitarbeiter höchstens ein Eintrag in tbl_MitarbeiterInfos
existiert. Die Definition als Fremdschlüssel stellt sicher, daß nur gültige Mitarbeiter-Id's verwendet werden.
Mitarbeiter Nr. 18 / Frank Schmidt ist freier Mitarbeiter, für ihn existiert kein Eintrag in tbl_MitarbeiterInfos.
Konsequenzen bei der Nutzung von Fremdschlüssel-Einschränkungen
Werden bei der Definition einer Tabelle Fremdschlüssel-Einschränkungen verwendet, so gelten zusätzlich die
folgenden Regeln:
- Das Ziel der Fremdschlüsseleinschränkung muß eine Spalte sein, die in der Basis-Tabelle Primärschlüssel
ist. Denn ein Fremdschlüssel bedeutet, daß zur aktuellen Zeile genau eine Zeile der Basistabelle zugeordnet wird,
folglich muß diese eindeutig identifizierbar sein.
- Das DBMS stellt sicher, daß in die mit einem Fremdschlüssel definierte Spalte der Detailtabelle nur
Werte eingetragen werden können, welche bereits in der Basistabelle existieren.
- Ein Datensatz in der Basistabelle, dessen ID bereits in der Detailtabelle genutzt wird, kann nicht
mehr gelöscht werden. Um ein Löschen zu ermöglichen, müssen zunächst alle auf dieser ID basierenden Detail-Datensätze
gelöscht werden. Ausnahme: Das DBMS unterstützt kaskadierendes Löschen und diese Option wurde beim Erstellen
des Fremdschlüssels aktiviert. In diesem Fall führt ein Löschen einer Zeile in der Basistabelle zum automatischen
Löschen aller Detail-Zeilen.
- Im Regelfall können Spalten, die zu Fremdschlüssel-Bezügen gehören, nicht umbenannt oder gelöscht werden.
Zunächst muß der Fremdschlüssel gelöscht und nach der Umbenennung wieder neu deklariert werden.
- Da über Fremdschlüssel-Spalten bei späteren Abfragen Tabellen wieder mit dem JOIN-Operator verknüpft
werden, sind Fremdschlüssel-Spalten gute Kandidaten für Indizes zur Verbesserung der Leistung.
TOP
Erste Normalform: Attribute sollen atomar sein, wiederholende Gruppen sind auszulagern
Die
erste Normalform (
NF1) ist durch zwei Forderungen charakterisiert:
- Attribute müssen atomar sein. Das meint: Einem Attribut dürfen nicht mehrere Werte aus dem definierten Gültigkeitsbereich zugeordnet sein.
- Wiederholende Gruppen, also mehrwertige Relationen, sind in eine eigene Tabelle auszulagern und zu verknüpfen.
Beispiele:
- Trennen von Nachname, Vorname, eventuell Namenszusatz (... van der ...) in ein eigenes Feld, also Vergrößerung
der Zahl der Spalten. Wird beim Namenszusatz auf eine eigene Spalte verzichtet, so kann es später zu inkonsistenten
Einträgen kommen. Einmal wird der Vorname um den Namenszusatz ergänzt {Schmidt, Sybille van der}, andere Varianten
sind {van der Schmidt, Sybille} oder {Schmidt van der, Sybille}. In sämtlichen Fällen können bei Suchabfragen
nach 'Schmidt' oder 'Sybille' Zeilen ignoriert werden, so daß der Datensatz nicht gefunden und ein zweites Mal
eingetragen wird.
- Schauspieler zu einem Film, an einem Projekt beteiligte Mitarbeiter, Etagen in einem Haus, Zimmer einer Etage:
Eine handschriftliche Aufzeichnung
würde die beteiligten Personen / Etagen / Zimmer der Reihe nach aufführen, sie damit alle in eine Zelle schreiben. Die Lösung,
mehrere Spalten für mehrere Teilnehmer usw. anzufügen, ist ungeeignet, da entweder zu viele Zellen leer bleiben oder
zu wenige Spalten vorhanden sind. Also wird eine Detailtabelle (Mitwirkende, Projektbeteiligte, Etagen, Zimmer) erstellt,
die in einer n:1 - Beziehung zur Grundtabelle (Filme, Projekte, Häuser, Etagen) steht. Die Gruppe dieser gleichartig
strukturierten Detail-Informationen wird in diese Detail-Tabelle ausgelagert.
Zuordnung von Werttypen / Datentypen zu Attributen
Um eine zusätzliche Konsistenz der atomaren Einträge zu erzwingen, werden den Attributen (= Spalten) Datentypen
zugeordnet. Damit kann das DBMS bei allen Einfügungs- und Aktualisierungsoperationen überprüfen, ob die Eingabedaten
dem geforderten Datentyp entsprechen und die Ausführung der Operation gegebenenfalls untersagen.
Standard-Datentypen
- binary(Länge): Binärdaten fester Länge. Dieser kann mit analogen Datentypen
verwendet werden, um Bilder, Videos, Word- oder PDF-Dokumente abzuspeichern.
- Boolean bzw. bit: Boolsche Wahr / Falsch
bzw. Ja / Nein - Werte. Dieser Werttyp eignet sich zur Darstellung von zwei einander ausschließenden Werten
und kann als Checkbox zum Anhaken oder Pulldown-Liste (Ja/Nein) dargestellt werden.
- char(Länge): Zeichenfolge (1-Byte-Zeichen) mit fester Länge (char(5) = fünf Zeichen).
Dies erlaubt nur Zeichen aus dem Ascii bzw. den ISO-Codierungen, so daß maximal 256 verschiedene Zeichen
gespeichert werden können.
- datetime: Datums- und Zeitfelder. Beide Informationen werden gemeinsam gespeichert.
Wurde nur das Datum angegeben, so wird als Zeit '00:00:00' genommen, bei einer reinen Zeitangabe wird meist
als Bezugsdatum der 01.01.1900 verwendet. Jedes DBMS kennt Funktionen, mit welchen aus einem datetime-Feld
nur der Datums- oder nur der Zeitanteil herausgezogen werden kann. Ebenso gibt es in jedem DBMS Funktionen
für Datumsoperationen, mit welchen Datumsangaben addiert oder Datums-Differenzen ausgegeben werden können.
- decimal[(p[, s])], Synonym numeric: Dezimal-Datentyp
für numerische Datentypen mit einer fest vorgegebenen Genauigkeit und fest definierten Anzahl von Dezimalstellen. p legt
die Zahl aller Stellen vor und nach dem Dezimalpunkt fest und kann üblicherweise zwischen 1 und knapp 40 liegen. s fixiert
die Nachkommastellen und liegt zwischen 0 und p. s = 0 bedeutet, daß der Datentyp nur Ganzzahlen akzeptiert.
decimal(15,5) deklariert einen Datentyp mit fünf Nachkomma- und maximal 10 Vorkomma-Stellen.
- float bzw. real: Ungefähre Zahlendatentypen,
die für numerische Gleitkomma-Daten genutzt werden können. Intern werden diese in der wissenschaftlichen
Darstellung 5.79E2 dargestellt, also als 5.79 * 10^2 = 579. Diese Datentypen sind ungefähr, da nicht alle
Werte im zulässigen Zahlbereich genau dargestellt werden. float ist in der Regel doppelt so genau wie
real und benötigt 8 statt 4 Byte.
- image: Binary-Daten variabler Länge, meist mit einer maximalen Länge
in der Größenordnung 2^31
- int bzw. Integer: Ganzzahl mit negativen oder positiven Werten. Im Regelfall
mit 4 Byte implementiert, damit kann ein Wertebereich von +/- 2^31, also -2.147.483.648 bis +2.147.483.648
abgedeckt werden. Ist eine Spalte als Primärschlüssel definiert, so hat sie diesen Datentyp, eine Spalte
mit einer Fremdschlüsseleinschränkung muß dementsprechend ebenfalls vom Typ Integer sein.
- money: Währungsdatentyp. Währungen werden mit hinreichend vielen Nachkomma-Stellen
gespeichert
- nchar(Länge): Unicode-Zeichenfolge mit fester Länge, benötigt zwei Byte
pro Zeichen.
- ntext: Unicode-Zeichenfolge variabler Länge, in der Regel bis zu 2^30
Zeichen.
- nvarchar(Länge): Unicode-Zeichenfolge variabler Länge, beim Ms-Sqlserver
beschränkt auf 4000 Zeichen.
- text: Nicht-Unicode-Zeichenfolge variabler Länge, in der Regel bis zu 2^31
Zeichen.
- varbinary(Länge): Binary-Daten variabler Länge zwischen 0 und dem durch
'Länge' festgelegten Wert. Beim Ms-SqlServer existiert eine zusätzliche Obergrenze von 8000 Zeichen.
- varchar(Länge): Zeichenfolge (1-Byte-Zeichen) mit variabler Länge, maximal
der durch 'Länge' festgelegte Wert.
Spezielle Datentypen
- timestamp: Ein datenbankweit eindeutiger Ausdruck, der bei jeder Änderung
der Daten automatisch hochgezählt wird. Wird einer Tabelle eine Spalte vom Typ timestamp hinzugefügt, so
kann diese zur Versionskennung der Zeile verwendet werden. Beim Lesen der Daten wird dieser Wert mitgesandt,
beim Schreiben geht der timestamp-Wert zusammen mit dem Primärschlüssel in die Where-Bedingung ein. Hat ein
anderer Nutzer in der Zwischenzeit diese Zeile geändert, so scheitert die Aktualisierung.
- uniqueidentifier: Ein global eindeutiger Bezeichner (GUID,
Global unique Identifier). Auto-Werte sind nur innerhalb der Tabelle eindeutig. Werden
global eindeutige Werte benötigt, so können GUID's genutzt und in uniqueidentifier-Spalten abgelegt werden.
Die Verwendung als Primärschlüssel ist jedoch nicht zu empfehlen, da die Indizes, welche den Primärschlüssel
nutzen, dann sehr groß werden.
- int Identity(1, 1), eine Identitätsspalte: Dies definiert
eine Spalte vom Typ Integer, die zusätzlich einen Identitätswert erhält, eine Zahl, die vom DBMS automatisch hochgezählt und
deren Eindeutigkeit damit garantiert ist. Solche Spalten sind gute Kandidaten für Primärschlüssel. Die erste Zahl legt den
Startwert fest, die zweite Zahl deklariert die Schrittweite. Werden Datensätze eingefügt, so wird üblicherweise für eine
Identitätsspalte kein Wert angegeben. MsAccess läßt es zu, daß ein Wert angegeben wird, sofern dieser bislang noch nicht
verwendet wurde. Beim Ms-Sqlserver muß zuvor die Spalte mit SET IDENTITY_INSERT <Tabellenname> ON
für solche Aktionen zugelassen und anschließend mit 'OFF' wieder deaktiviert werden.
Veränderte Schrittweiten lassen sich bsp. dann verwenden, falls später Zeilen eingefügt werden, bei welchen bereits an
der Id deutlich werden soll, daß es sich um besondere Objekte handelt. So legt eine Anweisung 'int Identity(2, 2)'
fest, daß nur die Zahlen 2, 4, 6, ... für das automatische Einfügen vom Benutzer verwendet werden können. Vom System
eingefügte Zeilen mögen ungerade Zahlen erhalten. Ein späterer Test modulo 2 unterscheidet beide Typen.
Hinweise zum Gebrauch der Datentypen
- Verwenden Sie als Primärschlüssel den Integer-Datentyp, meist kombiniert mit einer automatischen
Erhöhung. Vermeiden Sie Zeichendaten als Primärschlüssel. Zum einen werden Integer mit einer festen
Größe abgespeichert. Zum anderen genügt ein Bit-für-Bit-Vergleich, da keine Groß-Klein-Schreibung berücksichtigt
werden muß. Damit ist der Vergleich zwischen zwei Integer-Zellen auf jeden Fall schneller als zwischen
zwei Text-Zellen. Schließlich wird der Primärschlüssel bei anderen Indizes verwendet. Lange Textfelder blähen
diese Indizes unnötig auf. Der Aufwand für eine zusätzliche Spalte dürfte immere gering im Vergleich zu
solchen Seiteneffekten sein. Die Eindeutigkeit einer Spalte (etwa Telefonnummern) läßt sich auch mit
einer UNIQUE-Einschränkung in Kombination mit NOT NULL erzwingen.
- Für Telefonnummern sind in der Regel char- oder varchar-Datentypen notwendig. Würden stattdessen
Integer-Werte genutzt, so würden führende Nullen entfernt werden. Telefonnummern oder isoliert abgelegte
Hausnummern sollten ohne Leerzeichen abgespeichert und die bei der Suche vom Nutzer eingegebenen Leerzeichen
entfernt werden. Ansonsten könnten Werte nicht gefunden werden.
- Bei der Verwendung von Unicode-Datentypen (nchar, nvarchar, ntext) anstelle der gewöhnlichen
Text-Datentypen (char, varchar, text) ist zu beachten, daß diese zwar
korrekt gespeichert werden, die Anzeige jedoch fehlerhaft sein kann. Denn wird zur Anzeige nicht ein
passender Unicode-Font genutzt, so werden Unicode-Zeichen oberhalb Ascii nur als leere Quadrate dargestellt.
Hier ist bsp. im Ms-Access-Formular als Font 'MS Arial Unicode' zu verwenden. Im deutschen bzw. europäischen
Sprachraum bzw. für eine nur in dieser Region verwendeten Datenbank kann es genügen, nur Ascii-Texte zuzulassen.
Für internationale Namen, Produktbeschreibungen in verschiedenen Sprachen oder wissenschaftliche Texte, bei
welchen Sonderzeichen aus verschiedenen ISO-Codepages benötigt werden, sollten sofort Unicode-Datentypen
verwendet werden.
- Ein Nachteil von Unicode beim Einsatz auf dem Ms-SqlServer ist zu beachten: Dort beträgt die maximale
Datengröße pro Zeile etwa 8000 Byte. Man kann folglich Ascii-Texte bis zu 8000 oder Unicode-Text bis 4000 Zeichen
ablegen. Sollte sich eine Tabelle dieser Grenze annähern, so kann man nach dem Muster der 1:1 - Beziehung Texte
mit nvarchar-Datentyp auch in eine Nebentabelle auslagern.
- Die Verwendung der binary large object (BLOB)
Datentypen text-, ntext- und image-Daten bedeutet, daß in der Zeile nur ein Zeiger gespeichert
und der eigentliche Inhalt ausgelagert wird. Damit sind zwar einerseits hinreichend große Daten verwaltbar,
jedoch können diese Daten nicht in einer üblichen Where-Klausel genutzt werden. Auf text/ntext läßt sich allerdings
der LIKE-Operator anwenden. Beim Ms-SqlServer ist es möglich, diese BLOB-Datentypen in einer eigenen Dateigruppe abzulegen, so
daß die Dateigruppe für die Hauptdaten hiervon unabhängig ist.
- Allgemein wird davon abgeraten, Bilder, Videos und ähnliches in Datenbank-Spalten abzulegen. Denn
die eigentlichen Stärken eines DBMS wie Vergleiche, Filterung und Sortierung können bei BLOB-Daten ohnehin kaum
verwendet werden. Diese blähen jedoch die Datenbank unnötig auf und erschweren damit Sicherungen. Da BLOB-Daten
normalerweise nur sehr selten geändert werden, ist es sinnvoller, diese im Dateisystem abzulegen und lediglich
relative Pfadangaben sowie das Datum der letzten Aktualisierung in der Datenbank zu speichern.
TOP
Zweite Normalform - volle funktionale Abhängigkeit der Attribute vom Primärschlüssel
Ein System von Tabellen ist dann in der
zweiten Normalform (
NF2), wenn die Tabellen
in der ersten Normalform sind und wenn zusätzlich alle Nichtschlüssel-Attribute voll funktional vom Primärschlüssel
abhängig sind. Umgekehrt formuliert heißt dies: Eine Tabelle ist noch nicht in zweiter Normalform, wenn sie
einen zusammengesetzten Primärschlüssel hat und ein Nichtschlüssel-Attribut nicht vom ganzen Primärschlüssel,
sondern nur von einem Teilschlüssel abhängt. In diesem Fall wird das Nichtschlüssel-Attribut mit dem Primärschlüssel-Teil,
von dem es funktional abhängig ist, in eine eigene Tabelle herausgezogen.
Am einem Ausschnitt von Beispiel 1 deutlich gemacht, die notwendige Zerlegung von V_NAME und V_ANSCH wird aktuell
ignoriert:
| U_NR | A_NAME | A_PREIS | A_STUECK | DATUM | V_NAME | V_ANSCH |
| 1 | Oberhemd | 39,80 | 40 | 24.06.1999 | Meyer, Emil | Wendeweg 10, 2800 Bremen |
| 2 | Mantel | 360,00 | 10 | 24.06.1999 | Meier, Franz | Kohlstr. 1, 2800 Bremen |
| 3 | Oberhemd | 44,20 | 70 | 24.06.1999 | Meyer, Emil | Wendeweg 10, 2800 Bremen |
| 4 | Oberhemd | 44,20 | 20 | 25.06.1999 | Schulze, Fritz | Gemüseweg 3, 2800 Bremen |
Offenkundig ist es wesentlich, welcher Vertreter diesen Umsatz erbracht hat. Also gehört bei einer Umsatz-Zeile
die Spalte V_NAME zum Schlüssel mit hinzu. Die Vertreteranschrift V_ANSCH hat jedoch nichts mit dem aktuellen Umsatz zu tun, ist also
ein Nicht-Schlüssel-Attribut. V_ANSCH hängt nur von V_NAME, nicht vom einzelnen Umsatz, dessen Datum oder dem
beteiligten Artikel ab - im Gegensatz zu A_STUECK, das offenbar für die einzelne Umsatzzeile charakteristisch ist. Also können
V_NAME und V_ANSCH in eine kleine Tabelle herausgezogen und um einen Primärschlüssel ergänzt werden, der in eine zusätzliche Spalte
hinzugefügt wird.
Wurden die beteiligten 'Handlungspartner' oder die 'agierenden Instanzen' korrekt identifiziert und in eigene Tabellen
ausgelagert, so scheint die zweite Normalform trivial zu sein. Denn sie ist automatisch erfüllt, wenn die Attribute, also die Spalten einer Tabelle,
'sinnvoll' zum Primärschlüssel gehören und der Primärschlüssel aus einer Spalte besteht, es also keinen zusammengesetzten
Primärschlüssel gibt.
Um ein interessantes Beispiel für die Nicht-Erfüllung der zweiten Normalform zu finden, muß nach einem
Beispiel gesucht werden, bei dem ein Attribut scheinbar von einem zusammengesetzten Schlüssel abhängt, eine
tiefere Analyse jedoch lehrt, daß das Attribut in Wirklichkeit nur von einem Teilschlüssel abhängt.
Betrachtet man die obige Tabelle, so liegt es nahe, die Artikel mit ihren Preisen in eine eigene Tabelle herauszuziehen
und durch eine Spalte mit den Artikelnummern zu ersetzen. Wird dasselbe mit den Vertreter-Informationen durchgeführt,
so ergibt sich die bereits bekannte Tabelle
UMSATZ:
| UMSATZ_NR | V_NR | A_NR | A_STUECK | DATUM |
| 1 | 8413 | 12 | 40 | 24.06.1999 |
| 2 | 5016 | 22 | 10 | 24.06.1999 |
| 3 | 8413 | 11 | 70 | 24.06.1999 |
| 4 | 1215 | 11 | 20 | 25.06.1999 |
| 5 | 5016 | 22 | 35 | 25.06.1999 |
| 6 | 8413 | 13 | 35 | 24.06.1999 |
| 7 | 1215 | 13 | 5 | 24.06.1999 |
| 8 | 1215 | 12 | 10 | 24.06.1999 |
| 9 | 8413 | 11 | 20 | 25.06.1999 |
Es gibt also Artikel mit festen Preisen, ein eindeutiger Schlüssel in der Tabelle UMSATZ ist eine Kombination
aus V_NR, A_NR, A_STUECK und DATUM. Da sich eine solche Aufteilung gut eignet, um Erfahrungen mit dem Sql-Select-Befehl
zu sammeln, wurde diese Normalisierung den befehlsbezogenen Beispielen zugrundegelegt.
Ein Problem dieser zunächst plausiblen Aufteilung wird deutlich, falls sich der Preis eines Artikels ändert. Wird
dies direkt in der Tabelle ARTIKEL durchgeführt, so werden auch bereits abgeschlossene Verkäufe geändert, dies ist
offenkundig falsch. Um dieses Problem zu vermeiden, könnte man den Preis in der UMSATZ-Tabelle belassen, also
die folgende Tabelle verwenden:
| UMSATZ_NR | V_NR | A_NR | A_STUECK | A_PREIS | DATUM |
| 1 | 8413 | 12 | 40 | 39.80 | 24.06.1999 |
| 2 | 5016 | 22 | 10 | 360.00 | 24.06.1999 |
Damit ist A_PREIS ein Nichtschlüssel-Attribut in der Tabelle UMSATZ. Würden nach diesem Muster viele Einzelumsätze
aufgezeichnet werden, so sind zwei Alternativen denkbar.
- Der Preis eines Artikels ändert sich jede Minute. Man denke an den Preis eines Artikels, der von
Währungsschwankungen abhängt und dessen Preis sich aus dem aktuellen, minutengenauen Währungsverhältnis ergibt. In diesem
Fall ist der Preis ein unabhängiges Nicht-Schlüssel-Attribut von Datum und Uhrzeit, ein Artikel kann
an einem Tag zu verschiedenen End-Preisen verkauft werden. A_PREIS wäre ein korrektes Attribut in der Tabelle
UMSATZ.
- Der Preis eines Artikels ändert sich höchstens einmal pro Tag oder höchstens zu einem Termin in der Woche
bzw. zu Monatsbeginn. In diesem Fall ist der Preis nicht von der Uhrzeit, sondern vom Datum abhängig,
das Datum ist ein Schlüsselattribut. Derselbe Artikel wird zwar nicht an allen, jedoch an vielen aufeinanderfolgenden
Tagen zu demselben Preis verkauft. Damit ist der Preis als Nicht-Schlüssel-Attribut nicht abhängig vom
gesamten, zusammengesetzten Schlüssel, sondern bloß abhängig von der Kombination aus Datum und Artikel-Nummer, unabhängig
dagegen vom Vertreter. Man würde eine neue Tabelle erstellen, die wie folgt aussieht:
| A_NR | DATUM | A_PREIS |
| 11 | 01.06.1999 | 44.20 |
| 12 | 01.06.1999 | 39.80 |
| 11 | 25.06.1999 | 29.90 |
| 11 | 27.06.1999 | 44.20 |
Hier sind A_NR und DATUM die beiden Schlüsselspalten, A_PREIS ist Nicht-Schlüsselattribut. Der Artikel
mit der Nummer 11 wurde am 25.06.1999 für zwei Tage mit einem reduzierten Preis angeboten. Die Tabelle UMSATZ
behält die Spalten A_NR sowie DATUM, die Spalte A_PREIS wird entfernt.
Man sieht an diesem Beispiel, daß die Frage nach abhängigen Attributen nicht ausschließlich unter mathematischen
Gesichtspunkten oder anhand vorhandener Daten entschieden werden kann. Bei Preisen, die über lange Zeiträume stabil
sind, könnten die Preise auch in der Artikel-Tabelle belassen werden und bei Preisänderungen ein neuer Artikel eingeführt
werden. Ebenso könnten die aktuellen Werte von A_NAME (Hose, Mantel, Oberhemd) in eine schmale, zwei Spalten
(Id und Begriff) umfassende Tabelle herausgezogen werden und als Kategorie-Begriffe verwendet werden. Ein Artikel
ist dann nur noch durch eine Nummer gekennzeichnet, ihm wird eine Kategorie und ein Preis in ARTIKEL zugeordnet,
bei einer Preisänderung wird ein neuer Artikel definiert. Werden dagegen Lagerbestände mitberücksichtigt, so darf diese
Technik des 'neuen Artikels' bei einer Preisänderung offenkundig nicht verwendet werden, da das einzelne Objekt nicht
verschwunden ist, sondern nur einen anderen Preis erhält.
TOP
Dritte Normalform - ein Nicht-Schlüssel-Attribut darf nicht von einem Schlüssel und einem anderen Nicht-Schlüssel abhängen
Die
dritte Normalform (
NF3) verlangt, daß bei allen in der Datenbank definierten Tabellen, die
bereits in der zweiten Normalform sind, kein Attribut C existiert, das bereits funktional von einem Nicht-Schlüssel-Attribut B abhängt. Denn da
das Attribut C zur Tabelle gehört, sollte es vom Primärschlüssel A dieser Tabelle voll funktional abhängig sein. A umfaßt nur
ein Attribut, da die Tabelle bereits in der zweiten Normalform ist. Ist C jedoch zusätzlich vom Nicht-Schlüssel-Attribut B abhängig und ist das Nicht-Schlüssel-Attribut vom Primärschlüssel A
abhängig, so ist die Folgerung A -> C zwingend. Das transitiv abhängige Attribut C und das Attribut B bilden eine
neue, bislang noch nicht in einer eigenen Tabelle isolierte Relation. Diese kann aus der aktuellen Tabelle herausgezogen werden,
in die aktuelle Tabelle wird nur noch Attribut B bzw. der Primärschlüssel der neu gebildeten Tabelle eingetragen.
Der Unterschied zur vollen funktionalen Abhängigkeit der zweiten Normalform besteht darin, daß es sich hier um
eine zusätzliche Beziehung zwischen zwei Nicht-Schlüssel-Attributen handelt. Die Frage nach der zweiten Normalform
lautet dagegen, ob ein Nicht-Schlüssel-Attribut vom gesamten Schlüssel oder nur von einem Teilschlüssel abhängt. Falls
letzteres der Fall ist, wird das Attribut in eine bereits vorhandene Tabelle, in welcher der Teilschlüssel der
Primärschlüssel ist, verschoben.
Betrachten Sie den folgenden Ausschnitt aus dem ursprünglichen Beispiel 2:
| lfNr | Liefer-Nr | Datum |
Artikel (E) | Lieferant | EP | Zahl |
Artikel (V) | Empfänger | EP | Zahl |
| 1 | 24 | 15.2.2003 |
Hosen, blau | FA Muster-Liefer GbR, Nürnberg | 39.90 | 50 |
| | | |
| 2 | 24 | 15.2.2003 |
Hose, braun | FA Muster-Liefer GbR, Nürnberg | 39.90 | 50 |
| | | |
Zunächst ist offensichtlich, daß der Lieferant herausgezogen und die Adresse aufgesplittet wird. In die aktuelle
Tabelle wird lediglich die Lieferanten-Nummer eingetragen. Dann können Artikel-Namen und Farbbezeichnungen in schmale,
zweispaltige Tabellen bestehend aus Id und Begriff, ausgelagert werden. Dies erlaubt es, für die Eingabe Pulldown-Felder
zu nutzen und vermeidet Fehler aufgrund von Rechtschreib-Problemen bei der Texteingabe. Die Tabelle sieht
wie folgt aus:
| lfNr | Liefer-Nr | Datum |
Artikel-Nr | Farb-Id | Lieferanten-Id | EP | Zahl |
Es fällt auf, daß eine Lieferung, durchgeführt an einem
Tag, aus mehreren Artikeln bestehen kann, so daß mehrere Zeilen dieselbe Liefer-Nummer und dasselbe Datum enthalten
sowie sich auf einen Lieferanten beziehen. Besteht jede Lieferung nur aus einem Artikel, so ist die Spalte Liefer-Nr
überflüssig und könnte entfernt bzw. durch 'lfNr' ersetzt werden. Besteht eine Lieferung aus mehreren Artikeln, so sind Datum und Lieferanten-Nummer
abhängig von der Liefer-Nummer. Der Artikel, sein Einkaufspreis und die Artikel-Anzahl hängen jedoch nur vom Primärschlüssel
dieser Zeile ab und unterscheidet sich von anderen Zeilen, die zu derselben Lieferung gehören. Also kann eine Tabelle gebildet werden,
welche die Grunddaten jeder Lieferung enthält. Eine auf dieser basierende Detailtabelle 'Lieferdetails' beinhaltet pro Zeile eine
Kombination aus Artikel, Farbe und Preis. Damit ergibt sich für die Lieferungen die folgende Aufteilung:
Tabelle tbl_Lieferungen:
| Liefer-Nr | Datum | Lieferanten-Id |
Tabelle tbl_Lieferdetails:
| lfNr-Details | Liefer-Nr | Artikel-Nr | Farb-Nr | EP | Zahl |
Die detaillierte Betrachtung einzelner Liefer-Details führt zum Sichtbar-Werden von Wiederholungen, so daß
ein neues 'Objekt' 'Bestell-Rahmendaten', 'Grundtatsachen einer Lieferung' identifiziert und in eine eigene
Tabelle ausgelagert wird. Beachten Sie, daß dies nur dann gilt, falls tatsächlich eine Lieferung aus mehreren
Artikeln besteht. Selbstverständlich sind Szenarien denkbar, in welchen dies nicht gilt - dann ist eine solche
zusätzliche, eingeschobene Tabelle redundant.
Ein Beispiel mit partieller Abhängigkeit
Betrachten Sie die folgende denormalisierte Tabelle:
| Datum | Strasse | Haus | Etage | Wohnung | Zähler | Stand | Mitarbeiter |
Ein Mitarbeiter eines Strom- oder Gaslieferanten liest diverse Strom- oder Gaszähler ab. Zur Straßen-Id gehören
viele Häuser, zu jedem Haus mehrere Etagen, zu jeder Etage mehrere Wohnungen. In jeder Wohnung befindet sich ein
Zähler mit einer eindeutigen Nummer. Dessen Stand wird zu einem festgelegten Datum turnusgemäß von verschiedenen Mitarbeitern abgelesen.
Jede Zeile kann eindeutig durch die beiden Spalten Datum und Zählernummer identifiziert werden. Denn jeder Zähler
wird an einem Tag höchstens einmal abgelesen. Damit bilden diese beiden Spalten den zusammengesetzten Primärschlüssel.
Die Mitarbeiter-Id hängt natürlich von diesem Geschäftsvorfall der Zähler-Ablesung ab. Die Straße ist ein nur vom Zähler,
nicht vom Datum abhängiges Attribut. Denn von der Zählernummer ausgehend ist die Straße über die Wohnung, Etage und das Haus festgelegt.
Zum Erreichen der zweiten Normalform werden deshalb Wohnung, Etage, Haus und Straße herausgezogen, all diese Werte
sind durch die Zähler-Id festgelegt. Bei der zweiten Normalform ergibt sich damit die folgende Tabelle:
| Datum | Zähler | Stand | Mitarbeiter |
Werden in diese
Tabelle viele Zeilen eingetragen, so wird eine zusätzliche Regelmäßigkeit sichtbar: Ein Mitarbeiter sammelt an
einem Tag Informationen von allen Häusern einer Straße. Die Spalte Mitarbeiter ist also bereits determiniert
durch die Kombination aus Datum und Hausnummer und wird bsp. im Rahmen einer Arbeitsverteilung in voraus festgelegt.
Oder die Mitarbeiter sind von vornherein Ablesebereichen zugeteilt, so daß - unabhängig vom Datum - bekannt ist,
welcher Mitarbeiter für die Ablesung innerhalb eines Intervalls zuständig ist. Im datumsfreien Fall gilt also die folgende
transitive Abhängigkeit:
Zähler ―> Straße ―> Mitarbeiter
Hier kann das Attribut 'Mitarbeiter' zur Tabelle 'Straße' hinzugefügt und aus der aktuellen Tabelle entfernt werden.
Für diese Straße ist jener Mitarbeiter zuständig. Bei der Version mit Datum gilt:
Zähler ―> Straße
Datum + Straße―> Mitarbeiter
Hier gibt es eine neue Tabelle mit Straßen-Id und Datum als Primärschlüssel sowie der Mitarbeiter-Id als Nicht-Schlüssel-Attribut.
Die Straße darf nicht zur normalisierten Tabelle mit Datum, Zähler und Zählerstand hinzugenommen werden, weil sie
vom Zähler voll funktional abhängig ist.
Die restlichen Spalten werden gemäß der 1:n - Beziehung kanonisch normalisiert, so daß sich insgesamt die folgende Tabellenstruktur ergibt:
| Strassen-Id | Ort | Name | weitere für Straßen typische Eigenschaften |
| Haus-Id | Strassen-Id | Hausnummer | weitere für Häuser typische Eigenschaften |
| Etagen-Id | Haus-Id | Etagennummer | weitere für Etagen typische Eigenschaften |
| Wohnungs-Id | Etagen-Id | Hauptmieter | weitere für Wohnungen typische Eigenschaften |
| Zähler-Id | Wohnungs-Id | Werksnummer - 10-stellig mit Buchstaben | weitere für Zähler typische Eigenschaften |
Die neu identifizierte Tabelle mit Datum und Straßen-Id als Primärschlüssel:
| Datum | Straßen-Id | Mitarbeiter |
Die Tabelle mit den eigentlichen Ablesedaten sieht nun wie folgt aus:
| Datum | Zähler-Id | Zählerstand | Bemerkungen |
Erläuterungen zum Verhältnis zwischen der zweiten und der dritten Normalform
Die Notwendigkeit der ersten Normalform dürfte den meisten Lesern unmittelbar einleuchten. Die Forderung nach dem
Erfüllen der zweiten und der dritten Normalform bzw. nach dem Unterschied zwischen beiden Normalformen dürfte dagegen
eher irritieren, da es sich hierbei um relativ stark mathematisch geprägte Konzepte handelt. Es folgen deshalb einige
erläuternde Bemerkungen.
- Die zweite Normalform verlangt, daß Attribute in den korrekten Tabellen untergebracht sind. Normalerweise
lassen sich einige 'statische Entitäten' rasch identifizieren und in eigene Tabellen mit Primärschlüsseln exportieren.
Dann kann es zusätzliche Bewegungsdaten geben, welche diese Grunddaten, oft zeitpunktgebunden,
miteinander verknüpfen. Eine solche Tabelle enthält einige Fremdschlüssel auf die Grundtabellen sowie zusätzliche Spalten.
Damit umfaßt ein Schlüssel-Kandidat mehrere Attribute. Bei den Nicht-Schlüssel-Attributen muß geprüft werden, ob sie tatsächlich
zum aktuellen Geschäftsvorfall oder zu einer der Grundtabellen gehören, falls es sich um zeitlich unveränderliche Eigenschaften
handelt. Ändern sie sich dagegen in größeren Abständen, so daß zu verschiedenen Geschäftsvorfällen derselbe Attributwert gehört,
dieses jedoch nicht einer Grundtabelle zugewiesen werden kann, so ist das Attribut in eine eigene Tabelle herauszuziehen.
- Die dritte Normalform entspricht im wesentlichen der Forderung, 'eigenständige Einheiten' korrekt
zu identifizieren. Ist eine denormalisierte Tabelle mit Filmtiteln, Regisseuren und ihren Geburtsdaten gegeben,
so ist offenkundig das Geburtsdatum nur vom Regiseur, nicht jedoch von dem Film abhängig, in welchem der
Schauspieler mitwirkt. Die Personen werden mit dem Attribut 'Geburtsdatum' herausgezogen, in eine eigene Tabelle
mit Primärschlüssel abgelegt und nur dieser in die Filmtabelle eingebunden.
- Werden solche Tabellen, die gewisse 'Handelnde', 'eigenständige Objekte' oder 'Stammdaten' beschreiben,
mit einem zusätzlichen Primärschlüssel als Autowert definiert, so sind diese Tabellen in der Regel bereits
in der zweiten Normalform. Denn das 'eigenständige Objekt', das durch eine Zeile abgebildet ist, determiniert
seine Attribute. Da der hinzugefügte Primärschlüssel aus einer Spalte besteht, kann keine echte Teilmenge des
Primärschlüssels gebildet werden.
- Zusammengefaßt: Die Attribute innerhalb einer Tabelle müssen durch den Primärschlüssel vollständig
determiniert sowie voneinander unabhängig sein. Abhängigkeiten zwischen Attributen sind in eigene Tabellen
zu isolieren.
TOP
Sql - Structured Query Language - Merkmale und Besonderheiten dieser Programmiersprache
Die folgenden Bemerkungen skizzieren einige wesentliche Merkmale von Sql.
Das Verhältnis zwischen Sql und dem Datenbank-Management-System (DBMS)
Im Kapitel
Datenbank-Grundbegriffe wurde erläutert, daß das Datenbank-Managementsystem die einzelnen
Datenbanken vor dem direkten Zugriff abschirmt und lediglich Schnittstellen anbietet, so daß Nutzer über diese
Schnittstellen Daten abfragen und bearbeiten können. Handelt es sich bei dem DBMS um ein relationales DBMS, das
Sql unterstützt, dann nimmt diese Schnittstelle Sql-Befehle in Form von Texten / Strings entgegen. Diese werden
zunächst von einem passenden Modul auf syntaktische Korrektheit geprüft und von einem Parser in die logischen Einheiten
(Schlüsselwörter, Operatoren, Tabellen- und Spaltenbezeichner) zerlegt. Anschließend muß ein Ablaufplan festgelegt
werden. Denn für bald jede Sql-Anweisung gibt es verschiedene Möglichkeiten, in welcher Reihenfolge JOIN- und
WHERE-Bedingungen auf die beteiligten Tabellen angewandt werden können. Ebenso wird auf dieser Ebene entschieden,
ob für Zugriffe Indizes genutzt oder ob ein Tabellenscan vielleicht schneller ist. Der interne Optimierer versucht,
aus diesen Varianten jene auszuwählen, welche die geringsten Kosten verursacht, das Ergebnis wird als Ablaufplan im
Arbeitsspeicher abgelegt. Anschließend kann dieser ausgeführt und eventuell zurückgegebene Zeilen in eine Art
temporäre Tabelle geschrieben werden. Nach Abschluß der Datenoperation liegt eine Statusmeldung vor. Diese wird
- eventuell mit den Daten - an die aufrufende Instanz zurückgegeben. Ein Vorteil eines serverbasierten DBMS im Gegensatz
zu einem Desktop-System liegt darin, daß ein serverbasiertes DBMS bereits kompilierte Ablaufpläne im Arbeitsspeicher
halten und wiederverwenden kann. Bei der nächsten Anforderung mit derselben Befehlsfolge kann der erstellte Ablaufplan
wiederverwendet, die für seine Erstellung notwendige Zeit damit eingespart werden. Desktop-basierte Systeme, bei
welchen ausschließlich die CPU des Clients diese Operationen ausführt, erstellen in der Regel den Ablaufplan zwar
schneller, jedoch auch statischer. Diese Erstellung eines Ablaufplans wird bei jedem Aufruf wiederholt. Bei
Zugriffen mehrerer Clients sind voneinander unabhängige CPUs beteiligt, so daß jeder Client seinen eigenen Ablaufplan
erstellen muß.
Sql-Ausführung durch einen Interpreter oder eingebettet in eine Wirtssprache
Für das bislang vorgestellte Szenario sind zwei verschiedene Initialisierungen denkbar. Zum einen kann das DBMS selbst
oder ein Client eine interaktive Möglichkeit anbieten, welche das direkte Eintippen von Sql-Befehlen erlaubt.
Beispiele hierfür sind der 'SQL Query Analyzer' vom MS-SqlServer, das ebenfalls der MSDE-Engine beiliegende OSQL.Exe,
falls dieses im interaktiven Modus genutzt wird oder das Sql-interaktiv-lernen aus den
Freeware - Tools. Diese
Werkzeuge erlauben es, den Sql-Code direkt einzugeben und liefern das Recordset sowie eventuelle Statusmeldungen
zurück. Ferner kann ein solches clientseitig verwendetes Tool auch eine graphische Möglichkeit anbieten, Tabellen
und Spalten auszuwählen sowie Filterbedingungen zu setzen. Im Hintergrund generiert das Clientsystem aus den Nutzereingaben
den zugehörigen Sql-Befehl. Werkzeuge wie der Access-Entwurfsmodus für Abfragen, die Erstellung von Sichten
über die graphische Oberfläche innerhalb eines auf der MSDE basierenden Projektes oder eine über ein Webinterface
verwaltete Sql-Datenbank zum Mieten sind von diesem Typ. Manche dieser Werkzeuge erlauben ein Umschalten zwischen
graphischer Oberfläche und erzeugtem Sql-Code, so daß Sql hierüber unmittelbar zu lernen ist.
Die andere Möglichkeit besteht darin, innerhalb einer Programmiersprache ein Objekt zu verwenden, welches
Kontakt zu einem DBMS aufnehmen, diesem Sql-Befehle übergeben und Recordsets sowie Statusmeldungen empfangen und
weiterverarbeiten kann. In diesem Fall wird die umgebende Programmiersprache, die Sql-Strings an das DBMS weiterreicht,
als
Wirtssprache bezeichnet. Der Vorteil einer solchen Architektur besteht darin, daß
bei verschiedenen Clients, die womöglich sogar verschiedene Programmiersprachen nutzen, die Ebene des Datenzugriffs
getrennt werden kann von der Ebene der clientseitigen Verarbeitung. Den Programmierern des Clients muß nur bekannt
sein, welche Daten sie anfordern, dies übergeben sie als Sql-String. Als Ergebnis erhalten sie eine Statusmeldung
sowie eine Tabelle, welche sie weiterverarbeiten können. Diese Konzeption läßt sich nochmals entscheidend dadurch
verbessern, daß als Befehle nur noch gültige Namen
gespeicherter Prozeduren
(
stored Procedures) zulässig sind, für welche der Client die Ausführungs- (Execute-) Berechtigung
besitzt. Damit benötigt die DBMS-Benutzerkennung, über welche sich der Client am Datenbankserver anmeldet,
keine der SELECT-, INSERT-, UPDATE- oder DELETE-Berechtigungen, mit welchen er ganze Tabellen bearbeiten könnte.
Ebenso muß diese Benutzerkennung nicht Mitglied einer speziellen Nutzergruppe mit besonderen Rechten sein. Schließlich
lassen sich hierdurch bei korrekter Initialisierung des Clientobjektes alle Probleme mit
Sql Injections vermeiden, mit welchen es ansonsten möglich wäre, den Sql-Code durch die
Eingabe zusätzlicher Zeichen in die normalen Datenfelder um eigene Logik zu ergänzen und hierdurch Daten zu manipulieren.
Sql als mengenorientierte Sprache im Gegensatz zum datensatzorientierten Zugriff
Ein für manche Programmierer gewöhnungsbedürftiges Charakteristikum von Sql besteht darin, daß Sql
mengenorientiert
(
set-orientated) und nicht
datensatz-orientiert (
row-level-orientated)
arbeitet. Aus den Daten in mehreren Tabellen wird durch JOIN-Verknüpfungen eine große Tabelle gebildet und diese
als Menge abgefragt bzw. bearbeitet. Durch die Festlegung von Spalten und Where-Klauseln - also Zeilen - kann diese Menge in der Breite
und in der Höhe eingeschränkt werden. Muß die bislang verwendete Auswahl anhand weitergehender Kriterien verkleinert werden, können
Unterabfragen erstellt und diese per JOIN mit der Ausgangstabelle verknüpft werden. Ein datensatzorientiertes
Vorgehen würde dagegen eine Tabelle Zeile um Zeile von der Wirtssprache her abrufen, dort auf das Vorliegen gewisser
Kriterien prüfen und - falls die Kriterien erfüllt sind - die Daten weiterverarbeiten.
Die eigentlichen Stärken von Sql liegen in der Bearbeitung einer großen Menge 'auf einmal', der Nutzung der speziell optimierten
Indizes sowie der Verwendung eines internen Optimierers zur Erstellung eines Ablaufplans und dessen Wiederverwendbarkeit für mehrere Abfragen
Diese Stärken kommen nur dann wirklich
zum Tragen, wenn datensatzorientierte Befehlsfolgen möglichst selten eingesetzt werden. Alle Probleme der Datenauswahl
und der Datenbearbeitung müßten sich ohne die Verwendung zeilenorientierter Operationen bewältigen lassen.
Leider finden sich bei Recherchen wiederholt Beispiele, in welchen Datensätze etwa in eine temporäre Tabelle oder
in einen Cursor geladen und anschließend Zeile um Zeile verarbeitet werden. Im extremen Fall wird in einer Schleife aus der
temporär angelegten Tabelle oder aus dem Cursor eine Datenzeile geholt, diese innerhalb der Wirtssprache auf Eigenschaften
geprüft - anstatt diese Kriterien in der WHERE-Bedingung zu formulieren - und weiterverarbeitet. Tatsächlich jedoch
dürfte nur in jenen Fällen ein datensatzorientiertes Vorgehen notwendig sein, in welchen aus den selektierten Informationen
- etwa Nutzernamen - im Rahmen einer Metaverarbeitung dynamisch Sql-Befehle generiert werden sollen und diese
Befehle nur als Einzelanweisungen ausgeführt werden dürfen. Dies gilt beim MS-SqlServer bsp. für Befehle wie
Create Procedure ... As ... oder
Create View ... As .... Bei diesen speziellen,
objekterzeugenden Befehlen muß dieser Create-Befehl am Anfang des Befehlsstapels stehen und darf keinen weiteren
Befehl enthalten. Selbst in diesem Fall ist zu prüfen, ob der Befehl nicht bereits in der Sql-Select-Anweisung
zusammengesetzt werden kann. Falls ja, genügt es, die Zeile abzurufen und den String sofort auszuführen. Die
Alternative, von der Wirtssprache her die Werte abzurufen, erst dort den Sql-Befehl zusammenzusetzen und ihn
anschließend auszuführen, ist aufwendiger. Lediglich die aufgrund der Vervielfachung der Texte vergrößerte Tabelle
und die hierdurch produzierte größere Speicherauslastung des Clients wäre in wenigen, sehr seltenen Fällen ein
Argument, das gegen den Einsatz dieser Technik spricht.
Sql als Sprache der vierten Generation - 4GL
Ein zentrales Merkmal von Sql ist, daß nicht gesagt wird,
wie etwas gemacht werden
soll, sondern daß nur mitgeteilt wird,
was zu tun sei. SQL wird deshalb als eine
Sprache der vierten Generation klassifiziert. Programmiersprachen können grob gemäß der
folgenden Liste sortiert werden:
Die Zuordnung von SQL als Programmiersprache der vierten Generation ergibt sich daraus, daß Sql-Befehle nur noch
beschreiben, was gemacht werden soll: 'Erzeuge oder bearbeite ein Objekt, manipuliere diese Daten oder gib jene
Daten zurück'. Eine Sprache der dritten Generation müßte ein eigenes Tabellenobjekt definieren und eigenständig
Verfahren entwickeln, um bsp. eine Zeile möglichst effizient zu suchen und zurückzugeben. Solche Algorithmen
könnten als Objekte mit Methoden gekapselt werden. Damit wäre der Datenzugriff jedoch an diese Programmiersprache
gebunden. Wenn man mag, kann man ein relationales DBMS mit einer Sql-Schnittstelle interpretieren als ein allgemeines
Objekt, welches mittels geeigneter Connection-Objekte von verschiedenen Programmiersprachen genutzt werden kann.
Dieses RDBMS kennt gewissermaßen nur die Methode 'Führe den folgenden Sql-Befehl aus', dessen Eigenschaft (= der auszuführende Sql-Befehl)
wird zuvor als Stringvariable übergeben. Die Methode liefert einen Statuswert über Erfolg oder Fehlschlag sowie
eventuelle Rückgabedaten aus.
TOP
Einige historische Anmerkungen
Die Entwicklung von SQL bewegt sich zwischen Forschungsarbeiten, Aktivitäten von Firmen sowie staatlichen und
internationalen Organisationen. Der Sprachkern ist seit etwa 1989 durchaus stabil. Standards und Produkte
einzelner Firmen gehen jedoch weit über diesen Kern hinaus, so daß einige sich auf den ersten Blick widersprechende
Kurzbezeichnungen zu finden sind. Die folgenden Ausführungen orientieren sich an den angegebenen Fundstellen.
- 1970: Edgar Frank Codd veröffentlicht die Arbeit
A Relational Model of Data for Large Shared Data Banks. Der Artikel war ursprünglich unter
http://www.acm.org/classics/nov95/toc.html zugänglich.
Dort wird das relationale Datenmodell mit seinen fünf Normalformen vorgestellt.
- 1974: Basierend auf der Arbeit von Codd entwickelten D. D. Chamberlin und R. F. Boyce bei IBM die Sprache SEQUEL (Structured English Query Language)
für die VS/2 mainframes und veröffentlichten die Ergebnisse auf der 19 ACM SIGMOD Conference on the Management of Data
(zitiert nach T Spaces: The Next Wave).
1976 folgte eine Ergänzung zur Definition von Daten als Veröffentlichung im IBM Journal of Research and Development.
- 1979: Auslieferung des ersten kommerziellen relationalen DBMS durch die 1977 gegründete Relational Software.
Das Produkt hieß Oracle, die Firma wurde später in Oracle Corporation umbenannt.
- 1980 bietet IBM das SQL/DS-Produkt an, welches in die System/38 - Server integriert war.
Dasselbe Produkt wurde 1982 auf die Mainframe-Systeme VM und VSE übertragen. 1983 folgte das bis heute weiterentwickelte
Produkt DB2 (DATABASE 2), welches ab 1993 auch für AIX,
1994 für HP-UX und Solaris, 1995 für Windows und seit 1999 für Linux verfügbar ist (zur IBM-Historie siehe
The Big Picture: IBM DB2 Information Management Software and DB2 Universal Database).
- 1986/87: Ende des Jahres 1986 veröffentlichte das American National Standard Institute
(ANSI) die Erst-Edition eines formalen SQL-Standards als ANSI X3.135-1986, wenige Monate
später wurde ein identisches Dokument von der International Organization for Standardization (ISO)
als ISO 9075-1987 herausgegeben. Dieser Standard wird als SQL-86
oder SQL-87 zitiert.
- 1989: Die Erstedition wurde durch ein Dokument mit dem Titel Database Language SQL With Integrity Enhancements
ersetzt, das elementare referentielle Integrität hinzufügte, als ANSI X3.135-1989 bzw.
ISO 9075:1989 klassifiziert und informell als SQL-89 bezeichnet wurde. In
diesen Dokumenten wurde die Frage, wie SQL in eine Wirtssprache (host programming language)
einzubetten sei, jedoch nur in einem nicht normativen Anhang abgehandelt. Es gab deshalb Befürchtungen, daß
Firmen ein SQL entwickeln könnten, welches diese Einbettung in eine Wirtssprache nicht unterstützen würden,
da es der Standard nicht von ihnen verlangen würde. Das National Institute of Standards and Technology (NIST)
entwickelte deshalb einen zweiten Standard, der als ANSI X3.168-1989 bzw. Information Technology - Database Language Embedded SQL
diese Ergänzungen normativ forderte. Alle bis zu diesem Zeitpunkt festgelegten Standards waren minimal insofern,
als sie nur das von den Entwicklern verlangten, was ohnehin von den gängigen Produkten bereits erfüllt wurde
(Quelle: The Role of NIST in SQL Standardization).
- 1992: Die bisherigen Regelungen waren in vielen Bereichen ungenügend. Zum Beispiel konnten mit CREATE Tabellen
erstellt werden, es fehlten jedoch Spezifikationen für das Ändern / ALTER und Löschen / DROP von Tabellen. Zwar
hatte jeder Hersteller hierfür Lösungen, diese waren jedoch individuell. Hinzu kamen diverse weitere Ergänzungen,
die bisherigen Standards waren nur minimal. 1992 wurde deshalb SQL-92
als ANSI X3.135-1992 bzw. ISO/IEC 9075:1992 veröffentlicht.
Dieser Standard besteht aus drei Teilen:
- ENTRY SQL: Dieser Teil entspricht im wesentlichen dem SQL-89 - Standard
und wird von den gängigen RDBMS unterstützt.
- INTERMEDIATE SQL: Dies umfaßt weitergehende Befehle zur Manipulation
des Datenbank-Schemas (ALTER, DROP), zu dynamisch erstellten SQL-Befehlen, dem Diagnostik-Management,
kaskadierendem Löschen, DATE- und TIME-Datentypen, Domänen, Zeichenketten variabler Länge, der Unterstützung
nationaler Zeichenmengen / Unicode. Die Data-Manipulation-Erweiterungen umfassen Verzweigungen / CASE,
CAST zur Umwandlung von Datentypen, String-Operatoren, die JOIN-Typen NATURAL, OUTER und UNION JOIN
sowie Unterabfragen in Ausdrücken und Tabellenoperationen für UNION, INTERSECTION und COMPLEMENT.
- FULL SQL: Die maximale Version fordert ein Management für die
Verbindung zwischen Client und DBMS, eine Session-Verwaltung, vordefinierte Zeichentransformationen,
einen BIT-Datentyp, verschiebbare Einschränkungen. Letztere erlauben zeitweilige foreign-key-Verletzungen
innerhalb einer Transaktion, um bsp. den Primärschlüssel einer Zeile der Haupttabelle zu ändern, der
in einer Detailtabelle verwendet wird. Ferner werden abgeleitete Tabellen im FROM-Abschnitt,
Unterabfragen in CHECK-Klauseln, insensitive Cursor, selbstreferentielle Operationen und temporäre
Tabellen verlangt (nach Database Language SQL Appendix A - Facilities Provided by Standards Comprising TBITS SQL,
Quelle: http://www.tbs-sct.gc.ca/its-nit/standards/tbits02/spec02a_e.asp - der Link ist nicht mehr gültig).
INTERMEDIATE und FULL werden bislang nicht vollständig implementiert.
- SQL-3 oder SQL-99: Noch bevor SQL-92 veröffentlicht
war, begann bereits die Arbeit am Nachfolger. Dieser soll das bisherige relationale Modell um objektorientierte
Komponenten ergänzen und wurde in diverse Einzelteile zerlegt. Die Arbeit an einzelnen Abschnitten ist inzwischen
wieder eingestellt worden. ISO führt derzeit (02/2004) unter dem Stichwort 'ISO/IEC 9075' die folgenden Publikationen
auf:
- Teil 1 - Framework
- Teil 2 - Grundlagen
- Teil 3 - Call Level Interface (CLI)
- Teil 4 - Persistent Stored Modules (SQL/PSM)
- Teil 5 - Host Language Bindings (SQL/Bindings)
- Teil 9 - Management of External Data (SQL/MED)
- Teil 10 - Object Language Bindings (SQL/OLB)
- Teil 11 - Information and Definition Schemas (SQL/Schemata)
- Teil 13 - SQL Routines and Types Using the Java TM Programming Language (SQL/JRT)
- Part 14 - XML-Related Specifications (SQL/XML)
Hinzu kommt eine Veröffentlichung 'On-Line Analytical Processing (SQL/OLAP)'. Die fehlenden Nummern beziehen
sich auf Teile, die noch nicht fertiggestellt sind oder an welchen die Arbeit eingestellt wurde.
- Einerseits wird mit diesen Spezifikationen der Einsatzbereich von SQL drastisch vergrößert. Andererseits
wird es damit aufwendiger, ein tatsächliches DBMS zu entwickeln, welches einige der Spezifikationen erfüllt.
Der Vorteil einer solchen Modularisierung besteht darin, daß der Standard in Teilbereichen schon berücksichtigt
werden kann, obwohl der gesamte Standard unvollständig ist.
Inzwischen existieren einzelne Produkte, bei welchen (1) das DBMS http-Schnittstellen unterstützt sowie (2)
als Eingabe einen Sql-Befehl mit angehängter 'FOR XML'-Klausel akzeptiert. Die Daten werden nicht mehr in einem
binären Format, sondern direkt als Xml-Dokument zurückgegeben. Damit kann einerseits die Leistungsfähigkeit
heutiger Datenbanksysteme erhalten und weiterentwickelt werden. Andererseits lassen sich Daten nicht mehr nur noch
über Rechner-, sondern nun auch über Betriebssystemgrenzen hinweg in einer hochstrukturierten Form abfragen
und austauschen.
TOP
Zeilen einfügen mit INSERT
Syntax
INSERT INTO <Tabellenname>(<Spaltenname> [, weitere Spaltennamen])
VALUES (<Wert für die erste Spalte> [, weitere Werte])
Einfache Form: Hinter VALUES werden alle einzufügenden Werte der Reihe nach aufgelistet. Es wird genau eine neue
Zeile erzeugt.
INSERT INTO <Tabellenname>(<Spaltenname> [, weitere Spaltennamen])
SELECT <Spalte-1> [, weitere Ausgaben]
[FROM ...]
[WHERE ...]
[GROUP BY ...]
[HAVING ...]
Einfügen mehrerer Zeilen. Als Select-Anweisung ist jede beliebige Anweisung möglich, sofern
die Zahl der Ausgabespalten mit der Zahl der Zielspalten sowie die Datentypen übereinstimmen. Das Select-Recordset
kann auch keine Zeilen zurückliefern, dies ist kein Fehler, es werden keine Zeilen hinzugefügt.
Beispiele
INSERT INTO ARTIKEL(A_NR, A_NAME, A_PREIS)
VALUES(12, 'Oberhemd', 39.80)
Dies fügt in die Tabelle Artikel die erste Zeile aus der Artikel.txt ein. Dies gelingt
allerdings nur deshalb, da die Spalte A_NR keine automatisch erzeugte fortlaufende Nummer enthält und deshalb der Wert
für den Primärschlüssel per Hand festgelegt werden muß.
- Für den Fall, daß die Tabelle einen automatisch erzeugten Wert enthält, muß weder die Spalte noch ein Wert
angegeben werden. Beispiel:
CREATE TABLE [Artikel-mit-Id]
(A_NR int Identity(1, 1) Primary Key,
A_Name varchar(50),
A_Preis money)
Dies erzeugt eine Tabelle, bei welcher
der ersten Spalte ein automatisch erstellter Wert zugewiesen wird
INSERT INTO [Artikel-mit-Id](A_NAME, A_PREIS)
VALUES ('Oberhemd', 39.80)In die neue Tabelle wird eine Zeile eingetragen. Ist die Tabelle soeben neu erstellt worden,
so erhält die neue Zeile den Wert 1 als Primärschlüssel.
INSERT INTO ARTIKEL(A_NR, A_NAME, A_PREIS)
SELECT 12, 'Oberhemd', 39.80
UNION
SELECT 22, 'Mantel', 360.00
UNION
SELECT 11, 'Oberhemd', 44.20
UNION
SELECT 13, 'Hose', 110.50
Dies fügt Daten ein, die über eine Dummy-Select-Anweisung mit UNION zusammengestellt wurden.
Beachten Sie, daß eine solche Konstruktion mit Ms-Access nicht gelingt. Dort kann man mit dem Einfügen einer Dummy-Tabelle
sowie der TOP 1 - Klausel erreichen, daß eine Zeile zurückgegeben wird. Die UNION-Anweisung läßt sich jedoch nicht direkt
in eine Tabelle einfügen, sie muß in einer Unterabfrage versteckt werden. Die folgende Anweisung ist möglich:
CREATE TABLE ATest(A_NR int, A_NAME varchar(50), A_PREIS money)
INSERT INTO ATest(A_Nr, A_NAME,A_PREIS)
SELECT A.A_Nr, A.A_Name, A.A_Preis
From (
SELECT Top 1 12 As A_NR, 'Oberhemd' AS A_NAME,
39.80 AS A_PREIS From Artikel
UNION
SELECT Top 1 22, 'Mantel', 360.00 From Artikel
UNION
SELECT Top 1 11, 'Oberhemd', 44.20 From Artikel
UNION
SELECT Top 1 13, 'Hose', 110.50 From Artikel
Order By A_Nr)
As A
INSERT INTO ARTIKEL(A_NR, A_NAME, A_PREIS)
SELECT A.A_NR + 100,
A.A_NAME,
A.A_PREIS * 1.1
FROM ARTIKEL As A
Dies fügt in die Tabelle Artikel vier neue Zeilen ein, die Artikel-Nummer ist um 100, der Artikelpreis um 10% erhöht.
SELECT A.A_NR, A.A_NAME,
A.A_PREIS As Gesamt
INTO ArtikelAuswertung
FROM ARTIKEL As A
WHERE 0 = 1
INSERT INTO ArtikelAuswertung
(A_NR, A_NAME, Gesamt)
SELECT A.A_NR, A.A_NAME,
SUM(A.A_PREIS * U.A_STUECK)
FROM ARTIKEL As A INNER JOIN UMSATZ As U
On A.A_NR = U.A_NR
GROUP BY A.A_NR, A.A_NAME
Dies ist eine
typische Anwendung der INSERT-Anweisung, um Daten unabhängig von der Hauptdatenbank auswerten oder weiterverarbeiten zu können.
Zunächst wird die gewünschte Zieltabelle erstellt, indem eine passende SELECT-Anweisung mit INTO in eine neue Tabelle umgelenkt
und in diese mit '0 = 1' keine Daten übertragen werden. Anschließend werden die tatsächlich gewünschten Nutzdaten aggregiert
und kopiert. Eine solche Trennung anstelle eines direkten SELECT ... INTO ... FROM kann hilfreich sein, falls die Tabelle auf
jeden Fall erstellt werden soll, es jedoch bsp. zu Laufzeitfehlern bei der SELECT-Abfrage kommen kann, so daß
diese nicht ausgeführt wird.
- Verwenden einer gespeicherten Prozedur mit Parametern zum Einfügen von Daten:
CREATE PROCEDURE up_ins_Artikel
@A_NR int,
@A_NAME nvarchar(50),
@A_PREIS money
As
INSERT INTO ARTIKEL(A_NR, A_NAME, A_PREIS)
VALUES (@A_NR, @A_NAME, @A_PREIS)
Execute up_ins_Artikel 50, 'Schlapphut', 49.90
Dies ist die Standard-Syntax zur Erzeugung gespeicherter Prozeduren mit
Parametern. Die Prozedur nutzt die Parameterwerte, um eine Zeile einzufügen. Ms-Access hat seine eigene Syntax. Hier dürfen -
bei der Verwendung über .NET - die Parameter nicht deklariert werden und sind nach Position anzusprechen. Mit ADO ist eine
Syntax möglich, wie sie innerhalb DAO verwendet wird:
PARAMETERS [@a_nr] int, [@a_name] nvarchar(50), [@a_preis] money;
CREATE ...
Bemerkung zu allen Data Manipulation Language - Befehlen (DML)
- Falls Sie bislang noch wenig Erfahrung mit Datenbank-Befehlen haben: Beachten Sie, daß es auf dieser Ebene des Zugriffs
keinen 'Rückgängig-Button' mehr gibt, wie Sie ihn vielleicht von Word oder Excel gewohnt sind. Eine Datenbank stellt einen elementaren
Datenspeicher dar, INSERT/UPDATE/DELETE verändert diesen Speicher direkt. Und was Sie eingefügt, geändert oder gelöscht haben,
ist unwiderruflich eingefügt, geändert und gelöscht.
Falls es in Programmen wie Word einen Rückgängig-Button gibt, so setzt dies in irgendeiner Form eine Datenbankstruktur voraus, welche
sich die zuletzt durchgeführten Aktionen merkt und diese zurücksetzt. Benötigen Sie auf der Ebene einer Datenbank eine Historie,
so muß diese explizit in Form eines Tabellenschemas erstellt und zu einer Zeile nicht nur die aktuellen Werte, sondern zusätzlich
das Änderungsdatum erfaßt werden. Alternativ kann man eine zusätzliche Tabelle erstellen und immer die drei letzten Versionen in
dieser ablegen.
TOP
Zeilen bearbeiten mit UPDATE
Vorbemerkung
Der Update-Befehl nimmt unter den Sql-Anweisungen insofern eine Sonderposition ein, da sich die Syntax bei Verwenden einer
JOIN-Klausel zwischen Ms-Access und Ms-SqlServer unterscheidet. Damit die Beispiele mit dem Sql-interaktiv-lernen getestet
werden können, werden in diesen Fällen beide Versionen angegeben.
Syntax
- Update einer Tabelle ohne Verknüpfung mit anderen Tabellen
UPDATE <Tabelle>
SET <Name einer Spalte> = <Ausdruck aus Spalten,
Konstanten, Funktionen>
[, weitere Spaltennamen = Ausdruck]
WHERE <Bedingung>
- Update einer Tabelle mit JOIN (Ms-Access)
UPDATE <Tabelle> [INNER | LEFT | RIGHT] JOIN <Tabelle>
ON <Spalte-1 = Spalte-2>
SET <Name einer Spalte> = <Ausdruck aus Spalten,
Konstanten, Funktionen>
[, weitere Spaltennamen = Ausdruck]
WHERE <Bedingung>
- Update einer Tabelle mit JOIN (ANSI, Ms-SqlServer)
UPDATE <Tabelle | View>
SET <Name einer Spalte> = <Ausdruck aus Spalten,
Konstanten, Funktionen>
[, weitere Spaltennamen = Ausdruck]
[FROM <Tabelle> [INNER | LEFT | RIGHT] JOIN <Tabelle>
ON <Spalte-1 = Spalte-2>]
WHERE <Bedingung>
- Zunächst wird die zu aktualisierende Tabelle angegeben. Nach SET folgt die Liste der zu aktualisierenden
Zellen, für die rechts vom Gleichheitszeichen der neue Wert angegeben wird. Im ANSI-Standard kann anschließend
eine JOIN-Verknüpfung folgen, durch welche die von der Aktualisierung betroffenen Zeilen genauer eingeschränkt werden.
Schließlich werden mit einer WHERE-Klausel einzelne Zeilen ausgewählt.
Beispiele
Fügen Sie zu den beiden Tabellen ARTIKEL und UMSATZ mit dem folgenden Befehl eine neue Spalte 'Gesamt' hinzu:
ALTER TABLE ARTIKEL ADD Gesamt money
ALTER TABLE UMSATZ ADD Gesamt money
Diese Spalten können zu Testzwecken beliebig manipuliert werden, ohne daß sich die Grunddaten deshalb ändern.
UPDATE ARTIKEL
SET Gesamt = 0
Einfachste Version einer Update-Anweisung: Eine Spalte wird mit einem neuen Wert, hier mit der
Konstanten 0 belegt, die Aktualisierung wird für alle Zeilen ausgeführt.
UPDATE ARTIKEL
SET Gesamt = A_PREIS
Jeder Zelle 'Gesamt' ist die Zelle 'A_PREIS' derselben Zeile zugeordnet. Also wird der Wert
der letzteren in die Zelle 'Gesamt' kopiert.
UPDATE ARTIKEL
SET Gesamt = A_PREIS * 1.19
Dasselbe wie zuvor, nun ergänzt um die Multiplikation mit einer Konstanten. Eine solche
Spalte mag bsp. den Bruttopreis eines Artikels enthalten.
UPDATE ARTIKEL
SET Gesamt = A_PREIS * 1.19
WHERE A_PREIS > 100
Nun wird die Menge der zu aktualisierenden Zeilen eingeschränkt, auf daß nicht alle, sondern nur
jene Zeilen aktualisiert werden, deren Preis vor der Aktualisierung größer 100 ist.
- Erstellung der gespeicherten Prozedur:
CREATE PROCEDURE up_upd_Artikel
@A_Nr int,
@A_Name nvarchar(50),
@A_Preis money
As
UPDATE ARTIKEL
SET A_NAME = @A_Name,
A_PREIS = @A_Preis
WHERE A_Nr = @A_Nr
Ausführung:Execute up_upd_Artikel 11, 'Oberhemd (Doppelpackung)', 59.90
Dies ist ein kanonisches Beispiel für eine gespeicherte Prozedur, welche den Datensatz mit der übergebenen @A_Nr aktualisiert.
Für die Ms-Access-Version über .NET sind die Parameter zu entfernen.
- Ms-Access:
UPDATE ARTIKEL As A INNER JOIN UMSATZ AS U
On A.A_NR = U.A_NR
SET U.Gesamt = A.A_PREIS * U.A_STUECK
Ms-SqlServer:
UPDATE UMSATZ
SET Gesamt = U.A_STUECK * A.A_PREIS
FROM UMSATZ AS U INNER JOIN ARTIKEL As A
On U.A_NR = A.A_NR
Diese Anweisung
berechnet in der Tabelle 'Umsatz' den Wert jeder einzelnen Zeile, indem der zum Artikel gehörende Preis mit 'A_Stueck'
multipliziert wird. Da es zu jeder Umsatz-Zeile genau eine Zeile in der Tabelle 'Artikel' gibt, ist diese Anweisung eindeutig.
- Aktualisieren der Spalte ARTIKEL.Gesamt mit dem Produkt aus ARTIKEL.A_PREIS und der Summe aller UMSATZ.A_STUECK für
diesen Artikel.
Zur Lösung dieser Aufgabe scheint es zunächst zu genügen, den Aktualisierungsausdruck
SET A.Gesamt = A.A_PREIS * SUM(U.A_STUECK)
zu verwenden, für Ms-SqlServer würde man das Präfix bei
'A.Gesamt' entfernen. Eine solche Abfrage läßt sich mit Ms-Access sogar speichern, jedoch nicht ausführen. Es wird die
von Gruppierungsabfragen bekannte Fehlermeldung "Sie wollten eine Abfrage ausführen, die den angegebenen Ausdruck 'Gesamt'
nicht als Teil der Aggregatfunktion einschließt" ausgibt (vgl. Mit GROUP BY Daten aggregieren und auswerten - Bemerkungen).
Jeder Versuch, die Update-Anweisung um eine Group-By-Klausel zu ergänzen, scheitert jedoch ebenfalls. Beim Ms-SqlServer
ist die Fehlermeldung genauer: Ein Aggregat kann nicht in der SET-Liste einer UPDATE-Anweisung auftreten.
Diese Aufgabe kann deshalb nur mit einer eigenständigen Unterabfrage gelöst werden, welche für jeden Artikel bereits die Summation
über die diesem Artikel zugeordneten A_Stueck enthält. Tatsächlich gelingt dies nur beim Ms-SqlServer. Bei Ms-Access wird nur eine
einzige virtuelle Tabelle als das zu aktualisierende Recordset betrachtet. Die Hilfe für Office XP (Access, Abschnitt Microsoft
JET-SQL-Referenz) erwähnt die Möglichkeit eines JOIN nicht einmal. Diese virtuelle Tabelle muß vollständig aktualisierbar sein, eine
Abfrage, welche Aggregatfunktionen verwendet und gruppiert, ist jedoch schreibgeschützt. Deshalb kann nur eine Lösung für Ms-SqlServer
angegeben werden:
UPDATE ARTIKEL
SET Gesamt = A.A_PREIS * U1.A_Stueck_Gesamt
FROM ARTIKEL As A INNER JOIN
(SELECT B.A_NR, SUM(B.A_STUECK
FROM UMSATZ As B
GROUP BY B.A_NR) As U1
On A.A_NR = B.A_NR
Für Ms-Access müßte die Unterabfrage als eigenständige SELECT-Anweisung in eine temporäre Tabelle kopiert, über diese
die Verknüpfungs-Aktualisierung durchgeführt und die temporäre Tabelle am Schluß gelöscht werden. Dies gilt analog
für DBMS, welche Unterabfragen nicht unterstützen.
- Zerlegung von Vertreter.V_Name in Vorname und Nachname:
Offenkundig ist die Tabelle VERTRETER nicht
normalisiert. So sind Nachname und Vorname in einer Spalte zusammengefaßt, dies verletzt die Forderung nach atomaren
Attributen. Als Folge kann bsp. eine Suche nach einem Nachnamen nicht einfach diesen angeben und mit dem Gleichheitsoperator (=)
überprüfen, sondern es muß eine Anweisung der Form
A_NAME LIKE 'Meyer,%'
verwendet werden. Mittels der Update-Anweisung und unter Verwendung von Funktionen
können solche Spalten in ihre Bestandteile zerlegt werden.
ALTER TABLE VERTRETER ADD Nachname nvarchar(50)
ALTER TABLE VERTRETER ADD Vorname nvarchar(50)
Dies fügt zwei neue Spalten 'Nachname' und 'Vorname' ein.
Ms-Access:
UPDATE VERTRETER
SET Nachname = Left(V_NAME, InStr(VERTRETER, ',') - 1),
Vorname = Trim(Mid(VERTRETER,
InStr(VERTRETER, ',') + 1))
Ms-SqlServer:
UPDATE VERTRETER
SET Nachname = Left(V_NAME, CharIndex(',', V_NAME) - 1),
Vorname = LTrim(SubString(V_NAME,
CharIndex(',', V_NAME) + 1))
- Berechnung des höchsten Einzelumsatzes pro Artikel, eingetragen in der Tabelle UMSATZ (Ms-SqlServer):
UPDATE UMSATZ SET Gesamt = Null
UPDATE UMSATZ
SET Gesamt = A.A_PREIS * U.A_STUECK
FROM ARTIKEL As A INNER JOIN UMSATZ As U
On A.A_NR = U.A_NR
WHERE A.A_PREIS * U.A_STUECK =
(SELECT MAX(A.A_PREIS * U.A_STUECK
FROM ARTIKEL As A INNER JOIN UMSATZ As V
On B.A_NR = V.A_NR
WHERE B.A_NR = A.A_NR)
Diese Anweisung setzt zunächst alle Einträge in UMSATZ.Gesamt zurück und trägt nur in jene Zeilen den Gesamtumsatz ein,
die für diesen Artikel den höchsten Einzelumsatz darstellen. Hier wird sowohl ein JOIN als auch eine korrelierte Unterabfrage
genutzt, die sich allerdings in einen JOIN mit Unterabfrage auflösen läßt:
UPDATE UMSATZ
SET Gesamt = A.A_PREIS * U.A_STUECK
FROM ARTIKEL As A INNER JOIN UMSATZ As U
On A.A_NR = U.A_NR INNER JOIN
(SELECT B.A_NR, MAX(B.A_PREIS * U1.A_STUECK) As Maximum
FROM ARTIKEL As B INNER JOIN UMSATZ As U1
On B.A_NR = U1.A_NR
GROUP BY B.A_NR) As C
On U.A_NR = C.A_NR AND
A.A_PREIS * U.A_STUECK= C.Maximum
Diese Anweisung ist ein Beispiel dafür, wie eine JOIN-Klausel dafür genutzt werden kann, wenige Zeilen aus der zu aktualisierenden
Tabelle auszuwählen. Aktualisiert wird die Tabelle UMSATZ, das Hinzunehmen von ARTIKEL reduziert die Zahl der Zeilen nicht.
Erst die zusätzliche JOIN-Klausel, die pro Artikel nur noch eine Zeile enthält, vermindert die Zahl der vom UPDATE betroffenen
Zeilen auf vier.
Bemerkungen
- Deterministische und nichtdeterministische Anweisungen: Bei UPDATE-Befehlen
mit JOIN-Klausel muß sichergestellt sein, daß der JOIN zu jeder zu aktualisierenden Zeile nur einen Ausdruck liefert.
Ist dies der Fall, so spricht man von deterministischen Anweisungen. Ansonsten handelt es sich um eine
nichtdeterministische Anweisung, deren Ergebnis nicht eindeutig definiert ist. Denn in diesem Fall ist nicht vorhersehbar,
welcher Ausdruck tatsächlich in die Zelle eingetragen wird. Dieses Problem taucht hauptsächlich dann auf, wenn eine Spalte in
der Grundtabelle aktualisiert werden soll und im JOIN eine Detailtabelle herangezogen wird, so daß in der Detailtabelle kein,
ein oder mehrere Datensätze für die Grundzeile zur Verfügung stehen.
Ms-Access:
UPDATE ARTIKEL As A INNER JOIN UMSATZ AS U
On A.A_NR = U.A_NR
SET A.Gesamt = A.A_PREIS * U.A_STUECK
Ms-SqlServer:
UPDATE ARTIKEL
SET Gesamt = U.A_STUECK * A.A_PREIS
FROM UMSATZ AS U INNER JOIN ARTIKEL As A
On U.A_NR = A.A_NR
Dies ist fast dasselbe Beispiel wie oben (6), nur wird nun die Spalte ARTIKEL.Gesamt aktualisiert. Da zu einem Artikel mehrere
Einzelumsätze existieren, ist nicht bekannt, welcher Einzelumsatz zur Berechnung ausgewählt wird.
- Anmerkungen zur Syntax: Bei Ms-Access kann nur ein Ausdruck aktualisiert werden, der eine gänzlich aktualisierbare
Tabelle darstellt. Diese kann real oder virtuell, als JOIN deklariert sein, so daß praktisch mehrere Tabellen neue Einträge
erhalten. So funktioniert die folgende Anweisung in Ms-Access:
UPDATE ARTIKEL As A INNER JOIN UMSATZ As U
On A.A_NR = U.A_NR
SET A.Gesamt = Null,
U.Gesamt = A.A_PREIS * U.A_STUECK
Gemäß dem ANSI-Standard ist jedoch nur die Aktualisierung eines einzigen Objektes möglich. Bei diesem kann es sich allerdings um
einen mehrere Tabellen umfassenden View handeln, als Objektbezeichner darf auf die UPDATE-Anweisung jedoch nur ein Ausdruck folgen.
Für die in Ms-Access funktionierende obige Anweisung, die eine temporäre Sicht erstellt, wäre in Ms-SqlServer zunächst ein
eigenständiger View zu erstellen, dieser könnte genutzt werden.
Eine ernstzunehmende Einschränkung bei Ms-Access ist die Unmöglichkeit, zwischen SET und WHERE einen JOIN festzulegen, mit welchem
entweder gewisse Zeilen zur Aktualisierung ausgewählt werden oder mit welchem zusätzliche Spalten zur Berechnung des neuen
Wertes herangezogen werden können.
Bei Ms-SqlServer ist zu beachten, daß das Objekt, welches upgedated werden soll, ohne Alias-Namen verwendet werden muß. Ferner
darf im folgenden JOIN-Abschnitt dieses Objekt höchstens einmal aufgeführt werden, hier sind Alias-Namen erlaubt und wünschenswert.
- Die Technik, Zellen mittels Funktionen in atomare Werte zu zerlegen, wird in vielfältiger Weise benötigt, falls
schwach strukturierte Daten in eine Datenbank eingelesen werden sollen. Im Regelfall ist es das beste, die Daten zunächst in eine
einzige Textspalte einzulesen und diese Spalte anschließend mit Datenbank-Techniken schrittweise zu zerlegen. Ein zeilenweises
Zerlegen vor dem Einlesen in die Datenbank dürfte in den meisten Fällen unter Performance-Gesichtspunkten schlechter sein,
da die Leistung eines DBMS gerade darin besteht, eine Menge vieler Zeilen auf einmal zu verarbeiten. Eventuelle Ein- bzw.
Ausschließungskriterien (etwa ein fehlendes Komma zwischen Nachname und Vorname) können über die WHERE-Bedingung ausgeschlossen
werden. Die Funktionen sind leider in den verschiedenen DBMS höchst unterschiedlich implementiert, so daß für ein konkretes Problem die jeweilige
Dokumentation zu Rate gezogen werden muß.
TOP
Zeilen löschen mit DELETE
Syntax
Beispiele
Vorbemerkung: Wenn Sie die folgenden Beispiele mit dem Sql-interaktiv-lernen testen wollen, dann erstellen Sie sich am besten mit
Select * Into <neue-Tabelle> From Artikel einige neue Tabellen, welche Sie zum Löschen
verwenden. Dies erspart Ihnen das ständige Neueingeben der Daten bzw. das Zurückkopieren der gesicherten Access-Datenbank.
DELETE FROM ARTIKEL
Dies löscht alle Zeilen der Tabelle Artikel.
DELETE FROM ARTIKEL
WHERE A_NR = 11
Dies löscht nur die Zeile mit der Artikel-Nummer 11.
DELETE FROM UMSATZ
FROM UMSATZ As U INNER JOIN ARTIKEL As A
On U.A_NR = A.A_NR
WHERE A_NR = 11
Dies löscht in der Tabelle UMSATZ alle Zeilen, welche sich auf den Artikel 11 beziehen.
DELETE FROM UMSATZ
FROM ARTIKELAs A INNER JOIN UMSATZ As U
ON A.A_NR = U.A_NR INNER JOIN
(SELECT B.A_NR,
MAX(B.A_PREIS * C.A_STUECK) As Maximum
FROM Artikel As B Inner Join Umsatz As C
On B.A_Nr = C.A_Nr
Group By B.A_NR) As D
On U.A_NR = D.A_NR And A.A_PREIS * U.A_STUECK < D.Maximum
Diese nur noch auf ANSI-kompatiblen DBMS funktionierende Beispiel bestimmt zunächst über die Unterabfrage zu jeder Artikelnummer
den höchsten Einzelumsatz. Die JOIN-Anweisung verknüpft diese aus zwei Spalten und vier Zeilen bestehende Tabelle mit den beiden
anderen Tabellen, so daß zunächst die Artikelnummern einander zugeordnet sind und zusätzlich alle Zeilen ausgewählt werden,
deren Produkt aus A_Preis und A_Stueck kleiner dem Maximum für diesen Artikel ist. Diese Zeilen werden gelöscht.
- Für das folgende Beispiel fügen Sie zunächst in die Tabelle ARTIKEL einen neuen Artikel ein. Da eine JOIN-Klausel
genutzt wird, gelingt dies nicht mit Ms-Access bzw. dem Sql-interaktiv. Dort müssen Sie sich mit der korrespondierenden SELECT-Klausel
begnügen.
INSERT INTO ARTIKEL (A_NR, A_NAME, A_PREIS)
VALUES(55, 'Jeans', 99.90)
Mit diesem Artikel ist kein Umsatz gemacht worden, er soll deshalb gelöscht werden.
DELETE FROM ARTIKEL
FROM ARTIKEL As A LEFT JOIN UMSATZ As U
On A.A_NR = U.A_NR
WHERE U.UMSATZ_NR IS NULL
Bemerkungen
- Ersetzen Sie bei komplexen Anweisungen mit eigenständigem FROM-Abschnitt zunächst die erste Zeile
'DELETE FROM <Tabelle>' durch 'SELECT *'. Damit werden Ihnen alle Zeilen ausgegeben, die als Kandidaten zum
Löschen ausgewählt werden.
- Wird eine interaktive Umgebung genutzt, die Transaktionen unterstützt, dann läßt sich das Testen von DELETE-Befehlen
vereinfachen. Vor dessen Ausführung wird eine Transaktion gestartet, dann der Löschbefehl ausgeführt und das Ergebnis
anschließend mit einer SELECT-Abfrage überprüft. Am Ende wird die Transaktion per RollBack wieder zurückgesetzt, so daß
keine Änderungen an den Tabellen gespeichert werden.
Wenn Sie mit dem OSql.exe interaktiv eine DosBox geöffnet haben, so können Sie schrittweise die folgenden Zeilen aus dem
linken Fenster eingeben und jeweils anschließend mit Return bestätigen.
Begin Transaction
go
Select * From Umsatz
go
Delete From Umsatz Where A_Nr < 13
go
Select * From Umsatz
go
RollBack Transaction
go
Select * From Umsatz
| Transaktion starten
Ausgabe von 9 Zeilen
Löschen aller Zeilen mit A_Nr < 13
Ausgabe 4 Zeilen
Transaktion zurücksetzen
Ausgabe von wieder 9 Zeilen
|
© 2003-2008 Jürgen Auer, Berlin.
, Original:
http://www.sql-und-xml.de/sql-tutorial/Copyright der Druckversion: Sie dürfen diese Version für den privaten Gebrauch und / oder zu Lehrzwecken nutzen, ausdrucken
und in unveränderter Form kostenlos weitergeben. Sie können diese Version online stellen, sofern auf der hinführenden
Seite auf das Original verwiesen wird. Die Meta-Angabe robots='noindex' verhindert eine Indizierung
durch Suchmaschinen.