Universal Unique Identifier – das bessere Auto Increment

Rate this post

Für Entwickler ist das Thema Datenbanken ein Bereich der Anwendungsentwicklung, den man nicht auf die leichte Schulter nehmen sollte. In diesem Artikel widme ich mich der Frage, was geeignete Primärschlüssel für relationale Datenbanken wie MySQL oder PostgreSQL sein können. Doch bevor ich mich auf die technischen Details stürze, möchte ich kurz ein Szenario beschreiben, mit dem ich vor einer Weile wieder einmal konfrontiert wurde.

Für einen Auftrag sollte ich ein Shopsystem migrieren. Da dieses System bereits über 10 Jahre im produktiven Einsatz war, ging es nun darum, auf eine neue Majorversion zu aktualisieren. Da wir bereits 3 Major-Releases im Rückstand zur aktuellen Version waren, haben wir uns entschieden, diese Gelegenheit zu nutzen, auch einige Altlasten loszuwerden. Es sollte sozusagen ein neuer Shop mit neuem Design und aktualisierter Funktionalität so weit neu aufgesetzt werden, dass dann die vorhandenen Artikel, Bestellungen und natürlich auch Kundendaten auf den neuen Shop importiert werden. Soweit erst einmal ein klassischer Routineauftrag.

Die Komplikation entstand durch den Umstand, das natürlich das alte System so lange im Betrieb bleiben musste, bis es nahtlos durch die neue Version ersetzt werden konnte. Wie es nun auch immer so ist, entwickelt sich Software weiter. So gab es in der neuen Version auch erhebliche Veränderungen, die ein direktes Datenmapping verkomplizierten. Im Konkreten ging es darum, wie intern die Artikelattribute gespeichert werden. Das heißt, wenn wir zum Beispiel T Shirts verkaufen, dann gibt es möglicherweise ein Modell aus Baumwolle, in weißer Farbe mit V-Ausschnitt in unterschiedlichen Größen. Jetzt wird man bei der Katalogauswahl in der Shopdarstellung nicht jedes einzelne Shirt in seiner Größe präsentieren. Vielmehr gibt es bei dem Artikel dann eine Auswahlbox mit den verschiedenen Größen. Diese Artikeleigenschaften können je nach Shop durchaus extrem komplex werden.

Mittlerweile gibt es sehr leistungsfähige Werkzeuge, die nicht gleich den Preis eines Mittelklassewagens haben, um zwischen den Versionen des Datenbankschemas ein Mapping zu definieren und die Daten dann automatisiert in die neue Version zu übernehmen. Das Ganze wird dann zu einem sogenannten ‚Höllenritt‘, wenn die Primärschlüssel als generische Auto-Increment erzeugt werden. Das alte System bleibt ja weiter aktiv und erzeugt permanent neue Primärschlüssel, die möglicherweise im neuen System bereits vergeben sind. Diesen Effekt versucht man durch sogenannte Freezes zu minimieren. Das bedeutet, der Shopbetreiber kann, solange die Migration nicht abgeschlossen ist, keine neuen Artikel im Shop aufnehmen und auch bestehende Artikeleigenschaften nur unter sehr eingeschränkten Bedingungen ändern.

Damit die Datenmigration einerseits leichter wird, aber andererseits auch weniger fehleranfällig ist, ist es in kommerziellen Umgebungen eher verpönt, auto increment für die Generierung des Primärschlüssels zu nutzen. Professionelle Datenbankmanagementsysteme (DBMS) wie Oracle und PostgreSQL können sogar nicht ohne Weiteres ein auto increment erstellen. Will man das dennoch nutzen, ist das meist über das Persistenzframework implementiert und nicht wie bei MySQL oder MariaDB eine Funktion im SQL.

