Blogs / Best Practices für UUID Primary Keys

Best Practices für UUID Primary Keys

Veröffentlicht
3. August 2026
Autor
Faizan Nadeem
Tags
Python Database Design Backend Development UUID
Hexadezimale Bezeichner-Strings, die eindeutige Datenbankschlüssel darstellen
Foto von Artturi Jalli auf Unsplash

Fragt man die meisten Backend-Entwickler, was eine UUID ist, bekommt man meist dieselbe Antwort: „ein zufälliger String, der praktisch eindeutig ist, also muss ich nicht weiter darüber nachdenken.“ Genau das ist der häufige Fehler — eine UUID wie etwas Einheitliches und Austauschbares zu behandeln, statt als Familie unterschiedlicher Erzeugungsstrategien mit tatsächlich verschiedenen Trade-offs. Es gibt nicht die eine UUID. Es gibt eine zufällige, eine zeitstempelbasierte, zwei deterministische hashbasierte und — seit der finalisierten RFC-9562-Spezifikation — neuere zeitlich sortierbare Versionen, die die meisten Teams noch immer nicht standardmäßig verwenden, obwohl sie es vermutlich sollten.

Diese Lücke ist wichtiger, als es zunächst scheint. Wählt man die falsche Version für einen Primary Key, bekommt man keinen Fehler — man bekommt eine Datenbank, die beim Wachsen stillschweigend langsamer schreibt, weil niemand darüber nachgedacht hat, wie eine zufällige 128-Bit-Zahl mit der tatsächlichen Funktionsweise von B-tree-Indizes zusammenwirkt. Dieser Beitrag erklärt, was in einer UUID wirklich codiert ist, wie jede Version ihre Bits zusammensetzt, wie die tatsächliche Mathematik hinter möglichen UUID-Kollisionen aussieht und warum „verwende einfach uuid4()“ genau die Art von Ein-Schritt-Denken ist, die ein Jahr später in Production Schmerzen verursacht.

Sie lernen:

  • Was tatsächlich in den 128 Bits jeder UUID codiert ist — einschließlich der Version- und Variant-Nibbles, die man auf einen Blick erkennen kann
  • Den echten Unterschied zwischen UUID v1, v4, v3/v5 und den neueren zeitlich sortierbaren v6/v7/v8
  • Die tatsächliche Mathematik hinter der Wahrscheinlichkeit von UUID-Kollisionen und warum doppelte UUIDs in der Praxis fast nie aus genau dieser Mathematik entstehen
  • Warum zufällige UUIDs (v4) die Performance von B-tree-Indizes stillschweigend verschlechtern, wenn sie als Primary Keys verwendet werden
  • Warum UUIDv7 zur Standardwahl für Primary Keys wird und wie man sie in Python erzeugt
  • Wie man ein bestehendes Schema mit UUID-Schlüsseln migriert, ohne ein riskantes Big-Bang-Rewrite

Inhaltsverzeichnis

  1. Die Grundlagen
  2. Die vollständige Architektur
  3. Die Kernschichten erklärt
  4. End-to-End-Durchlauf
  5. Sonderfälle
  6. Skalierungs- & Production-Herausforderungen
  7. Codebeispiele
  8. Häufige Fallstricke
  9. Best Practices für Production

Die Grundlagen

Was eine UUID tatsächlich ist

Eine UUID (Universally Unique Identifier) ist ein 128-Bit-Wert, der üblicherweise als 32 Hex-Zeichen geschrieben wird, aufgeteilt in fünf Gruppen im Muster 8-4-4-4-12:

550e8400-e29b-41d4-a716-446655440000

Zwei dieser Hex-Ziffern sind überhaupt keine Zufalls- oder Zeitstempeldaten — sie sind Metadaten, die die UUID selbst beschreiben. Das Version-Nibble sagt, wie sie erzeugt wurde (zeitstempelbasiert, zufällig, hashbasiert oder mit neueren zeitgeordneten Verfahren), und das Variant-Feld sagt, welcher Layout-Standard verwendet wird — fast alles, dem man begegnet, folgt dem in RFC 9562 festgelegten Standard, dem Update von 2024, das die neueren Versionen formell zur ursprünglichen RFC 4122 hinzugefügt hat. Das ist der elegante Teil des Designs: Man kann jede UUID ansehen und aus ein paar Zeichen ihr „Rezept“ erkennen, ganz ohne Lookup-Tabelle.

