Premium
Performance
6 Min. Lesezeit·

SQL-Abfragen optimieren: 10 Wege, eine langsame Abfrage zu beschleunigen

Eine langsame Abfrage wird fast nie durch „einen stärkeren Server" geheilt. In neun von zehn Fällen liegt die Ursache in der Abfrage selbst oder in einem fehlenden Index — und ist in Minuten behoben. Hier ist eine Checkliste mit zehn Techniken in der Reihenfolge, in der man sie anwenden sollte.

1. Beginnen Sie mit EXPLAIN: Was tut das DBMS eigentlich

Blind zu optimieren ist sinnlos. Der Befehl EXPLAIN zeigt den Ausführungsplan — wie genau das DBMS die Abfrage ausführen will:

MySQL 8.1
EXPLAIN SELECT * FROM orders WHERE user_id = 1;
Seq Scan on orders  (cost=0.00..10.70 rows=60 width=44)
  Filter: (user_id = 1)

Seq Scan bedeutet vollständiges Durchlaufen der Tabelle: Das DBMS liest alle Zeilen nacheinander und prüft die Bedingung auf jeder. Bei tausend Zeilen unsichtbar, bei zehn Millionen — Sekunden. Ziel der meisten Optimierungen ist es, einen Seq Scan über eine große Tabelle in einen Index Scan zu verwandeln, den punktgenauen Zugriff über den Index.

EXPLAIN ANALYZE führt die Abfrage zusätzlich aus und zeigt die tatsächliche Zeit und Zeilenzahl jedes Schritts — genau damit sollte jede Analyse einer langsamen Abfrage beginnen.

2. Indizieren Sie die Spalten aus WHERE und JOIN

Der Index ist das wichtigste Werkzeug für schnelles Lesen: Statt die ganze Tabelle zu durchlaufen, steigt das DBMS über eine sortierte Struktur direkt zu den passenden Zeilen hinab. Indexkandidaten sind Spalten, die regelmäßig in WHERE, JOIN-Bedingungen und ORDER BY auftauchen:

MySQL 8.1
CREATE INDEX idx_orders_user_id ON orders (user_id);

Wie sehr das das Bild verändert, haben wir auf einer Tabelle mit einer Million Bestellungen gemessen (PostgreSQL 17). Die Suche nach den Bestellungen eines Kunden vor und nach dem Anlegen des Index:

ohne Index:  Seq Scan, alle 1 000 000 Zeilen durchsucht — 23.0 ms
mit Index:   Index Scan, nur die 20 passenden Zeilen gelesen — 0.1 ms

Ein Unterschied um den Faktor zweihundert bei exakt derselben Abfrage — und er wächst mit der Tabelle. Die absoluten Zahlen hängen von der Hardware ab, die Größenordnung nicht.

Prüfen Sie gesondert die Fremdschlüssel: Primärschlüssel werden automatisch indiziert, die verweisenden Spalten (user_id in der Bestelltabelle) dagegen nicht — obwohl genau über sie die Joins laufen. Wie Indizes funktionieren, zeigt die Lektion Indizes in SQL.

Die Kehrseite: Jeder Index verlangsamt Einfügen und Aktualisieren, denn er muss gepflegt werden. Indizieren Sie, wonach Sie suchen — nicht alles auf Vorrat.

3. Wickeln Sie indizierte Spalten nicht in Funktionen ein

Eine Bedingung, die die Spalte in eine Funktion einwickelt, nimmt dem DBMS die Möglichkeit, den Index zu nutzen — es muss die Funktion für jede Zeile auswerten:

MySQL 8.1
-- der Index auf order_date wird nicht genutzt
SELECT * FROM orders WHERE EXTRACT(YEAR FROM order_date) = 2024;
Seq Scan on orders  (cost=0.00..11.84 rows=2 width=44)
  Filter: (EXTRACT(year FROM order_date) = '2024'::numeric)

Schreiben Sie die Bedingung so um, dass die Spalte „nackt" bleibt — als Bereich:

MySQL 8.1
-- der Index auf order_date greift
SELECT * FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';

Dieselbe Regel brechen WHERE LOWER(email) = ..., WHERE price * 1.2 > 100, WHERE CAST(...). Der Universaltrick: die Berechnung von der Spalte auf die Konstante verschieben (price > 100 / 1.2).

Der Preis des Fehlers auf einer Million Zeilen mit Index auf order_date — ein Eintagesfilter auf zwei Arten:

WHERE order_date::date = '2025-03-01'                Plan: Seq Scan — 24.1 ms
WHERE order_date >= '2025-03-01'
  AND order_date < '2025-03-02'                      Plan: Index Only Scan — 0.16 ms

Beide Abfragen liefern dieselben Zeilen, doch die erste ist rund hundertfünfzigmal langsamer: Der ::date-Cast ist dieselbe Funktion um die Spalte.

Die Abfrage SELECT * FROM users WHERE YEAR(created_at) = 2026 ist langsam, obwohl created_at indiziert ist. Woran liegt es?

4. Streichen Sie SELECT *

SELECT * schleppt alle Spalten mit: schwere Textfelder, JSON, alles, was Sie nicht brauchen. Das ist überflüssiger Festplatten-I/O, überflüssiger Traffic zur Anwendung und eine verpasste Chance: Reichen einer Abfrage die Spalten, die schon im Index stehen, kann das DBMS antworten, ohne die Tabelle überhaupt zu öffnen. Listen Sie nur auf, was Sie verwenden:

