Relationale Datenbanken
Tabellen, Schlüssel und SQL – und die zwei Dinge, die im Alltag wirklich zählen: warum ein Index aus zehn Millionen Lesezugriffen vier macht und warum zwei gleichzeitige Buchungen trotz Transaktion denselben letzten Platz bekommen können.
Eine relationale Datenbank speichert Daten in Tabellen: Zeilen sind Datensätze, Spalten sind Eigenschaften. Jede Zeile hat einen Primärschlüssel, und Tabellen verweisen über Fremdschlüssel aufeinander. Abgefragt wird mit SQL – man beschreibt, was man will, die Datenbank entscheidet, wie sie es holt.
Zwei Dinge machen den Unterschied in der Praxis: Indizes – meist B-Bäume, die eine Suche in einer Milliarde Zeilen mit vier Lesezugriffen erledigen. Und Transaktionen – sie garantieren, dass zusammengehörige Änderungen ganz oder gar nicht passieren. Wie gut sich gleichzeitige Transaktionen voneinander abschirmen, regelt das Isolationslevel – und das Standardlevel schützt weniger, als die meisten denken.
Tabellen, Schlüssel, Beziehungen
Ein kleiner Online-Shop: Kunden geben Bestellungen auf, eine Bestellung enthält mehrere Positionen, jede Position verweist auf einen Artikel.
Warum die Daten auf mehrere Tabellen verteilt werden, statt Name und E-Mail des Kunden in jede Bestellung zu schreiben? Weil sonst eine geänderte E-Mail-Adresse an hundert Stellen geändert werden müsste – und irgendwann irgendwo nicht. Diese Aufteilung heißt Normalisierung: jede Tatsache genau einmal. Für Auswertungen, bei denen nur gelesen wird, verdoppelt man manchmal bewusst Daten, um Abfragen zu beschleunigen – das ist dann Denormalisierung.
SQL und JOINs
Verteilte Daten muss man beim Lesen wieder zusammenführen. Das macht der JOIN: Er verknüpft Zeilen zweier Tabellen, deren Schlüssel zusammenpassen. Welche Art von JOIN man nimmt, entscheidet, was mit Zeilen ohne Partner passiert:
Indizes: vier Zugriffe statt zehn Millionen
Ohne Hilfe muss die Datenbank für WHERE email = '…' jede Zeile anschauen – ein Full Table Scan. Ein Index ist eine zusätzliche, sortierte Struktur, meist ein B-Baum (genauer ein B+-Baum): ein sehr breiter, sehr flacher Suchbaum, dessen Knoten genau eine Speicherseite füllen. In jedem Knoten steht, in welchem Teilbaum ein Wert liegen muss. Gesucht wird von oben nach unten – wie im Telefonbuch erst die richtige Seite, dann der Name.
Weil jeder Knoten Hunderte Verweise hat, wächst die Tiefe des Baums extrem langsam: Von einer Million auf eine Milliarde Zeilen kommt gerade eine Ebene hinzu. Die oberen Ebenen liegen ohnehin fast immer im Arbeitsspeicher. Und weil die Blätter sortiert und miteinander verkettet sind, helfen B-Bäume auch bei Bereichen (BETWEEN, >) und bei ORDER BY.
Umsonst ist ein Index nicht: Jedes INSERT, UPDATE und DELETE muss ihn mitpflegen, und er braucht Platz. Ob die Datenbank ihn nutzt, sieht man mit EXPLAIN – das zeigt den Ausführungsplan, den der Abfrageoptimierer gewählt hat.
Transaktionen und ACID
Eine Buchung besteht aus mehreren Schritten: Platz reduzieren, Buchung anlegen, vielleicht Zahlung vermerken. Eine Transaktion fasst sie zusammen. Sie garantiert vier Dinge, zusammengefasst als ACID:
Atomarität
Alles oder nichts. Stürzt etwas mittendrin ab, wird alles zurückgerollt.
Konsistenz
Nach der Transaktion gelten alle Regeln wieder – Fremdschlüssel, Eindeutigkeit, Prüfbedingungen.
Isolation
Gleichzeitige Transaktionen stören sich nicht – in welchem Maß, bestimmt das Isolationslevel.
Dauerhaftigkeit
Was bestätigt ist, übersteht auch einen Stromausfall. Dafür schreibt die Datenbank jede Änderung zuerst in ein Write-Ahead-Log und erzwingt es mit fsync – dasselbe Prinzip wie das Journal im Dateisystem.
Der letzte freie Platz
Das „I“ ist das tückischste. Die meisten Datenbanken – PostgreSQL, Oracle, SQL Server – arbeiten standardmäßig mit READ COMMITTED: Jede Abfrage sieht alles, was andere bis dahin bestätigt haben. Das verhindert viel, aber nicht alles. Zwei Kunden buchen gleichzeitig den letzten Platz eines Flugs. Probier die Varianten durch:
| Isolationslevel | Was es verhindert | Standard bei |
|---|---|---|
| READ UNCOMMITTED | praktisch nichts – man sieht sogar Unbestätigtes | – |
| READ COMMITTED | Lesen von Unbestätigtem | PostgreSQL, Oracle, SQL Server |
| REPEATABLE READ | zusätzlich: Ein zweites Lesen derselben Zeile liefert dasselbe | MySQL (InnoDB) |
| SERIALIZABLE | alles: Ergebnis wie bei strikt nacheinander ausgeführten Transaktionen – mit dem Preis, dass manche abbrechen und wiederholt werden müssen | – |
Für Entwickler übersetzt
void buche(long flugId, long kundeId) throws SQLException {
try (Connection con = dataSource.getConnection()) { // aus dem Pool, z. B. HikariCP
con.setAutoCommit(false); // Transaktion beginnt
try {
try (var ps = con.prepareStatement(
"UPDATE flug SET frei = frei - 1 WHERE id = ? AND frei > 0")) {
ps.setLong(1, flugId);
if (ps.executeUpdate() == 0) throw new AusgebuchtException(flugId);
}
try (var ps = con.prepareStatement(
"INSERT INTO buchung (flug_id, kunde_id) VALUES (?, ?)")) {
ps.setLong(1, flugId);
ps.setLong(2, kundeId);
ps.executeUpdate();
}
con.commit();
} catch (SQLException | RuntimeException e) {
con.rollback();
throw e;
}
}
}| Thema | Was man wissen sollte |
|---|---|
PreparedStatement | Werte immer als Parameter übergeben, nie per String-Verkettung ins SQL – sonst SQL-Injection. |
| Auto-Commit | JDBC bestätigt standardmäßig jede einzelne Anweisung sofort. Für eine Transaktion über mehrere Anweisungen muss man es abschalten – Spring erledigt das mit @Transactional. |
| Optimistisches Locking | In JPA genügt ein Feld mit @Version. Bei einem Konflikt kommt eine OptimisticLockException, die man mit einem neuen Versuch beantwortet. |
| Das N+1-Problem | Lädt man 100 Bestellungen per JPA und greift dann auf bestellung.getKunde() zu, feuert Hibernate oft 100 einzelne Abfragen hinterher. Abhilfe: JOIN FETCH in der Abfrage oder ein @EntityGraph. Im SQL-Log sofort zu erkennen. |
| Connection-Pool | Eine Verbindung aufzubauen kostet einen Netzwerk-Handshake und eine Anmeldung. Pools halten Verbindungen offen. Zu große Pools schaden aber: Die Datenbank hat nur begrenzt Kerne. |
Häufige Irrtümer
WHERE spalte = NULL findet leere Felder.“ – Es findet nichts. NULL bedeutet „unbekannt“, und unbekannt = unbekannt ist wieder unbekannt. Richtig ist IS NULL.