Warum die Wahl der richtigen Version eine wichtige Entscheidung ist

  • UUIDs tauchen überall auf — Primary Keys, Session-Tokens, Request-IDs, S3-Objektschlüssel, Idempotency Keys — daher wird eine falsche Annahme für einen Anwendungsfall meist in alle anderen hinein kopiert.
  • Die falsche Version als Primary Key hat reale, messbare Performance-Kosten. Zufällige UUIDs, die in einen B-tree-Index eingefügt werden, „funktionieren“ nicht nur ein wenig langsamer — sie können Insert-Latenz und Indexgröße im großen Maßstab deutlich erhöhen, aus Gründen, die nichts mit Kollisionsrisiko zu tun haben.
  • Nicht jeder Anwendungsfall will Zufälligkeit. Manche benötigen Reproduzierbarkeit (dieselbe Eingabe ergibt immer dieselbe ID), und dort zum falschen Werkzeug zu greifen erzeugt doppelte Datensätze, statt sie zu verhindern.

Die vollständige Architektur

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
   │        │    │   │        │
 time/random   version  variant   time/random/node
              (how built)  (layout std)

Das Leitprinzip: Das Version-Nibble ist das eingebaute Anweisungslabel einer UUID dafür, wie jedes andere Bit in ihr interpretiert werden soll. Nichts an der Eindeutigkeit oder Sortierbarkeit einer UUID ist dem „UUID-Sein“ an sich inhärent — alles hängt vollständig davon ab, welches versionsspezifische Rezept sie erzeugt hat.

Die Kernschichten erklärt

1. UUID1 — Zeitstempel + Node

Was sie ist: Kombiniert den aktuellen Zeitstempel mit einem Node-Identifier (historisch eine MAC-Adresse) und einer Clock Sequence, um gegen Clock-Rollbacks abzusichern.

Warum das wichtig ist: Die Eindeutigkeit entsteht hier aus der Struktur, nicht aus Wahrscheinlichkeit — zwei Maschinen, die im selben Moment eine UUID1 erzeugen, erhalten dennoch unterschiedliche Werte, weil sich der Node-Identifier unterscheidet.

Production-Tipp: UUID1 enthält einen echten Zeitstempel und potenziell einen echten Hardware-Identifier. Wenn Sie diese IDs extern exponieren, behandeln Sie das als leichtes Privacy- und Security-Leak, und vermeiden Sie sie für alles mit Benutzerkontakt.

2. UUID4 — Fast vollständig zufällig

Was sie ist: 122 Bits reine Zufälligkeit (die anderen 6 sind für Version und Variant festgelegt) — kein Zeitstempel, kein Maschinen-Identifier, nichts, das sich rückwärts analysieren ließe.

import uuid
uuid.uuid4()
# UUID('a3b8f9d2-1c4e-4b7a-9f2d-6e8c1a0b3d5f')

Warum das wichtig ist: Deshalb ist UUID4 der Standard für API-Tokens und Idempotency Keys — sie verrät nichts darüber, wann oder wo sie erzeugt wurde.

Production-Tipp: UUID4 ist eine ausgezeichnete Wahl für Tokens. Als Datenbank-Primary-Key ist sie eine deutlich schwächere Wahl, als die meisten annehmen — weiter unten im Abschnitt zu Primary Keys wird genau erklärt, warum.

3. UUID3 und UUID5 — Deterministisch, hashbasiert

Was sie ist: Überhaupt nicht zufällig. Man gibt einen Namespace und einen Namen hinein, hasht beides (MD5 für v3, SHA-1 für v5), und dieselbe Kombination aus Namespace plus Name erzeugt immer dieselbe UUID.

uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")
# deterministic — identical input always yields identical output

