SQL ist die Sprache, mit der man Daten in relationalen Datenbanken abfragt und verändert. Die Grundlagen sind in wenigen Stunden gelernt; die eigentliche Kunst liegt darin, Abfragen zu formulieren, die auch bei Millionen Zeilen schnell bleiben. Dieser Artikel führt von einfachen Abfragen zu Verknüpfungen und Indizes.
\nDie vier Grundoperationen
\n-- Lese n (SELECT)\nSELECT id, name, port FROM services WHERE aktiv = 1 ORDER BY name LIMIT 10;\n\n-- Einfuegen (INSERT)\nINSERT INTO services (name, port, aktiv) VALUES ('web01', 443, 1);\n\n-- Aendern (UPDATE) -- WHERE nie vergessen!\nUPDATE services SET aktiv = 0 WHERE name = 'web01';\n\n-- Loeschen (DELETE)\nDELETE FROM services WHERE id = 12;\nDie wichtigste Regel beim Ändern: Ein UPDATE oder DELETE ohne WHERE trifft alle Zeilen. Wer das einmal versehentlich in Produktion ausgelöst hat, prüft danach für immer. Bewährter Ablauf: Erst mit SELECT dieselbe Bedingung testen und die Zahl der Treffer prüfen, dann ändern.
Aggregatfunktionen und Gruppierung
\n-- Anzahl, Summe, Durchschnitt\nSELECT COUNT(*) AS anzahl, AVG(antwortzeit_ms) AS mittel FROM anfragen;\n\n-- Gruppieren: je Kunde eine Zeile\nSELECT kunde_id,\n COUNT(*) AS anfragen,\n AVG(antwortzeit_ms) AS mittel,\n MAX(antwortzeit_ms) AS maximum\nFROM anfragen\nGROUP BY kunde_id\nHAVING COUNT(*) > 100 -- HAVING filtert nach der Gruppierung\nORDER BY mittel DESC;\n\n-- Bedingte Aggregation in einem Durchlauf\nSELECT\n SUM(CASE WHEN status = 200 THEN 1 ELSE 0 END) AS ok,\n SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END) AS fehler\nFROM anfragen\nWHERE zeitpunkt >= NOW() - INTERVAL 1 DAY;\nDer Unterschied zwischen WHERE und HAVING ist prüfungsrelevant und im Alltag entscheidend: WHERE filtert vor der Gruppierung einzelne Zeilen, HAVING filtert nach der Gruppierung die Ergebnisgruppen. WHERE COUNT(*) > 100 ist ein Fehler.
Verknüpfungen (JOIN)
\n-- INNER JOIN: nur Zeilen, die in beiden Tabellen vorkommen\nSELECT b.id, b.titel, a.name AS autor\nFROM buecher b\nJOIN autoren a ON a.id = b.autor_id;\n\n-- LEFT JOIN: alle Buecher, auch ohne Autor (dann NULL)\nSELECT b.id, b.titel, a.name AS autor\nFROM buecher b\nLEFT JOIN autoren a ON a.id = b.autor_id;\n\n-- Zeilen ohne Gegenstueck finden (Anti-Join)\nSELECT b.id, b.titel\nFROM buecher b\nLEFT JOIN autoren a ON a.id = b.autor_id\nWHERE a.id IS NULL;\n\n-- Ausleihvorgaenge mit Anzahl je Buch\nSELECT b.titel, COUNT(a.id) AS ausleihen\nFROM buecher b\nLEFT JOIN ausleihen a ON a.buch_id = b.id\nGROUP BY b.id, b.titel\nORDER BY ausleihen DESC;\nDer klassische Fehler: Beim Join über mehrere Tabellen entstehen schnell unerwartet viele Zeilen (Kreuzprodukt), wenn eine Verknüpfungsbedingung fehlt oder zu unscharf ist. Zählregel: Nach jedem neuen Join die Zeilenzahl mit COUNT(*) prüfen – wächst sie unerwartet stark, ist die Bedingung falsch.
Indizes: warum Abfragen schnell oder langsam sind
\nOhne Index liest die Datenbank für jede Abfrage die gesamte Tabelle (Full Table Scan). Ein Index ist eine sortierte Zusatzstruktur, in der die Datenbank Zieldatensätze direkt findet – vergleichbar mit dem Register eines Buchs.
\n-- Index anlegen\nCREATE INDEX idx_anfragen_kunde ON anfragen (kunde_id);\nCREATE INDEX idx_anfragen_zeit ON anfragen (zeitpunkt);\n\n-- Mehrspaltig: Reihenfolge ist entscheidend (linke Spalte zuerst nutzbar)\nCREATE INDEX idx_anfragen_kunde_zeit ON anfragen (kunde_id, zeitpunkt);\n\n-- Eindeutig\nCREATE UNIQUE INDEX idx_benutzer_mail ON benutzer (email);\n\n-- Pruefen, ob der Index genutzt wird\nEXPLAIN SELECT * FROM anfragen WHERE kunde_id = 7;\nEXPLAIN ANALYZE SELECT ... ; -- tatsaechliche Ausfuehrungszeiten\nWann ein Index (nicht) hilft:
\n- \n
- Hilft bei
WHERE,JOIN-Bedingungen,ORDER BYundGROUP BYauf indizierten Spalten. \n - Hilft nicht, wenn eine Funktion auf der Spalte angewendet wird:
WHERE YEAR(zeitpunkt) = 2026ist langsam,WHERE zeitpunkt >= '2026-01-01' AND zeitpunkt < '2027-01-01'nutzt den Index. \n - Hilft nicht bei sehr geringer Selektivität – eine Spalte mit nur zwei Ausprägungen wie ein Ja/Nein-Feld ist als einzelner Index meist nutzlos. \n
- Kostet Schreibgeschwindigkeit und Speicher. Jeder Index muss bei jedem
INSERTundUPDATEmitgepflegt werden. \n
Unterabfragen und Window-Funktionen
\n-- Unterabfrage im WHERE\nSELECT name FROM produkte\nWHERE preis > (SELECT AVG(preis) FROM produkte);\n\n-- EXISTS statt IN bei grossen Ergebnismengen\nSELECT k.* FROM kunden k\nWHERE EXISTS (SELECT 1 FROM bestellungen b WHERE b.kunde_id = k.id);\n\n-- Window-Funktion: Rang ohne Gruppierung zu verlieren\nSELECT kunde_id, betrag,\n ROW_NUMBER() OVER (PARTITION BY kunde_id ORDER BY betrag DESC) AS rang,\n SUM(betrag) OVER (PARTITION BY kunde_id) AS summe\nFROM bestellungen;\nWindow-Funktionen sind seit SQL:2003 im Standard und in allen gängigen Systemen verfügbar. Sie lösen Aufgaben, für die man früher verschachtelte Abfragen oder Selbstverknüpfungen brauchte.
\nPraktische Regeln
\n- \n
- Nur die Spalten wählen, die man braucht.
SELECT *verhindert in manchen Systemen die Nutzung eines Covering Index und verschwendet Bandbreite. \n - Bei großen Ergebnissen immer
LIMITsetzen – auch beim Testen. \n EXPLAINvor dem Optimieren. Ohne Ausführungsplan ist jede Verbesserung geraten. \n - Indizes an den Abfragen ausrichten, nicht an den Tabellen. Ein Index, den keine Abfrage nutzt, bremst nur das Schreiben. \n
- Parameter verwenden, keine Zeichenkettenverkettung. Das verhindert SQL-Injection und lässt die Datenbank Abfragepläne wiederverwenden. \n
- Transaktionen bewusst setzen. Mehrere zusammengehörige Änderungen gehören in
BEGIN/COMMIT, damit ein Fehler sie vollständig zurücknimmt. \n
Verwandte Grundlagen: SQL: Abfragen und Grundlagen, MySQL-Befehle, SQL-Injection, SQL-Index.