PostgreSQL ist die leistungsstärkste Open-Source-Datenbank der Welt — und mit Claude Code wird das Schreiben komplexer, performanter Queries zum Kinderspiel. In diesem Guide zeigen wir, wie du 2026 Window Functions, CTEs, JSONB, Partial Indexes und Full-Text Search mit KI-Unterstützung meisterst.
🔮 Window Functions
ROW_NUMBER, RANK, LAG/LEAD für Analytics
Window Functions berechnen Werte über eine Menge von Zeilen, ohne sie zu gruppieren. Ideal für Rankings, gleitende Durchschnitte und Vergleiche mit Vorperioden.
-- Claude Code Prompt: "Schreib eine Query mit ROW_NUMBER, RANK und LAG für Umsatz-Analytics"
WITH monthly_revenue AS (
SELECT
DATE_TRUNC('month', created_at) AS month,
product_id,
SUM(amount) AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1, 2
)
SELECT
month,
product_id,
revenue,
-- Rang innerhalb des Monats nach Umsatz
RANK() OVER (PARTITION BY month ORDER BY revenue DESC) AS rank_in_month,
-- Fortlaufende Zeilennummer global
ROW_NUMBER() OVER (ORDER BY month, revenue DESC) AS row_num,
-- Umsatz des Vormonats (LAG)
LAG(revenue) OVER (PARTITION BY product_id ORDER BY month) AS prev_month_revenue,
-- Umsatz des Folgemonats (LEAD)
LEAD(revenue) OVER (PARTITION BY product_id ORDER BY month) AS next_month_revenue,
-- Wachstum in Prozent vs Vormonat
ROUND(
(revenue - LAG(revenue) OVER (PARTITION BY product_id ORDER BY month))
/ NULLIF(LAG(revenue) OVER (PARTITION BY product_id ORDER BY month), 0) * 100, 2
) AS growth_pct
FROM monthly_revenue
ORDER BY month DESC, rank_in_month;
💡 Claude Code Tipp: Beschreibe einfach deine Business-Frage: "Zeig mir den Umsatz jedes Produkts im Vergleich zum Vormonat mit Wachstumsrate." Claude generiert die korrekte Window Function mit PARTITION BY und ORDER BY automatisch.
🔄 CTEs
Common Table Expressions: Rekursive Abfragen
CTEs machen komplexe Queries lesbar und ermöglichen rekursive Abfragen — z. B. für Baumstrukturen wie Kategorien, Mitarbeiterhierarchien oder Graph-Traversal.
-- Claude Code Prompt: "Traversiere eine Kategorie-Hierarchie rekursiv in PostgreSQL"
WITH RECURSIVE category_tree AS (
-- Anker: Wurzelkategorien (ohne parent)
SELECT
id,
name,
parent_id,
0 AS depth,
ARRAY[id] AS path,
name::TEXT AS full_path
FROM categories
WHERE parent_id IS NULL
UNION ALL
-- Rekursiver Teil: Kinder der bisherigen Knoten
SELECT
c.id,
c.name,
c.parent_id,
ct.depth + 1,
ct.path || c.id,
ct.full_path || ' > ' || c.name
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
WHERE ct.depth < 10 -- Zyklen verhindern
)
SELECT
REPEAT(' ', depth) || name AS indented_name,
full_path,
depth
FROM category_tree
ORDER BY path;
✅ Lesbarkeit
Komplexe Subqueries werden als benannte Blöcke strukturiert — leichter zu debuggen und zu warten.
🔁 Rekursion
WITH RECURSIVE traversiert Bäume und Graphen ohne externe Loops — direkt in SQL.
♻️ Wiederverwendung
Ein CTE kann mehrfach in derselben Query referenziert werden — ohne doppelte Berechnung.
🗂 JSONB
JSONB Spalten: Flexibles Schema in PostgreSQL
JSONB speichert semi-strukturierte Daten binär-optimiert. Perfekt für Event-Logs, Konfigurationen und flexible Metadaten — ohne separate NoSQL-Datenbank.
-- Claude Code Prompt: "JSONB Spalte für User-Events mit Index und Query"
-- Tabelle mit JSONB-Spalte anlegen
CREATE TABLE user_events (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- GIN-Index für effiziente JSONB-Suche
CREATE INDEX idx_events_payload ON user_events USING GIN (payload);
-- Spezifischer Index für häufig abgefragten Key
CREATE INDEX idx_events_plan ON user_events ((payload ->> 'plan'));
-- JSONB-Daten einfügen
INSERT INTO user_events (user_id, event_type, payload) VALUES
(gen_random_uuid(), 'signup', '{"plan": "trial", "source": "blog", "features": ["ai", "sql"]}'),
(gen_random_uuid(), 'upgrade', '{"plan": "pro", "amount": 49, "currency": "EUR"}');
-- JSONB abfragen: Operator -> (JSON), ->> (Text)
SELECT
user_id,
payload ->> 'plan' AS plan,
(payload ->> 'amount')::NUMERIC AS amount,
payload -> 'features' AS features_array
FROM user_events
WHERE
payload @> '{"plan": "trial"}' -- Containment-Operator nutzt GIN-Index
AND payload ? 'source' -- Key-Existenz-Check
AND created_at > NOW() - INTERVAL '30 days';
💡 JSONB vs JSON: Immer JSONB verwenden! Es wird beim Schreiben geparst und binär gespeichert — Queries sind 10–100x schneller als auf JSON-Spalten. Der GIN-Index macht @> Containment-Queries blitzschnell.
📍 Indexes
Partial Indexes und Expression Indexes
Nicht jede Zeile verdient einen Index-Eintrag. Partial Indexes indizieren nur relevante Teilmengen — kleiner, schneller, wartungsfreundlicher.
-- Claude Code Prompt: "Erstelle Partial und Expression Indexes für Orders-Tabelle"
-- Partial Index: Nur aktive, nicht abgeschlossene Bestellungen
CREATE INDEX idx_orders_pending
ON orders (created_at, user_id)
WHERE status IN ('pending', 'processing');
-- → 95% aller Rows werden ignoriert; Index bleibt winzig
-- Expression Index: Für Case-Insensitive E-Mail-Suche
CREATE INDEX idx_users_email_lower
ON users (LOWER(email));
-- Query MUSS denselben Ausdruck nutzen damit Index greift:
SELECT * FROM users WHERE LOWER(email) = LOWER('User@Example.com');
-- Partial + Expression kombiniert: Aktive Premium-User nach Domain
CREATE INDEX idx_premium_users_domain
ON users (SPLIT_PART(email, '@', 2))
WHERE plan = 'premium' AND deleted_at IS NULL;
-- BRIN Index für Zeitserien (sehr klein, gut für append-only Tabellen)
CREATE INDEX idx_events_time_brin
ON user_events USING BRIN (created_at)
WITH (pages_per_range = 128);
-- Index-Größen und Nutzung prüfen
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan AS times_used
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
🔍 EXPLAIN ANALYZE
Query-Optimierung verstehen
EXPLAIN ANALYZE zeigt den tatsächlichen Ausführungsplan mit Laufzeiten — der einzig zuverlässige Weg, Performance-Probleme zu diagnostizieren.
-- Claude Code Prompt: "Analysiere diese Query mit EXPLAIN ANALYZE und erkläre Bottlenecks"
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
SELECT
u.email,
COUNT(o.id) AS order_count,
SUM(o.amount) AS total_spent
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at > NOW() - INTERVAL '90 days'
GROUP BY u.id, u.email
HAVING SUM(o.amount) > 1000
ORDER BY total_spent DESC
LIMIT 20;
🔴 Seq Scan
PostgreSQL liest die gesamte Tabelle. Oft ein Zeichen für fehlende Indexes — oder sehr kleine Tabellen.
🟢 Index Scan
Nur relevante Rows werden gelesen. Optimal für selektive WHERE-Bedingungen auf indizierten Spalten.
🟡 Hash Join
Effizient für große Joins. Teuer wenn Tabelle nicht in den Work_mem passt — dann Disk-Spills!
-- Häufige Bottlenecks mit Claude Code identifizieren
-- Prompt: "Was bedeutet dieser EXPLAIN-Plan, wo ist der Flaschenhals?"
-- Praktische Diagnose-Queries:
-- 1) Langsamste Queries finden (pg_stat_statements nötig)
SELECT
LEFT(query, 100) AS query_preview,
ROUND(mean_exec_time::NUMERIC, 2) AS avg_ms,
calls,
ROUND(total_exec_time::NUMERIC / 1000, 2) AS total_sec
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 2) Tabellen ohne passende Indexes (hohe Seq-Scan-Rate)
SELECT
relname AS table_name,
seq_scan,
idx_scan,
ROUND(seq_scan::NUMERIC / NULLIF(seq_scan + idx_scan, 0) * 100, 1) AS seq_pct
FROM pg_stat_user_tables
WHERE seq_scan + idx_scan > 100
ORDER BY seq_pct DESC;
💡 Claude Code Workflow: Kopiere deinen EXPLAIN-Output in Claude Code und schreibe: "Erkläre mir diesen Query-Plan und schlage konkrete Optimierungen vor." Claude erkennt Seq Scans, schlechte Join-Strategie und fehlende Indexes sofort.
🔎 Full-Text Search
Full-Text Search mit tsvector und tsquery
PostgreSQL hat eingebaute Volltextsuche auf Enterprise-Niveau — ohne Elasticsearch. tsvector indiziert Dokumente, tsquery formuliert Suchanfragen.
-- Claude Code Prompt: "Volltext-Suche für Artikel-Tabelle mit Ranking und Highlighting"
-- Tabelle mit FTS-Spalte
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
author TEXT,
published BOOLEAN DEFAULT FALSE,
-- Generierte tsvector-Spalte (automatisch aktualisiert!)
search_vec TSVECTOR GENERATED ALWAYS AS (
setweight(to_tsvector('german', COALESCE(title, '')), 'A') ||
setweight(to_tsvector('german', COALESCE(content, '')), 'B') ||
setweight(to_tsvector('german', COALESCE(author, '')), 'C')
) STORED
);
-- GIN-Index für schnelle Volltextsuche
CREATE INDEX idx_articles_fts
ON articles USING GIN (search_vec);
-- Partial Index: Nur veröffentlichte Artikel durchsuchen
CREATE INDEX idx_articles_fts_published
ON articles USING GIN (search_vec)
WHERE published = TRUE;
-- Suche mit Ranking und Highlighting
SELECT
id,
title,
author,
-- Relevanz-Score (0–1)
ts_rank(search_vec, query) AS relevance,
-- Treffer-Highlighting im Content
ts_headline(
'german', content, query,
'MaxWords=50, MinWords=20, StartSel=<mark>, StopSel=</mark>'
) AS excerpt
FROM
articles,
to_tsquery('german', 'PostgreSQL & (Performance | Optimierung)') query
WHERE
search_vec @@ query
AND published = TRUE
ORDER BY relevance DESC
LIMIT 10;
-- Phrase-Suche und Wildcards
SELECT * FROM articles
WHERE search_vec @@ to_tsquery('german', 'claude:* & query:*')
AND published = TRUE;
Fazit: Claude Code als PostgreSQL-Copilot
Die sechs Techniken in diesem Guide — Window Functions, rekursive CTEs, JSONB, Partial Indexes, EXPLAIN ANALYZE und Full-Text Search — decken 90% aller fortgeschrittenen PostgreSQL-Anforderungen ab. Mit Claude Code brauchst du keine Syntax auswendig zu lernen: Beschreibe dein Problem in natürlicher Sprache, erhalte produktionsreife SQL — und lass dir den EXPLAIN-Plan direkt erklären.
🚀 Schneller
Kein manuelles Index-Tuning mehr. Claude erkennt fehlende Indexes aus dem Query-Kontext.
🛡 Sicherer
Korrekte Parameterization, kein SQL-Injection-Risiko, NULLIF für Division-by-Zero.
📐 Lesbarer
CTEs statt verschachtelter Subqueries. Kommentare direkt im generierten Code.