10 Punkte von GN⁺ 2025-12-16 | 1 Kommentare | Auf WhatsApp teilen
  • UUID v4 hat einen hohen Grad an Zufälligkeit und verursacht dadurch ineffiziente Indizes und übermäßiges I/O, was bei der Verwendung als Primärschlüssel in PostgreSQL zu Leistungseinbußen führt
  • Durch zufällige Inserts kommt es häufig zu Page Splits und Indexfragmentierung, was zu größeren WAL-Logs und Schreibverzögerungen führt
  • UUIDs sind 16 Byte groß und belegen damit doppelt so viel Platz wie bigint, was die Cache-Trefferquote senkt und zu Speicherverschwendung führt
  • Sie werden oft fälschlich als Sicherheitskennungen verstanden, aber laut RFC 4122 sind UUIDs kein Sicherheitsmechanismus zum Schutz vor Erraten
  • Für neue Datenbanken wird empfohlen, ganzzahlige sequenzbasierte Schlüssel zu verwenden; falls unvermeidbar, sollte man zeitlich sortierbare UUID v7 einsetzen

Leistungsprobleme von UUID v4

  • Datenbanken in PostgreSQL, die UUID-v4-Primärschlüssel verwenden, zeigen seit den letzten 10 Jahren konsistent Leistungseinbußen und übermäßiges I/O
    • Bei UUID v4 werden 122 Bit zufällig erzeugt, wodurch eine Sortierung im Index nicht möglich ist
    • Da Inserts nicht auf sequenziellen Seiten gespeichert werden, kommt es zu Random Access; auch bei Updates und Deletes sind ineffiziente Suchvorgänge nötig
  • B-Tree-Indizes setzen sortierte Daten voraus, aber UUID v4 hat keine natürliche Reihenfolge, wodurch die Insert-Effizienz gering ist
    • Jeder Insert wird auf eine beliebige Seite geschrieben, wodurch Splits in der Mitte von Seiten häufig auftreten
    • Das führt zu Schreiblatenz und einem Anstieg des WAL

Struktur von UUIDs und Alternativen

  • UUIDs sind 128-Bit-Identifikatoren (16 Byte) und werden in PostgreSQL als binärer uuid-Typ gespeichert
  • UUID v4 basiert auf Zufallsbits, UUID v7 enthält in den ersten 48 Bit einen Zeitstempel und ist dadurch indexfreundlicher
  • In PostgreSQL 18 (geplant für 2025) soll UUID v7 standardmäßig unterstützt werden
  • UUID v7 lässt sich zeitlich sortieren und verbessert dadurch Seitendichte und Cache-Effizienz

Warum UUIDs gewählt werden – und ihre Grenzen

  • UUIDs werden verwendet, wenn in Multi-Client- oder Microservices-Umgebungen kollisionsfreie Identifikatoren erzeugt werden müssen
    • Beispiel: gleichzeitige ID-Erzeugung in mehreren Datenbankinstanzen
  • RFC 4122 weist jedoch ausdrücklich darauf hin, dass man nicht davon ausgehen sollte, dass UUIDs schwer zu erraten sind; als Sicherheitskennung sind sie daher ungeeignet
  • Die Kollisionswahrscheinlichkeit liegt bei 50 % erst nach der Erzeugung von 2.71×10¹⁸ UUIDs; praktisch sind Kollisionen zwar unwahrscheinlich, aber die Leistungskosten sind hoch

Platz- und I/O-Ineffizienz von UUIDs

  • UUIDs benötigen doppelt so viel Platz wie bigint (8 Byte) und viermal so viel wie int (4 Byte)
    • Bei großen Tabellen führt das zu mehr Speicherbedarf sowie längeren Backup- und Restore-Zeiten
  • Ergebnisse eines Experiments zur Dichte von Indexseiten
    • Integer-Index: 97.64%
    • UUID-v4-Index: 79.06%
    • UUID-v7-Index: 90.09%
  • In einem Test von Cybertec wurden bei Index-Lookups mit UUID v4 8.5 Millionen zusätzliche Seitenzugriffe und 31229% mehr I/O gemessen
    • Unter denselben Bedingungen hatte ein bigint-Index 27,332 Buffer-Zugriffe, UUID v4 dagegen 8,562,960 Buffer-Zugriffe

Auswirkungen auf Cache und Speicher

  • Aufgrund ihrer zufälligen Verteilung senken UUIDs die Cache-Hit-Rate des Buffer Cache
    • Es müssen mehr Seiten in den Cache geladen werden, und benötigte Seiten werden häufiger verdrängt (Eviction)
  • Die schlechtere Cache-Effizienz verursacht Query-Latenz und erhöht den Speicherverbrauch
  • Um die Leistung zu erhalten, werden regelmäßige Index-Neuaufbauten (REINDEX CONCURRENTLY) oder der Einsatz von pg_repack empfohlen

Maßnahmen zur Leistungsverbesserung

  • Mehr Speicher: empfohlen werden RAM-Kapazitäten in Höhe des Vierfachen der Datenbankgröße (z. B. DB 25 GB → 128 GB RAM)
  • work_mem anpassen: Bei Sortieroperationen kann mehr zugewiesener Speicher die Leistung verbessern
  • In Rails-Umgebungen kann über die Einstellung implicit_order_column statt UUID ein sortierbares Feld wie created_at verwendet werden
  • Mit dem CLUSTER-Befehl lässt sich eine Tabelle anhand eines sortierbaren Feldes neu anordnen, allerdings ist dafür ein exklusiver Lock erforderlich

Empfehlung: Ganzzahlige Schlüssel und Sequenzen

  • Für neue Datenbanken werden ganzzahlige, sequenzbasierte Schlüssel empfohlen
    • integer (4 Byte) bietet etwa 2 Milliarden Werte, bigint (8 Byte) deutlich mehr eindeutige Werte
  • Für die meisten Business-Anwendungen reicht integer aus; für größere Systeme ist bigint geeignet
  • Ein realistischer Ersatz für UUID v4 ist UUID v7 oder die Erweiterung sequential_uuids

Zusammenfassung

  • UUID v4 führt aufgrund seiner Zufälligkeit zu ineffizienten Indizes, hohem I/O und geringer Cache-Effizienz
  • Es ist nicht als Sicherheitskennung geeignet und verursacht erheblichen Platzverbrauch
  • Sequenzbasierte Ganzzahlschlüssel sind für die meisten Anwendungen besser geeignet
  • Wenn UUIDs unvermeidbar sind, sollte man zeitlich sortierbare UUID v7 wählen
  • In PostgreSQL sollte man vermeiden, gen_random_uuid() als Primärschlüssel zu verwenden

