Eine UUID (Universally Unique Identifier) ist ein 128-Bit-Wert, der ohne zentrale Koordination weltweit eindeutig sein soll. Praktisch in verteilten Systemen, als „undurchschaubare” Public-IDs in URLs und überall dort, wo bigint-IDs Probleme bereiten würden. PostgreSQL hat einen eigenen uuid-Typ und seit Version 13 die eingebaute gen_random_uuid()-Funktion — keine Extension mehr nötig.
Wie eine UUID aussieht
myapp=> SELECT gen_random_uuid();
gen_random_uuid
--------------------------------------
3f9a8b1c-4e2d-4a1f-9e7c-1a2b3c4d5e6f128 Bit = 32 Hex-Zeichen, formatiert in 5 Gruppen mit Bindestrichen (8-4-4-4-12). Postgres speichert das intern binär in 16 Bytes, nicht als 36-Zeichen-String — dadurch sind UUID-Vergleiche und -Indexe so schnell wie bigint (mit dem doppelten Speicher).
Tabellen-Definition mit UUID
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id uuid NOT NULL,
total numeric(10,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO orders (customer_id, total) VALUES
('11111111-1111-1111-1111-111111111111', 99.95),
('11111111-1111-1111-1111-111111111111', 49.95);
SELECT * FROM orders;
-- id | customer_id | total | created_at
-- --------------------------------------+--------------------------------------+-------+--------------
-- 3f9a8b1c-4e2d-4a1f-9e7c-1a2b3c4d5e6f | 11111111-1111-1111-1111-111111111111 | 99.95 | 2026-05-07 …
-- a1b2c3d4-e5f6-7890-abcd-ef0123456789 | 11111111-1111-1111-1111-111111111111 | 49.95 | 2026-05-07 …gen_random_uuid() als Default sorgt dafür, dass die id automatisch generiert wird, sobald jemand ohne explizite ID einfügt — analog zu GENERATED AS IDENTITY bei bigint.
UUID-Versionen
Die UUID-Spec definiert mehrere „Versionen”, die sich darin unterscheiden, wie der Wert generiert wird:
| Version | Wie generiert | Vorteile | Nachteile |
|---|---|---|---|
| UUIDv4 | komplett zufällig | einfach, kollisionssicher genug | nicht sortierbar, schlechte Index-Lokalität |
| UUIDv1 | Zeitstempel + MAC-Adresse | sortierbar nach Zeit | leakt MAC-Adresse, MAC-Spoofing möglich |
| UUIDv6 | wie v1, aber Zeitstempel zuerst | sortierbar | selten verwendet |
| UUIDv7 | Unix-Timestamp + Zufallsbits | sortierbar nach Zeit, gute Index-Lokalität | neueste Spec (RFC 9562, 2024) |
gen_random_uuid() produziert UUIDv4. Für Tabellen mit hohen Insert-Raten wird UUIDv4 zur Performance-Falle: weil die Werte zufällig sind, kann der B-tree-Index nicht inkrementell wachsen — jeder Insert landet an zufälliger Stelle, was zu Page-Splits und schlechter Cache-Lokalität führt.
UUIDv7 — sortierbare UUIDs (PG 18+)
PostgreSQL 18 bringt eine eingebaute uuidv7()-Funktion:
myapp=> SELECT uuidv7();
uuidv7
--------------------------------------
018f1234-5678-7abc-def0-123456789abc
^^^^^ Zeitstempel (Millisekunden seit Unix-Epoche)
^ Version 7Vorteile gegenüber UUIDv4:
- Sortierbar nach Erstellungszeit (
ORDER BY idist quasiORDER BY created_at). - Bessere Index-Performance — neue Werte landen am Ende des B-trees.
- Globale Eindeutigkeit wie v4.
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7(),
event_type text NOT NULL,
occurred_at timestamptz NOT NULL DEFAULT now()
);Auf PG 17 oder älter: per Extension oder mit eigener PL/pgSQL-Funktion — Bibliotheken wie pg_uuidv7 schließen die Lücke.
uuid-ossp — die alte Extension
Vor PostgreSQL 13 brauchte man eine Extension für UUID-Generierung:
CREATE EXTENSION "uuid-ossp";
SELECT uuid_generate_v4(); -- entspricht heute gen_random_uuid()
SELECT uuid_generate_v1(); -- v1 mit MAC-Adresse
SELECT uuid_generate_v1mc(); -- v1 mit Random-MAC (privater)Auf neuen Setups (PG 13+) braucht man uuid-ossp nur noch, wenn man andere Versionen als v4 oder v7 will (also v1, v3, v5). Für den Standard-Use-Case gen_random_uuid() / uuidv7() reicht das Built-in.
UUID vs. bigint — Vergleich
| Aspekt | bigint IDENTITY | uuid (v4) | uuid (v7) |
|---|---|---|---|
| Speicher | 8 Bytes | 16 Bytes | 16 Bytes |
| Generierung | Sequenz im Server | überall, ohne Server | überall, ohne Server |
| Sortierbar | ja (nach Erstellung) | nein | ja (nach Erstellung) |
| Index-Performance | optimal | schlecht (Random-Inserts) | gut |
| Verteilbar | nein (zentrale Sequenz) | ja | ja |
| In URLs | Zähler-leak | undurchschaubar | undurchschaubar |
| Lesbar / merkbar | ja | nein | nein |
| Kollisionssicher | ja (innerhalb DB) | praktisch ja | praktisch ja |
Empfehlungen:
- Klassische Web-App, eine DB →
bigint IDENTITYreicht. - Du brauchst Public-IDs in URLs, ohne dass User die Reihenfolge erraten können → eigene
uuid-Spalte zusätzlich zurbigint id(interne PK). - Verteiltes System mit mehreren Schreibern, die ohne Sync IDs vergeben →
uuid(idealerweise v7). - Ereignis-Logs mit hohem Insert-Volumen →
uuid v7für Index-Lokalität.
Hybrid-Pattern: bigint intern + uuid extern
Häufig in Web-Apps:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
customer_id bigint NOT NULL REFERENCES customers(id),
total numeric(10,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);Vorteile:
idals interner Primary Key bleibt klein (8 Bytes) und ermöglicht effiziente Foreign Keys.public_idwird in URLs verwendet (/orders/3f9a8b1c-...), erlaubt keine Reihenfolge-Erratung.- Foreign Keys laufen weiterhin über
id— performant, klein, und verraten in der Tabelle nichts.
Nachteile: zwei Spalten statt einer, also etwas mehr Speicher und ein zusätzlicher UNIQUE-Index.
Besonderheiten
UUIDv4 sind nicht sicher — nur unvorhersehbar
UUIDs sind nicht kryptographisch — sie verraten dem Empfänger nichts, sind aber nicht für Authentifizierung gedacht. Wer einen privaten Token braucht, sollte gen_random_bytes(32) plus Base64-Encoding nehmen, nicht UUID.
UUID-Strings akzeptiert Postgres in mehreren Schreibweisen.
Mit oder ohne Bindestriche, in Klammern, mit urn:uuid:-Präfix — alles wird akzeptiert: '3f9a8b1c4e2d4a1f9e7c1a2b3c4d5e6f', '{3f9a...}', 'urn:uuid:3f9a...'. Postgres normalisiert das beim Einlesen auf die Standard-Schreibweise.
uuid_generate_v1 leakt die MAC-Adresse.
Die alte v1-UUID enthält die MAC-Adresse des erstellenden Hosts und einen Zeitstempel. In Multi-Tenant-Umgebungen kann das die Server-Infrastruktur verraten. v1mc (Random MAC) entschärft das, ist aber nicht mehr deterministisch eindeutig pro Host. Modern: v4 oder v7.
Storage-Overhead vs. Performance-Lokalität.
16 Bytes pro UUID statt 8 Bytes pro bigint klingt klein, summiert sich aber: bei 100 Mio. Zeilen sind das 800 MB Differenz allein für die Primary-Key-Spalte. Plus aufgeblähte Foreign Keys in jeder referenzierenden Tabelle. Auf modernen Disks meist nicht der Engpass — aber Cache-Lokalität schon.
UUIDs in JSON-APIs: als String, nicht als Binary.
jsonb speichert UUIDs als String ({"id": "3f9a..."}), wenn man to_jsonb(uuid_col) nutzt. Das ist gut — JSON kennt keine Binär-Werte. Beim Lesen aus JSON: (payload->>'id')::uuid für expliziten Cast.
UUIDv7 ist neu — Tooling holt nach.
ORM-Frameworks und einige Treiber unterstützen UUIDv7 erst seit kurzem nativ. Wer es einsetzt, sollte prüfen, ob die eigene Stack-Schicht mitspielt. Im Zweifel funktioniert UUIDv7 als opaker Wert — die Sortier-Eigenschaft bleibt erhalten, weil sie aus der UUID selbst kommt, nicht aus dem Treiber.
Weiterführende Ressourcen
Externe Quellen
- UUID Type – PostgreSQL Documentation
- UUID Functions – PostgreSQL Documentation
- uuid-ossp Extension
- RFC 9562 — UUID Versions 6, 7, 8
- Release Notes 13: gen_random_uuid
- Release Notes 18: uuidv7