Warum das wichtig ist: Das ist wirklich nützlich, wenn man eine stabile, reproduzierbare ID braucht — zum Beispiel einen konsistenten Bezeichner, der aus einer URL oder der Record-ID eines externen Systems abgeleitet wird — ohne eine Lookup-Tabelle zu benötigen, um zu prüfen, ob diese Eingabe schon einmal gesehen wurde.

Production-Tipp: Bevorzugen Sie UUID5 gegenüber UUID3 für neuen Code. SHA-1 (v5) ist ein stärkerer Hash als MD5 (v3), und es gibt selten einen Grund, die ältere, schwächere Option zu wählen, außer wenn Sie die Ausgabe eines bestehenden Systems nachbilden müssen.

4. UUID6, UUID7 und UUID8 — Zeitlich sortierbare UUIDs

Was sie ist: Die neueren Versionen, die in RFC 9562 formalisiert wurden. UUID7 kombiniert insbesondere einen Unix-Zeitstempel mit nachfolgenden Zufallsbits und erzeugt so IDs, die eindeutig sind und sich natürlich nach Erstellungszeit sortieren lassen. UUID6 ist eine umgeordnete Variante von UUID1, die für dieselbe datenbankfreundliche Sortierung entworfen wurde; UUID8 ist ein benutzerdefiniertes Layout für individuelle Verfahren.

# Python 3.13+
uuid.uuid7()
# UUID('01928a47-3b30-7c5e-9d1a-f0b8c4a7e923')

In älteren Python-Versionen ergänzt das Paket uuid-utils (Rust-basiert und ein Drop-in-Ersatz für den Standardbibliothekstyp) Unterstützung für uuid7() sowie einen spürbaren Geschwindigkeitsvorteil sogar bei uuid4().

Warum das wichtig ist: Zeitlich geordnete IDs clustern in einem B-tree-Index, statt sich zufällig darin zu verteilen. Das ist der wichtigste praktische Grund, warum UUIDv7 v4 als Standardwahl des Jahres 2026 für alles verdrängt, was ein Primary Key wird. PostgreSQL 18 bringt native uuidv7()-Erzeugung mit, und MySQL 8.4 hat Helper-Support hinzugefügt — das Ökosystem hat zur Spezifikation aufgeschlossen.

Production-Tipp: Wenn eine UUID jemals ein Primary Key oder ein Clustered Index sein wird, verwenden Sie standardmäßig v7, sofern es keinen konkreten Grund dagegen gibt. Wenn es sich rein um ein externes Token handelt, das nie einen Index berührt, ist v4 weiterhin vollkommen in Ordnung.

5. UUID als Primary Key: Warum zufällige UUIDs der B-tree-Performance schaden

Was sie ist: Die Performance-Kosten von UUID4 als Primary Key haben nichts mit Kollisionen zu tun — es geht darum, wie relationale Datenbanken Primary Keys physisch speichern. PostgreSQL, MySQL und SQLite verwenden alle eine B-tree-Struktur für Primary-Key-Indizes, und B-trees sind für Werte optimiert, die in ungefähr sortierter Reihenfolge eintreffen: Neue Zeilen werden an die ganz rechte Seite angehängt, die im Speicher „hot“ bleibt und Schreibvorgänge schnell und lokal hält.

Warum das wichtig ist: Eine zufällige UUID4 landet bei jedem einzelnen Insert auf einer zufälligen Seite dieses Indexes. Im großen Maßstab bedeutet das Page Splits, Indexfragmentierung und Cache Thrashing — die Working Set an „hot“-Seiten, die die Datenbank im Speicher halten muss, wächst so, dass sie den gesamten Index abdeckt statt nur dessen Ende, und Inserts werden messbar langsamer, während der Index messbar größer wird als bei einer äquivalenten Tabelle mit sequentiellen Schlüsseln. Öffentliche Benchmarks auf Tabellen im Bereich von zehn Millionen Zeilen haben gezeigt, dass UUID4-Inserts ungefähr dreimal langsamer laufen als Integer-Schlüssel mit Auto-Increment, bei einem resultierenden Index, der etwa 40 % größer ist — und diese Lücke wird nur größer, je weiter die Tabelle wächst, weil sich die Fragmentierung verstärkt statt konstant zu bleiben.

