UUID vs bigint in PostgreSQL: IDs wählen, die mitwachsen
UUID vs bigint in PostgreSQL: Vergleiche Indexgröße, Sortierreihenfolge, Sharding-Bereitschaft und wie IDs durch APIs, Web- und Mobile-Apps wandern.

Warum die Wahl der ID wichtiger ist, als sie scheint
Jede Zeile in einer PostgreSQL-Tabelle braucht eine stabile Art, wiedergefunden zu werden. Genau das macht eine ID: Sie identifiziert einen Datensatz eindeutig, ist meist der Primary Key und wird zum Bindeglied für Beziehungen. Andere Tabellen speichern sie als Fremdschlüssel, Abfragen joinen darauf und Apps geben sie als Handle für „diesen Kunden“, „diese Rechnung“ oder „dieses Support-Ticket“ weiter.
Weil IDs überall auftauchen, ist die Entscheidung nicht nur ein Datenbankdetail. Sie zeigt sich später in Indexgröße, Schreibmustern, Abfragegeschwindigkeit, Cache-Hit-Raten und sogar in Produktarbeit wie Analytics, Imports und Debugging. Sie beeinflusst auch, was du in URLs und APIs zeigst und wie leicht eine mobile App Daten sicher speichern und synchronisieren kann.
Die meisten Teams vergleichen letztlich UUID vs bigint in PostgreSQL. Einfach gesagt wählst du zwischen:
- bigint: einer 64-Bit-Zahl, oft von einer Sequenz generiert (1, 2, 3...).
- UUID: einem 128-Bit-Identifier, oft zufällig aussehend oder in zeitlicher Reihenfolge erzeugt.
Keine Option gewinnt in allen Fällen. bigint ist kompakt und freundlich für Indizes und Sortierung. UUIDs passen gut, wenn du global eindeutige IDs über Systeme brauchst, sichere öffentliche IDs möchtest oder Daten an vielen Orten erzeugt werden (mehrere Services, offline Mobile oder späteres Sharding).
Eine nützliche Faustregel: Entscheide basierend darauf, wie deine Daten erstellt und geteilt werden, nicht nur danach, wie sie heute gespeichert sind.
Grundlagen zu Bigint und UUID in einfachen Worten
Wenn Leute UUID vs bigint in PostgreSQL vergleichen, geht es um zwei Arten, Zeilen zu benennen: eine kleine zählerartige Zahl oder ein längerer global eindeutiger Wert.
Eine bigint-ID ist ein 64-Bit-Integer. In PostgreSQL erzeugst du sie normalerweise mit einer identity column (oder dem älteren serial-Pattern). Die Datenbank führt intern eine Sequenz und gibt bei jedem Insert die nächste Zahl aus. IDs sind daher oft 1, 2, 3, 4... – simpel, leicht lesbar und praktisch in Tools und Reports.
Eine UUID (Universally Unique Identifier) ist 128 Bit. Du siehst sie oft als 36 Zeichen mit Bindestrichen, z. B. 550e8400-e29b-41d4-a716-446655440000. Gängige Typen sind:
- v4: zufällige UUIDs. Einfach überall zu erzeugen, aber nicht in Erstellungsreihenfolge sortierbar.
- v7: zeit-geordnete UUIDs. Ebenfalls eindeutig, aber so gestaltet, dass sie ungefähr mit der Zeit zunehmen.
Der Speicher ist einer der ersten praktischen Unterschiede: bigint braucht 8 Bytes, während UUID 16 Bytes benötigt. Diese Größenlücke zeigt sich in Indizes und beeinflusst die Cache-Hit-Raten (die Datenbank kann weniger Indexeinträge in den Speicher laden).
Denk auch an die Orte außerhalb der Datenbank, wo IDs auftauchen. bigint-IDs sind kurz in URLs und leicht in Logs oder Support-Tickets lesbar. UUIDs sind länger und mühsamer zu tippen, aber schwerer zu erraten und können bei Bedarf sicher auf Clients erzeugt werden.
Indexgröße und Tabellenaufblähung: was sich ändert
Der größte praktische Unterschied zwischen bigint und UUID ist die Größe. bigint sind 8 Bytes; UUIDs 16 Bytes. Das klingt klein, bis man bedenkt, dass Indizes deine IDs vielfach wiederholen.
Dein Primary-Key-Index muss im Hot-Path im Speicher bleiben, damit alles schnell wirkt. Ein kleinerer Index passt besser in Shared Buffers und CPU-Cache, sodass Lookups und Joins weniger Festplattenzugriffe brauchen. Mit UUID-Primary-Keys ist der Index in der Regel deutlich größer bei gleicher Zeilenanzahl.
Der Multiplikator sind Sekundärindizes. In PostgreSQL B-Tree-Indizes speichert jeder Sekundärindex auch den Primary-Key-Wert (damit die DB die Zeile finden kann). Breitere IDs vergrößern also nicht nur den Primary-Key-Index, sondern jeden weiteren Index, den du hinzufügst. Hast du drei Sekundärindizes, erscheinen die zusätzlichen 8 Bytes der UUID effektiv an vier verschiedenen Stellen.
Fremdschlüssel und Join-Tabellen spüren das ebenfalls. Jede Tabelle, die deine ID referenziert, speichert diesen Wert in ihren eigenen Zeilen und Indizes. Eine Many-to-Many-Join-Tabelle besteht meist hauptsächlich aus zwei Fremdschlüsseln plus etwas Overhead – verdoppelt sich die Schlüsselbreite, kann sich ihr Platzbedarf stark verändern.
In der Praxis:
- UUIDs vergrößern in der Regel Primary- und Sekundärindizes, und der Unterschied potenziert sich mit mehr Indizes.
- Größere Indizes erzeugen mehr Speicherdruck und mehr Page-Reads unter Last.
- Je mehr Tabellen den Key referenzieren (Events, Logs, Join-Tabellen), desto wichtiger wird der Größenunterschied.
Wenn eine Nutzer-ID in users, orders, order_items und einem audit_log auftaucht, wird derselbe Wert in all diesen Tabellen gespeichert und indexiert. Die Wahl eines breiteren Schlüssels ist damit genauso sehr eine Speicherentscheidung wie eine ID-Entscheidung.
Sortierreihenfolge und Schreibmuster: sequenziell vs. zufällig
Die meisten PostgreSQL-Primary-Keys liegen in einem B-Tree-Index. Ein B-Tree arbeitet am effizientesten, wenn neue Zeilen nahe dem Ende des Index landen, weil die DB so anhängen kann, ohne viel umzuschichten.
Sequenzielle IDs: vorhersehbar und freundlich zur Speicherung
Mit einer bigint-Identity oder Sequenz steigen die IDs im Laufe der Zeit. Inserts treffen meist den rechten Teil des Index, Seiten bleiben gefüllt, der Cache bleibt warm und PostgreSQL muss weniger Zusatzarbeit leisten.
Das ist wichtig, selbst wenn du nie ORDER BY id nutzt. Der Schreibpfad muss jeden neuen Key in sortierter Reihenfolge im Index ablegen.
Zufällige UUIDs: mehr Streuung, mehr Fluktuation
Eine zufällige UUID (häufig bei UUIDv4) verteilt Inserts über den gesamten Index. Das erhöht die Chance auf Page-Splits, bei denen PostgreSQL neue Indexseiten alloziert und Einträge verschieben muss, um Platz zu schaffen. Das Ergebnis ist mehr Schreibamplifikation: mehr Index-Bytes werden geschrieben, mehr WAL entsteht und oft mehr Hintergrundarbeit später (VACUUM und Bloat-Management).
Zeit-geordnete UUIDs ändern die Situation. UUIDs, die größtenteils mit der Zeit zunehmen (wie UUIDv7-artige oder andere zeitbasierte Schemata), stellen einen Großteil der Lokalität wieder her, während sie weiterhin 16 Bytes belegen und in APIs wie UUIDs aussehen.
Diese Unterschiede spürst du besonders bei hohen Insert-Raten, großen Tabellen, die nicht in den Speicher passen, und mehreren Sekundärindizes. Wenn du empfindlich auf Latenzspitzen durch Page-Splits reagierst, vermeide vollständig zufällige IDs in stark geschriebenen Tabellen.
Beispiel: Eine viel genutzte Events-Tabelle, die den ganzen Tag Mobile-App-Logs empfängt, läuft in der Regel mit sequenziellen Keys oder zeit-geordneten UUIDs ruhiger als mit komplett zufälligen UUIDs.
Performance-Effekte, die du tatsächlich bemerkst
Echte Performance-Probleme sind selten „UUIDs sind langsam“ oder „bigints sind schnell“. Entscheidend ist, welche Bereiche die DB berühren muss, um deine Abfrage zu beantworten.
Query-Pläne interessieren sich hauptsächlich dafür, ob ein Index-Scan für Filter nutzbar ist, ob Joins schnell auf dem Key ausgeführt werden können und ob die Tabelle physisch geordnet (oder nah dran) ist, sodass Range-Reads billig sind. Mit einem bigint-Primary-Key landen neue Zeilen ungefähr in Einfügungsreihenfolge, sodass der Primary-Key-Index kompakt und lokalitätsfreundlich bleibt. Mit zufälligen UUIDs streuen Inserts den Index und können mehr Page-Splits und eine unordentlichere On-Disk-Ordnung erzeugen.
Reads bemerkt man oft zuerst. Größere Keys bedeuten größere Indizes, und größere Indizes bedeuten, dass weniger nützliche Seiten im RAM Platz finden. Das reduziert Cache-Hits und erhöht IO, besonders bei join-lastigen Ansichten wie „Bestellungen mit Kundeninfo listen“. Wenn dein Working Set nicht in den Speicher passt, können UUID-lastige Schemata dich schneller über diese Grenze schieben.
Writes können sich ebenfalls verschieben. Zufällige UUID-Inserts erhöhen den Churn im Index, was Autovacuum mehr Arbeit macht und sich als Latenzspitzen unter Last bemerkbar machen kann.
Wenn du UUID vs bigint benchmarkst, sei ehrlich: gleiche Schema, gleiche Indizes, gleicher Fillfactor und genug Zeilen, um den RAM zu übersteigen (nicht nur 10k). Miss p95-Latenz und IO, und teste sowohl mit warmem als auch kaltem Cache.
Wenn du Apps mit AppMaster auf PostgreSQL baust, zeigt sich das oft als langsamere Listen-Seiten und höhere DB-Last, lange bevor es wie ein „CPU-Problem“ aussieht.
Sicherheit und Bedienbarkeit in öffentlichen Systemen
Wenn deine IDs die Datenbank verlassen und in URLs, API-Antworten, Support-Tickets und Mobilbildschirmen auftauchen, beeinflusst die Wahl sowohl die Sicherheit als auch die tägliche Nutzbarkeit.
bigint-IDs sind für Menschen leichter. Sie sind kurz, man kann sie am Telefon vorlesen und das Support-Team erkennt schneller Muster wie „alle fehlerhaften Bestellungen liegen um 9.200.000“. Das beschleunigt Debugging, besonders wenn du aus Logs oder Kundenscreens arbeitest.
UUIDs sind nützlich, wenn du Identifikatoren öffentlich machst. Eine UUID ist schwer zu erraten, also funktioniert einfaches Scrapen wie /users/1, /users/2, /users/3 nicht. Außenstehende erkennen so auch schwerer, wie viele Datensätze du hast.
Die Falle ist zu denken, „nicht erratbar“ = „sicher“. Wenn Autorisierungsprüfungen schwach sind, können vorhersehbare bigint-IDs missbraucht werden, aber UUIDs können immer noch aus einem geteilten Link, einem geleakten Log oder einer zwischengespeicherten API-Antwort gestohlen werden. Sicherheit muss von Berechtigungsprüfungen kommen, nicht vom Verstecken der ID.
Praktische Herangehensweise:
- Erzwinge Besitz- oder Rollenprüfungen bei jedem Read und Write.
- Falls du IDs in öffentlichen APIs zeigst, nutze UUIDs oder separate öffentliche Tokens.
- Wenn du menschenfreundliche Referenzen willst, behalte intern ein
bigintfür den Betrieb. - Kodier keine sensiblen Bedeutungen in die ID selbst (z. B. Nutzertyp).
Beispiel: Ein Kundenportal zeigt Rechnungs-IDs. Wenn Rechnungen bigint nutzen und deine API nur prüft „Rechnung existiert“, kann jemand Nummern iterieren und fremde Rechnungen herunterladen. Behebe zuerst die Prüfung. Entscheide dann, ob UUIDs als öffentliche Rechnungs-IDs das Risiko und die Support-Last verringern.
In Plattformen wie AppMaster, wo IDs durch generierte APIs und mobile Apps fließen, ist das sicherste Default konsistente Autorisierung plus ein ID-Format, das deine Clients zuverlässig verarbeiten können.
Wie IDs durch APIs und Mobile-Apps wandern
Die Datenbank-Auswahl bleibt nicht in der DB. Sie durchsickert jede Grenze: URLs, JSON-Payloads, Client-Speicher, Logs und Analytics.
Wenn du später den ID-Typ ändern willst, ist das selten „nur eine Migration“. Fremdschlüssel müssen überall angepasst werden, nicht nur in der Haupttabelle. ORMs und Code-Generatoren erzeugen vielleicht Modelle neu, aber Integrationen erwarten weiterhin das alte Format. Selbst ein einfacher GET /users/123-Endpoint wird kompliziert, wenn die ID plötzlich 36 Zeichen lange UUIDs sind. Du musst Caches, Message-Queues und alle Orte aktualisieren, an denen IDs als Integer gespeichert wurden.
Für APIs ist die größte Entscheidung das Format und die Validierung. bigint-IDs werden als Zahlen übertragen, aber einige Systeme (und Sprachen) riskieren Präzisionsprobleme bei sehr großen Werten, wenn sie sie als Fließkommazahlen parsen. UUIDs werden als Strings übertragen, was das Parsen sicherer macht, aber du brauchst strikte Validierung, damit kein „fast UUID“-Müll in Logs und Datenbanken landet.
Auf Mobilgeräten werden IDs ständig serialisiert und gespeichert: JSON-Antworten, lokale SQLite-Tabellen und Offline-Queues, die Aktionen speichern, bis das Netzwerk zurückkommt. Numerische IDs sind kleiner, aber String-UUIDs sind oft leichter als opaque Tokens zu behandeln. Schmerz entsteht durch Inkonsistenz: eine Ebene speichert als Integer, eine andere als Text – Vergleiche oder Joins werden fragil.
Ein paar Regeln, die Teams Probleme ersparen:
- Wähle eine kanonische Repräsentation für APIs (häufig String) und bleibe dabei.
- Validier IDs an der Peripherie und gib klare 400-Fehler zurück.
- Speicher dieselbe Repräsentation in lokalen Caches und Offline-Queues.
- Logge IDs mit konsistenten Feldnamen und Formaten über alle Services hinweg.
Wenn du Web- und Mobile-Clients mit einem generierten Stack baust (z. B. AppMaster, das Backend und native Apps generiert), ist ein stabiler ID-Vertrag noch wichtiger, weil er Teil jedes generierten Modells und jeder Anfrage wird.
Sharding-Bereitschaft und verteilte Systeme
„Sharding-ready“ bedeutet meist, dass du IDs an mehr als einem Ort erzeugen kannst, ohne die Eindeutigkeit zu brechen, und dass du Daten später zwischen Knoten verschieben kannst, ohne jeden Fremdschlüssel umzuschreiben.
UUIDs sind in Multi-Region- oder Multi-Writer-Setups beliebt, weil jeder Knoten eine eindeutige ID erzeugen kann, ohne eine zentrale Sequenz zu fragen. Das reduziert Koordination und macht es einfacher, Writes in verschiedenen Regionen zu akzeptieren und Daten später zusammenzuführen.
bigint kann weiterhin funktionieren, erfordert aber einen Plan. Gängige Optionen sind numerische Bereiche pro Shard zu reservieren (Shard 1 nutzt 1–1B, Shard 2 nutzt 1B–2B), separate Sequenzen mit Shard-Präfix zu betreiben oder Snowflake-ähnliche IDs (zeitbasierte Bits plus Maschine-/Shard-Bits). Diese können Indizes kleiner halten als UUIDs und etwas Ordering bewahren, fügen aber operative Regeln hinzu, die eingehalten werden müssen.
Tägliche Trade-offs:
- Koordination: UUIDs brauchen fast keine;
bigintoft Range-Planung oder einen Generator-Service. - Kollisionen: UUID-Kollisionen sind extrem unwahrscheinlich;
bigintist nur sicher, wenn Zuweisungsregeln nie überlappen. - Ordering: Viele
bigint-Schemata sind ungefähr zeitgeordnet; UUID ist oft zufällig, außer du verwendest eine zeit-geordnete Variante. - Komplexität: Sharded
bigintbleibt einfach nur, wenn das Team diszipliniert ist.
Für viele Teams bedeutet „sharding-ready“ in Wahrheit „migrationsbereit“. Wenn du heute eine Single-DB hast, wähle die ID, die die aktuelle Arbeit erleichtert. Wenn du schon mehrere Writer aufbaust (z. B. durch generierte APIs und Mobile-Apps in AppMaster), entscheide früh, wie IDs über Services hinweg erzeugt und validiert werden.
Schritt-für-Schritt: die richtige ID-Strategie wählen
Beginne damit, die tatsächliche Form deiner App zu benennen. Eine einzelne PostgreSQL-Datenbank in einer Region hat andere Bedürfnisse als ein Multi-Tenant-System, ein Setup, das später nach Regionen aufgeteilt werden könnte, oder eine Offline-first-Mobile-App, die Datensätze offline erstellen und später synchronisieren muss.
Sei ehrlich darüber, wo IDs auftauchen. Bleiben Identifikatoren intern (Jobs, interne Tools, Admin-Panels), gewinnt oft Einfachheit. Erscheinen IDs in URLs, mit Kunden geteilten Logs, Support-Tickets oder Mobile-Deep-Links, sind Vorhersagbarkeit und Datenschutz wichtiger.
Nutze Ordering als Entscheidungsfaktor, nicht als Nachgedanken. Wenn du auf „neueste zuerst“-Feeds, stabile Pagination oder auditierbare Trails angewiesen bist, reduzieren sequenzielle oder zeit-geordnete IDs Überraschungen. Wenn Ordering nicht an den Primary Key gebunden ist, kannst du PK und Sortierung getrennt halten und stattdessen nach einem Timestamp sortieren.
Ein praktischer Entscheidungsfluss:
- Klassifiziere deine Architektur (Single DB, Multi-Tenant, Multi-Region, Offline-First) und ob du Daten aus mehreren Quellen zusammenführen könntest.
- Entscheide, ob IDs öffentliche Identifikatoren sind oder rein intern bleiben.
- Bestätige deine Ordering- und Pagination-Anforderungen. Wenn du Insert-Reihenfolge brauchst, vermeide rein zufällige IDs.
- Gehst du mit UUIDs, wähle Version bewusst: zufällig (v4) für Unvorhersagbarkeit, oder zeit-geordnet für bessere Index-Lokalität.
- Lege Konventionen früh fest: eine kanonische Textform, Groß-/Kleinschreibungsregeln, Validierung und wie jede API IDs zurückgibt und annimmt.
Beispiel: Wenn eine Mobile-App „Draft Orders“ offline erstellt, erlauben UUIDs dem Gerät, IDs sicher zu erzeugen, bevor der Server sie sieht. In Tools wie AppMaster ist das praktisch, weil dasselbe ID-Format vom DB über API bis zu Web- und Native-Apps ohne Sonderfälle fließt.
Häufige Fehler und Stolperfallen
Die meisten ID-Debatten gehen schief, weil Teams einen Typ aus einem Grund wählen und später von den Nebenwirkungen überrascht werden.
Ein häufiger Fehler ist, vollständig zufällige UUIDs in einer stark beschriebenen Tabelle zu verwenden und sich dann zu wundern, warum Inserts stottern. Zufällige Werte verteilen neue Zeilen über den Index, was mehr Page-Splits und mehr Arbeit für die DB unter hoher Last bedeutet. Wenn die Tabelle stark schreiblastig ist, denke an Insert-Lokalität, bevor du dich festlegst.
Ein weiteres Problem ist das Mischen von ID-Typen über Services und Clients hinweg. Ein Service nutzt bigint, ein anderer UUID, und deine API liefert am Ende beides – numerische und stringbasierte IDs. Das führt zu subtilen Bugs: JSON-Parser, die Präzision großer Zahlen verlieren, Mobile-Code, der IDs in einem Screen als Zahl und in einem anderen als String behandelt, oder Caching-Keys, die nicht mehr übereinstimmen.
Eine dritte Falle ist, „nicht erratbare IDs“ als Sicherheit zu betrachten. Selbst mit UUIDs brauchst du ordentliche Autorisierung.
Schließlich ändern Teams den ID-Typ spät ohne Plan. Das Schwierigste ist nicht der Primary Key selbst, sondern alles, was daran hängt: Fremdschlüssel, Join-Tabellen, URLs, Analytics-Events, Mobile-Deep-Links und gespeicherter Client-Status.
Um Schmerz zu vermeiden:
- Wähle einen ID-Typ für öffentliche APIs und halte dich daran.
- Behandle IDs in Clients als opaque Strings, um numerische Edge-Cases zu vermeiden.
- Verlass dich nie auf ID-Zufälligkeit als Zugangskontrolle.
- Wenn du migrieren musst, versioniere die API und plane für lang lebende Clients.
Wenn du mit einem Code-generierenden Tool wie AppMaster arbeitest, ist Konsistenz noch wichtiger, weil dasselbe ID-Format vom DB-Schema in das generierte Backend und in Web- und Mobile-Apps fließt.
Kurze Checkliste vor der Entscheidung
Wenn du feststeckst, fang nicht mit Theorie an. Fang mit dem Produktbild in einem Jahr an und wo die ID überall auftauchen wird.
Frag dich:
- Wie groß werden die größten Tabellen in 12–24 Monaten und bewahrst du Jahre an Historie?
- Brauchst du IDs, die grob nach Erstellungszeit sortieren, für einfache Paginierung und Debugging?
- Werden mehrere Systeme gleichzeitig Datensätze erstellen, inklusive offline Mobile oder Background-Jobs?
- Tauchen IDs in URLs, Support-Tickets, Exporten oder mit Kunden geteilten Screenshots auf?
- Können alle Clients die ID gleich behandeln (Web, iOS, Android, Skripte), inkl. Validierung und Speicherung?
Hast du das beantwortet, prüfe die Infrastruktur. Wenn du bigint verwendest, sorge dafür, dass du in allen Umgebungen einen klaren Plan zur ID-Generierung hast (besonders Dev- / Import-Szenarien). Bei UUIDs stelle sicher, dass API-Contracts und Client-Modelle String-IDs konsistent handhaben und das Team komfortabel darin ist, sie zu lesen und zu vergleichen.
Ein schneller Realitäts-Test: Wenn eine Mobile-App eine Bestellung offline erstellen und später synchronisieren muss, reduzieren UUIDs oft den Koordinationsaufwand. Ist deine App überwiegend online und willst du einfache, kompakte Indizes, ist bigint meistens einfacher.
Wenn du Apps in AppMaster baust, entscheide früh, weil die ID-Konvention durch dein PostgreSQL-Modell, generierte APIs und Web- sowie Native-Clients fließt.
Ein realistisches Beispiel-Szenario
Ein kleines Unternehmen hat ein internes Ops-Tool, ein Kundenportal und eine Mobile-App für Außendienstmitarbeiter. Alle drei nutzen dieselbe PostgreSQL-Datenbank über eine API. Den ganzen Tag werden neue Datensätze erstellt: Tickets, Fotos, Status-Updates und Rechnungen.
Mit bigint-IDs sind die API-Payloads kompakt und leicht lesbar:
{ "ticket_id": 4821931, "customer_id": 91244 }
Pagination wirkt natürlich: ?after_id=4821931&limit=50. Sortierung nach id stimmt oft mit Erstellungszeit überein, sodass „neueste Tickets“ schnell und vorhersehbar ist. Debugging ist einfach: Support fragt nach „Ticket 4821931“ und die meisten können es ohne Fehler tippen.
Mit UUIDs werden die Payloads länger:
{ "ticket_id": "3f9b3c0a-7b9c-4bf0-9f9b-2a1b3c5d1d2e" }
Bei zufälligen UUID v4 landen Inserts überall im Index. Das kann mehr Index-Churn bedeuten und das tägliche Debugging etwas umständlicher machen (Copy/Paste wird zur Norm). Pagination wechselt oft zu Cursor-Style-Tokens statt „after id“.
Verwendest du zeit-geordnete UUIDs, behältst du den Großteil des „neueste zuerst“-Verhaltens, vermeidest aber erratbare IDs in öffentlichen URLs.
In der Praxis merken Teams meist vier Dinge:
- Wie oft IDs von Menschen getippt vs. kopiert werden
- Ob „sort by id“ mit „sort by created“ übereinstimmt
- Wie sauber und stabil Cursor-Pagination sich anfühlt
- Wie leicht ein Datensatz über Logs, API-Aufrufe und Mobile-Screens nachverfolgt werden kann
Nächste Schritte: Default wählen, testen und standardisieren
Die meisten Teams blockieren sich, weil sie eine perfekte Antwort wollen. Du brauchst kein Perfekt. Du brauchst ein Default, das heute zu deinem Produkt passt, plus eine schnelle Möglichkeit zu prüfen, dass es dich später nicht behindert.
Regeln zum Standardisieren:
- Nutze
bigint, wenn du die kleinsten Indizes, vorhersehbare Ordering und einfaches Debugging willst. - Nutze UUID, wenn IDs schwer zu erraten in URLs sein müssen, du Offline-Erzeugung (Mobile) erwartest oder du weniger Kollisionen zwischen Systemen willst.
- Wenn du später nach Tenant oder Region splitten könntest, bevorzuge einen ID-Plan, der über Knoten hinweg funktioniert (UUID oder ein koordiniertes
bigint-Schema). - Wähle eines als Default und mache Ausnahmen selten. Konsistenz schlägt oft Mikro-Optimierung einer einzelnen Tabelle.
Bevor du dich festlegst, mach einen kleinen Spike. Erstelle eine Tabelle mit realistischer Zeilengröße, füge 1–5 Millionen Zeilen ein und vergleiche (1) Indexgröße, (2) Insert-Zeit und (3) einige häufige Queries mit Primary Key und ein paar Sekundärindizes. Mach das auf deiner realen Hardware und mit deiner echten Datenform.
Wenn du befürchtest, später wechseln zu müssen, plane die Migration so, dass sie langweilig ist:
- Füge die neue ID-Spalte und einen eindeutigen Index hinzu.
- Dual-Write: fülle beide IDs für neue Zeilen.
- Backfille alte Zeilen in Batches.
- Update APIs und Clients, sodass sie die neue ID akzeptieren (halte die alte während der Transition weiter funktionsfähig).
- Schalte die Reads um und droppe den alten Key erst, wenn Logs und Metriken sauber aussehen.
Wenn du auf AppMaster (appmaster.io) baust, lohnt es sich, früh zu entscheiden, weil die ID-Konvention durch dein PostgreSQL-Modell, generierte APIs und Web- sowie native Mobile-Clients läuft.
FAQ
Präferiere bigint, wenn du eine einzelne PostgreSQL-Datenbank betreibst, die meisten Writes auf dem Server stattfinden und du kompakte Indizes sowie vorhersehbares Insert-Verhalten willst. Wähle UUIDs, wenn IDs an vielen Orten erzeugt werden müssen (mehrere Services, offline Mobile, zukünftiges Sharding) oder wenn öffentliche IDs schwer zu erraten sein sollen.
Weil die ID an vielen Stellen kopiert wird: in den Primary-Key-Index, in jeden Sekundärindex (als Zeiger auf die Zeile), in Fremdschlüsselspalten anderer Tabellen und in Join-Tabellen. UUIDs belegen 16 Bytes statt 8 Bytes bei bigint, und dieser Größenunterschied multipliziert sich durch dein Schema und kann die Cache-Hit-Raten reduzieren.
Auf stark schreiblastigen Tabellen: ja. Zufällige UUIDs (z. B. v4) verteilen Inserts über den gesamten B-Tree, was Page-Splits und Index-Churn unter Last erhöht. Wenn du UUIDs willst, aber ruhigere Writes brauchst, nutze eine zeit-geordnete UUID-Strategie, damit neue Keys größtenteils am Ende landen.
Oft zeigt es sich eher als mehr IO als als höhere CPU-Last. Größere Keys bedeuten größere Indizes, und größere Indizes bedeuten, dass weniger Seiten in den RAM passen. Das führt zu mehr Leseoperationen, besonders bei Join-lastigen Abfragen und großen Tabellen, deren Working Set nicht in den Speicher passt.
UUIDs erschweren einfaches Erraten wie /users/1, ersetzen aber keine Autorisierung. Wenn Prüfungen falsch sind, können UUIDs trotzdem geleakt und wiederverwendet werden. Behandle UUIDs als Komfort für öffentliche Identifikatoren und verlasse dich für echte Sicherheit auf strikte Zugriffskontrollen.
Nutze eine einheitliche Repräsentation und halte dich daran. Ein praktischer Default ist, IDs in APIs als Strings zu behandeln, selbst wenn die DB bigint benutzt, weil so Client-seitige numerische Edge-Cases vermieden werden und die Validierung einfacher bleibt. Was auch immer du wählst: konsistent sein über Web, Mobile, Logs und Caches.
Große numerische bigint-Werte können in einigen Clients Probleme machen, wenn sie als Fließkommazahlen geparst werden und dadurch Genauigkeit verlieren. UUIDs vermeiden das, weil sie Strings sind, aber sie sind länger und müssen strikt validiert werden. Die sicherste Vorgehensweise ist Konsistenz: ein Typ überall und klare Validierung an der API-Grenze.
UUIDs sind eine einfache Wahl, weil sie ohne zentrale Sequenz auf jedem Knoten erzeugt werden können. bigint funktioniert auch, erfordert aber Regeln—z. B. numerische Bereiche pro Shard oder einen Snowflake-ähnlichen Generator. Wenn du die einfachste verteilte Story willst, wähle UUIDs (vorzugsweise zeit-geordnet).
Eine Typänderung betrifft weit mehr als eine Spalte. Du musst Fremdschlüssel, Join-Tabellen, API-Contracts, Client-Speicher, Caches, Analytics-Events und Integrationen updaten. Plane eine schrittweise Migration mit Dual-Write und einem langen Übergangszeitraum, wenn ein Wechsel wahrscheinlich ist.
Ja. Behalte einen internen bigint-Key für Datenbankeffizienz und füge zusätzlich eine öffentliche UUID (oder ein Token) für URLs und externe APIs hinzu. So erhältst du kompakte Indizes und gleichzeitig vermeidest du leichte Enumeration nach außen. Entscheide früh, welche ID die „öffentliche“ ist, und mische sie nicht sorglos.


