UUID, ULID oder Auto-Increment: Primärschlüssel-Wahl mit Index-Folgen
AI generated
SELECT
JOIN
SQL / Primärschlüssel
UUID, ULID oder Auto-Increment?
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.

11 Min. Lesezeit UUIDv4 vs. UUIDv7 B-Tree-Fragmentierung

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.

11. FAQ: Primärschlüssel-Typen im Vergleich

1Warum fragmentiert ein UUIDv4-Primärschlüssel den Index stärker als Auto-Increment?
Weil jeder neue UUIDv4-Wert statistisch gleichverteilt über den gesamten Wertebereich liegt, trifft jede Einfügung eine zufällige Position im B-Tree, was regelmäßige Seiten-Splits und schlechter gefüllte Indexseiten erzeugt.
2Was ist der wesentliche Unterschied zwischen ULID und UUIDv7?
Beide stellen einen Millisekunden-Zeitstempel voran, ULID nutzt dafür ein eigenes, Base32-basiertes Format mit 26 Zeichen, während UUIDv7 als offizieller IETF-Standard innerhalb der bestehenden UUID-Struktur mit 36 Zeichen bleibt.
3Kann man UUIDv4-Spalten nachträglich auf UUIDv7 migrieren?
Bestehende UUIDv4-Werte lassen sich nicht in UUIDv7 umwandeln, ohne ihre Bedeutung zu verändern. Praktikabel ist meist nur, neue Zeilen ab einem Stichtag mit UUIDv7 zu erzeugen und alte Werte unverändert zu belassen.
4Warum sollte man UUID-Werte als BINARY(16) statt als String speichern?
Die binäre Speicherung halbiert den Platzbedarf gegenüber der 36-Zeichen-Textdarstellung mit Bindestrichen und reduziert dadurch auch die Größe aller Fremdschlüssel-Spalten und Sekundärindizes, die den Schlüssel referenzieren.
5Verrät ein Auto-Increment-Primärschlüssel sensible Informationen?
Ja, die fortlaufende Nummerierung erlaubt Rückschlüsse auf die ungefähre Gesamtzahl und das relative Alter von Datensätzen, was in öffentlich sichtbaren URLs oder APIs als unerwünschtes Informationsleck gelten kann.
6Ist die Kollisionswahrscheinlichkeit bei UUIDv7 höher als bei UUIDv4?
Nein, der Zufallsanteil bleibt bei UUIDv7 groß genug, um Kollisionen bei paralleler Erzeugung innerhalb derselben Millisekunde praktisch auszuschließen, das Kollisionsrisiko unterscheidet sich in der Praxis kaum von UUIDv4.
7Lohnt sich ULID oder UUIDv7 auch für eine kleine, einfache Anwendung?
Bei kleinen, vollständig im Arbeitsspeicher gecachten Tabellen ist der Fragmentierungseffekt kaum spürbar, sodass der einfachere Auto-Increment-Schlüssel dort meist ausreicht, sofern keine dezentrale Erzeugung benötigt wird.
8Was passiert beim Merge zweier Datenbanken mit Auto-Increment-Schlüsseln?
Weil beide Systeme unabhängig voneinander denselben Wertebereich vergeben haben, entstehen fast immer Kollisionen, die eine aufwendige Remapping-Migration aller betroffenen Fremdschlüssel-Beziehungen erfordern.
9Unterstützen alle gängigen Datenbanksysteme UUIDv7 bereits nativ?
Die native Unterstützung wächst, PostgreSQL bietet seit Version 18 eine eingebaute Funktion, in älteren Versionen und anderen Systemen lässt sich UUIDv7 aber problemlos in der Anwendungsschicht mit einer Bibliothek erzeugen.
10Beeinflusst der Primärschlüssel-Typ auch die Leseperformance, nicht nur Inserts?
Ja, ein fragmentierter Index mit über viele Seiten verstreuten, logisch zusammengehörigen Zeilen führt auch bei Bereichsabfragen und Sortierungen nach dem Primärschlüssel zu mehr Random I/O als ein kompakter, sequentiell gefüllter Index.