1 Kommentare

 
GN⁺ 2025-12-16
Hacker-News-Kommentare
  • Das ist ein typisches Beispiel für verfrühte Optimierung
    Daten in eine permanente Kennung zu packen, ist ein Tabu der Datenverwaltung
    Wenn man wie bei der norwegischen Personenkennziffer das Geburtsdatum in die ID aufnimmt, entstehen später Probleme, etwa bei Einwanderern, deren Geburtsdatum zunächst falsch bekannt war, oder wenn es zu viele am 1. Januar Geborene gibt und die Nummern knapp werden
    Zu Zeiten von Zettelkatalogen war es nachvollziehbar, Daten und Kennung zu vermischen, um Abfragekosten zu senken, aber heute gibt es leistungsfähige Datenbanken, daher ist das nicht mehr nötig

    • Ich denke, dieses Beispiel ist eigentlich eher ein Problem einer falschen Standardwert-Setzung
      Das eigentliche Problem war, unbekannte Geburtstage auf den 1. Januar zu setzen, nicht dass ein Datum im Schlüssel enthalten war
      Hätte man einen Nicht-Datumswert wie 00 oder 99 verwendet, wäre es nicht zu Kollisionen gekommen
      Einen Zeitstempel in eine UUID einzubauen dient nicht der Bedeutungsaufladung, sondern der Performance-Optimierung
      Zeitlich ansteigende Schlüssel senken die Kosten für B-Tree-Umschreibungen und verbessern dadurch die Einfüge-Performance der DB
    • Die italienische Personenkennziffer enthält ebenfalls das Geschlecht, was nach einer Geschlechtsangleichung problematisch werden kann
      „Keine Daten in permanente Kennungen packen“ ist nur eine allgemeine Regel, je nach Situation kann man den Trade-off aber bewusst in Kauf nehmen
      Wenn man zum Beispiel einen md5-Hash als UUID für einen Index verwendet, entsteht zwar Fragmentierung, aber in einem beherrschbaren Rahmen
    • UUIDv7 ist lediglich eine Erzeugungsmethode mit Zeit-Bias (random bias) und enthält keine echten Informationen
      Die Wahl zwischen zufälliger und zeitbasierter UUID kann Performance-Unterschiede in Sekunden statt in Millisekunden verursachen
    • Bei kleinen DBs ist das verfrühte Optimierung, bei größerem Maßstab braucht man eher den gegenteiligen Ansatz
      In großen DBs sind Sharding und Verteilung unverzichtbar, daher funktioniert UUID oft besser als Auto-Increment
    • Zum Beispiel mit der norwegischen Personenkennziffer frage ich mich, ob es wirklich so viele Menschen geben kann, die am 1. Januar geboren sind
      Wenn das Format DDMMYYXXXXX ist, müsste es bis zu 100.000 Personen abdecken können; ich frage mich, ob sich wirklich so viele auf dieses Datum konzentrieren können
      Vermutlich wäre das nur in Sondersituationen der Fall, etwa bei einem starken Zustrom von Flüchtlingen in einem bestimmten Jahr
  • UUIDs sollte man nicht wie Security-Token verwenden
    Es ist riskant, sie allein deshalb als Sicherheitsfunktion zu verwenden, weil sie schwer zu erraten sind
    Der Zweck zufälliger Werte ist nicht nur, das Erraten zu verhindern, sondern auch, die Beziehung zwischen aufeinanderfolgenden IDs zu verbergen

  • Je nach DB-Typ ist die PK-Strategie völlig unterschiedlich
    In Postgres sind zufällige PKs ineffizient, aber in verteilten DBs wie Cockroach oder Spanner verursachen monoton steigende Schlüssel eher ein Hot-Shard-Problem

    • Auch in verteilten DBs sind tendenziell ansteigende Schlüssel besser als vollständig zufällige
      Bei UUIDv7 sind die oberen Bits sortierbar und die unteren Bits zufällig, sodass man sowohl Verteilung zwischen Nodes als auch effiziente lokale Speicherung erhält
    • Man sollte es eher als Unterschied in der DB-Struktur als im DB-Typ sehen
      In gewöhnlichen nicht geshardeten DBs führen zufällige Schlüssel zu B-Tree-Fragmentierung
    • In Google Cloud Bigtable verwendet man sequenzielle Schlüssel in umgekehrter Reihenfolge (reverse), um automatische Verteilung zu erreichen
    • In einem geshardeten Postgres sind zufällige PKs vorteilhaft
      Wenn es jedoch viele Bereichsabfragen (range queries) gibt, sind zufällige Schlüssel nachteilig
      Letztlich muss man nach den Workload-Eigenschaften entscheiden
    • Bei schreiblastigen Workloads mit starker Zeit-Bias kann in Postgres auch ein zufälliger PK besser sein
  • Der Artikel benennt die Nachteile von UUIDv4 als PK gut, aber das vorgeschlagene Integer-Verschleierungsverfahren scheint für echte Produktivsysteme ungeeignet
    Für kleine DBs ist UUIDv7 ein vernünftiger Kompromiss

    • Ich bevorzuge UUIDv4 gegenüber UUIDv7
      weil ich nicht möchte, dass der Erzeugungszeitpunkt offengelegt wird
      Solange die Datenmenge nicht so groß ist, dass die Zufälligkeit von UUIDv4 Performance-Probleme verursacht, ist v4 die sicherere Wahl
    • In Postgres verwende ich gern eine einzelne Sequenz
      Es gibt zwar eine gewisse Informationsoffenlegung, aber operativ ist sie hinreichend obskur
    • Wenn man nur die Nutzerzahl verbergen will, kann man auf einen Auto-Increment-Schlüssel einfach eine Verschlüsselungs-Permutation anwenden
      Zum Beispiel kann man per AES-128 transformieren und dann in base64 kodieren, sodass es wie eine YouTube-Video-ID aussieht
  • Ich sehe bei technischen Due-Diligence-Prüfungen viele Unternehmen, und die schnelle Sharding-Fähigkeit ist entscheidend für das Wachstum
    Wenn man UUIDs in allen Tabellen verwendet, kann man beim Sharding ohne strukturelle Änderungen skalieren
    Das bringt einen viel größeren Skalierungsvorteil als der leichte Verlust bei Platz und Zeit

    • UUIDv7 hat dank seiner monoton steigenden Eigenschaft auch in Postgres Performance-Vorteile
    • Wir hatten beim Sharding ebenfalls Probleme, weil uns UUIDs fehlten
      Letztlich war die Migration selbst wegen des komplexen Datenmodells schwierig, unabhängig davon, ob UUIDs vorhanden waren oder nicht
  • Unsere App verschlüsselt Integer-PKs, damit sie wie UUIDs aussehen
    Denn wenn sequenzielle IDs sichtbar sind, kann man die Kundenzahl schätzen oder Dictionary-Angriffe durchführen
    Verschlüsselte IDs ermöglichen es, Scan-Versuche durch fehlschlagende Entschlüsselung sofort zu erkennen

    • Allerdings kann beim Verlust oder Austausch von Schlüsseln das Problem entstehen, dass Entschlüsselung unmöglich wird
    • Mich würde interessieren, wie das Schlüsselmanagement aussieht — ob per Umgebungsvariable injiziert, im Code eingebettet oder mit einem AEAD-Schema wie AES-GCM; das Sicherheitsmanagement ist hier wichtig
  • Die Aussage „2 Milliarden reichen aus“ ist gefährlich
    Jeder DBA kennt mindestens einen alptraumhaften Fall, der mit genau so einer Entscheidung begonnen hat

  • Im Artikel hieß es, „zufällige Werte lassen sich ineffizient sortieren“, aber tatsächlich ist Sortierung nach Byte-Reihenfolge möglich
    Allerdings führen zufällige Schlüssel nicht zu sequenziellen Einfügungen, daher kommt es häufiger zu B-Tree-Rebalancing und damit zu Performance-Verlusten

    • UUIDv4 ist in verteilten Umgebungen nützlich, aber man muss den Preis des 128-Bit-Raums und der Nicht-Sequenzialität zahlen
    • Der Autor habe später noch einen Vergleichstest von B-Tree-Indizes ergänzt
      Integer-PKs passen gut in den Speicher, während UUIDv4 wegen der vielen Seitenzugriffe zu höherer Latenz führte
    • Es gab auch die Meinung, die technische Begründung sei schwach
    • Je stärker ein B-Tree ansteigende Schlüssel hat, desto effizienter sind Einfügungen; zufällige Schlüssel sind weniger cache-freundlich
    • Je enger der Datenzugriff mit dem Erzeugungszeitpunkt zusammenhängt, desto vorteilhafter ist zeitliche Sortierung für die Performance
  • Dieser Artikel wirkt wie verfrühte Optimierung, bei der die Lösung vor dem Problem kam
    UUIDv4 ist in den meisten Fällen völlig ausreichend
    Performance-Probleme sollte man erst berücksichtigen, wenn sie tatsächlich auftreten

    • Wenn man aber einmal mit UUIDv4 angefangen hat, ist ein späteres Rekeying auf int64 fast unmöglich
    • Wenn echte Performance-Probleme auftreten, ist man meist schon in einer Wachstumsphase, in der keine Zeit bleibt, den PK zu ändern
  • Zusammengefasst zeigt UUIDv7 in Postgres etwas bessere Performance als v4
    In neueren Versionen ist UUIDv7-Unterstützung auch ohne Plugin möglich

    • Der Kern des Artikels ist allerdings die Empfehlung, nach Möglichkeit Sequenz- bzw. Integer-PKs zu verwenden
    • Ab Postgres 18 gibt es eine eingebaute Funktion uuidv7(), aber ob Erweiterungen mehr Funktionen bieten, ist noch unklar
    • Die meisten Nutzer werden nun vermutlich keine separate Erweiterung mehr brauchen