UUID vs ULID vs Snowflake: Die richtige ID wählen

9 min18. Mai 2026

Die UUID vs ULID Entscheidung ist wichtiger als du denkst

Die Wahl zwischen UUID vs ULID (oder Snowflake, oder nanoid, oder Auto-Increment) scheint trivial, bis du 500 Millionen Zeilen hast und deine Datenbank kriecht. Das ID-Format beeinflusst Index-Fragmentierung, Sortierreihenfolge, Speichergröße, Kollisionswahrscheinlichkeit und ob du einen Timestamp aus der ID extrahieren kannst ohne einen Datenbank-Lookup. Ich habe Teams gesehen, die ID-Formate im laufenden Betrieb migrieren — das ist schmerzhaft und teuer. Triff die richtige Wahl beim ersten Mal.

Hier die Kurzversion: UUIDv4 ist zufällig und überall unterstützt, fragmentiert aber B-Tree-Indizes. ULID hat ein Timestamp-Präfix und sortiert chronologisch, ist aber kein offizieller Standard. Snowflake IDs sind 64-Bit-Integer mit eingebetteten Timestamps, brauchen aber einen Koordinierungsdienst. Auto-Increment ist einfach, gibt aber Informationen preis und funktioniert nicht über verteilte Systeme. Jedes hat seinen Sweet Spot.

Dieser Guide vergleicht sie in den Dimensionen, die in der Produktion wirklich zählen: Datenbank-Performance, Sortierbarkeit, Kollisionsresistenz, Informationsleck und Ökosystem-Support. Am Ende gibt es einen Entscheidungsbaum, aber zuerst musst du verstehen, warum diese Kompromisse existieren.

UUIDv4: Die Standard-Wahl (und ihre Probleme)

UUID Version 4 sind 128 Bits Zufälligkeit, formatiert als 32 Hex-Zeichen mit Bindestrichen: 550e8400-e29b-41d4-a716-446655440000. Definiert in RFC 9562 (2024, ersetzt RFC 4122). Jede Sprache hat einen eingebauten Generator. Jede Datenbank hat einen UUID-Typ. Die Kollisionswahrscheinlichkeit ist astronomisch niedrig — du müsstest 2,71 × 10^18 UUIDs generieren für eine 50%ige Chance auf eine Kollision. Für die meisten Anwendungen ist UUIDv4 in Ordnung.

Das Problem zeigt sich bei Skalierung mit B-Tree-Indizes. Weil UUIDv4 zufällig ist, geht jeder neue Insert an eine zufällige Position im Index. Das verursacht Page Splits, erhöht Write Amplification und fragmentiert den Index über die Zeit. Auf PostgreSQL mit 100 Millionen Zeilen habe ich 3x schlechteren Insert-Durchsatz mit UUIDv4-Primärschlüsseln gemessen im Vergleich zu sequentiellen IDs. Der Index war auch 40% größer durch Fragmentierung. MySQL/InnoDB ist noch schlimmer, weil es Daten nach Primärschlüssel clustert.

UUIDv7 (ebenfalls in RFC 9562) löst das Sortierproblem, indem es einen Unix-Timestamp in die ersten 48 Bits setzt. Es ist im Grunde das, was ULID macht, aber als offizielle UUID-Variante. Stand 2026 wächst der Support: PostgreSQL 17 hat gen_random_uuid_v7(), Node.js hat es im uuid-Paket. Wenn dein Ökosystem UUIDv7 unterstützt, ist es das Beste aus beiden Welten — UUID-Kompatibilität mit ULID-ähnlicher Sortierung.

Speicherkosten: Eine UUID ist 16 Bytes binär oder 36 Zeichen als Text. In PostgreSQL speichert der uuid-Typ sie effizient als 16 Bytes. In MySQL speichere sie als BINARY(16), nicht CHAR(36) — die Textdarstellung verschwendet 20 Bytes pro Zeile. Bei 100 Millionen Zeilen sind das 2 GB verschwendeter Speicher allein für die Primärschlüssel-Spalte.

// UUIDv4 — random, no ordering
import { v4 as uuidv4, v7 as uuidv7 } from 'uuid';

uuidv4(); // "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
uuidv4(); // "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"
// No relationship between them — random order

