UUID v4 vs. v7 vs. ULID vs. Snowflake — welche ID wann?
Wer eine neue Tabelle anlegt, greift fast reflexhaft zur UUIDv4 als Primärschlüssel — sie ist überall verfügbar, eindeutig und einfach. Doch v4 hat in großen Tabellen ein hartnäckiges Performance-Problem, das mit UUIDv7 (RFC 9562, 2024), ULID oder Snowflake elegant gelöst wird. Dieser Artikel ordnet die vier Varianten ein und gibt eine pragmatische Empfehlung.
Warum überhaupt zufällige IDs?
Klassische auto-incrementierende Integer-IDs sind kompakt und sortierbar, haben aber zwei harte Probleme. Erstens leaken sie Geschäftsinformationen: wer /invoices/4711 abruft, weiß sofort, dass es ca. 4711 Rechnungen gibt — und kann durch Inkrementieren weitere Tickets, Bestellungen oder User-Profile entdecken. Zweitens skalieren sie schlecht über verteilte Systeme, weil sie eine zentrale Sequenz brauchen.
Zufällige oder pseudo-zufällige IDs lösen beide Probleme: sie sind nicht enumerierbar und können von jedem Knoten unabhängig erzeugt werden, ohne Kollisionsrisiko. Der Standard dafür ist seit Jahrzehnten die UUID — ein 128-Bit-Wert, der je nach Variante unterschiedliche Eigenschaften hat.
UUIDv4 — der Klassiker
UUIDv4 ist im Kern 122 Bit Zufall plus 6 Bit Versions- und Varianten-Marker. Erzeugt wird sie aus einem kryptografisch sicheren Zufallsgenerator. Die Kollisionswahrscheinlichkeit ist astronomisch niedrig — selbst bei einer Billion erzeugter IDs pro Sekunde über 100 Jahre würde man rein statistisch keine Doppelung erwarten.
Genau diese Zufälligkeit ist Stärke und Schwäche zugleich. Stärke: keine zentrale Koordination, kein Leak, kein Timestamp drin. Schwäche: in einem B-Tree-Index sind aufeinanderfolgende Einfügungen über die gesamte Index-Struktur verteilt — jeder Insert berührt eine andere Page, der Cache hilft kaum, der Index fragmentiert.
Das Index-Problem in der Praxis
Ein Beispiel: eine MySQL-Tabelle mit 100 Millionen Zeilen und UUIDv4 als Primärschlüssel hat einen InnoDB-B-Tree von mehreren GB. Beim Einfügen einer neuen Zeile landet sie irgendwo mitten im Index, eine Page muss gelesen, modifiziert und zurückgeschrieben werden. Bei zehntausend Inserts pro Sekunde sind das zehntausend zufällige Disk-Seeks (oder zumindest Cache-Misses) — die Schreibperformance bricht ein.
Bei einer monoton wachsenden ID (auto-increment, Timestamp-basiert, …) landen Inserts dagegen immer am Ende des Index, in derselben oder einer benachbarten Page. Der Cache trifft fast immer, der Index bleibt kompakt. Genau hier setzen die jüngeren ID-Schemata an.
UUIDv7 — Zeitstempel-Präfix nach RFC 9562
UUIDv7 wurde 2024 mit RFC 9562 standardisiert. Sie kombiniert die ersten 48 Bit als Unix-Timestamp in Millisekunden mit 74 Bit Zufall plus den üblichen 6 Bit Versions-Marker. Ergebnis: die ID ist zeitlich sortierbar, bleibt aber pro Millisekunde durch 2^74 mögliche Zufalls-Suffixe praktisch kollisionsfrei.
Für Datenbanken ist das ein Gamechanger: Einfügen erfolgt fast monoton steigend, der Index bleibt sequenziell, die Schreibperformance entspricht praktisch der von Integer-IDs. Gleichzeitig bleibt der Vorteil der Random-UUID erhalten — keine Enumerierbarkeit, keine zentrale Sequenz, Multi-Region-fähig. Postgres, MySQL und SQL Server haben seit 2024/25 native Unterstützung oder Extensions.
ULID — der inoffizielle Vorläufer
ULID (Universally Unique Lexicographically Sortable Identifier, 2016) hatte UUIDv7 lange vorweggenommen: 48 Bit Timestamp, 80 Bit Zufall, lexikografisch sortierbar in Crockford-Base32 (26 Zeichen statt 36 Zeichen Standard-UUID). Viele Sprachen und Datenbanken haben ULID-Libraries, lange bevor RFC 9562 fertig war.
Funktional ist ULID fast identisch mit UUIDv7. Unterschiede: kürzere String-Form (gut für URLs), Crockford-Base32 vermeidet visuell ähnliche Zeichen, aber es fehlt der formale RFC-Standard. Wer heute neu startet, sollte UUIDv7 nehmen — wer bereits ULID nutzt, hat keinen Grund zu migrieren.
Snowflake — Twitter, Discord, X
Snowflake-IDs sind 64-Bit-Integer (kein UUID-Format!): typisch 41 Bit Timestamp, 10 Bit Maschinen-ID, 12 Bit Sequenz-Counter pro Millisekunde. Damit passen sie in einen BIGINT, sind extrem kompakt, sortierbar nach Zeit und können auf bis zu 1.024 Knoten parallel ohne Koordination 4.096 IDs pro Millisekunde pro Knoten erzeugen.
Trade-off: Snowflake leakt den Zeitstempel sehr genau (und damit das Erstellungsdatum von Objekten — wie bei Twitter, wo das Erstellungsdatum eines Tweets direkt aus der ID rekonstruiert werden kann). Wer das nicht will, nimmt UUIDv7. Snowflake glänzt da, wo extreme Schreibrate und kompakte IDs zählen — und Implementierungen wie Sonyflake oder TSID erweitern das Konzept.
Entscheidungshilfe
Eine kompakte Faustregel:
- UUIDv4: für kleine Tabellen, externe API-IDs, Tokens — überall wo Sortierung egal ist und maximale Zufälligkeit zählt.
- UUIDv7: heutiger Default für neue Tabellen mit hohem Schreibvolumen. Standardisiert, weit unterstützt, monoton, kein Index-Schmerz.
- ULID: wenn Sie schon Bestands-Code damit haben oder die kürzere String-Form (26 Zeichen) für URLs wichtig ist.
- Snowflake: bei sehr hohen Schreibraten (Twitter, Discord, Trading), wenn 64-Bit-BIGINT statt 128-Bit-UUID viel Speicher spart und Timestamp-Leak akzeptabel ist.
Mein UUID-v4-Trauma und der Wechsel auf v7
2020 habe ich für ein E-Commerce-Projekt von 2 Mio Bestellungen pro Jahr UUID-v4 als Primärschlüssel gewählt. Funktionierte hervorragend — bis die Tabelle 60 Mio Zeilen erreicht hatte. Plötzlich dauerten INSERTs nicht mehr 2 ms, sondern 80-150 ms. Der Grund: B-Tree-Indizes mögen sequenzielle Inserts (am Ende anhängen ist schnell), zufällige UUIDs landen aber überall im Index. Jedes INSERT triggert Page-Splits und Index-Rebuilds an wechselnden Positionen.
Die Migration auf UUID-v7 dauerte ein Wochenende. Bestehende v4-UUIDs blieben unverändert (kein Daten-Migrations-Problem), nur neue IDs werden als v7 erzeugt. Eine triviale Codeänderung — statt Uuid::uuid4() einfach Uuid::uuid7(). INSERT-Performance war eine Woche später wieder bei 2-5 ms. Die wertvollste Lehre: bei jeder Architektur-Entscheidung »will ich diese ID-Strategie noch bei 100x meiner aktuellen Daten?« mitdenken.
Was ich zusätzlich gelernt habe: UUID v7 leakt einen Zeitstempel. In einem öffentlichen Kontext (z. B. URLs mit IDs) kann ein Beobachter sehen, in welcher Reihenfolge Ressourcen erstellt wurden, und mit Glück auch die ungefähre Uhrzeit. Das ist meistens ok — Bestellungen werden ja eh zeitlich sortiert. Aber bei sensitiven Daten (z. B. interne Tickets mit »erste Beschwerde war XYZ«) sollte man v4 oder ULID mit Crockford-Base32 (weniger offensichtlich) wählen.
Migration: wie du von Auto-Increment auf UUIDs umsteigst
Wenn dein Bestandssystem mit Auto-Increment-INTs läuft und du auf UUIDs migrieren willst, hast du drei Optionen. (1) Hybrid: bestehende INT-IDs behalten, neue Spalten für UUIDs ergänzen. Vorteil: kein Daten-Risiko, parallele Existenz. Nachteil: dauerhaft komplexer. (2) Big-Bang: Tabellen-Schemata komplett umstellen, alle Fremdschlüssel mit. Sehr aufwendig, hohes Risiko, lange Downtime. (3) Phased: in einem Schritt UUID-Spalten ergänzen und parallel pflegen, im zweiten Schritt INT-Spalten loswerden.
Mein realer Plan bei einem ERP-System 2024: zuerst alle Tabellen mit uuid CHAR(36) NOT NULL DEFAULT (UUID_v7()) erweitert (MySQL 8.0+ unterstützt das nativ über eine Custom-Function). 6 Wochen lang parallel betrieben, alle Schreibvorgänge füllten beide IDs. Dann APIs auf UUIDs umgestellt — die alte INT-ID blieb als Fallback in 5 % der Aufrufe noch nötig. Nach 12 Wochen war die INT-ID nur noch im Migration-Skript referenziert und wurde gedroppt. Gesamt-Downtime: 3 Mal jeweils 15 Minuten für die Schema-Changes.
Wichtigster Tipp: NIE die Bestands-IDs ändern. Wenn du eine UUID zusätzlich vergibst, behalte die alte INT-ID auf jeden Fall in einer Spalte legacy_id. Externe Systeme (Buchhaltung, Analytics, Logs) referenzieren oft die alten IDs. Diese Spalte hat keinen Index, kostet wenig Platz, ermöglicht aber Forensik bei Bedarf — eine kleine Versicherung mit großem Nutzen.
Storage-Optionen im Detail
Wie du UUIDs/ULIDs in der Datenbank physisch ablegst, hat massiven Einfluss auf Performance und Speicherplatz. Eine Übersicht der gängigen Optionen 2026:
- CHAR(36) — String mit Bindestrichen. Lesbar, debug-freundlich, aber 36 Byte plus Overhead. Bei 60 Mio Zeilen sind das 2,1 GB nur für die ID-Spalte. Indexe entsprechend langsam.
- BINARY(16) — kompakte Variante. Genau die 128 Bit, die eine UUID braucht. 16 Byte pro Zeile, also gut 4x weniger als CHAR(36). Indexe sind entsprechend schneller. Nachteil: in WHERE-Clauses musst du mit
UNHEX(REPLACE(uuid, '-', ''))arbeiten — etwas Code-Overhead. - Native uuid-Typen. Postgres hat
uuid, SQL Server hatuniqueidentifier, MariaDB ab 10.7 hatuuid. MySQL hat bis heute keinen nativen Typ — du musst BINARY(16) oder CHAR(36) wählen. Wer Postgres nutzt: einfachuuidverwenden und vergessen, dass das ein gespeichertes Format ist. - Compression on top. InnoDB unterstützt seit Version 5.6 Page-Compression. Mit
ROW_FORMAT=COMPRESSEDauf einer UUID-Tabelle erreichst du oft 40-60 % Speicherersparnis bei minimalem CPU-Overhead. Kombiniert mit BINARY(16): aus 60 Mio Zeilen × (16+overhead) Byte werden mit Compression nur noch ca. 600 MB.
Meine Default-Konfiguration für neue MySQL-Projekte 2026: BINARY(16) für UUIDs, ROW_FORMAT=COMPRESSED für Tabellen mit > 1 Mio Zeilen. Bei Postgres-Projekten: nativer uuid-Typ ohne Komplikation. Bei MariaDB: nativer uuid-Typ ab 10.7. Wer Mongo nutzt: ObjectIDs sind eigentlich Sortable-IDs ähnlich zu ULID — kein Migration-Bedarf.
Implementierung in den gängigen Sprachen 2026
Wie du UUID v4/v7 oder ULID konkret erzeugst — die wichtigsten Sprachen mit aktuellen Library-Empfehlungen 2026:
- Python. Standard-Library:
uuid.uuid4(). Für v7:uuid6-Library (pip install uuid6, dannuuid6.uuid7()). Für ULID:python-ulid(pip install python-ulid, dannulid.new()). Seit Python 3.13 (geplant) ist UUID v7 in der stdlib. - JavaScript/TypeScript. Im Browser:
crypto.randomUUID()liefert v4 nativ (alle modernen Browser). Für v7:uuidv7-Package (npm install uuidv7,uuidv7()). Für ULID:ulid-Package (npm install ulid,ulid()). Beide sind tree-shakeable und winzig. - PHP. Standard:
ramsey/uuid(Composer, am meisten genutzt).Uuid::uuid4(),Uuid::uuid7(). ULID:robinvdvleuten/php-ulid. Native PHP-Funktionuniqid()ist KEIN UUID, sondern ein milliseconds-basierter String — nicht verwenden. - Go.
github.com/google/uuidfür v4 (uuid.New()). Für v7:github.com/gofrs/uuid/v5(uuid.NewV7()). ULID:github.com/oklog/ulid/v2. Go-Performance ist hier marginal besser als JS oder Python — bei sehr hohen ID-Generierungs-Raten relevant. - PostgreSQL nativ. Seit Postgres 13:
gen_random_uuid()für v4 (Extensionpgcryptowird automatisch geladen). Für v7 ab Postgres 17 (geplant):uuidv7(). ULID in Postgres: Extensionpg_ulid. Native Generierung in der DB ist 5-10x schneller als Application-Layer-Generierung über Netzwerk-Roundtrips.
Häufige Fragen
Kann ich von UUIDv4 auf UUIDv7 migrieren?
Bestehende IDs bleiben gültig — das UUID-Format ist abwärtskompatibel. Du kannst ab einem bestimmten Stichtag neue Inserts mit v7 anlegen und die alten v4-IDs unverändert lassen. Der Index profitiert dann zumindest für die neuen Einträge sofort. Eine echte Backfill-Migration ist meist nicht sinnvoll, weil alte UUIDs in externen Systemen verlinkt sind.
Ist UUIDv7 wirklich Multi-Region-sicher?
Ja — der RFC fordert eine Clock-Synchronisation auf Millisekunden-Niveau, was über NTP problemlos erreichbar ist. Sollten zwei Knoten gleichzeitig im selben Millisekunden-Fenster IDs erzeugen, sorgen die 74 Zufalls-Bits dafür, dass eine Kollision praktisch ausgeschlossen ist. Lediglich für eine globale Sortierreihenfolge braucht man weiterhin eine eindeutige Ordnung — die ist im UUID-Standard nicht garantiert.
Soll ich UUIDs als BINARY(16) oder als String speichern?
Performance: immer BINARY(16). Eine UUID als String (CHAR(36)) belegt 2,25x so viel Platz, lädt langsamer und macht Indexe größer. Postgres hat einen nativen uuid-Typ, MySQL und SQL Server brauchen BINARY(16) mit Hilfsfunktionen. Der einzige Grund für Strings: Debug-Komfort beim Loggen — aber das löst man besser mit einem hex/uuid-Display in den Tools.
Sind UUIDs wirklich global einzigartig?
Praktisch ja, theoretisch nicht garantiert. Bei UUID v4 ist die Wahrscheinlichkeit einer Kollision bei 122 Bit Zufall extrem niedrig — wenn du 1 Mrd UUIDs pro Sekunde erzeugst, brauchst du über 600 Jahre, bevor die erste Kollision mit 50 % Wahrscheinlichkeit auftritt (Geburtstagsparadoxon). Für jede reale Anwendung: ja, einzigartig. Mathematische Garantie: nein, sondern nur statistische Sicherheit.
Was ist NanoID und wann sollte ich es nutzen?
NanoID ist eine kompaktere Alternative zu UUIDs: 21 Zeichen URL-safer String (gegenüber 36 Zeichen UUID), gleiche Kollisions-Sicherheit. Use Cases: in URLs (kürzer = schöner), als sichtbare Token (Einladungs-Links, kurze Share-IDs). Nicht für Datenbank-Primärschlüssel — da hat NanoID die gleichen Index-Performance-Probleme wie UUID v4 ohne den Vorteil eines sortierten Formats.
Soll ich UUIDs in URLs verwenden?
Vorsicht. UUIDs in URLs (z. B. /orders/550e8400-e29b-41d4-a716-446655440000) sind technisch sicher (nicht erratbar), aber benutzerunfreundlich. Wer eine URL teilen will, klagt über die Länge. Empfehlung: für interne API-Endpunkte UUIDs ok, für öffentliche User-facing URLs lieber ein zusätzliches kurzes Slug-System (/orders/abc123 mit interner Mapping zu UUID). Bei B2B-Tools (Admin-Panels), wo UX zweitrangig ist, sind UUIDs in URLs unproblematisch.
Kommentare
Die Kommentare werden von Disqus bereitgestellt. Bevor sie geladen werden, brauchen wir deine Einwilligung — Disqus ist ein Drittanbieter und setzt eigene Cookies.