Speziell in MySQL sind diese Kosten sogar noch schärfer, als es klingt, weil der Primary Key von InnoDB der Clustered Index ist — jeder andere Index auf der Tabelle speichert eine Kopie des Primary-Key-Werts, daher verlangsamt ein aufgeblähter, fragmentierter Primary-Key-Index nicht nur Primary-Key-Lookups, sondern bläht auch jeden darauf aufbauenden Secondary Index mit auf.

Production-Tipp: Genau diese Lücke schließt UUID7 — weil sie einen Zeitstempel-Präfix trägt, verhält sie sich für Indexierungszwecke wie ein sequentieller Schlüssel und kann dennoch ohne Koordination über Services hinweg erzeugt werden. Eine bestehende Tabelle umzustellen ist eine Schema-Migration, kein Config-Flag, also planen Sie es als bewusstes Projekt statt als Ein-Zeilen-Fix ein.

6. Wissenswerte Alternativen: TSID und ULID

Was sie ist: Zeitlich sortierbare ID-Formate, die überhaupt keine UUIDs sind — TSID ist typischerweise ein kompaktes 64-Bit-Format, und ULID ist ein 128-Bit-, UUID-kompatibles Format mit einem ähnlichen Design aus Zeitstempel plus Zufälligkeit wie UUID7.

Warum das wichtig ist: Wenn Sie den kleinstmöglichen Index-Footprint wollen und das spezifische 128-Bit-Format von UUID nicht benötigen, lohnt es sich, TSID als Alternative der 2026er-Ära zu evaluieren. Wenn Sie UUID-kompatibel bleiben und trotzdem Index-Lokalität gewinnen möchten, passen sowohl UUID7 als auch ULID zu diesem Bedarf.

End-to-End-Durchlauf

Verfolgen wir eine neue Benutzerregistrierung durch ID-Erzeugung, Speicherung und das, was zur Insert-Zeit tatsächlich passiert:

  1. Request empfangen. Ein neuer Benutzer registriert sich; die Anwendung muss vor oder während des Inserts einen Primary Key für die neue Zeile erzeugen.
  2. ID erzeugt. Mit einer UUID4-Strategie erzeugt uuid.uuid4() einen vollständig zufälligen 128-Bit-Wert. Mit einer UUID7-Strategie erzeugt uuid.uuid7() einen Wert mit führender Zeitstempelkomponente.
  3. Zeile eingefügt. Der B-tree-Index der Datenbank für den Primary Key erhält den neuen Wert. Eine UUID4 landet auf einer effektiv zufälligen Seite des Baums; eine UUID7 landet auf der äußersten rechten, bereits „hotten“ Seite (oder sehr nahe daran), zusammen mit allen anderen kürzlich eingefügten Zeilen.
  4. Indexpflege. Unter UUID4 kann dieses Insert einen Page Split auslösen, wenn die zufällige Zielseite voll ist. Unter UUID7 wird dieses überwiegend anhängende Muster deutlich öfter vermieden.
  5. Lesepfad. Ein Lookup per Primary Key funktioniert in beiden Fällen identisch — der gesamte Unterschied zeigt sich nur im Schreibpfad und in der gesamten Indexgröße, nicht bei der Leseperformance einzelner Zeilen.
  6. Im großen Maßstab. Multipliziert man das mit Millionen von Zeilen, ist der Index der UUID4-Tabelle allein durch das Einfügemuster größer und fragmentierter geworden, als es der der UUID7-Tabelle wäre — sonst hat sich am Schema nichts geändert.

Sonderfälle

Idempotente, reproduzierbare Ressourcen-IDs. Wenn dieselbe externe Eingabe immer auf dieselbe interne ID abgebildet werden soll — etwa um Datensätze zu deduplizieren, die aus einem externen System übernommen wurden — ist UUID5 genau deshalb das richtige Werkzeug, weil sie nicht zufällig ist. Die erneute Verarbeitung derselben Eingabe erzeugt nie einen doppelten Datensatz.