MySQL 8.1
SELECT order_id, total_amount FROM orders WHERE user_id = 1;

5. Vorsicht bei LIKE mit führendem Prozentzeichen

Das Muster LIKE 'SUMMER%' nutzt den Index: eine Präfixsuche wie im Wörterbuch. Das Muster LIKE '%SUMMER%' nicht: Der Treffer kann überall beginnen, dem DBMS bleibt nur, alle Zeilen durchzugehen.

Wird die Teilstringsuche regelmäßig auf einer großen Tabelle gebraucht, ist das ein Fall für die Volltextsuche (FULLTEXT in MySQL, tsvector/pg_trgm in PostgreSQL) — nicht für LIKE.

Welches der LIKE-Muster kann einen gewöhnlichen Index auf der Spalte name nutzen?

6. Prüfen Sie die Unterabfragen: EXISTS vs. IN

Für die Prüfung „gibt es einen verknüpften Datensatz" bevorzugen Sie EXISTS — es stoppt beim ersten Treffer, während IN zuerst die gesamte Liste materialisiert:

MySQL 8.1
SELECT u.user_id
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);

Eine eigene Falle ist NOT IN mit Unterabfrage: Enthält die Liste auch nur ein NULL, liefert die Bedingung gar keine Zeilen. NOT EXISTS ist frei von diesem Problem und meist schneller.

7. Filtern Sie vor der Aggregation, nicht danach

WHERE verwirft Zeilen vor der Gruppierung, HAVING danach. Bedingungen, die nicht von Aggregaten abhängen, gehören immer in WHERE:

MySQL 8.1
-- vergebliche Arbeit: alles gruppiert, dann die Hälfte weggeworfen
SELECT user_id, SUM(total_amount)
FROM orders
GROUP BY user_id
HAVING MIN(order_date) >= '2024-01-01';

-- geht es um einzelne Zeilen, filtern Sie sofort
SELECT user_id, SUM(total_amount)
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY user_id;

Beachten Sie: Die beiden Abfragen unterscheiden sich inhaltlich — die zweite summiert nur die Bestellungen von 2024. Das Prinzip bleibt: Je früher das DBMS überflüssige Zeilen loswird, desto weniger Arbeit haben alle folgenden Schritte.

8. Pagination: OFFSET liest alles, was es überspringt

Die klassische Pagination LIMIT 20 OFFSET 100000 zwingt das DBMS, hunderttausend Zeilen zu lesen und wegzuwerfen, um zwanzig zurückzugeben. Je tiefer die Seite, desto langsamer die Abfrage.

Die Lösung ist Keyset-Pagination: Wir merken uns den zuletzt gezeigten Wert und machen von dort über den Index weiter:

MySQL 8.1
SELECT order_id, order_date
FROM orders
WHERE order_id > 100020
ORDER BY order_id
LIMIT 20;

Diese Abfrage läuft für die erste Seite genauso schnell wie für die tausendste. Auf derselben Tabelle mit einer Million Zeilen: eine Seite nach OFFSET 500000 braucht 79.6 ms, die Keyset-Abfrage mit WHERE order_id > 500000 — 0.05 ms.

9. Zusammengesetzter Index: die Spaltenreihenfolge entscheidet

Filtert eine Abfrage nach zwei Spalten gleichzeitig, sind zwei einzelne Indizes schlechter als ein zusammengesetzter:

MySQL 8.1
CREATE INDEX idx_orders_user_status ON orders (user_id, status);

Die Regel: Ein Index auf (user_id, status) bedient Bedingungen auf user_id und auf das Paar user_id + status, hilft aber nicht einer Abfrage nur nach status — so wie ein nach Nachnamen sortiertes Telefonbuch für die Suche nach Vornamen nutzlos ist. Stellen Sie die Spalte nach vorn, die öfter und schärfer filtert.

10. Beseitigen Sie N+1 und zeilenweise Änderungen

Manchmal ist langsames SQL nicht eine Abfrage, sondern tausend kleine. Der Klassiker ist eine Schleife in der Anwendung: erst die Bestellliste holen, dann zu jeder Bestellung den Kunden mit einer eigenen Abfrage. Die Datenbank führt N+1 Abfragen statt eines JOIN aus:

MySQL 8.1
SELECT o.order_id, u.email
FROM orders o
JOIN users u ON u.user_id = o.user_id;

Dieselbe Logik gilt fürs Schreiben: Hundert Zeilen mit einem INSERT INTO ... VALUES (...), (...), ... einzufügen ist um ein Mehrfaches schneller als hundert einzelne INSERT — jede Anweisung zahlt die Netzwerkrunde und den Transaktions-Overhead.

Wo es tiefer geht

Diese Checkliste deckt die typischen Fälle ab, doch hinter jedem Punkt steckt eigene Tiefe: Selektivität von Indizes, abdeckende und partielle Indizes, das Kostenmodell des Optimizers, JOIN- und Sortieroptimierung. Genau dem widmet sich der Kurs SQL-Optimierung in PostgreSQL — mit echten Ausführungsplänen und Messungen zu jeder Technik.

Wie weiter

Passende Artikel