Primärschlüssel-Wahl und ihre Folgen für Index und Skalierung
Die Wahl des Primärschlüssel-Typs wirkt auf den ersten Blick wie eine Detailentscheidung, hat aber direkte Auswirkungen auf die physische Struktur des Index, auf Fragmentierung, Cache-Verhalten und die Fähigkeit eines Systems, ohne zentrale Koordination Schlüssel zu vergeben. Klassischer Auto-Increment liefert kompakte, sortierte Werte, scheitert aber an verteilten Schreibpfaden. Zufällige UUIDs lösen das Koordinationsproblem, zerstören dafür die Einfügereihenfolge im Index. Sortierbare Varianten wie ULID und UUIDv7 versuchen, beide Welten zu verbinden. Dieser Artikel ordnet die drei Ansätze anhand ihrer tatsächlichen Auswirkungen auf Indexstruktur und Betrieb ein, nicht anhand von Bauchgefühl.
Inhaltsverzeichnis
- 1. Warum der Primärschlüssel-Typ die Indexstruktur direkt beeinflusst
- 2. Auto-Increment: kompakt, sortiert, aber an eine Sequence gebunden
- 3. UUID v4: global eindeutig ohne zentrale Koordination
- 4. Das konkrete Fragmentierungsproblem zufälliger UUIDs im B-Tree
- 5. ULID und UUIDv7: sortierbare Alternativen mit Zeitstempel-Präfix
- 6. Speicherbedarf, Lesbarkeit und Kollisionsverhalten im Detail
- 7. Trade-offs in verteilten Systemen: Koordination versus Sortierreihenfolge
- 8. Praktische Implementierungsdetails: Speicherung und Konvertierung
- 9. Praktische Empfehlung: welcher Schlüsseltyp zu welchem Szenario passt
- 10. Zusammenfassung
- 11. FAQ
1. Warum der Primärschlüssel-Typ die Indexstruktur direkt beeinflusst
Ein Primärschlüssel ist in den meisten relationalen Datenbanken zugleich der Clustering-Key, also die physische Sortierreihenfolge, in der Zeilen auf der Platte abgelegt werden. Jeder neu eingefügte Wert muss an genau der Stelle im B-Tree eingefügt werden, die seiner Sortierposition entspricht. Wächst der Schlüssel monoton, landet jede neue Zeile am rechten Rand des Baums, was Einfügungen sehr günstig macht, weil kein bestehender Knoten durchsucht und keine bereits gefüllte Seite gesplittet werden muss.
Ist der Schlüssel dagegen zufällig verteilt, trifft jede Einfügung eine praktisch beliebige Stelle im Baum. Das erzwingt regelmäßige Seiten-Splits, verstreut logisch zusammengehörige Zeilen über viele physische Seiten und sorgt dafür, dass der für Schreibvorgänge relevante Teil des Index kaum noch in den Buffer Pool passt. Diese Mechanik ist unabhängig vom konkreten Datenbanksystem, sie folgt direkt aus der Funktionsweise von B-Tree-Indizes.
2. Auto-Increment: kompakt, sortiert, aber an eine Sequence gebunden
Ein klassischer Auto-Increment-Schlüssel ist ein Integer oder BigInt, dessen Wert von einer zentralen Sequence oder einem äquivalenten Zähler vergeben wird. Vier oder acht Byte pro Wert sind deutlich kompakter als jede Text- oder Binärdarstellung eines UUIDs, was sich direkt auf die Größe aller Fremdschlüssel-Spalten und Sekundärindizes auswirkt, die diesen Schlüssel referenzieren. Die streng monotone Reihenfolge sorgt für minimale Fragmentierung und eine sehr gute Cache-Lokalität bei Schreibvorgängen.
Der Nachteil liegt in der Zentralisierung: Der nächste Wert kann nur von einer Instanz vergeben werden, die die Sequence verwaltet. In einem einzelnen Datenbank-Cluster mit einem primären Schreibknoten ist das unproblematisch. Sobald mehrere unabhängige Schreibpfade denselben Namensraum an Schlüsseln benötigen, etwa mehrere Regionen, Offline-Clients oder eine spätere Merge-Operation zweier Datenbestände, wird die zentrale Sequence zum limitierenden Faktor oder zur Quelle von Kollisionen.
-- Klassischer Auto-Increment-Primärschlüssel
CREATE TABLE bestellung (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kunde_id BIGINT NOT NULL,
erstellt_am TIMESTAMP NOT NULL DEFAULT now()
);
3. UUID v4: global eindeutig ohne zentrale Koordination
Ein UUID Version 4 besteht aus 128 Bit, von denen 122 Bit echter Zufall sind, der Rest kennzeichnet Version und Variante. Die Kollisionswahrscheinlichkeit ist bei korrekter Zufallsgenerierung praktisch vernachlässigbar, selbst bei Milliarden generierter Werte. Der entscheidende Vorteil gegenüber Auto-Increment ist, dass jeder Client, jeder Service und jede Offline-Instanz einen gültigen Primärschlüssel erzeugen kann, ohne vorher mit einer zentralen Datenbank zu sprechen. Das macht UUIDs attraktiv für verteilte Architekturen, Microservices und Systeme mit vorübergehend getrennten Schreibpfaden.
Der Preis dafür ist vollständige Zufälligkeit in der Sortierreihenfolge. Ein UUIDv4 trägt keinerlei Information über den Erzeugungszeitpunkt, zwei aufeinanderfolgend generierte Werte landen an völlig unterschiedlichen Positionen im Index. Genau diese Eigenschaft, die die verteilte Erzeugung überhaupt erst ermöglicht, ist zugleich die Ursache für die im nächsten Abschnitt beschriebene Indexfragmentierung.
4. Das konkrete Fragmentierungsproblem zufälliger UUIDs im B-Tree
Weil jeder neue UUIDv4-Wert statistisch gleichverteilt über den gesamten 128-Bit-Raum liegt, trifft jede Einfügung eine zufällige Position im Index. Bei Tabellen mit mehreren Millionen Zeilen führt das zu einer hohen Rate an Seiten-Splits, weil praktisch jede bereits gefüllte Seite irgendwann von einer neuen, zufällig dort platzierten Zeile getroffen wird. Die Folge sind größere Indizes mit mehr, dafür schlechter gefüllten Seiten, deutlich mehr Random I/O bei Schreibvorgängen und eine schlechtere Trefferquote im Buffer Pool, weil die für aktuelle Schreibvorgänge relevanten Seiten über den gesamten Indexbaum verstreut sind statt am Ende konzentriert.
In der Praxis äußert sich das als spürbar sinkender Insert-Durchsatz, sobald eine Tabelle mit UUIDv4-Primärschlüssel eine Größe erreicht, bei der der Index nicht mehr vollständig im Arbeitsspeicher gehalten werden kann. Benchmarks verschiedener relationaler Datenbanken zeigen bei rein zufälligen UUID-Schlüsseln regelmäßig einen mehrfach höheren I/O-Aufwand pro Insert gegenüber sequentiellen Schlüsseln, während bei kleinen, komplett gecachten Tabellen der Unterschied kaum messbar ist.
5. ULID und UUIDv7: sortierbare Alternativen mit Zeitstempel-Präfix
ULID und UUIDv7 lösen das Fragmentierungsproblem, indem sie den Wert in zwei Teile gliedern: einen führenden Zeitstempel mit Millisekunden-Auflösung und einen nachfolgenden Zufallsanteil. Bei ULID sind das 48 Bit Zeitstempel und 80 Bit Zufall, bei UUIDv7, seit 2024 offiziell in RFC 9562 standardisiert, ist die Aufteilung ähnlich, aber innerhalb der bestehenden UUID-Struktur mit Version und Variante untergebracht. Weil der Zeitstempel an führender Position steht, sind innerhalb derselben Millisekunde generierte Werte zwar nicht streng sortiert, über größere Zeiträume hinweg ist die Reihenfolge aber praktisch monoton steigend.
Für den B-Tree bedeutet das: Neue Einfügungen landen fast immer nahe am rechten Rand des Index, genau wie bei Auto-Increment, während gleichzeitig weiterhin jeder Client dezentral einen gültigen Wert erzeugen kann, ohne eine zentrale Sequence anzufragen. Der Zufallsanteil bleibt groß genug, um Kollisionen bei paralleler Erzeugung innerhalb derselben Millisekunde praktisch auszuschließen.
-- UUIDv7 direkt in PostgreSQL 18+ erzeugen (native Unterstützung)
INSERT INTO bestellung (id, kunde_id)
VALUES (uuidv7(), 42);
-- ULID/UUIDv7 als BINARY(16) statt CHAR(36) speichern,
-- um Indexgröße und Fragmentierung zusätzlich zu reduzieren
CREATE TABLE ereignis (
id BINARY(16) PRIMARY KEY,
payload JSON NOT NULL
);
6. Speicherbedarf, Lesbarkeit und Kollisionsverhalten im Detail
Ein Auto-Increment-BigInt belegt acht Byte und ist für Menschen auf einen Blick vergleichbar und sortierbar, verrät dabei aber auch die ungefähre Reihenfolge und Anzahl der Datensätze, was in öffentlichen APIs als Informationsleck gelten kann. UUIDs und ULIDs belegen als Binärwert 16 Byte, in der textuellen Darstellung, wie sie in vielen Anwendungen üblich ist, sogar 36 beziehungsweise 26 Zeichen, was sich in jedem Fremdschlüssel und jedem Sekundärindex multipliziert, der diesen Schlüssel referenziert.
ULID codiert seinen Zufallsanteil zusätzlich in Base32 ohne Verwechslungsgefahr zwischen ähnlich aussehenden Zeichen, was die textuelle Darstellung etwas kompakter und in Logs leichter lesbar macht als die klassische UUID-Notation mit Bindestrichen. UUIDv7 wiederum hat den Vorteil, ein offizieller IETF-Standard zu sein und sich nahtlos in bestehende UUID-Spalten, -Bibliotheken und -Validierungen einzufügen, ohne dass Anwendungscode einen neuen Datentyp lernen muss.
7. Trade-offs in verteilten Systemen: Koordination versus Sortierreihenfolge
In einem verteilten System mit mehreren unabhängigen Schreibknoten, etwa mehreren Regionen mit jeweils eigener Schreib-Datenbank oder Clients, die auch offline Datensätze anlegen müssen, ist eine zentrale Sequence entweder gar nicht verfügbar oder erzeugt einen zusätzlichen Netzwerk-Roundtrip pro Insert. UUID, ULID und UUIDv7 lösen dieses Problem identisch, weil alle drei ohne zentrale Instanz erzeugt werden können. Der Unterschied liegt ausschließlich in den Indexeigenschaften der resultierenden Werte.
Bei einem späteren Merge zweier zuvor unabhängiger Datenbestände, etwa nach einer Unternehmensfusion oder beim Zusammenführen von Regional-Datenbanken, verhindern global eindeutige Schlüssel automatisch Kollisionen, während bei Auto-Increment-Schlüsseln aus getrennten Systemen praktisch immer Wertebereichs-Konflikte auftreten, die eine aufwendige Remapping-Migration erfordern. Dieser strukturelle Vorteil bleibt bei ULID und UUIDv7 vollständig erhalten, nur eben mit deutlich besserem Einfügeverhalten als bei UUIDv4.
8. Praktische Implementierungsdetails: Speicherung und Konvertierung
Unabhängig vom gewählten Typ lohnt es sich, UUID- oder ULID-Werte als BINARY(16) statt als 36-Zeichen-String zu speichern. Das halbiert den Speicherbedarf gegenüber der textuellen Darstellung mit Bindestrichen und reduziert dadurch auch die Größe aller referenzierenden Fremdschlüssel-Spalten spürbar. Die meisten Datenbanksysteme bieten dafür native Konvertierungsfunktionen oder einen dedizierten UUID-Datentyp, der intern bereits binär gespeichert wird.
Für die Anwendungsschicht bedeutet das, Konvertierungslogik zwischen der für Menschen lesbaren Textdarstellung und der binären Speicherform sauber an einer Stelle zu kapseln, etwa in einem Value Object oder einer ORM-Typ-Erweiterung, statt an jeder Stelle im Code manuell zu konvertieren. Bei ULID kommt hinzu, dass Bibliotheken für praktisch jede verbreitete Programmiersprache existieren, die Erzeugung, Sortierung und Konvertierung konsistent kapseln.
9. Praktische Empfehlung: welcher Schlüsseltyp zu welchem Szenario passt
Für einen Monolithen mit einer einzelnen primären Datenbank und ohne Anforderung an dezentrale Schlüsselerzeugung bleibt ein Auto-Increment-BigInt die einfachste und performanteste Wahl. Für Systeme mit mehreren unabhängigen Schreibpfaden, Offline-Erzeugung oder absehbaren Merge-Szenarien ist eine sortierbare Alternative wie UUIDv7 oder ULID fast immer die bessere Wahl als klassisches UUIDv4, weil sie dieselbe dezentrale Erzeugung bei deutlich besserem Indexverhalten bietet.
Reines UUIDv4 sollte man heute nur noch dort einsetzen, wo bereits ein bestehendes System oder eine externe Schnittstelle exakt diesen Standard verlangt und eine Migration zu einer sortierbaren Variante nicht praktikabel ist. Für neue Schemata gibt es kaum noch einen guten Grund, sich die Fragmentierung von UUIDv4 einzuhandeln, wenn UUIDv7 dieselbe Dezentralität ohne diesen Nachteil bietet.
| Kriterium | Auto-Increment | UUID v4 | ULID / UUIDv7 |
|---|---|---|---|
| Sortierreihenfolge im Index | Streng monoton | Zufällig verteilt | Praktisch monoton (Zeitstempel-Präfix) |
| Dezentrale Erzeugung | Nicht möglich | Ja, ohne Koordination | Ja, ohne Koordination |
| Speicherbedarf | 8 Byte | 16 Byte binär, 36 Zeichen als Text | 16 Byte binär, 26 Zeichen als Text |
| Index-Fragmentierung | Minimal | Hoch bei großen Tabellen | Gering, ähnlich Auto-Increment |
| Merge zweier Datenbestände | Kollisionsträchtig | Kollisionsfrei | Kollisionsfrei |
Mironsoft
Datenbank-Optimierung, Query-Tuning und Migrationen
SQL-Abfragen, die bei Wachstum immer langsamer werden?
Wir analysieren und optimieren SQL-Datenbanken unabhängig vom eingesetzten System, planen sichere Migrationen und Schema-Änderungen und bringen Teams Query-Optimierung praxisnah bei.
Query-Optimierung
Langsame Abfragen analysieren und mit Indizes und Explain-Plänen gezielt beschleunigen.
Migrations-Planung
Schema-Änderungen und Datenmigrationen sicher und ohne Downtime umsetzen.
Team-Schulung
SQL-Grundlagen und Performance-Denken praxisnah im Entwicklerteam verankern.
10. Zusammenfassung
Primärschlüssel-Typen im Vergleich
Kernproblem
Zufällige UUIDv4-Werte treffen beliebige Positionen im B-Tree-Index, was Seiten-Splits, Fragmentierung und sinkenden Insert-Durchsatz bei großen Tabellen verursacht.
Lösung
ULID und UUIDv7 stellen einen Zeitstempel voran und erzeugen dadurch eine praktisch monotone Sortierreihenfolge bei gleichzeitig dezentraler Erzeugung ohne zentrale Sequence.
Speicherung
UUID- und ULID-Werte als BINARY(16) statt als 36-Zeichen-String speichern, um Indexgröße und referenzierende Fremdschlüssel-Spalten spürbar kleiner zu halten.
Empfehlung
Auto-Increment für einfache Monolithen, UUIDv7 oder ULID für verteilte Systeme mit mehreren Schreibpfaden, reines UUIDv4 nur noch bei zwingender Kompatibilitätsanforderung.