Datenmodellierung und Datenbanken
Abfragen: aus Daten Informationen gewinnen
Auswählen, filtern, sortieren, verbinden und zusammenfassen: die fünf Dinge, aus denen jede Datenbankabfrage besteht.
Benötigte Grundlagen
Dieses Vorwissen brauchst du für das Kapitel. Schau kurz nach, wenn dir etwas davon nicht mehr präsent ist, sonst leg direkt los.
Einführung
Die Datenbank ist gebaut, die Tabellen sind gefüllt. Und jetzt?
Eine Datenbank, aus der man nichts herausbekommt, ist nur ein sehr ordentlicher Aktenschrank. Ihren Wert bekommt sie erst durch Abfragen: „Welche Bücher sind gerade ausgeliehen?“, „Wer hat mehr als drei Bücher?“, „Wie viele Ausleihen gab es im März?“
Das Bemerkenswerte daran ist, wie wenig man dafür lernen muss. Fast jede Abfrage besteht aus denselben fünf Bausteinen, und man beschreibt darin nicht, wie gesucht werden soll, sondern nur, was man haben will.
Das kannst du nach diesem Kapitel
eine Abfrage aus Auswahl, Filter, Sortierung, Verbindung und Zusammenfassung aufbauen.
einfache Abfragen in SQL lesen und selbst formulieren.
Bedingungen mit und, oder und nicht verknüpfen.
zwei Tabellen über ihren verbinden.
erklären, warum eine Abfrage beschreibt statt vorschreibt, und was daraus folgt.
Unsere Beispieldatenbank
Wir arbeiten mit den Tabellen aus dem vorigen Kapitel:
Schueler(schuelerNr, vorname, nachname, klasse)
Buch(buchNr, titel, autor, verlag, jahr)
Ausleihe(ausleihNr, schuelerNr, buchNr, ausleihdatum, rueckgabedatum)
Die Ausgangstabelle
Fünf Zeilen, vier Spalten, klein genug, um jede Abfrage von Hand nachzuprüfen. Genau dafür ist sie da: Wer ein Abfrageergebnis glaubt, statt es nachzuzählen, merkt einen Denkfehler erst bei einer Tabelle, die zu groß zum Nachzählen ist.
Baustein 1: Auswahl
Zuerst sagt man, welche Spalten man sehen will und aus welcher Tabelle.
SELECT titel, autor
FROM Buch;
Gelesen: „Wähle Titel und Autor aus der Tabelle Buch.“ Das Ergebnis ist wieder eine Tabelle, nur mit weniger Spalten.
Ein Stern steht für alle Spalten:
SELECT * FROM Buch;
Baustein 2: Filter
Meistens will man nicht alle Zeilen. Die Bedingung steht hinter dem Wort WHERE:
SELECT titel, jahr
FROM Buch
WHERE jahr > 2000;
Die Bedingung wird für jede Zeile einzeln geprüft. Trifft sie zu, kommt die Zeile ins Ergebnis, sonst nicht.
Zur Verfügung stehen die Vergleiche , , , , , sowie die Textsuche mit LIKE, bei der ein Prozentzeichen für beliebig viele Zeichen steht:
WHERE nachname LIKE 'Web%'
Mehrere Bedingungen verknüpft man mit AND, OR und NOT:
WHERE jahr > 2000 AND verlag = 'Dressler'
🔴 Beachte den Unterschied zwischen AND und OR genau. „Bücher von Kästner und von Ende“ heißt umgangssprachlich, dass man beide haben will, in der Abfrage aber:
WHERE autor = 'Kästner' OR autor = 'Ende'
Mit AND käme kein einziges Buch heraus, denn kein Buch hat gleichzeitig zwei verschiedene Autoren. Die Bedingung wird je Zeile geprüft, nicht über die Menge hinweg. Das ist eine der häufigsten Fehlerquellen überhaupt.
Filter und Projektion an einem Bild
Zwei verschiedene Dinge, absichtlich verschieden markiert. Der Filter streicht Zeilen: getönt sind genau die drei mit mehr als Punkten, , und ; Ben mit und Eren mit fallen heraus. Die Projektion streicht Spalten: farbig sind die Köpfe von name und punkte. Übrig bleibt, wo beides zutrifft, drei Zeilen mit je zwei Spalten. Und die Reihenfolge ist egal: Erst filtern und dann Spalten wegnehmen ergibt dasselbe wie umgekehrt.
Baustein 3: Sortierung
Weil die Zeilenreihenfolge einer Tabelle nichts bedeutet, muss man sie ausdrücklich anfordern:
SELECT titel, jahr
FROM Buch
ORDER BY jahr DESC;
ASC sortiert aufsteigend (der Standard), DESC absteigend.
Baustein 4: Tabellen verbinden
Jetzt kommt der Schritt, der Datenbanken erst mächtig macht. Die Ausleihtabelle enthält nur Nummern. Wer wissen will, welcher Schüler welches Buch hat, muss drei Tabellen zusammenführen.
Verbunden wird über die Stelle, an der und Primärschlüssel denselben Wert tragen:
SELECT Schueler.nachname, Buch.titel, Ausleihe.ausleihdatum
FROM Ausleihe
JOIN Schueler ON Ausleihe.schuelerNr = Schueler.schuelerNr
JOIN Buch ON Ausleihe.buchNr = Buch.buchNr;
Gelesen: „Nimm jede Ausleihe. Suche den Schüler, dessen Nummer dort steht, und das Buch, dessen Nummer dort steht. Zeige Nachname, Titel und Datum.“
Hier zahlt sich die Trennung aus dem Modellierungskapitel aus: Weil der Name einmal gespeichert ist, ist er überall gleich, und die Verbindung stellt ihn bei jeder Ausleihe zur Verfügung. Man speichert einmal und liest beliebig oft zusammen.
Zwei Tabellen verbinden
Der Verbund ersetzt jeden durch das, worauf er zeigt. Prüfe eine Zeile nach: Anna hatte klasseID , und die steht oben bei der 10a, also erscheint unten „10a“. Nichts geht dabei verloren und nichts kommt hinzu: Aus fünf Schülerzeilen werden genau fünf Ergebniszeilen. Erst dadurch wird die Zerlegung aus 10.2 wieder bequem lesbar, man speichert getrennt und liest zusammengesetzt.
Baustein 5: Zusammenfassen
Oft will man keine Liste, sondern eine Zahl. Dafür gibt es Funktionen, die über viele Zeilen hinweg rechnen:
| Funktion | Bedeutung |
|---|---|
| COUNT(*) | Anzahl der Zeilen |
| SUM(spalte) | Summe |
| AVG(spalte) | Mittelwert |
| MIN, MAX | kleinster, größter Wert |
SELECT COUNT(*)
FROM Ausleihe
WHERE ausleihdatum >= '2026-03-01';
Will man je Gruppe zusammenfassen, etwa je Schüler, kommt GROUP BY dazu:
SELECT schuelerNr, COUNT(*)
FROM Ausleihe
GROUP BY schuelerNr;
Das Ergebnis hat eine Zeile je Schülernummer und daneben die Anzahl seiner Ausleihen. Man kann sich das als Sortieren nach Schülernummer und anschließendes Zusammenzählen jeder Gruppe vorstellen.
Beschreiben statt vorschreiben
Und jetzt der Gedanke, der über SQL hinausreicht.
In den Algorithmus-Kapiteln hast du Handlungsanweisungen geschrieben: nimm die erste Zahl, vergleiche, gehe zur nächsten. Eine Abfrage tut das gerade nicht. Sie beschreibt nur die Eigenschaften des gewünschten Ergebnisses, und wie es gefunden wird, überlässt sie dem System.
Das hat zwei praktische Folgen:
Erstens kann das System den Weg selbst optimieren. Bei einer Million Bücher wählt es einen ganz anderen Weg als bei zehn, ohne dass die Abfrage sich ändert.
Zweitens bleibt dieselbe Abfrage bei wachsendem Bestand gültig. Ein selbstgeschriebener Suchalgorithmus müsste bei einer Million Zeilen neu durchdacht werden.
Man nennt diese Art zu arbeiten deklarativ („beschreibend“) im Unterschied zum imperativen Programmieren („befehlend“), das du beim kennengelernt hast. Beide haben ihren Platz; sie beantworten verschiedene Arten von Fragen.
Die Reihenfolge im Kopf
Beim Schreiben hilft es, die Bausteine immer in derselben Ordnung durchzugehen:
- Woher? (FROM, JOIN) Welche Tabellen brauche ich?
- Welche Zeilen? (WHERE) Welche Bedingung müssen sie erfüllen?
- Gruppieren? (GROUP BY) Will ich je Gruppe eine Zahl?
- Was zeigen? (SELECT) Welche Spalten sollen im Ergebnis stehen?
- In welcher Ordnung? (ORDER BY)
Aufgeschrieben wird zwar mit SELECT zuerst, gedacht wird aber von der Tabelle her. Wer sich an diese Reihenfolge hält, vergisst seltener ein JOIN.
Die Reihenfolge im Kopf
Die rechte Spalte ist der Grund, warum diese Reihenfolge wichtig ist: Bei jedem Schritt bleiben weniger Zeilen übrig, und jeder Schritt arbeitet auf dem Ergebnis des vorigen. Wer zuerst zusammenfasst und dann filtert, filtert bereits verdichtete Zeilen und bekommt etwas anderes heraus. Dass man SELECT ganz vorne hinschreibt, obwohl es fast zuletzt ausgeführt wird, ist eine Eigenheit der Schreibweise, nicht der Verarbeitung.
Von der Frage zur Abfrage
„Welche Bücher des Verlags Dressler sind nach 2000 erschienen? Zeige Titel und Jahr, das neueste zuerst.“
- 1
Woher? Alle Angaben stehen in der Tabelle Buch. Also FROM Buch, kein JOIN nötig.
- 2
Welche Zeilen? Zwei Bedingungen müssen gleichzeitig gelten, also AND: verlag = 'Dressler' AND jahr > 2000.
- 3
Gruppieren? Nein, es ist eine Liste gefragt und keine Zahl.
- 4
Was zeigen? Titel und Jahr.
- 5
In welcher Ordnung? Neuestes zuerst, also absteigend nach Jahr.
SELECT titel, jahr FROM Buch WHERE verlag = 'Dressler' AND jahr > 2000 ORDER BY jahr DESC;
Hier ist AND richtig, weil eine Zeile beide Bedingungen erfüllen muss und das möglich ist.
Verbinden und zählen
„Wie viele Bücher hat jeder Schüler ausgeliehen? Zeige Nachname und Anzahl, die Vielleser zuerst.“
- 1
Woher? Die Anzahl steckt in der Ausleihtabelle, der Nachname in der Schülertabelle. Also beide, verbunden über schuelerNr.
- 2
Welche Zeilen? Alle, es gibt keine Einschränkung.
- 3
Gruppieren? Ja, „jeder Schüler“ heißt: je Schüler eine Zeile. Also GROUP BY über den Schüler.
- 4
Was zeigen und wie ordnen? Nachname und die Anzahl, absteigend.
SELECT Schueler.nachname, COUNT(*) AS anzahl FROM Ausleihe JOIN Schueler ON Ausleihe.schuelerNr = Schueler.schuelerNr GROUP BY Schueler.schuelerNr, Schueler.nachname ORDER BY anzahl DESC; - 5
Gruppiert wird bewusst nach der Nummer und nicht nur nach dem Nachnamen: Zwei Schüler können denselben Nachnamen haben, und die dürfen nicht in einer Zeile zusammenfallen. Genau dafür gibt es den .
Eine Zeile je Schüler mit seiner Ausleihzahl, absteigend sortiert.
Typischer Fehler
Für „Zeige alle Bücher von Kästner und von Ende“ die Bedingung
Diese Abfrage liefert null Zeilen, und zwar zuverlässig.
Der Grund liegt darin, worauf die Bedingung angewendet wird: auf jede Zeile einzeln. Für ein einzelnes Buch würde geprüft, ob sein Autor gleichzeitig Kästner und Ende ist. In der Spalte autor steht aber genau ein Wert, also kann die Bedingung nie erfüllt sein.
Richtig ist OR:
Jetzt wird je Zeile geprüft, ob eine der beiden Bedingungen zutrifft, und beide Autorengruppen landen im Ergebnis.
Die Ursache der Verwechslung ist die Alltagssprache: Das „und“ in „Bücher von Kästner und Ende“ verbindet die gewünschte Ergebnismenge, nicht die Bedingungen an eine Zeile. Merke dir die Übersetzung: Wo du im Deutschen zwei Sorten aufzählst, steht in der Abfrage OR. Ein AND ist nur dann richtig, wenn eine einzelne Zeile beide Bedingungen zugleich erfüllen kann, etwa „von Kästner und nach 2000 erschienen“.
Übung 1
leichtFormuliere die Abfragen zur Tabelle Buch(buchNr, titel, autor, verlag, jahr):
a) alle Titel b) alle Bücher, die vor 1990 erschienen sind c) alle Titel von Kästner, alphabetisch sortiert d) die Anzahl aller Bücher
Tipp anzeigen
Zu d): Gefragt ist eine Zahl, keine Liste.
Lösung anzeigen
a) SELECT titel FROM Buch;
b) SELECT * FROM Buch WHERE jahr < 1990;
c) SELECT titel FROM Buch WHERE autor = 'Kästner' ORDER BY titel;
d) SELECT COUNT(*) FROM Buch;
Detaillierte Schritterklärung anzeigen
Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.
✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.
- 1
Den Bauplan jeder Abfrage bereitlegen
Jede Abfrage beantwortet drei Fragen in fester Reihenfolge: Welche Spalten (SELECT), aus welcher Tabelle (FROM), welche Zeilen (WHERE). Dazu kommen bei Bedarf ORDER BY für die Reihenfolge und Funktionen wie COUNT für Zusammenfassungen.
- 2
a) und b): Spalten wählen und Zeilen filtern
Für alle Titel genügt die Spaltenauswahl ohne Bedingung. Für die Bücher vor 1990 bleiben alle Spalten (der Stern), aber die Zeilen werden über WHERE eingeschränkt.
- 3
c) Bedingung und Sortierung zusammen
Gefordert sind Titel (Spalte), nur von Kästner (Bedingung) und alphabetisch (Reihenfolge). Das ergibt alle drei Teile in einer Abfrage:
- 4
d) Eine Zahl statt einer Liste
Gefragt ist die Anzahl, nicht die Bücher selbst. Dafür gibt es eine Zusammenfassungsfunktion: Das Ergebnis ist eine einzige Zeile mit einer einzigen Zahl.
Übung 2
mittelNutze die drei Tabellen Schueler, Buch und Ausleihe.
a) Zeige Nachname und Buchtitel aller laufenden Ausleihen (rueckgabedatum ist leer). b) Zeige, wie viele Bücher jede Klasse insgesamt ausgeliehen hat. c) Erkläre, warum in a) zwei JOIN nötig sind. d) Was passiert, wenn man in a) das JOIN auf Buch weglässt?
Tipp anzeigen
„Leer“ heißt in SQL: IS NULL.
Lösung anzeigen
a) Abfrage:
SELECT Schueler.nachname, Buch.titel FROM Ausleihe JOIN Schueler ON Ausleihe.schuelerNr = Schueler.schuelerNr JOIN Buch ON Ausleihe.buchNr = Buch.buchNr WHERE Ausleihe.rueckgabedatum IS NULL;
b) Abfrage:
SELECT Schueler.klasse, COUNT(*) AS anzahl FROM Ausleihe JOIN Schueler ON Ausleihe.schuelerNr = Schueler.schuelerNr GROUP BY Schueler.klasse ORDER BY anzahl DESC;
c) Weil die gewünschten Angaben in drei Tabellen liegen. Die Ausleihtabelle enthält nur Nummern; der Nachname steht in Schueler, der Titel in Buch. Jede Verbindung holt genau eine dieser Angaben dazu, also braucht man zwei.
d) Dann steht die Spalte Buch.titel gar nicht zur Verfügung, und die Abfrage wird mit einer Fehlermeldung abgelehnt. Ersetzt man titel durch buchNr, läuft sie, liefert aber nur Nummern: Man sähe, dass Mara Buch 17 hat, ohne zu erfahren, welches Buch das ist.
Detaillierte Schritterklärung anzeigen
Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.
✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.
- 1
Teil a): Woher kommen die Angaben?
Zuerst wird für jede gewünschte Spalte notiert, in welcher Tabelle sie steht. Diese Liste bestimmt, wie viele Verbindungen nötig sind.
Zwischenergebnis
nachname → Schueler · titel → Buch · rueckgabedatum → Ausleihe
Startpunkt ist immer die Tabelle, die die Fremdschlüssel trägt, hier die Ausleihe. Von dort holt man sich der Reihe nach die Ergänzungen.
- 2
Teil a): Verbindungen aufschreiben
Jede Verbindung setzt Fremdschlüssel und Primärschlüssel gleich. Es gibt zwei Fremdschlüssel in der Ausleihtabelle, also zwei Verbindungen.
\text{Ausleihe.schuelerNr} = \text{Schueler.schuelerNr} \qquad \text{Ausleihe.buchNr} = \text{Buch.buchNr}
Zwischenergebnis
Zwei JOIN, jeweils mit ihrer Bedingung.
- 3
Teil a): Filter für „laufend“
Eine Ausleihe ist laufend, wenn noch kein Rückgabedatum eingetragen ist. Ein fehlender Wert heißt in Datenbanken NULL, und darauf prüft man mit IS NULL, nicht mit einem Gleichheitszeichen.
\texttt{WHERE Ausleihe.rueckgabedatum IS NULL}
Zwischenergebnis
Nur die noch nicht zurückgegebenen Bücher bleiben übrig.
- 4
Teil b): Gruppieren statt auflisten
„Wie viele je Klasse“ verlangt eine Zeile je Klasse. Man verbindet mit der Schülertabelle, weil nur dort die Klasse steht, gruppiert nach ihr und zählt die Zeilen jeder Gruppe.
\texttt{GROUP BY Schueler.klasse} \quad\text{mit}\quad \texttt{COUNT(*)}
Zwischenergebnis
Eine Zeile je Klasse mit der Anzahl ihrer Ausleihen.
Hier ist die Gruppierung nach dem Klassennamen ausnahmsweise richtig, denn es gibt jede Klassenbezeichnung nur einmal. Bei Personennamen wäre sie falsch.
Übung 3
schwera) Formuliere eine Abfrage, die alle Bücher zeigt, die noch nie ausgeliehen wurden. Erkläre die Schwierigkeit. b) Formuliere „Alle Schüler der Klasse 10a, die mehr als zwei Bücher ausgeliehen haben“. c) Ein Mitschüler schreibt WHERE klasse = '10a' OR klasse = '10b' AND jahr > 2000 und wundert sich über das Ergebnis. Erkläre. d) Begründe, warum eine Abfrage bei einer Million Zeilen nicht neu geschrieben werden muss, ein selbstgeschriebener Suchalgorithmus aber möglicherweise schon.
Tipp anzeigen
Zu a): Gesucht ist das Fehlen einer Zeile in einer anderen Tabelle.
Lösung anzeigen
a) Abfrage:
SELECT titel FROM Buch WHERE buchNr NOT IN (SELECT buchNr FROM Ausleihe);
Die Schwierigkeit ist, dass man nach etwas sucht, das nicht da ist. Ein einfaches JOIN verbindet nur, wo Zeilen zueinander passen; ein Buch ohne Ausleihe hat keinen Partner und fiele damit heraus, obwohl es genau das Gesuchte ist. Man muss deshalb erst die Menge aller ausgeliehenen Bücher bestimmen und dann alles nehmen, was nicht darin vorkommt.
b) Abfrage:
SELECT Schueler.nachname, COUNT() AS anzahl FROM Ausleihe JOIN Schueler ON Ausleihe.schuelerNr = Schueler.schuelerNr WHERE Schueler.klasse = '10a' GROUP BY Schueler.schuelerNr, Schueler.nachname HAVING COUNT() > 2;
Die Bedingung an die Klasse steht im WHERE, weil sie einzelne Zeilen betrifft. Die Bedingung an die Anzahl steht im HAVING, weil sie erst nach dem Gruppieren geprüft werden kann; im WHERE gäbe es die Anzahl noch gar nicht.
c) AND bindet stärker als OR. Die Bedingung wird deshalb gelesen als: klasse = '10a' ODER (klasse = '10b' UND jahr > 2000). Aus der 10a kommen also alle, aus der 10b nur die mit jahr > 2000. Gewollt war vermutlich (klasse = '10a' OR klasse = '10b') AND jahr > 2000. Klammern setzen, sobald AND und OR gemischt werden.
d) Weil eine Abfrage nur beschreibt, welches Ergebnis gewünscht ist, und nicht, wie es zu finden ist. Das Datenbanksystem wählt den Weg selbst und kann ihn bei einer Million Zeilen anders wählen als bei zehn, etwa über einen Index. Ein selbstgeschriebener legt den Weg dagegen fest: Wer alle Zeilen der Reihe nach durchsucht, braucht bei einer Million Zeilen hunderttausendmal so lange wie bei zehn, und daran ändert sich nichts, solange der Quelltext gleich bleibt.
Detaillierte Schritterklärung anzeigen
Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.
✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.
- 1
a) Nach etwas suchen, das nicht da ist
Die Schwierigkeit: Gesucht wird das Fehlen einer Zeile. Ein JOIN verbindet nur, wo Zeilen zueinander passen, ein Buch ohne Ausleihe hat keinen Partner und fiele heraus, obwohl es genau das Gesuchte ist. Man bestimmt deshalb erst die Menge aller ausgeliehenen Bücher und nimmt dann alles, was nicht darin vorkommt.
- 2
b) WHERE oder HAVING: Wann existiert die Anzahl?
Die Bedingung an die Klasse steht im WHERE, weil sie einzelne Zeilen betrifft. Die Bedingung an die Anzahl steht im HAVING, weil sie erst nach dem Gruppieren geprüft werden kann, im WHERE gäbe es die Anzahl noch gar nicht.
- 3
c) AND bindet stärker als OR, deshalb Klammern
wird gelesen als . Aus der 10a kommen also alle, aus der 10b nur die mit . Gewollt war vermutlich .
- 4
d) Beschreiben statt vorschreiben: warum SQL mitwächst
Eine Abfrage beschreibt nur, welches Ergebnis gewünscht ist, nicht, wie es zu finden ist. Das Datenbanksystem wählt den Weg selbst und kann ihn bei einer Million Zeilen anders wählen als bei zehn, etwa über einen Index. Ein selbstgeschriebener Algorithmus legt den Weg dagegen fest.
Zusammenfassung
Abfragen machen aus gespeicherten . Fast jede besteht aus denselben Bausteinen: der Auswahl der Spalten, der Angabe der Quelltabellen, einem Filter über die Zeilen, der Verbindung mehrerer Tabellen über Fremd- und , der Gruppierung mit einer zusammenfassenden Funktion und der Sortierung. Der Filter prüft jede Zeile einzeln; deshalb wird aus dem umgangssprachlichen „und“ bei zwei Sorten ein OR, und deshalb braucht eine Mischung aus AND und OR Klammern. Verbunden wird immer dort, wo Fremdschlüssel und Primärschlüssel denselben Wert tragen, und gruppiert wird nach dem Schlüssel statt nach dem Namen. Der grundsätzliche Unterschied zum Programmieren: Eine Abfrage beschreibt das gewünschte Ergebnis, statt den Weg dorthin vorzuschreiben, und überlässt die Ausführung dem System.