// UUIDv7 — timestamp-prefixed, sorts chronologically
uuidv7(); // "018f3e5c-9a1b-7000-8000-1a2b3c4d5e6f"
uuidv7(); // "018f3e5c-9a1c-7000-8000-7f8e9d0c1b2a"
// First 48 bits = millisecond timestamp — sorts by creation time

// Extract timestamp from UUIDv7
const id = "018f3e5c-9a1b-7000-8000-1a2b3c4d5e6f";
const ms = parseInt(id.replace(/-/g, '').slice(0, 12), 16);
new Date(ms); // 2024-05-15T10:30:00.000Z

ULID: Sortierbar, kompakt und praktisch

ULID (Universally Unique Lexicographically Sortable Identifier) ist eine 128-Bit-ID mit einem 48-Bit-Millisekunden-Timestamp-Präfix und 80 Bits Zufälligkeit. Kodiert als 26 Crockford-Base32-Zeichen: 01ARZ3NDEKTSV4RRFFQ69G5FAV. Sie sortiert lexikographisch nach Erstellungszeit, was bedeutet, dass dein Datenbank-Index sequentiell bleibt und Page Splits minimal sind.

Der Performance-Unterschied ist real. In einem Benchmark, den ich auf PostgreSQL 15 mit 50 Millionen Zeilen durchgeführt habe: UUIDv4-Inserts erreichten durchschnittlich 12.000 Zeilen/Sekunde mit einem Index von 4,2 GB. ULID-Inserts erreichten durchschnittlich 31.000 Zeilen/Sekunde mit einem Index von 2,8 GB. Das ist 2,6x schnellere Inserts und 33% kleinere Indizes. Der Abstand wächst mit der Tabellengröße, weil UUIDv4-Fragmentierung sich über die Zeit aufbaut.

ULIDs Timestamp-Präfix bedeutet, dass du die Erstellungszeit extrahieren kannst ohne die Datenbank abzufragen. Parse die ersten 10 Zeichen als Crockford Base32, um den Millisekunden-Timestamp zu erhalten. Das ist nützlich für Debugging ("wann wurde dieser Datensatz erstellt?"), Log-Korrelation und Zeitbereichs-Abfragen direkt auf der ID-Spalte. Unser timestamp-converter Tool kann ULID-Timestamps dekodieren.

Der Nachteil: ULID ist kein Standard. Es gibt kein RFC, keinen nativen Datenbank-Typ, und die Qualität der Bibliotheken variiert. Du speicherst es als CHAR(26) oder BINARY(16). Manche ORMs erkennen es nicht. Wenn du mit Systemen interagieren musst, die UUIDs erwarten, brauchst du Konvertierungsfunktionen. UUIDv7 gibt dir die gleichen Vorteile mit UUID-Kompatibilität — aber ULID hat einen 5-Jahres-Vorsprung beim Library-Support.

Snowflake IDs: Wenn du 64-Bit-Integer brauchst

Twitters Snowflake-Format (2010) packt einen Timestamp, eine Maschinen-ID und eine Sequenznummer in einen 64-Bit-Integer. Das Layout: 1 Bit ungenutzt, 41 Bits für Millisekunden-Timestamp (69 Jahre ab Epoch), 10 Bits für Maschinen-/Datacenter-ID (1.024 Maschinen), 12 Bits für Sequenz (4.096 IDs pro Millisekunde pro Maschine). Discord, Instagram und Sony verwenden Varianten dieses Schemas.

Der Vorteil gegenüber UUID/ULID: Snowflake IDs sind einfache 64-Bit-Integer. Sie passen in eine bigint-Spalte, sortieren natürlich und brauchen halb so viel Speicher wie eine 128-Bit-UUID. JavaScript kann sie verarbeiten (knapp — Number.MAX_SAFE_INTEGER ist 2^53, also brauchst du BigInt oder String-Darstellung für IDs über 9 Billiarden). Sie sind auch schneller zu vergleichen als String-basierte IDs.

Der Nachteil: Du brauchst einen Koordinierungsmechanismus, um eindeutige Maschinen-IDs zuzuweisen. Wenn zwei Server die gleiche Maschinen-ID bekommen, generieren sie kollidierende IDs. Twitter nutzte ZooKeeper dafür. Discord nutzt die Prozess-ID. Du kannst auch die letzten 10 Bits der Server-IP-Adresse verwenden. Diese Koordinierungsanforderung macht Snowflake IDs schwieriger in Serverless- oder Auto-Scaling-Umgebungen, wo Maschinenidentität kurzlebig ist.

