Index-Interna: B-Tree, GIN, GiST & BRIN Architektur unter Linux
Erstellt: 08.11.2026 · Zuletzt aktualisiert: 08.11.2026 · Nejat Hakan
Management Summary: Ein Index tauscht Speicherplatz auf der NVMe und Schreib-Overhead gegen drastische Reduktionen der Lese-Latenz ein – und verwandelt einen teuren O(N)-Tabellenscan in einen schnellen O(log N)-Baumdurchlauf oder einen O(1)-Bitmap-Lookup. Wer jedoch Standard-B-Trees wahllos auf JSON-Dokumente, geometrische Datenbereiche oder Multi-Terabyte-Append-Only-Zeitreihen anwendet, erzeugt massiven Speicher-Bloat und verstopft den Pufferpool. Dieser Leitfaden analysiert die physischen Datenstrukturen und Algorithmen von B-Tree, GIN, GiST und BRIN sowie die Produktivmuster Covering-Indizes (
INCLUDE), partielle Indizes und das unterbrechungsfreieCREATE INDEX CONCURRENTLY.
-
B-Tree-Architektur
Balancierte N-näre Suchbäume durchlaufen: Wurzel- und Verzweigungsknoten passieren und über doppelt verkettete Blattknoten Bereiche scannen.
-
GIN für JSONB & Arrays
Semi-strukturierte Dokumente in invertierte Posting-Listen aufbrechen: Blitzschnelle Containment- (
@>) und Key-Lookups inJSONBermöglichen. -
:material-compress:{ .lg .middle } BRIN Zeitreihen-Kompression
Multi-Terabyte-Tabellen mit 99 % weniger Speicherplatz indizieren: Minimal- und Maximalwerte je physischem 1-MB-Blockbereich zusammenfassen.
-
Covering- & Partielle Indizes
Heap-Zeiger-Hops über
INCLUDEfür Index-Only-Scans eliminieren und gezielt nur aktive Datensätze (WHERE status = 'PENDING') indizieren.
1. Index-Architektur & mathematische Komplexität
Die Auswahl des richtigen Indextyps richtet sich nach den mathematischen Eigenschaften der Daten und der Abfrage-Operatoren:
| Indextyp | Zugrundeliegende Datenstruktur | Optimale Abfrage-Operatoren | Primärer Produktiv-Einsatzzweck |
|---|---|---|---|
| B-Tree (Standard) | Balancierter N-närer Baum mit horizontal verknüpften Blattknoten | =, <, <=, >, >=, BETWEEN, IN, ORDER BY |
Hochkardinale Skalarwerte (id, email, created_at, Fremdschlüssel). |
| GIN (Generalized Inverted) | Invertierte Nachschlagetabelle: Einzelner Schlüssel → Posting-Liste von ctids |
@>, ?, ?&, ?|, @@ (Volltextsuche) |
Verschachtelte JSONB-Dokumente, Arrays (TEXT[]) und Volltextsuche (tsvector). |
| GiST (Generalized Search Tree) | Balancierter Suchbaum mit beliebigen Hüllkörpern (Bounding Boxes) | && (Überlappung), <-> (KNN-Distanz), @> |
PostGIS Geokoordinaten, IP-Netzwerk-Bereiche (INET) und Exclusion-Constraints. |
| BRIN (Block Range Index) | Min/Max-Grenzwerte je physischem Blockbereich (Standard: 128 Pages = 1 MB) | <, >, BETWEEN auf physisch sortierten Daten |
Riesige chronologische Log-Tabellen, Sensor- und IoT-Telemetrie. |
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
subgraph BTree ["1. B-Tree: Hochpräzises O(log N)"]
direction TB
B_ROOT["Wurzelknoten (Root)<br/>Schlüssel: [1000 | 5000]"]
B_INT["Interner Routing-Knoten<br/>Schlüssel: [250 | 750]"]
B_LEAF1["Blattseite 1<br/>ctid → (Block 4, Slot 1)"]
B_LEAF2["Blattseite 2<br/>ctid → (Block 4, Slot 2)"]
B_ROOT --> B_INT
B_INT --> B_LEAF1
B_INT --> B_LEAF2
B_LEAF1 <-->|"Scan"| B_LEAF2
end
subgraph GIN ["2. GIN: Invertierte Key-Map"]
direction TB
G_KEY["JSON-Schlüssel: 'role'"]
G_POST["Posting-Liste<br/>ctid: [(1,2), (5,8), (9,1)]"]
G_KEY --> G_POST
end
subgraph BRIN ["3. BRIN: Block-Zusammenfassung"]
direction TB
BR_RANGE["Blöcke 0–127 (1 MB)"]
BR_SUM["Min: 2026-01-01<br/>Max: 2026-01-07"]
BR_RANGE --> BR_SUM
end
BTree ==> GIN ==> BRIN
2. B-Tree-Interna: Warum doppelt verkettete Blattknoten entscheiden
In einem B-Tree-Index liegen alle Blattknoten auf exakt derselben Tiefe ausgehend von der Wurzel (was eine gleichmäßige O(log N)-Suchzeit garantiert).
Schnelle Bereichsscans ohne erneuten Wurzel-Durchlauf
Fragt eine Anwendung WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31' ab:
1. Traversiert die Engine den Baum von der Wurzel (Root) über die internen Verzweigungsknoten nach unten, bis sie den ersten passenden Blattknoten für '2026-01-01' findet.
2. Ab diesem Moment durchläuft die Engine die Wurzel nie wieder. Sie folgt den horizontalen, doppelt verketteten Zeigern (next_page / prev_page) direkt von Blattseite zu Blattseite, bis '2026-01-31' erreicht ist.
3. Dadurch laufen sequenzielle Bereichsscans und ORDER BY-Sortierungen mit der vollen Bandbreite des Arbeitsspeichers.
3. GIN (Generalized Inverted Index) für JSONB
Klassische B-Trees indizieren den Wert einer Spalte als unteilbare Gesamtheit. Ein GIN-Index zerlegt semi-strukturierte Werte (wie JSONB-Dokumente oder Arrays) in ihre einzelnen Schlüssel und Werte:
Dokument 1: {"role": "admin", "team": "devops"}
Dokument 2: {"role": "viewer", "team": "devops"}
GIN-Invertierte Map:
"admin" --> [Doc 1]
"viewer" --> [Doc 2]
"devops" --> [Doc 1, Doc 2]
jsonb_ops vs. jsonb_path_ops
PostgreSQL stellt zwei Operatorklassen für GIN-Indizes bereit:
- jsonb_ops (Standard): Indiziert jeden Pfad, Schlüssel und Wert separat. Unterstützt alle Operatoren (?, ?|, @>), führt jedoch zu größeren Indizes.
- jsonb_path_ops: Bildet Hashes aus kompletten Pfad-Wert-Paaren (z. B. hash("team.devops")). Der Index ist bis zu 60 % kleiner und schneller, unterstützt aber ausschließlich Containment-Abfragen (@>).
-- Best Practice: Schneller Containment-Index auf JSONB
CREATE INDEX CONCURRENTLY idx_customer_metadata_gin
ON customer_orders USING GIN (metadata jsonb_path_ops);
4. BRIN (Block Range Index): Skalierung im Terabyte-Bereich
Auf riesigen Zeitreihen-Tabellen (z. B. 500 Millionen Zeilen an Server-Telemetrie) würde ein B-Tree hunderte Gigabyte an RAM verschlingen.
Werden Daten in natürlicher chronologischer Reihenfolge per INSERT angehängt, sind sie auf der Festplatte bereits physisch vorsortiert:
Physischer Speicher auf NVMe:
Blöcke 0–127 --> Zeitstempel von 2026-01-01 bis 2026-01-05
Blöcke 128–255 --> Zeitstempel von 2026-01-06 bis 2026-01-10
Blöcke 256–383 --> Zeitstempel von 2026-01-11 bis 2026-01-15
Ein BRIN-Index speichert keine Zeiger für einzelne Datensätze. Er merkt sich lediglich die Minimal- und Maximalwerte für jeden zusammenhängenden Blockbereich (Standard: 128 Pages = 1 MB):
- Bei einer Abfrage nach WHERE recorded_at = '2026-01-08' überspringt die Engine die Blöcke 0–127 und 256–383 komplett.
- Ein BRIN-Index ist typischerweise 100- bis 1000-mal kleiner als ein B-Tree und passt vollständig in den L3-Cache des Prozessors.
-- Best Practice: Extrem schlanker BRIN-Index für chronologische Log-Tabellen
CREATE INDEX CONCURRENTLY idx_server_metrics_brin
ON server_metrics USING BRIN (recorded_at)
WITH (pages_per_range = 128);
5. Fortgeschrittene Indizierungsmuster
A. Covering-Indizes (INCLUDE) für Index-Only-Scans
Muss eine Abfrage Spalten zurückliefern, die nicht im Index stehen, muss die Engine für jeden Treffer einen wahlfreien I/O-Sprung auf die Heap-Tabelle ausführen.
Mit INCLUDE werden zusätzliche Nutzdaten an die Blattknoten des B-Trees angehängt, ohne den Suchschlüssel zu vergrößern:
-- Ermöglicht Index-Only Scans: Abfrage wird vollständig aus dem Index beantwortet!
CREATE INDEX CONCURRENTLY idx_orders_customer_covering
ON customer_orders (customer_id)
INCLUDE (total_cents, created_at);
-- Abfrage:
SELECT customer_id, total_cents, created_at
FROM customer_orders
WHERE customer_id = 101; -- Null Heap-Hops, wenn die Visibility-Map sauber ist!
B. Partielle Indizes: Nur das Wesentliche indizieren
Sind 99 % aller Bestellungen im Status COMPLETED und nur 1 % im Status PENDING, verschwendet ein Index über die gesamte Tabelle Speicherplatz. Ein partieller Index filtert bereits bei der Indexerstellung:
-- Der Index schrumpft um 99 %, Schreibvorgänge auf abgeschlossene Bestellungen verursachen 0 Index-Overhead!
CREATE INDEX CONCURRENTLY idx_orders_pending_queue
ON customer_orders (created_at)
WHERE status = 'PENDING';
6. Die goldene Produktionsregel: CREATE INDEX CONCURRENTLY
Ein Standard-CREATE INDEX-Befehl fordert eine SHARE-Sperre auf der Tabelle an und blockiert damit alle parallelen INSERT-, UPDATE- und DELETE-Operationen für die gesamte Dauer der Indexerstellung:
-- NIEMALS IM PRODUKTIVBETRIEB AUF GROSSEN TABELLEN AUSFÜHREN:
CREATE INDEX idx_orders_customer ON customer_orders (customer_id);
-- Blockiert alle Kassiervorgänge und Bezahlungen, bis der Index fertig ist!
-- IMMER CONCURRENTLY VERWENDEN:
CREATE INDEX CONCURRENTLY idx_orders_customer ON customer_orders (customer_id);
-- Scannt die Tabelle im Hintergrund in zwei Durchläufen; Schreibvorgänge laufen ungehindert weiter!
Umgang mit fehlgeschlagenen Concurrent Builds
Bricht ein gleichzeitiger Indexaufbau ab (z. B. durch einen Deadlock), markiert PostgreSQL den Index als INVALID. Ungültige Indizes belegen Speicher und bremsen Schreibvorgänge aus, ohne Abfragen zu beschleunigen. Bereinige diese Indizes: