Warum man UUID Version 4 als Primärschlüssel in Postgres vermeiden sollte
(andyatkinson.com)- 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 wieint(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
- Unter denselben Bedingungen hatte ein
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 vonpg_repackempfohlen
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_memanpassen: Bei Sortieroperationen kann mehr zugewiesener Speicher die Leistung verbessern- In Rails-Umgebungen kann über die Einstellung
implicit_order_columnstatt UUID ein sortierbares Feld wiecreated_atverwendet 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
integeraus; für größere Systeme istbigintgeeignet - 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
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
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
„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
Die Wahl zwischen zufälliger und zeitbasierter UUID kann Performance-Unterschiede in Sekunden statt in Millisekunden verursachen
In großen DBs sind Sharding und Verteilung unverzichtbar, daher funktioniert UUID oft besser als Auto-Increment
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
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
In gewöhnlichen nicht geshardeten DBs führen zufällige Schlüssel zu B-Tree-Fragmentierung
Wenn es jedoch viele Bereichsabfragen (range queries) gibt, sind zufällige Schlüssel nachteilig
Letztlich muss man nach den Workload-Eigenschaften entscheiden
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
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
Es gibt zwar eine gewisse Informationsoffenlegung, aber operativ ist sie hinreichend obskur
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
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
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
Integer-PKs passen gut in den Speicher, während UUIDv4 wegen der vielen Seitenzugriffe zu höherer Latenz führte
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
Zusammengefasst zeigt UUIDv7 in Postgres etwas bessere Performance als v4
In neueren Versionen ist UUIDv7-Unterstützung auch ohne Plugin möglich
uuidv7(), aber ob Erweiterungen mehr Funktionen bieten, ist noch unklar