Extern sichtbare IDs vs. interne Foreign Keys. Ein verbreitetes Hybridmuster: Verwenden Sie intern einen schnellen, sequentiellen Schlüssel (einen Integer oder eine TSID) für Foreign-Key-Beziehungen und join-intensive Indizes und exponieren Sie eine separate UUID-Spalte als extern sichtbaren Identifier. So vermeidet man das Offenlegen sequentieller IDs (ein IDOR-Risiko — die Vermutung, dass /orders/1002 existiert, weil /orders/1001 existiert), ohne intern überall die vollen B-tree-Kosten zu zahlen.

Offline-First- und Multi-Writer-Systeme. Das ist der ursprüngliche Grund, warum UUIDs überhaupt existieren: Jeder Node, Service oder Offline-Client kann ohne jede Koordination und ohne Round-Trip zu einer zentralen Sequenz eine global gültige ID erzeugen. Diese Eigenschaft verschwindet mit v7 nicht — man erhält koordinationsfreie Erzeugung und besseres Indexverhalten gleichzeitig.

Skalierungs- & Production-Herausforderungen

Index-Bloat verstärkt sich mit dem Wachstum von Tabellen. Die Lücke zwischen einer Tabelle mit zufälligen UUID-Schlüsseln und einer mit sequentieller oder zeitlich geordneter Struktur wird mit der Tabellengröße größer, nicht nur mit der Zeilenzahl — die Fragmentierung wird im Lauf der Zeit schlechter, nicht besser, je länger eine Tabelle Inserts in zufälliger Reihenfolge erhält.

Migration eines bestehenden UUID4-Schemas ohne Downtime. Fügen Sie eine neue UUID7-Spalte hinzu, backfillen Sie sie für bestehende Zeilen, schreiben Sie während eines Übergangsfensters in beide Spalten und schalten Sie Lese- und Schreibzugriffe erst um, wenn der Backfill verifiziert vollständig ist — eine Änderung des Primary-Key-Formats in einem Live-System ist nichts, was man in einem einzigen Migrationsschritt versuchen sollte.

Service-übergreifende ID-Erzeugung ohne Koordination. In einem verteilten System mit mehreren Services, die unabhängig schreiben, löst jede UUID-Version weiterhin das Problem „keine zentrale Autorität“, das UUIDs ursprünglich motiviert hat — die Wahl der Version ändert nur Index-Performance und Sortierbarkeit, nicht die Garantie koordinationsfreier Erzeugung.

Woher echte doppelte UUIDs tatsächlich kommen. Die theoretische Kollisionsmathematik (unten) ist praktisch nie die Ursache für ein reales Production-Duplikat. Tatsächliche Vorfälle lassen sich auf schwache oder vorhersagbare Zufallszahlengeneratoren zurückführen, auf Prozesse, die geforkt werden und unbeabsichtigt einen Seed teilen, auf fehlerhafte Concurrency, bei der gemeinsamer Zustand über Worker hinweg falsch wiederverwendet wird, oder auf einfache Implementierungsbugs — nicht darauf, dass der zugrunde liegende 128-Bit-Raum zu klein wäre.

Codebeispiele

import uuid

uuid.uuid4()      # random — 122 bits of entropy
uuid.uuid1()      # timestamp + node
uuid.uuid7()      # Python 3.13+: timestamp-prefixed, sortable
uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")  # deterministic, SHA-1

Für Versionen, die Ihre Python-stdlib noch nicht enthält, oder für einen spürbaren Geschwindigkeitsvorteil sogar bei uuid4():

# pip install uuid-utils
import uuid_utils as uuid

uuid.uuid7()  # drop-in compatible with stdlib UUID objects

Ein minimales PostgreSQL-Schema, das das Muster „v7 als Primary Key“ widerspiegelt:

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMPTZ DEFAULT now()
);

Häufige Fallstricke

Fehler: „UUID“ als eine austauschbare Einheitslösung behandeln. Man greift zu dem uuid()-Helper, der gerade am nächsten liegt, ohne zu überlegen, welche Version zum Anwendungsfall passt. Lösung: Wissen Sie vor jeder Erzeugung, ob Sie Zufälligkeit (v4), Reproduzierbarkeit (v5) oder Sortierbarkeit (v7) brauchen.

