Ein Partial Index indiziert nur die Zeilen, die einer bestimmten WHERE-Bedingung genügen. Das macht den Index kleiner (weniger Speicher), schneller (weniger zu durchsuchen) und günstiger beim Schreiben (nur passende Inserts pflegen den Index). Klassischer Use-Case: 95 % der Subscriptions sind „cancelled", aber 99 % der Queries fragen nach „active".
Die Grundform
CREATE TABLE subscriptions (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL,
status text NOT NULL,
...
);
CREATE INDEX subscriptions_active_idx
ON subscriptions (user_id)
WHERE status = 'active';Der Index enthält nur Zeilen mit status = 'active'. Bei 1 Mio. Subscriptions, von denen 50 K aktiv sind, ist der Index 95 % kleiner als ein vollständiger Index auf user_id.
Wann der Optimizer den Partial-Index nimmt
Der Plan-Optimizer kann den Partial-Index nur nutzen, wenn die Query-Bedingung die Index-Bedingung mit-impliziert:
-- nutzt den Partial-Index (status = 'active' wird mit-gefiltert)
SELECT * FROM subscriptions
WHERE user_id = 42 AND status = 'active';
-- nutzt den Partial-Index AUCH (Postgres erkennt: status = 'active'
-- ist die einzige im Index enthaltene Variante)
SELECT * FROM subscriptions
WHERE user_id = 42 AND status = 'active' AND created_at > '2026-01-01';
-- nutzt den Partial-Index NICHT (status nicht eingeschränkt)
SELECT * FROM subscriptions WHERE user_id = 42;Wichtig: die WHERE-Klausel der Query muss die WHERE-Klausel des Index enthalten, nicht nur thematisch passen.
Klassische Use-Cases
Soft-Delete-Pattern
CREATE TABLE records (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
data text NOT NULL,
deleted_at timestamptz
);
-- Index nur über lebende Records
CREATE INDEX records_alive_idx ON records (id) WHERE deleted_at IS NULL;Alle Queries, die auf WHERE deleted_at IS NULL filtern (also der App-Layer, der Soft-Delete versteckt), nutzen einen sehr schlanken Index.
Sparse Boolean
CREATE TABLE tickets (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
urgent boolean NOT NULL DEFAULT false,
...
);
-- 99 % der Tickets sind nicht urgent → Index nur auf 1 %
CREATE INDEX tickets_urgent_idx ON tickets (id) WHERE urgent;Zustands-Eindeutigkeit
-- ein User darf nur EINE aktive Subscription haben
CREATE UNIQUE INDEX subscriptions_one_active_per_user
ON subscriptions (user_id)
WHERE status = 'active';Klassiker — die einzige Möglichkeit für „Eindeutigkeit unter Bedingung". Ein Standard-UNIQUE-Constraint kann das nicht.
Bedingungen — was geht?
Die WHERE-Klausel kann beliebige Expressions enthalten — solange sie immutable sind:
CREATE INDEX ... WHERE status = 'active';
CREATE INDEX ... WHERE status IN ('active','trial');
CREATE INDEX ... WHERE deleted_at IS NULL;
CREATE INDEX ... WHERE total > 100;
CREATE INDEX ... WHERE lower(email) LIKE '%@example.com';-- now() ist VOLATILE
CREATE INDEX ... WHERE created_at > now() - interval '1 day';
-- Subqueries sind nicht erlaubt
CREATE INDEX ... WHERE customer_id IN (SELECT id FROM premium);Performance-Vorteile
| Vorteil | Erklärung |
|---|---|
| Kleinerer Index | Weniger Disk-I/O bei Index-Lookups |
| Schnelleres Suchen | Weniger Tree-Tiefe |
| Günstigere Inserts | Inserts, die nicht zur WHERE-Klausel passen, sparen Index-Pflege |
| Cache-freundlicher | Mehr Index-Pages passen in den Buffer Cache |
In der Praxis: bei einem Sparse-Boolean-Pattern (1 % der Zeilen erfüllt die Bedingung) kann der Partial-Index 100× kleiner sein als sein vollständiges Pendant.
Häufige Stolperfallen
Plan-Optimizer muss die WHERE-Klausel implizieren — sonst keine Nutzung.
WHERE status = 'active' im Index, WHERE status IN ('active','trial') in der Query → der Index wird NICHT genutzt, weil 'trial' nicht im Index ist. Postgres ist hier strikt. Wer flexibler sein will: Bedingung im Index entsprechend weiter fassen.
Bedingung in der App muss exakt zum Index passen.
Wenn die App WHERE deleted_at IS NULL schreibt und der Index das auch will — perfekt. Wer aber WHERE deleted_at IS NULL OR deleted_at > now() macht: nicht mehr durch Partial-Index abgedeckt. Code-Review schließt das oft nicht ab.
UNIQUE partial = wundervoll für Soft-State.
„Genau eine aktive Session pro User" oder „nur eine pending Order pro Cart" — klassisch in der App ständig race-anfällig. Mit UNIQUE partial Index erzwingt die DB die Eindeutigkeit, atomar und ohne Lock-Choreographie im Code.
Bei sehr unselektiven Bedingungen ist der Partial-Index nutzlos.
Wenn 95 % der Zeilen die Bedingung erfüllen, ist der Partial-Index praktisch ein Vollindex — kein Gewinn. Faustregel: lohnt erst ab Selektivität unter 10-20 %.
Nach DELETE/UPDATE auf das Bedingungs-Feld: Index wird automatisch angepasst.
Wenn eine Zeile von status = 'active' zu status = 'cancelled' ändert: Postgres entfernt sie aus dem Partial-Index. Konsistent, aber Update-Cost. Bei sehr häufigen Statuswechseln messen.
Partial-Indexe nicht mit Composite verwechseln.
CREATE INDEX ... ON t (a, b) WHERE c = 1 ist Composite + Partial. Macht Sinn, wenn a und b zusammen abgefragt werden, aber nur unter der Bedingung c = 1. Häufig vergessen, kann ein bestehender Composite-Index als Partial deutlich kompakter werden.