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 Daten

Server-Daten: Die Web - Datenbank als CRM - Lösung ist:

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)

Grundlagen für relationale Datenbank-Systeme und Entwurf der Speicherstruktur

Eintragen, Ändern und Löschen von Daten (Data Manipulation Language - DML-II)

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>
Kontaktformular, Tell a friend / Diese Seite weiterempfehlen, Druckversion - das Tutorial in einer Datei

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

Beispiele

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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


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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. WHERE 39.8 <= A.A_PREIS And A.A_PREIS <= 100
    Diese Version ist gleichwertig zur vorherigen Version.
  8. 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.
  9. 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.
  10. 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.
  11. 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]'
  12. 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


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

Beispiele

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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)
    
    DatumA_NR
    24.06.199912
    24.06.199913

    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

  1. 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.
  2. 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.
  3. 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.TitelId
    CoalEsce 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

Syntax

Beispiele

  1. 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.
  2. 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.
  3. 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_NRA_NAMEA_PREIS
    22Mantel360.00
  4. 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.
  5. 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_NRUmsatz-dieses-Artikels
    114.862,00
    121.990,00
    134.420,00
    2216.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.
  6. 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:
    DATUMMax-Tages-Einzelumsatz
    24.06.19993.867,50
    25.06.199912.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:
    DATUMA_NREinzelumsatz
    24.06.1999133.867,50
    25.06.19992212.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.
  7. 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:
    DATUMA_NRTagesumsatz
    24.06.1999113.094,00
    24.06.1999121.990,00
    24.06.1999134.420,00
    24.06.1999223.600,00
    25.06.1999111.768,00
    25.06.19992212.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.
  8. 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:
    DATUMA_NRUmsatz-Anzahl
    24.06.1999122
    24.06.1999132
    25.06.1999112

    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.
  9. 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

  1. 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.
  2. MIN(<Spaltenname>) / MAX(<Spaltenname>): Ermittelt das Minimum / Maximum der angegebenen Spalte.
  3. SUM(<Spaltenname>): Summation über alle Zellwerte
  4. 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


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_NRDATUMV_NR
2224.06.19995016
1124.06.19998413
2225.06.19995016
1324.06.19998413

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

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. 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

Drei Typen von Datenbank-Systemen

Bei Datenbank-Management-Systemen werden drei verschiedene Typen unterschieden:

TOP

Daten verwalten - die denormalisierte Tabelle als Ausgangspunkt für die Definition der Relationen / des Tabellenschemas

Merkmale und Nachteile einer vollständig denormalisierten Tabelle

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: 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:

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:

TOP

Erste Normalform: Attribute sollen atomar sein, wiederholende Gruppen sind auszulagern

Die erste Normalform (NF1) ist durch zwei Forderungen charakterisiert:
  1. Attribute müssen atomar sein. Das meint: Einem Attribut dürfen nicht mehrere Werte aus dem definierten Gültigkeitsbereich zugeordnet sein.
  2. Wiederholende Gruppen, also mehrwertige Relationen, sind in eine eigene Tabelle auszulagern und zu verknüpfen.
Beispiele:

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

Spezielle Datentypen

Hinweise zum Gebrauch der Datentypen


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_NRA_NAMEA_PREISA_STUECKDATUMV_NAMEV_ANSCH
1Oberhemd39,804024.06.1999Meyer, EmilWendeweg 10, 2800 Bremen
2Mantel360,001024.06.1999Meier, FranzKohlstr. 1, 2800 Bremen
3Oberhemd44,207024.06.1999Meyer, EmilWendeweg 10, 2800 Bremen
4Oberhemd44,202025.06.1999Schulze, FritzGemü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_NRV_NRA_NRA_STUECKDATUM
18413124024.06.1999
25016221024.06.1999
38413117024.06.1999
41215112025.06.1999
55016223525.06.1999
68413133524.06.1999
7121513524.06.1999
81215121024.06.1999
98413112025.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_NRV_NRA_NRA_STUECKA_PREISDATUM
18413124039.8024.06.1999
250162210360.0024.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. 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:

lfNrLiefer-NrDatum Artikel (E)LieferantEPZahl Artikel (V)EmpfängerEPZahl
12415.2.2003 Hosen, blauFA Muster-Liefer GbR, Nürnberg39.9050
22415.2.2003 Hose, braunFA Muster-Liefer GbR, Nürnberg39.9050

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:

lfNrLiefer-NrDatum Artikel-NrFarb-IdLieferanten-IdEPZahl

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-NrDatumLieferanten-Id

Tabelle tbl_Lieferdetails:

lfNr-DetailsLiefer-NrArtikel-NrFarb-NrEPZahl

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:

DatumStrasseHausEtageWohnungZählerStandMitarbeiter

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:

DatumZählerStandMitarbeiter

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-IdOrtNameweitere für Straßen typische Eigenschaften

Haus-IdStrassen-IdHausnummerweitere für Häuser typische Eigenschaften

Etagen-IdHaus-IdEtagennummerweitere für Etagen typische Eigenschaften

Wohnungs-IdEtagen-IdHauptmieterweitere für Wohnungen typische Eigenschaften

Zähler-IdWohnungs-IdWerksnummer - 10-stellig mit Buchstabenweitere für Zähler typische Eigenschaften

Die neu identifizierte Tabelle mit Datum und Straßen-Id als Primärschlüssel:

DatumStraßen-IdMitarbeiter

Die Tabelle mit den eigentlichen Ablesedaten sieht nun wie folgt aus:

DatumZähler-IdZählerstandBemerkungen

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.

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. 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

Beispiele

Bemerkung zu allen Data Manipulation Language - Befehlen (DML)


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

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.
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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))
  9. 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


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.

Bemerkungen


© 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.