- verstehen, wie ein Index Abfragen beschleunigt
- die Kosten von Indizes beim Schreiben und Speichern einschätzen
- zusammengesetzte Indizes in der richtigen Reihenfolge anlegen
- langsame Abfragen mit EXPLAIN analysieren und typische Probleme wie N+1 erkennen
Die häufigste Ursache für langsame Webanwendungen ist nicht die Programmiersprache, nicht der Server und nicht das Framework, sondern eine langsame Datenbankabfrage. Und die häufigste Ursache für eine langsame Abfrage ist ein fehlender oder falscher Index. Die gute Nachricht: Solche Probleme lassen sich meist mit wenig Aufwand beheben, wenn man versteht, wie Indizes funktionieren und wie man die Datenbank fragt, was sie gerade tut.
1. Was ein Index ist
Ohne Index muss die Datenbank für eine Abfrage wie „finde den Nutzer mit dieser E-Mail-Adresse“ jede Zeile der Tabelle lesen und prüfen. Bei tausend Zeilen fällt das nicht auf, bei zehn Millionen dauert es Sekunden. Mit einem B-Baum-Index findet sie den Eintrag in wenigen Schritten, weil sie wie in einem Telefonbuch gezielt zur richtigen Stelle springt. Die Zahl der Schritte wächst nur logarithmisch mit der Tabellengröße: Bei einer Million Zeilen sind es etwa zwanzig, bei einer Milliarde etwa dreißig. Probieren Sie es aus:
SELECT * FROM nutzer WHERE email = 'anna@beispiel.de';
1.000.000
gelesene Einträge
~50 ms
geschätzte Dauer
2. Indizes kosten auch etwas
Wenn Indizes so wirksam sind, warum legt man nicht auf jede Spalte einen an? Weil jeder Index bei jedem Schreibvorgang aktualisiert werden muss und Speicherplatz belegt. Eine Tabelle mit zehn Indizes wird bei jedem Einfügen elfmal beschrieben. Bei schreiblastigen Tabellen, etwa für Protokolle oder Ereignisse, können zu viele Indizes die Schreibleistung spürbar senken. Die Kunst liegt darin, genau die Indizes anzulegen, die die wichtigen Abfragen brauchen – nicht mehr und nicht weniger.
| Abfrage | Index hilft? | Hinweis |
|---|---|---|
| WHERE email = … | ja | klassischer Fall, oft als eindeutiger Index |
| WHERE erstellt_am > … | ja | Bereichsabfragen funktionieren mit B-Baum |
| ORDER BY erstellt_am LIMIT 20 | ja | Index liefert bereits sortiert |
| WHERE lower(email) = … | nur mit Index auf lower(email) | Funktionen verhindern normale Indizes |
| WHERE name LIKE '%muster%' | nein (B-Baum) | dafür Volltext- oder Trigramm-Index |
| WHERE status = 'aktiv' bei 95 % aktiv | kaum | geringe Selektivität – Datenbank liest lieber alles |
3. Zusammengesetzte Indizes
Viele Abfragen filtern nach mehreren Spalten, etwa „alle Buchungen eines Salons an einem Tag“. Dafür gibt es zusammengesetzte Indizes über mehrere Spalten. Dabei ist die Reihenfolge entscheidend. Ein Index auf (salon_id, datum) funktioniert wie ein Telefonbuch, das zuerst nach Nachnamen und dann nach Vornamen sortiert ist: Er hilft bei Abfragen nach dem Salon allein und nach Salon und Datum, aber kaum bei Abfragen nur nach dem Datum. Als Faustregel gehören Spalten mit Gleichheitsbedingungen nach vorn und Spalten mit Bereichen oder Sortierung nach hinten.
4. Abfragen analysieren
Die Datenbank verrät, wie sie eine Abfrage ausführt. In PostgreSQL zeigt der Befehl EXPLAIN den geplanten Ablauf, EXPLAIN ANALYZE führt die Abfrage tatsächlich aus und zeigt die gemessenen Zeiten. Die wichtigsten Hinweise darin: Ein „Seq Scan“ auf einer großen Tabelle bedeutet, dass jede Zeile gelesen wird. Ein „Index Scan“ oder „Index Only Scan“ bedeutet, dass ein Index genutzt wird. Und große Abweichungen zwischen geschätzten und tatsächlichen Zeilenzahlen deuten auf veraltete Statistiken hin. Zusätzlich lohnt die Erweiterung pg_stat_statements, die für alle Abfragen misst, wie oft sie laufen und wie viel Zeit sie insgesamt kosten. Sie zeigt, welche Abfragen die größte Last erzeugen – oft nicht die langsamste einzelne, sondern eine mittelschnelle, die tausendmal pro Minute läuft.
5. Das N+1-Problem
Ein Problem, das in fast jeder Anwendung mit einem ORM auftaucht, ist das N+1-Problem. Eine Seite zeigt zwanzig Kurse mit dem jeweiligen Namen der Lehrkraft. Der Code lädt zuerst die zwanzig Kurse mit einer Abfrage und dann für jeden Kurs die Lehrkraft mit einer eigenen Abfrage – insgesamt 21 Abfragen statt einer oder zwei. Jede einzelne ist schnell, aber zusammen mit der Netzwerkzeit zur Datenbank wird die Seite spürbar langsam, und unter Last vervielfacht sich das Problem. Die Lösung ist, zusammengehörige Daten gemeinsam zu laden, etwa mit einem JOIN oder den Funktionen des ORM zum vorausschauenden Laden. Viele Überwachungswerkzeuge erkennen N+1-Muster automatisch.
Weitere häufige Ursachen langsamer Abfragen sind das Laden aller Spalten, obwohl nur wenige gebraucht werden, fehlende Begrenzungen bei Listen – eine Seite ohne LIMIT lädt irgendwann hunderttausend Zeilen – und die Paginierung mit großen OFFSET-Werten, bei der die Datenbank alle übersprungenen Zeilen trotzdem lesen muss. Für lange Listen ist die Paginierung über einen Schlüssel, etwa „die nächsten 20 nach ID 4711“, deutlich schneller.
6. Eine Routine für schnelle Abfragen
In der Praxis bewährt sich eine einfache Routine. Beim Entwurf werden die wichtigsten Abfragen notiert und passende Indizes geplant. Beim Entwickeln wird jede neue Abfrage einmal mit EXPLAIN geprüft. Im Betrieb zeigt pg_stat_statements regelmäßig die teuersten Abfragen, und das Protokoll langsamer Abfragen meldet Ausreißer. Nicht genutzte Indizes werden gelegentlich entfernt, weil sie nur Schreibleistung kosten. Mit dieser Routine bleiben die meisten Anwendungen auch bei wachsenden Datenmengen schnell, lange bevor Themen wie Replikation oder Partitionierung relevant werden, um die es in der nächsten Lektion geht.
Ein Beispiel aus der Praxis zeigt, wie groß die Wirkung sein kann. Eine Lernplattform lud die Übersichtsseite eines Kurses in über zwei Sekunden. Die Analyse mit pg_stat_statements zeigte zwei Ursachen: Für jede der vierzig Lektionen wurde der Fortschritt des Nutzers einzeln abgefragt, und diese Abfrage filterte nach Nutzer und Lektion ohne passenden Index. Ein zusammengesetzter Index auf (nutzer_id, lektion_id) und eine gemeinsame Abfrage für alle Lektionen senkten die Ladezeit auf unter hundert Millisekunden. Es waren weder neue Server noch ein Cache nötig – nur ein besseres Verständnis dafür, was die Datenbank tatsächlich tat. Solche Verbesserungen sind in fast jeder gewachsenen Anwendung möglich und gehören zu den lohnendsten Maßnahmen überhaupt.
Ein Hinweis zum Schluss: Wiederholen Sie diese Analyse regelmäßig, denn mit wachsenden Datenmengen und neuen Funktionen verändern sich die teuersten Abfragen. Was heute schnell ist, kann in einem Jahr zum Engpass werden – und umgekehrt können Indizes überflüssig werden, die niemand mehr braucht.
Legen Sie sich dafür am besten eine kleine Checkliste an und gehen Sie sie bei jeder größeren Änderung am Datenmodell durch: Welche neuen Abfragen entstehen, welche Indizes brauchen sie, und welche alten werden überflüssig?
- [1]PostgreSQL Global Development Group (2026): Documentation – Indexes; Using EXPLAIN; pg_stat_statements.
- [2]Winand, M. (2012): SQL Performance Explained. Wien: Eigenverlag (use-the-index-luke.com).
- [3]Bayer, R.; McCreight, E. (1972): Organization and Maintenance of Large Ordered Indexes. Acta Informatica 1(3), 173–189.
- [4]Kleppmann, M. (2017): Designing Data-Intensive Applications. Sebastopol: O'Reilly, Kap. 3.
Stand: September 2026. Kursmaterial der Klarwerk Akademie. Zahlen zu Latenzen und Kosten sind Größenordnungen zur Orientierung, keine Messwerte.