Hallo zusammen. In diesem Artikel zeige ich einige einfache Wege, SQL-Abfragen zu beschleunigen und effizienter zu machen. Die Beispiele fokussieren sich auf PostgreSQL, aber die meisten Ideen gelten auch fuer andere relationale Datenbanken.
Das Ziel ist nicht, einzelne Tricks auswendig zu lernen. Das Ziel ist zu verstehen, warum bestimmte Query-Formen es der Datenbank leichter machen, Indizes zu nutzen, wiederholte Scans zu vermeiden und Ergebnisse schneller zurueckzugeben.

1. IN durch JOIN auf eine virtuelle Tabelle ersetzen
Problem (1)
Eine grosse Werteliste in einer IN-Klausel kann zu vielen Pruefungen pro Zeile fuehren. Bei kleinen Listen ist das oft egal, aber bei langen Listen kann es den Planner belasten und unnoetige CPU-Arbeit erzeugen.
Loesung (1)
Nutze eine virtuelle Tabelle mit VALUES und joine sie mit der Haupttabelle. Dadurch bekommt die Datenbank oft eine Query-Form, die leichter zu optimieren ist.
-- VORHER:EXPLAINSELECT order_id, city FROM orders WHERE city IN ('Berlin', 'Paris', 'Rome');
-- NACHHER:EXPLAINSELECT o.order_id, o.city FROM orders AS o JOIN ( VALUES ('Berlin'), ('Paris'), ('Rome') ) AS v(city_name) ON o.city = v.city_name;Warum das schneller sein kann (1)
Wenn die Werteliste wie eine Tabelle behandelt wird, kann PostgreSQL manchmal einen effizienteren Ausfuehrungsplan erstellen als bei einer langen IN-Klausel.
Das ist besonders nuetzlich, wenn die Werteliste gross ist, wiederholt verwendet wird oder dynamisch von der Anwendung erzeugt wird.
2. ANY(ARRAY[]) statt IN in PostgreSQL verwenden
Problem (2)
Eine grosse IN-Liste kann eine Abfrage verlangsamen, weil jeder moegliche Treffer geprueft werden muss.
Loesung (2)
In PostgreSQL kann die Syntax = ANY(ARRAY[...]) eine gute Alternative sein. Sie ist besonders praktisch, wenn die Anwendung Werte ohnehin als Array verarbeitet, und PostgreSQL kann die Pruefung beenden, sobald ein Treffer gefunden wurde.
-- VORHER:EXPLAINSELECT product_id, quantity FROM order_items WHERE product_id IN (101, 202, 303, 404);
-- NACHHER:EXPLAINSELECT product_id, quantity FROM order_items WHERE product_id = ANY(ARRAY[101, 202, 303, 404]);Warum das schneller sein kann (2)
ANY kann in manchen Faellen unnoetige Vergleiche reduzieren, besonders in Kombination mit einem sauberen Query-Plan und indizierten Spalten.
Diese Technik ist PostgreSQL-spezifisch, daher sollte das Verhalten vor der Uebertragung auf andere Datenbanksysteme geprueft werden.
3. JOIN statt korrelierter Subquery verwenden
Problem (3)
Korrelierte Subqueries laufen in Beziehung zu jeder Zeile der aeusseren Query. Dadurch kann dieselbe Tabelle mehrfach gescannt werden, was bei wachsenden Datenmengen teuer wird.
Loesung (3)
Ersetze die korrelierte Subquery nach Moeglichkeit durch einen normalen JOIN oder eine nicht-korrelierte Subquery.
-- VORHER:EXPLAINSELECT c.customer_id, c.name FROM customers AS c WHERE EXISTS ( SELECT 1 FROM orders AS o WHERE o.customer_id = c.customer_id AND o.amount > 1000 );
-- NACHHER:EXPLAINSELECT DISTINCT c.customer_id, c.name FROM customers AS c JOIN orders AS o ON c.customer_id = o.customer_id WHERE o.amount > 1000;Warum das schneller sein kann (3)
Ein JOIN gibt PostgreSQL mehr Spielraum, Indizes zu nutzen und die Beziehung zwischen den Tabellen zu optimieren. Gleichzeitig wird vermieden, dieselbe Suche fuer jede Zeile der aeusseren Query erneut auszufuehren.
Das DISTINCT ist hier notwendig, weil ein Kunde mehrere passende Bestellungen haben kann.
4. BETWEEN statt Datumsfunktionen verwenden
Problem (4)
Ausdruecke wie dieser sind haeufig:
EXTRACT(YEAR FROM order_date) = 2026Das Problem: Wenn eine Funktion auf eine Spalte angewendet wird, kann die Datenbank einen normalen Index auf dieser Spalte oft nicht effizient nutzen. PostgreSQL muss die Funktion fuer viele Zeilen auswerten, statt einen sauberen Datumsbereich zu scannen.
Loesung (4)
Nutze explizite Datumsbereiche mit BETWEEN, oder fuer Timestamp-Spalten noch besser eine inklusive Untergrenze und eine exklusive Obergrenze.
-- VORHER:EXPLAINSELECT * FROM orders WHERE EXTRACT(YEAR FROM order_date) = 2026 AND EXTRACT(MONTH FROM order_date) = 5;
-- NACHHER:EXPLAINSELECT * FROM orders WHERE order_date BETWEEN '2026-05-01'::DATE AND '2026-05-31'::DATE;Fuer Timestamp-Spalten ist diese Form oft sicherer:
SELECT * FROM orders WHERE order_date >= '2026-05-01'::DATE AND order_date < '2026-06-01'::DATE;Warum das schneller sein kann (4)
Das direkte Filtern auf der indizierten Datumsspalte erlaubt einen Index-Range-Scan. Die Datenbank kann direkt zum relevanten Indexbereich springen, statt Datumsfunktionen zeilenweise auszufuehren.
5. EXISTS statt JOIN verwenden, wenn nur Existenz geprueft wird
Problem (5)
Wenn nur geprueft werden soll, ob mindestens eine passende Zeile in einer anderen Tabelle existiert, kann ein JOIN mehr Daten laden als noetig.
Loesung (5)
Nutze EXISTS. Die Datenbank kann stoppen, sobald der erste passende Datensatz gefunden wurde.
-- VORHER:EXPLAINSELECT COUNT(DISTINCT o.order_id) FROM orders AS o JOIN order_items AS i ON o.order_id = i.order_id;
-- NACHHER:EXPLAINSELECT COUNT(DISTINCT o.order_id) FROM orders AS o WHERE EXISTS ( SELECT 1 FROM order_items AS i WHERE i.order_id = o.order_id );Warum das schneller sein kann (5)
Die Query muss nicht alle verknuepften Zeilen abrufen. Sobald PostgreSQL einen passenden Datensatz findet, ist die Bedingung erfuellt und die Engine kann fortfahren.
Das ist ein sauberes Muster fuer Berechtigungspruefungen, Relationship-Checks und Filter, bei denen die tatsaechlichen Join-Daten nicht gebraucht werden.
6. Kleine Query-Hygiene konsequent anwenden
Einige Optimierungen wirken einzeln unspektakulaer, addieren sich aber in Produktionssystemen schnell.
Nur benoetigte Spalten auswaehlen
Vermeide SELECT *, wenn nur wenige Felder gebraucht werden. Unnoetige Spalten erhoehen IO, Speicherverbrauch, Netzwerktransfer und verhindern manchmal Index-only Scans.
-- VORHER:SELECT * FROM customers;
-- NACHHER:SELECT customer_id, name, email FROM customers;LIMIT verwenden, wenn nur ein Ausschnitt benoetigt wird
Wenn UI oder Skript nur wenige Zeilen brauchen, nutze LIMIT.
SELECT order_id, created_at, status FROM orders ORDER BY created_at DESC LIMIT 50;Funktionen in WHERE-Bedingungen vermeiden
Bevorzuge indexfreundliche Praedikate:
-- VORHER:WHERE SUBSTRING(code, 1, 3) = 'ABC'
-- NACHHER:WHERE code LIKE 'ABC%'Die zweite Form gibt der Datenbank eine bessere Chance, einen Index auf code zu verwenden.
Bedingte Aggregationen optimieren
In PostgreSQL kann FILTER bedingte Zaehlungen klarer machen als wiederholte Subqueries oder lange CASE-Ausdruecke.
-- VORHER:EXPLAINSELECT SUM(CASE WHEN status = 'NEW' THEN 1 END) AS new_orders, SUM(CASE WHEN status = 'CLOSED' THEN 1 END) AS closed_orders FROM orders;
-- NACHHER:EXPLAINSELECT COUNT(*) FILTER (WHERE status = 'NEW') AS new_orders, COUNT(*) FILTER (WHERE status = 'CLOSED') AS closed_orders FROM orders;Logische Ausdruecke fuer einfache Boolean-Checks nutzen
Fuer einfache Boolean-Logik ist diese Form:
table1.is_deleted OR table2.is_deletedmeist klarer als ein grosser CASE-Ausdruck, der einen Boolean zurueckgibt.
7. ROW_NUMBER() als Alternative zu DISTINCT verwenden
Ziel
Eindeutige Werte effizient extrahieren, besonders in grossen Datenmengen.
Loesung (7)
Manchmal kann ROW_NUMBER() mit Partitionierung schneller sein als DISTINCT, besonders wenn die Partitionierungsspalten indiziert sind und die Query mehr Kontrolle darueber braucht, welche Zeile behalten wird.
-- VORHER:EXPLAINSELECT COUNT(DISTINCT user_id) FROM logins WHERE login_date BETWEEN '2026-01-01'::DATE AND '2026-01-31'::DATE;
-- NACHHER:EXPLAINSELECT COUNT(user_id) FROM ( SELECT user_id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY user_id) AS rn FROM logins WHERE login_date BETWEEN '2026-01-01'::DATE AND '2026-01-31'::DATE ) AS tmp WHERE rn = 1;Warum das schneller sein kann (7)
ROW_NUMBER() kann eine schwere Distinct-Sortierung vermeiden, indem innerhalb jeder Gruppe Zeilennummern vergeben werden. Ausserdem gibt es mehr Flexibilitaet, wenn pro Gruppe die neueste, aelteste oder wichtigste Zeile behalten werden soll.
Trotzdem gilt: Immer mit EXPLAIN ANALYZE pruefen. DISTINCT kann fuer einfache Faelle weiterhin die bessere Wahl sein.
Meine Gedanken
SQL Query Optimization ist ein zentraler Schritt in jedem datenbankgestuetzten Projekt. Die besten Verbesserungen entstehen oft durch kleine Aenderungen an der Query-Form:
- Lange
IN-Listen durch virtuelle Tabellen oder PostgreSQL-Arrays ersetzen, wenn es passt. - Range-Praedikate nutzen, statt Funktionen auf indizierte Spalten anzuwenden.
EXISTSwaehlen, wenn nur geprueft werden soll, ob eine verknuepfte Zeile existiert.- Unnoetige Spalten, Zeilen, wiederholte Scans und schwere Sortierungen vermeiden.
- Queries so lesbar halten, dass der naechste Engineer sie warten kann.
Die wichtigste Gewohnheit ist Messen. Nutze EXPLAIN, EXPLAIN ANALYZE, echte Datenmengen und realistische Filter. Eine Query, die isoliert elegant aussieht, kann sich mit Produktionsdaten anders verhalten.
Gute SQL-Performance ist keine Magie. Meist entsteht sie dadurch, dass die Datenbank weniger unnoetige Arbeit erledigen muss.
