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

SQL
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:

SQL
-- 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

SQL
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

SQL
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

SQL
-- 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:

SQL Erlaubt
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';
SQL Verboten
-- 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

VorteilErklärung
Kleinerer IndexWeniger Disk-I/O bei Index-Lookups
Schnelleres SuchenWeniger Tree-Tiefe
Günstigere InsertsInserts, die nicht zur WHERE-Klausel passen, sparen Index-Pflege
Cache-freundlicherMehr 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.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Indexes

Zur Übersicht