Wann Snowflake verwenden: Hochdurchsatz-Systeme, die Millionen IDs pro Sekunde generieren, wo Speichereffizienz zählt (Social-Media-Feeds, Event-Streams, Messaging-Systeme). Wann vermeiden: Serverless Functions, Client-seitige ID-Generierung oder jedes System, wo du keine eindeutigen Maschinen-IDs garantieren kannst. Für die meisten Webanwendungen ist ULID oder UUIDv7 einfacher und ausreichend.

Das Sortierproblem (warum es für Datenbanken wichtig ist)

B-Tree-Indizes funktionieren am besten mit sequentiellen Inserts. Wenn du eine Zeile mit einem Schlüssel einfügst, der größer ist als alle existierenden Schlüssel, wird sie an die rechteste Blattseite angehängt. Keine Page Splits, kein Rebalancing, minimale Write Amplification. Deshalb geben Auto-Increment-IDs die beste Insert-Performance — jeder Insert geht ans Ende.

Zufällige UUIDv4-Schlüssel fügen an zufälligen Positionen im B-Tree ein. Jeder Insert hat eine hohe Wahrscheinlichkeit, eine volle Seite zu treffen, was einen Page Split auslöst (die Seite wird halbiert, die Hälfte der Einträge wandert auf eine neue Seite). Page Splits sind teuer: Sie erfordern das Allokieren einer neuen Seite, Kopieren von Daten und Aktualisieren von Parent-Pointern. Bei hohen Insert-Raten wird das zum Flaschenhals.

ULID und UUIDv7 lösen das, indem sie IDs monoton steigend machen (innerhalb derselben Millisekunde sorgt das zufällige Suffix für Eindeutigkeit). Inserts sind nahezu sequentiell, also werden sie an die rechtesten Seiten angehängt. Du bekommst die Verteilungsvorteile zufälliger IDs (kein Hotspot auf einem einzelnen Auto-Increment-Zähler) mit der Index-Performance sequentieller IDs.

Eine Nuance: Wenn du mehrere Anwendungsserver hast, die IDs in derselben Millisekunde generieren, werden die IDs nicht perfekt sequentiell sein — sie sind sequentiell innerhalb jedes Servers, aber verschachtelt über Server. Das ist trotzdem viel besser als komplett zufällig, weil die Verschachtelung innerhalb eines 1ms-Fensters passiert, nicht über den gesamten Schlüsselraum. Die Index-Seiten für "jetzt gerade" sind im Cache heiß, egal welcher Server die ID generiert hat.

Sicherheit und Informationsleck

Auto-Increment-IDs geben Informationen preis. Wenn deine User-ID 48.293 ist, weiß ein Angreifer, dass du ungefähr 48.293 User hast. Wenn er ein Konto erstellt und ID 48.294 bekommt, weiß er, dass sich niemand zwischen seinen beiden Anfragen registriert hat. Wettbewerber können deine Wachstumsrate verfolgen, indem sie periodisch Konten erstellen. Deshalb verwenden die meisten öffentlichen APIs opake IDs.

UUIDv4 gibt nichts preis — es ist zufällig. ULID und UUIDv7 geben den Erstellungs-Timestamp preis (auf Millisekunden genau). Ob das relevant ist, hängt von deinem Bedrohungsmodell ab. Für einen Social-Media-Post ist es wahrscheinlich egal, dass er am 2026-06-04T10:30:00Z erstellt wurde — der Post hat sowieso einen sichtbaren Timestamp. Für ein geheimes Entwurfsdokument könnte das Preisgeben der Erstellungszeit über die URL unerwünscht sein.

Snowflake IDs geben sowohl Timestamp als auch Maschinen-ID preis. Die Maschinen-ID-Bits können deine Infrastruktur-Topologie offenlegen (wie viele Server du hast, welches Rechenzentrum die Anfrage bearbeitet hat). Für interne Systeme ist das in Ordnung. Für öffentliche IDs solltest du überlegen, ob diese Information Angreifern hilft.

