1. SQL-Minimum zum Lesen von Daten
Wenn du anfängst, ein Backend zu schreiben, stellst du sehr schnell fest: Die meiste Zeit „speicherst" du keine Daten, sondern liest sie. Du zeigst einen Katalog an, suchst nach Produkten, baust eine Bestellliste, holst die Details einer einzelnen Bestellung. Dafür brauchst du keinen „Kurs über den PostgreSQL-Optimierer", aber du brauchst ein solides Grundvokabular: SELECT, Filter, Sortierung, Ergebniseinschränkung und die Fähigkeit, Daten aus mehreren Tabellen zusammenzuführen.
SQL ist eine deklarative Sprache: Du beschreibst was du bekommen möchtest, nicht wie genau die Datenbank das berechnen soll. Das ist gleichzeitig praktisch und tückisch. Praktisch – weil sich ein Query wie eine Frage liest. Tückisch – weil die Datenbank nicht verpflichtet ist, Zeilen in einer „gewohnten Reihenfolge" zurückzugeben, wenn du diese Reihenfolge nicht festgelegt hast. Und genau hier beginnen die Fragen wie „warum springt meine Liste?" und „warum zeigt die Paginierung doppelte Einträge?".
Damit du dir das große Bild leichter merken kannst, lass uns direkt eine Mini-Cheatsheet festhalten – ohne Fanatismus:
| Teil des Querys | Was er macht | Menschliche Frage |
|---|---|---|
|
wählt Spalten/Ausdrücke für das Ergebnis aus | „Was möchte ich in der Antwort sehen?" |
|
legt die Datenquelle fest | „Woher lese ich das?" |
|
filtert Zeilen | „Welche Zeilen passen?" |
|
legt die Reihenfolge des Ergebnisses fest | „In welcher Reihenfolge anzeigen?" |
|
nimmt einen „Ausschnitt" der Liste | „Wie viele Zeilen nehmen und wo anfangen?" |
|
verbindet Tabellen | „Wie baue ich eine verknüpfte Antwort aus mehreren Tabellen zusammen?" |
Als Nächstes gehen wir das alles der Reihe nach durch und beziehen es jedes Mal auf die mini‑shop-Domäne: Kategorien, Produkte, Lagerbestände und Bestellpositionen.
2. SELECT und FROM: grundlegende Auswahl
Wenn du SQL zum ersten Mal siehst, scheint SELECT einfach der „Befehl zum Holen von Daten" zu sein, und FROM – „woher". Das stimmt, aber es ist wichtig, einen weiteren Gedanken hinzuzufügen: SELECT legt die Form der Antwort fest, also welche Felder genau im Ergebnis landen. Und hier gibt es bei Anfängern fast immer die Versuchung, SELECT * zu schreiben, weil „es einfacher ist". Das stimmt zwar… solange du eine Spalte hast und keine Kollegen. Dann fängt der Zirkus an.
Stell dir vor, SELECT * wäre so, als würdest du in ein Café gehen und sagen: „Bringen Sie mir *die gesamte Speisekarte*. Ich wähle dann aus." Der Kellner würde sich zwar wundern, aber bringen. Und dann zahlst du dafür – manchmal buchstäblich, weil unnötiges Lesen von Daten unnötige Arbeit bedeutet.
Für die Beispiele nehmen wir an, dass wir eine Tabelle product mit Spalten wie id, sku, name, price, status, category_id haben. Das ist dasselbe Mini-Lernschema mini‑shop: Kategorien, Produkte, Lagerbestände und Bestellungen sind bereits auf separate Tabellen aufgeteilt – wir nehmen jetzt einfach nur die Teile heraus, die zum Lesen von Daten benötigt werden. Wenn weiter unten category, stock_item oder order_item auftauchen, sind das keine neuen abstrakten Tabellen, sondern benachbarte Teile desselben Schemas.
Mini-Beispiel 1: die „faulste" Variante (zu Lernzwecken – geht, als Gewohnheit – lieber nicht):
-- Lernvariante: alle Spalten aus der Tabelle holen
-- Im echten Code besser die benötigten Felder explizit aufzählen
SELECT *
FROM product;
Warum ist das als Stil gefährlich? Weil das Ergebnis von der Tabellenstruktur abhängt. Kommt eine neue Spalte hinzu – ändert sich das Ergebnis. Im echten Code führt das oft dazu, dass „es kaputt gegangen ist, weil jemand eine Spalte hinzugefügt hat". Das klingt absurd, ist aber Realität.
Mini-Beispiel 2: nur die benötigten Spalten auswählen (das sieht schon nach einem erwachsenen Ansatz aus):
-- Spalten explizit aufzählen, die wirklich für die Antwort benötigt werden
SELECT id, sku, name, price
FROM product;
Mini-Beispiel 3: Aliase für Spalten – nützlich, wenn du eine „Schaufenster-Ansicht" baust und verständliche Namen möchtest:
-- Felder im Ergebnis umbenennen, damit die Antwort selbstdokumentierend ist
SELECT
sku AS product_sku,
name AS product_name,
price AS product_price
FROM product;
Mini-Beispiel 4: Ausdrücke in SELECT (SQL kann nicht nur „holen", sondern auch einfache Dinge berechnen). Zum Beispiel den Preis mit 10 % Rabatt anzeigen:
-- Ein berechnetes Feld zum Ergebnis hinzufügen (das ändert keine Daten in der Tabelle)
SELECT
sku,
price,
price * 0.90 AS discounted_price
FROM product;
Wichtig zu beachten: SELECT ändert keine Daten in der Tabelle. Das ist ein „virtuelles Ergebnis", als hättest du eine neue Tabelle über der alten gezeichnet.
Mini-Beispiel 5: Tabellen-Aliase (bei einzelnen Tabellen nicht unbedingt nötig, aber bei JOIN fast obligatorisch). Auch wenn es jetzt überflüssig erscheint, ist es besser, sich den Stil anzugewöhnen:
-- Ein Tabellen-Alias ist praktisch für JOIN und hilft, Mehrdeutigkeiten bei Spaltennamen zu vermeiden
SELECT p.id, p.sku, p.name
FROM product p;
Das liest sich so: „Wähle Felder aus product, das wir p nennen werden." In den folgenden Abschnitten spart uns das Nerven und schützt vor „ambiguous column".
3. WHERE: Zeilen filtern
Wenn SELECT die Frage beantwortet „welche Spalten anzeigen", beantwortet WHERE die Frage „welche Zeilen überhaupt passen". Das ist der Moment, wo SQL anfängt, wie Filter in einem Online-Shop auszusehen: „zeige nur aktive Produkte", „Preis von X bis Y", „diese Kategorie".
Das Wichtigste, was ein Anfänger verstehen muss: WHERE ist kein Schema à la „erst alles lesen, dann filtern, wie in Java-Code mit if". In SQL beschreibst du direkt die Bedingung, und die Datenbank gibt bereits das gefilterte Ergebnis zurück. Deshalb ist die richtige Gewohnheit, Bedingungen auf Datenbankseite zu formulieren, anstatt alles in die Anwendung zu ziehen nach dem Motto „wir holen alles und sortieren es dann".
Für den Anfang reichen die grundlegendsten Operatoren: =, <> (ungleich), >, >=, <, <=, plus Logik AND/OR. Und ein besonders wichtiges Thema – NULL – nehmen wir sorgfältig in Angriff.
Mini-Beispiel 1: Produkte nach Preis filtern:
-- Zeilen nach dem Wert in der Spalte price filtern
SELECT id, sku, name, price
FROM product
WHERE price >= 100;
Mini-Beispiel 2: nach Status filtern (angenommen, der Status ist als String gespeichert):
-- Nur Produkte mit einem bestimmten Status behalten
SELECT sku, name, status
FROM product
WHERE status = 'ACTIVE';
Mini-Beispiel 3: mehrere Bedingungen (liest sich wie „das und das"):
-- AND bedeutet, dass beide Bedingungen wahr sein müssen
SELECT sku, name, price, category_id
FROM product
WHERE category_id = 10
AND price < 500;
Mini-Beispiel 4: OR und Klammern. Ohne Klammern bekommt man leicht „das Falsche":
-- Klammern fixieren den „menschlichen" Sinn des Ausdrucks mit AND/OR
SELECT sku, name, status, price
FROM product
WHERE (status = 'ACTIVE' OR status = 'DRAFT')
AND price >= 100;
Wenn man die Klammern entfernt, kann sich die Bedeutung ändern. SQL errät deinen Gedanken nicht, genau wie die Mathematik – er folgt Prioritätsregeln für Operatoren. Und dein Gedanke ist dann beleidigt.
Mini-Beispiel 5: Suche nach einem Textteil über LIKE. Das ist eine grundlegende Sache für die „Suche nach Name" auf einfachem SQL-Niveau:
-- LIKE + % ermöglicht einfache Suche: „Teilstring kommt irgendwo vor"
SELECT sku, name
FROM product
WHERE name LIKE '%Lego%';
% bedeutet „beliebige Anzahl von Zeichen". Wichtig: Groß-/Kleinschreibung und Vergleichsbesonderheiten hängen vom DBMS und der Collation ab. Für jetzt reicht uns die Idee: LIKE ist ein einfaches Werkzeug für „enthält Teilstring".
Mini-Beispiel 6: Vorsicht bei NULL. In SQL ist NULL „unbekannt" oder „kein Wert", und man kann es nicht mit = vergleichen. Man braucht IS NULL / IS NOT NULL:
-- NULL wird über IS NULL / IS NOT NULL geprüft, nicht über =
SELECT id, sku, name
FROM product
WHERE category_id IS NULL;
In unserem Projekt wird category_id Pflichtfeld sein, sodass dieser Query eher der Übung dient. Aber der Fehler „ich prüfe = NULL" ist so verbreitet, dass es besser ist, ihn jetzt zu sehen als ihn später eine halbe Stunde lang zu suchen.
Wenn du ein „Merkblatt für Bedingungen" möchtest, hier eine kompakte Tabelle. Das ist fast alles, was du jetzt gerade brauchst:
| Was wir wollen | Wie wir es in WHERE schreiben |
|---|---|
| gleich | |
| ungleich | |
| größer/kleiner | |
| mehrere Bedingungen | |
| „enthält Text" | |
| leer/nicht leer | |
4. ORDER BY: Reihenfolge festlegen
Eine sehr menschliche Erwartung von Anfängern lautet: „Ich habe Zeilen der Reihe nach hinzugefügt, also gibt die Datenbank sie auch der Reihe nach zurück." Leider (oder zum Glück) ist die Datenbank nicht verpflichtet, unsere Erwartungen zu erfüllen. Eine Tabelle ist keine Liste, sondern eine Menge von Zeilen, und ohne ORDER BY ist die Reihenfolge des Ergebnisses nicht garantiert. Manchmal scheint sie „stabil" zu sein, aber das ist nur Glück, kein Vertrag.
ORDER BY ist der Ort, wo du den Vertrag festlegst: „Zeige mir das Ergebnis nach Preis absteigend" oder „nach Name aufsteigend". Außerdem wird ORDER BY kritisch wichtig, wenn wir Ergebnisse in Portionen über LIMIT/OFFSET holen wollen (nächster Abschnitt). Ohne Reihenfolge werden die Portionen „schwimmen".
Mini-Beispiel 1: Produkte nach Preis sortieren, teuerste oben:
-- DESC sortiert absteigend (teurer -> günstiger)
SELECT sku, name, price
FROM product
ORDER BY price DESC;
DESC – descending, absteigend. Schreibt man ASC (ascending) oder gar nichts, wird in der Regel aufsteigend angenommen.
Mini-Beispiel 2: Sortierung nach mehreren Feldern. Zum Beispiel zuerst nach Status, dann nach Name:
-- Sortierung nach mehreren Spalten wird von links nach rechts angewendet
SELECT sku, name, status
FROM product
ORDER BY status ASC, name ASC;
Das liest sich so: „Erst nach Status gruppieren (in alphabetischer Reihenfolge), innerhalb jedes Status nach Name sortieren."
Mini-Beispiel 3: Sortierung + Filterung – das häufigste „reale" Szenario:
-- Erst die Auswahl filtern (WHERE), dann die Reihenfolge festlegen (ORDER BY)
SELECT sku, name, price
FROM product
WHERE status = 'ACTIVE'
ORDER BY name;
Hier schränken wir zunächst die Auswahl ein und bringen das Ergebnis dann in eine verständliche Reihenfolge. Und ja, obwohl WHERE im Query-Text vor ORDER BY steht, sollte man nicht „wie geschrieben" denken, sondern „was es tut": Die Datenbank bildet zunächst die Menge passender Zeilen und sortiert sie dann.
5. LIMIT und OFFSET: Paginierung
In einer echten Anwendung kann man fast nie „ehrlich alle Produkte zurückgeben". Der Katalog kann riesig sein, und selbst wenn er es nicht ist, hat der Benutzer keine Lust, 50.000 Zeilen zu sehen. Deshalb holen wir das Ergebnis in Portionen. Das SQL-Minimum dafür ist LIMIT (wie viele Zeilen nehmen) und OFFSET (wie viele Zeilen überspringen).
Aber hier gibt es eine Regel, die man in Stein meißeln sollte: LIMIT/OFFSET sollten fast immer zusammen mit ORDER BY stehen. Ohne Reihenfolge nimmst du „zufällige" 20 Zeilen, dann die „nächsten zufälligen" 20, und das sieht nicht mehr nach normaler Paginierung aus – das sieht nach einem Überraschungsgenerator aus.
Mini-Beispiel 1: „die ersten 20 Produkte" (in einer verständlichen Reihenfolge):
-- Die ersten 20 Zeilen in stabiler Reihenfolge holen (ORDER BY ist für Paginierung Pflicht)
SELECT id, sku, name
FROM product
ORDER BY id
LIMIT 20;
Mini-Beispiel 2: „zweite Seite mit 20 Produkten" (also 20 überspringen und weitere 20 nehmen):
-- OFFSET überspringt die angegebene Anzahl Zeilen, LIMIT begrenzt die Seitengröße
SELECT id, sku, name
FROM product
ORDER BY id
LIMIT 20 OFFSET 20;
Um nicht den Überblick zu verlieren, ist es hilfreich, sich eine einfache Formel zu merken:
offset = (Seitennummer - 1) * Seitengröße
Wenn Seiten ab 1 gezählt werden. Wenn ab 0 – wird die Formel noch einfacher: offset = page * size. Die Idee selbst ist wichtiger als die konkrete Nummerierung.
Hier eine kleine Tabelle für eine Seitengröße von 20 (1-basierte Seiten):
| Seite | OFFSET |
|---|---|
| 1 | |
| 2 | |
| 3 | |
| 4 | |
Mini-Beispiel 3: „zeige 10 teuerste aktive Produkte":
-- Filter, Sortierung und Ergebniseinschränkung kombinieren
SELECT sku, name, price
FROM product
WHERE status = 'ACTIVE'
ORDER BY price DESC
LIMIT 10;
Das ist schon fast ein reales Szenario: „Top-10" oder „Schaufenster".
Und noch ein sehr wichtiges Detail aus der Praxis: LIMIT/OFFSET arbeiten mit dem Ergebnis nach der Sortierung. Das heißt, du hast zuerst sortiert und dann erst einen „Ausschnitt herausgeschnitten". Deshalb ist die Reihenfolge das Schlüsselelement der Stabilität.
6. JOIN: verknüpfte Daten
Würde das Projekt aus einer einzigen Tabelle everything bestehen, wäre JOIN nicht nötig. Aber wir haben die Normalisierung bereits besprochen: Kategorien leben separat, Produkte separat, Bestellungen separat, Bestellpositionen separat. Und das ist richtig so. Der Preis dafür ist die Notwendigkeit, manchmal ein „Bild" aus mehreren Tabellen zusammenzusetzen. Genau hier kommt JOIN ins Spiel.
JOIN kann man als „Zusammenkleben von Zeilen" aus zwei Tabellen nach einer ON-Bedingung verstehen. Normalerweise verbindet ON einen Fremdschlüssel mit einem Primärschlüssel: zum Beispiel verweist product.category_id auf category.id. Dann ermöglicht JOIN, Produkt und seine Kategorie in einem Ergebnis zu lesen, ohne category.name in jede Produktzeile zu kopieren.
Für den Anfang verwenden wir das normale JOIN (in den meisten DBMS ist das inner join). Über die verschiedenen Join-Typen sprechen wir später; jetzt brauchen wir die grundlegende Mechanik.
Mini-Beispiel 1: Produkte zusammen mit dem Kategorienamen anzeigen:
-- JOIN verbindet Tabellen nach der ON-Bedingung (üblicherweise FK -> PK)
SELECT
p.sku,
p.name,
c.name AS category_name
FROM product p
JOIN category c ON c.id = p.category_id
ORDER BY p.name;
Hier stecken gleich mehrere wichtige Fähigkeiten:
Erstens sind p und c Tabellen-Aliase. Zweitens qualifizieren wir Felder als p.name und c.name, sonst ist unklar, welches name genau gemeint ist (beide Tabellen können eine Spalte name haben). Drittens geben wir im Ergebnis den verständlichen Alias category_name.
Mini-Beispiel 2: den Inhalt einer bestimmten Bestellung anzeigen (Bestellung + Positionen + Produkt). Angenommen, es gibt Tabellen customer_order, order_item, product:
-- „Bestellansicht" zusammenbauen: Bestellung + Positionen + Produktdaten
SELECT
o.order_number,
p.sku,
p.name AS product_name,
oi.quantity,
oi.line_total
FROM customer_order o
JOIN order_item oi ON oi.order_id = o.id
JOIN product p ON p.id = oi.product_id
WHERE o.order_number = 'ORD-1001'
ORDER BY p.name;
Das ist schon ein ziemlich „echter" Query: Der Benutzer möchte eine Bestellung sehen und braucht nicht nur die product_id, sondern den Produktnamen, die Menge und den Betrag pro Position.
Mini-Beispiel 3: Lagerbestände nach Produkten anzeigen. Angenommen, es gibt stock_item (Bestände) und product (Katalog):
-- Bestände zusammen mit verständlichen Produktfeldern ausgeben (sku, name)
SELECT
p.sku,
p.name,
si.available_quantity,
si.reserved_quantity
FROM stock_item si
JOIN product p ON p.id = si.product_id
ORDER BY si.available_quantity ASC;
Als Ergebnis erhalten wir eine „Admin-Tabelle": welche Produkte zur Neige gehen.
Mini-Beispiel 4: derselbe Query, aber nur für „wenig übrig" (konventionell weniger als 5):
-- Filter „wenig übrig" hinzufügen und Ergebnis für das Admin-Panel einschränken
SELECT
p.sku,
p.name,
si.available_quantity
FROM stock_item si
JOIN product p ON p.id = si.product_id
WHERE si.available_quantity < 5
ORDER BY si.available_quantity ASC
LIMIT 20;
Beachte, wie schön sich die Query-Teile zusammenfügen: JOIN liefert den verknüpften Datensatz, WHERE filtert, ORDER BY macht die Reihenfolge verständlich, und LIMIT verhindert, dass man versehentlich „das gesamte Lager" zurückgibt.
7. Mentales Modell der Query-Ausführung
Am Anfang verwirrt SQL oft: Ein Query ist in einer Reihenfolge geschrieben, funktioniert aber logisch wie eine Pipeline. Wenn du diese Pipeline im Kopf behältst, machst du seltsame Queries viel seltener und liest fremde viel schneller. Lass uns ein einfaches, lehrreiches, aber nützliches Modell festhalten: Erst die Quellen bestimmen (FROM + JOIN), dann filtern (WHERE), dann die Antwort formen (SELECT), dann sortieren (ORDER BY), und erst dann das Ergebnis schneiden (LIMIT/OFFSET).
Hier das visuelle Schema:
flowchart TD
A["FROM + JOIN
Zeilenquellen zusammengestellt"] --> B["WHERE
Zeilen gefiltert"]
B --> C["SELECT
Spalten/Ausdrücke ausgewählt"]
C --> D["ORDER BY
Reihenfolge festgelegt"]
D --> E["LIMIT/OFFSET
Portion des Ergebnisses genommen"]
Jetzt nehmen wir einen Query und „lesen" ihn mit den Augen dieser Pipeline:
-- Beispiel eines „Schaufenster"-Querys: Produkt + Kategorie + Filter + Sortierung + Paginierung
SELECT
p.sku,
p.name,
c.name AS category_name,
p.price
FROM product p
JOIN category c ON c.id = p.category_id
WHERE p.status = 'ACTIVE'
ORDER BY p.price DESC
LIMIT 10 OFFSET 0;
Die Logik ist einfach: Zuerst haben wir Produkte mit Kategorien „zusammengeklebt", dann nur die aktiven behalten, dann die benötigten Felder ausgewählt, dann nach Preis sortiert und erst danach die ersten 10 genommen. Das ist keine Magie – das ist eine lesbare Pipeline.
Und deshalb ist die Reihenfolge wichtig: Entfernt man ORDER BY, wird LIMIT 10 zu „gib 10 zufällige aktive Produkte". Manchmal hat man Glück, und sie sehen „wie die richtigen" aus. Aber das ist schon Schamanismus – unser Ziel ist ein ingenieurmäßiger Ansatz.
8. Typische Fehler bei SELECT/JOIN
Fehler in SQL sehen selten aus wie „eine rote Zeile des Compilers auf dem ganzen Bildschirm". Meistens sehen sie aus wie „der Query funktioniert, aber gibt etwas Seltsames aus" – und das ist sogar gefährlicher. Deshalb ist es besser, sich im Voraus an einige typische Fallen zu gewöhnen und sie an ihrer Art zu erkennen.
Fehler Nr. 1: SELECT * als Lebensstil.
Während du lernst, hilft SELECT * dabei, „eine Tabelle anzusehen". Aber wenn du anfängst, so Queries für konkrete Antworten zu schreiben, machst du das Ergebnis fragil und schwer. Beim Hinzufügen einer neuen Spalte ändert sich „plötzlich" das Ergebnis und die Datenmenge, und du merkst es nicht mal, dass du unnötiges herschleppst. Die Gewohnheit, Felder explizit aufzuzählen, macht Queries verständlicher und stabiler.
Fehler Nr. 2: LIMIT/OFFSET ohne ORDER BY.
Ein solcher Query „funktioniert" oft und erzeugt die Illusion einer korrekten Paginierung. Und dann siehst du plötzlich Duplikate auf verschiedenen Seiten oder Lücken. Der Grund ist einfach: Ohne ORDER BY verspricht die Datenbank keine Reihenfolge. Deshalb wird LIMIT zu „die ersten N wie es geht".
Fehler Nr. 3: logische Bedingungen ohne Klammern (besonders mit AND/OR).
Wenn du A OR B AND C schreibst, liest SQL das nicht „menschlich". Es liest nach Prioritätsregeln, und das Ergebnis kann von deiner Absicht abweichen. Wenn in einer Bedingung AND und OR gemischt sind, ist es besser, einmal Klammern zu setzen und nicht im Wettbewerb „sparsamstes Symbolanzahl" zu gewinnen versuchen.
Fehler Nr. 4: falsche JOIN-Bedingung (falsche Spalten verbinden).
Das beliebteste Szenario: Jemand sieht zwei id-Felder und verbindet product.id = category.id, weil „na, das ist doch id". Und dann bekommt man ein seltsames Ergebnis oder gar nichts. Man muss genau den Fremdschlüssel mit dem verbinden, worauf er verweist: product.category_id = category.id. Behält man die Verbindungen (FK → PK) im Kopf, wird JOIN mechanisch und vorhersehbar.
Fehler Nr. 5: „ambiguous column" und Namensverwirrung bei JOIN.
Wenn man Tabellen verbindet, wiederholen sich Spaltennamen oft: id, name, created_at. Deshalb führt die Gewohnheit, SELECT name ohne Prefix zu schreiben, schnell zu einem Fehler oder zu Unklarheiten. Tabellen-Aliase (p, c, oi) und explizite p.name, c.name sind keine Bürokratie, sondern ein Heilmittel gegen Chaos.
GO TO FULL VERSION