Fehler: Für jeden Primary Key standardmäßig UUID4 verwenden. Das ist die häufigste Ursache für Vorfälle der Art „Warum wurden unsere Writes langsamer, als die Tabelle gewachsen ist?“. Lösung: Verwenden Sie UUID7 (oder ein hybrides internes/externes Schlüsseldesign) für alles, was ein Clustered oder Primary-Key-Index wird.

Fehler: Sich über die Kollisionswahrscheinlichkeit der Birthday-Paradox-Mathematik Sorgen zu machen statt über die echten Ursachen. Die Kollisionswahrscheinlichkeit von UUID4 ist astronomisch gering — man bräuchte in der Größenordnung von 2,7 Quintillionen erzeugten Werten, bevor überhaupt eine 50%ige Chance auf eine einzige Kollision bestünde. Lösung: Wenn Sie in Production tatsächlich ein Duplikat sehen, prüfen Sie Ihren Zufallszahlengenerator und Ihr Prozess-/Concurrency-Modell — dorther kommen echte Duplikate, nicht aus der Mathematik.

Fehler: UUID3 für neuen Code verwenden. MD5 ist ein schwächerer Hash als SHA-1 und bietet für neue Systeme keinen Vorteil. Lösung: Verwenden Sie standardmäßig UUID5, außer Sie müssen gezielt ein bestehendes UUID3-basiertes System nachbilden.

Fehler: UUIDs überall als 36 Zeichen langen Text speichern. Das verschwendet Storage und Index-Speicher im Vergleich zur nativen binären Darstellung, die die meisten Datenbanken unterstützen. Lösung: Verwenden Sie, wenn verfügbar, den nativen UUID-Typ Ihrer Datenbank (gespeichert als 16 Bytes) statt einer einfachen String-Spalte.

Best Practices für Production

  • Passen Sie die Version an den Anwendungsfall an, nicht an Gewohnheit. Zufällig für Tokens, deterministisch für reproduzierbare IDs, zeitlich geordnet für alles, was zu einem Index wird.
  • Verwenden Sie für neue Primary Keys standardmäßig UUID7 statt UUID4. Der Vorteil bei der Index-Lokalität ist nahezu kostenlos und wirkt mit dem Wachstum der Tabellen immer stärker zu Ihren Gunsten.
  • Trennen Sie interne Schlüssel von externen Identifiers, wenn es wichtig ist. Ein schneller interner Schlüssel plus ein UUID-basierter externer Identifier vermeidet sowohl das IDOR-Risiko sequentieller IDs als auch die vollen Indexierungskosten zufälliger UUIDs an allen internen Stellen.
  • Speichern Sie UUIDs in ihrem nativen Binärtyp. Sechzehn Bytes schlagen sechsunddreißig Zeichen in jedem realistischen Maßstab.
  • Wenn Sie ein Duplikat sehen, schauen Sie auf den Generator, nicht auf die Mathematik. Kollisionen in der Praxis entstehen fast immer durch schwache Zufälligkeit oder fehlerhafte Concurrency — behandeln Sie es als Implementierungsbug, den man aufspüren muss, nicht als Beweis dafür, dass UUIDs „nicht wirklich eindeutig“ seien.

Zum Abschluss

Eine UUID ist kein einzelnes Ding, über das man allgemein vernünftig urteilen kann — sie ist eine Familie verschiedener Bit-Erzeugungsstrategien, jeweils mit einem spezifischen Zweck, und für einen Primary Key die falsche zu wählen, ist genau die Art von Entscheidung, die kostenlos aussieht, bis die Tabelle zehn Millionen Zeilen hat. Die Mathematik rund um Kollisionen ist ein gelöstes, vernachlässigbares Thema; die wirklich wichtige Entscheidung betrifft die Version, nicht die Eindeutigkeit. Verwenden Sie standardmäßig v4 für Tokens, v5 für reproduzierbare IDs und v7 für alles, was ein Primary Key wird, und die meisten Fragen der Art „Warum ist unsere Datenbank langsamer als früher?“ stellen sich gar nicht erst.

Verwenden Sie für Primary Keys immer noch standardmäßig uuid4(), oder haben Sie bereits auf etwas zeitlich Geordnetes umgestellt?

Weitere Artikel