---
title: "SQL Query Optimization 2026: 7 einfache Techniken fuer schnellere Datenbank-Performance"
description: "Sieben praktische SQL-Query-Optimierungstechniken fuer schnellere Datenbank-Performance, mit PostgreSQL-Beispielen fuer JOINs, IN-Listen, EXISTS, Datumsbereiche, Aggregationen und Deduplizierung."
author: Aleksei Aleinikov
date: 2026-03-10
updated: 2026-06-27
lang: de
tags: [sql, database, query-optimization, postgresql, performance, indexing, data-engineering]
canonical: https://www.alekseialeinikov.com/de/blog/topics/data/sql-query-optimierung-2026-schnellere-datenbank-performance
source: alekseialeinikov.com
---

# SQL Query Optimization 2026: 7 einfache Techniken fuer schnellere Datenbank-Performance

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.

![SQL-Optimierung: weil deine Queries die schnelle Spur verdienen.](https://www.alekseialeinikov.com/blog/1_wgu4URsmjNvqHJqogQorFA.webp)

## 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.

```sql
-- VORHER:
EXPLAIN
SELECT order_id, city
  FROM orders
 WHERE city IN ('Berlin', 'Paris', 'Rome');

-- NACHHER:
EXPLAIN
SELECT 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.

```sql
-- VORHER:
EXPLAIN
SELECT product_id, quantity
  FROM order_items
 WHERE product_id IN (101, 202, 303, 404);

-- NACHHER:
EXPLAIN
SELECT 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.

```sql
-- VORHER:
EXPLAIN
SELECT 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:
EXPLAIN
SELECT 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:

```sql
EXTRACT(YEAR FROM order_date) = 2026
```

Das 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.

```sql
-- VORHER:
EXPLAIN
SELECT *
  FROM orders
 WHERE EXTRACT(YEAR FROM order_date) = 2026
   AND EXTRACT(MONTH FROM order_date) = 5;

-- NACHHER:
EXPLAIN
SELECT *
  FROM orders
 WHERE order_date BETWEEN '2026-05-01'::DATE
                     AND '2026-05-31'::DATE;
```

Fuer Timestamp-Spalten ist diese Form oft sicherer:

```sql
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.

```sql
-- VORHER:
EXPLAIN
SELECT COUNT(DISTINCT o.order_id)
  FROM orders AS o
  JOIN order_items AS i
    ON o.order_id = i.order_id;

-- NACHHER:
EXPLAIN
SELECT 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.

```sql
-- 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`.

```sql
SELECT order_id, created_at, status
  FROM orders
 ORDER BY created_at DESC
 LIMIT 50;
```

### Funktionen in WHERE-Bedingungen vermeiden

Bevorzuge indexfreundliche Praedikate:

```sql
-- 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.

```sql
-- VORHER:
EXPLAIN
SELECT 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:
EXPLAIN
SELECT 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:

```sql
table1.is_deleted OR table2.is_deleted
```

meist 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.

```sql
-- VORHER:
EXPLAIN
SELECT COUNT(DISTINCT user_id)
  FROM logins
 WHERE login_date BETWEEN '2026-01-01'::DATE
                     AND '2026-01-31'::DATE;

-- NACHHER:
EXPLAIN
SELECT 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.
- `EXISTS` waehlen, 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.

> Dasselbe Prinzip skaliert bis ins Cloud-Data-Warehouse. Auf BigQuery zahlst du fuer die Bytes, die eine Query scannt \u2014 "die Datenbank weniger arbeiten lassen" wird also zu einer Position auf deiner Rechnung. Siehe [BigQuery vs Snowflake 2026](https://www.alekseialeinikov.com/de/blog/topics/data/bigquery-vs-snowflake-2026-ehrlicher-vergleich) dazu, wie die Abrechnungsmodelle genau diese Disziplin belohnen.