Woher kommt überhaupt die Idee, einen solchen generischen Primärschlüssel zu nutzen? Sicher ist es ein sehr einfacher Mechanismus, der durchaus bewährt ist und gut in der Praxis funktioniert. Jedenfalls solange man nicht die Absicht hat eine Migration durchzuführen. Ein anderer Aspekt ist natürlich historisch bedingt, als Festplattenspeicher nocht teuer war und nicht so üppig verfügbar war wie heute. Damals zählte noch jedes einzelne Bit, das man sparen konnte. Diese Argument wird durch den verfügbaren günstigen Speicher entkräftet. Im Gegenteil, die Nachteile, die man sich im Bereich Wartung für ein paar eingesparte Megabyte erkauft, wichten sogar mehr.

Was sind demzufolge gut geeignete Primärschlüssel für Datensätze in relationalen Datenbanken? Hier unterscheiden wir in zwei Kategorien: natürliche und generierte Schlüssel. Da der Primärschlüssel eineindeutig sein muss und daher nicht zweimal vorkommen darf, gibt es wenig natürliche Kandidaten. Das klassische Beispiel für einen Nutzeraccount als Primärschlüssel ist die E-Mail. Auch die Telefonnummer hat die gewünschte Eigenschaft.

Automatisch generierte Primärschlüssel sind unter anderem der bereits beschriebene Auto Increment, den wir besser vermeiden sollten. Stadessen sollte man besser auf den Universal Unique Identifier, kurz UUID, zurückgreifen. Alle Programmiersprachen haben dazu eine Implementierung. Allerdings gibt es mittlerweile mehrere Versionen der UUID. Nicht allzulang wurde Version 7 der UUID veröffentlicht. Daher schauen wir uns die Eigenschaften der jeweiligen Versionen einmal genauer an. Die KI Grok stellt die Versionen wie folgt dar.

  • Version 1 (Time-based, RFC 4122/9562):
    • Kombiniert einen hochpräzisen Timestamp (60 Bit, 100-Nanosekunden-Intervalle seit 15.10.1582), eine 14-Bit-Clock-Sequence (gegen Rückwärtsdrehen der Uhr) und eine 48-Bit-Node-ID (meist MAC-Adresse des Rechners).
    • Nachteil: Die MAC-Adresse kann die Hardware verraten (Datenschutz). Die Sortierung ist nicht optimal, weil die Timestamp-Bits nicht in zeitlicher Reihenfolge liegen.
    • Beleg: RFC 9562, Section 5.1.
  • Version 2 (DCE Security):
    • Ähnlich wie v1, aber mit zusätzlichen Feldern für POSIX UID/GID (Local Domain). Der Timestamp ist weniger präzise.
    • Status: Kaum verwendet, in den meisten Bibliotheken nicht implementiert und nur für sehr spezielle Legacy-DCE-Anwendungen gedacht.
    • Beleg: RFC 9562 erwähnt es als reserviert mit Verweis auf alte DCE-Spezifikationen.
  • Version 3 & 5 (Name-based):
    • Deterministisch: Aus einem Namespace und einem Namen wird per Hash (v3: MD5, v5: SHA-1) ein UUID erzeugt. Gleicher Input → gleicher UUID.
    • Unterschied: Nur der Hash-Algorithmus. MD5 ist gebrochen, SHA-1 gilt als schwach → v5 ist etwas besser, aber beide nicht für kryptografische Sicherheit geeignet.
    • Beleg: RFC 9562, Sections 5.3 und 5.5.
  • Version 4 (Random):
    • Die klassische „zufällige“ UUID. 122 Bit sind echt zufällig (oder kryptografisch sicher pseudo-zufällig). Keine Zeitinformation, keine Node-ID.
    • Vorteil: Maximale Unvorhersagbarkeit und Privatsphäre.
    • Nachteil: Nicht sortierbar → schlechtere Performance als Primärschlüssel in Datenbanken (Fragmentierung der Indexe).
    • Beleg: RFC 9562, Section 5.4 – gilt als sicherer Standard für viele Anwendungen.
  • Version 6 (Reordered Time-based, RFC 9562):
    • Technisch fast identisch mit v1 (gleicher Timestamp, gleiche Clock-Sequence, gleiche Node), aber die Bits des Timestamps sind neu angeordnet (most-significant zuerst).
    • Dadurch sind v6-UUIDs byteweise sortierbar nach Erstellungszeit – ideal für Datenbanken.
    • Empfehlung im RFC: Nur als Drop-in-Ersatz für bestehende v1-Systeme verwenden; ansonsten v7 bevorzugen.
    • Beleg: RFC 9562, Section 5.6 – „field-compatible version of UUIDv1“.
  • Version 7 (Unix Time-based, RFC 9562):
    • Moderne Variante: 48 Bit Unix-Timestamp in Millisekunden (seit 1970), gefolgt von 12 Bit „rand_a“ (kann für Sub-Millisekunden oder Counter verwendet werden) und 62 Bit „rand_b“ (Zufall).
    • Vorteile:
    • Sehr gut sortierbar (Zeit steht vorne).
    • Hohe Entropie (74 Bit random).
    • Keine MAC-Adresse → besserer Datenschutz.
    • Gut für verteilte Systeme und Datenbank-Indexes.
    • Der RFC empfiehlt explizit: „Implementations SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible.“
    • Beleg: RFC 9562, Section 5.7.