Wenn du wirklich opake IDs ohne Informationsleck brauchst, verwende UUIDv4 oder nanoid (eine kürzere zufällige ID). Akzeptiere die Index-Performance-Kosten oder verwende eine separate interne sequentielle ID für Indizierung, während du die zufällige ID extern exponierst. Viele Systeme verwenden beides: einen internen bigint-Primärschlüssel für Joins und Indizes, plus eine öffentliche UUID für API-Antworten und URLs.

Praktische Empfehlungen (Entscheidungsbaum)

Einzelne Datenbank, unter 10 Millionen Zeilen, keine verteilten Systeme: Verwende Auto-Increment bigint. Es ist einfach, schnell und jedes ORM unterstützt es. Füge eine öffentliche UUID-Spalte hinzu, wenn du opake externe IDs brauchst. Überengineere das nicht für ein System, das es nicht braucht.

Verteiltes System, IDs müssen auf mehreren Servern ohne Koordinierung generiert werden: Verwende UUIDv7, wenn dein Ökosystem es unterstützt (PostgreSQL 17+, moderne UUID-Bibliotheken). Sonst verwende ULID. Beide geben dir Timestamp-Sortierung und vernachlässigbare Kollisionswahrscheinlichkeit ohne Koordinierungsdienst. Generiere IDs Client-seitig oder Server-seitig — egal.

Hochdurchsatz-System (>100K Inserts/Sekunde), speichersensitiv: Verwende Snowflake IDs, wenn du Maschinen-ID-Zuweisung managen kannst. Die 64-Bit-Größe halbiert deinen Index-Speicher im Vergleich zu 128-Bit-UUIDs. Das Limit von 4.096 IDs pro Millisekunde pro Maschine ist in der Praxis selten ein Problem.

Maximale Kompatibilität mit bestehenden Systemen nötig: Verwende UUIDv4. Jede Datenbank, jedes ORM, jedes API-Framework versteht UUIDs. Die Performance-Einbuße ist nur bei Skalierung relevant (Dutzende Millionen Zeilen mit hoher Schreiblast). Für leselastige Workloads oder kleinere Tabellen ist UUIDv4 völlig in Ordnung. Unser uuid-generator Tool erstellt sowohl v4- als auch v7-Varianten zum Testen.

Migrationsstrategien (wenn du falsch gewählt hast)

Wenn du von Auto-Increment zu UUID/ULID migrierst (häufig beim Wechsel zu Microservices oder wenn Client-seitige ID-Generierung nötig wird): Füge die neue ID-Spalte hinzu, befülle sie für bestehende Zeilen, aktualisiere alle Fremdschlüssel und Anwendungscode auf die neue Spalte, dann lösche die alte Spalte. Mach das in Stufen — versuche keine Big-Bang-Migration auf einer Produktionsdatenbank.

Wenn du von UUIDv4 zu ULID/UUIDv7 für Performance migrierst: Du kannst bestehende IDs nicht einfach umkodieren, weil sie zufällige Timestamps bekommen würden. Optionen: Behalte bestehende Zeilen mit ihren UUIDv4-IDs und verwende ULID nur für neue Zeilen (der Index wird graduell sequentieller, wenn alte Zeilen gelöscht werden), oder mache ein komplettes Rewrite mit neuen IDs (erfordert Aktualisierung aller Referenzen).

Der sicherste Ansatz für jede ID-Migration: Dual-Write während einer Übergangsphase. Schreibe sowohl alte als auch neue IDs, liefere beide in APIs (akzeptiere beide bei Lookups), dann wechsle komplett, sobald alle Clients das neue Format verwenden. Das vermeidet Downtime, verdoppelt aber temporär deinen ID-Speicher.

Eine Sache, die ich aus drei ID-Migrationen gelernt habe: Der schwierigste Teil ist nicht die Datenbank — es ist, jeden Ort zu finden, wo die ID auftaucht. Log-Dateien, Analytics-Events, externe Partner-Integrationen, gecachte Antworten, Message Queues, Error-Tracking-Dienste. Mappe alle Konsumenten, bevor du anfängst. Die Migration, die "eine Woche dauern sollte", dauert immer einen Monat wegen vergessener Referenzen in Systemen, die du nicht kontrollierst.