Die bisher verbreitetste Variante ist Version 4, die ich auch selbst nutze. Ein wichtiges Kriterium ist bereits aus den Hash Algorithmen bekannt. Bei Hash spricht man von Kollisionen, also wenn ein Hash auf zwei verschiedene Texte verweist. Ein ähnliches Problem haben wir bei der Generierung von UUIDs. Diese sollte im Produktivbetrieb auch bei großen Datenmengen nicht mehrfach erzeugt werden. Das würde einen Fehler im Datenbanksystem auslösen, da die Unique Bedingung verletzt wird. Problematischer ist die anschließende Fehlerbehandlung. Damit der Datensatz dennoch gespeichert werden kann, muss eine neue UIID erzeugt werden. Dieser Fall ist mir bei meiner bereits langjährigen Verwendung der UUID-Version 4 bisher nicht untergekommen.

Wieso sollte man nun auf UUID Version 7 zurückgreifen? Hier geht es um Sortierbarkeit. UUID 7 verspricht, dass neuere Einträge ein aufsteigendes Datum in den ersten Stellen berücksichtigen. Dadurch kann man ältere Einträge absteigend sortiert erkennen.

Um beispielsweise UUID 4 in Java zu benutzen, genügt der Aufruf UUID.randomUUID(). Der ORM Mapper Hibernate stellt über die Annotation @GeneratedValue auch andere Versionen der UUID bereit. Natürlich kann man auch zusätzliche Bibliotheken wie den uuid-creator unter der MIT Lizenz nutzen.

<dependency>
  <groupId>com.github.f4b6a3</groupId>
  <artifactId>uuid-creator</artifactId>
  <version>${version}</version>
</dependency>

Unter Java gibt es auch eine Möglichkeit, ohne zusätzliche Bibliothek festzustellen, welche UUID ein String verwendet hat.

UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");

Damit soll es auch für heute genug sein. Ich hoffe, ich konnte mit diesem Artikel ein wenig Aufmerksamkeit in das Thema lenken und würde mcih sehr freuen, wenn es weitere Verbreitung findet.


Dieser Eintrag wurde von Elmar Dott unter Artikel veröffentlicht und mit , verschlagwortet. Setze ein Lesezeichen für den Permalink.
Elmar Dott

Über Elmar Dott

Elmar Dott realisiert seit über 20 Jahren als freier Berater in internationalen Projekten große Web-Applikationen. Seine Schwerpunkte sind DevOps, Konfigurationsmanagement, Software-Architekturen & Release Management. Als Trainer teilt er sein Wissen in Schulungen und spricht auch regelmäßig auf Konferenzen über aktuelle Themen.

Schreibe einen Kommentar