UUID vs ULID vs serial IDs: Erfahre, wie sich jede Option auf Indexierung, Sortierung, Sharding sowie sichere Exporte und Importe in realen Projekten auswirkt.

Eine ID‑Entscheidung fühlt sich in Woche eins langweilig an. Dann gehst du live, die Daten wachsen, und diese "einfache" Entscheidung taucht überall auf: Indizes, URLs, Logs, Exporte und Integrationen.
Die eigentliche Frage ist nicht „welches ist am besten?" sondern „welchen Schmerz willst du später vermeiden?" IDs sind schwer zu ändern, weil sie in andere Tabellen kopiert, von Clients gecacht und von anderen Systemen abhängig werden.
Wenn die ID nicht zur Produktentwicklung passt, siehst du das meist an ein paar Stellen:
Es gibt immer einen Kompromiss zwischen Bequemlichkeit jetzt und Flexibilität später. Serielle Ganzzahlen sind leicht zu lesen und oft schnell, aber sie können die Anzahl der Datensätze preisgeben und das Zusammenführen von Datensätzen erschweren. Zufällige UUIDs sind großartig für Eindeutigkeit über Systeme hinweg, belasten aber Indizes stärker und sind für Menschen beim Log‑Scannen schwerer. ULIDs versuchen globale Eindeutigkeit mit zeitartiger Sortierung zu verbinden, haben aber trotzdem Speicher‑ und Tooling‑Tradeoffs.
Eine nützliche Denkweise: Für wen ist die ID hauptsächlich gedacht?
Wenn die ID vor allem für Menschen ist (Support, Debugging, Ops), gewinnen kürzere und besser lesbare IDs. Wenn sie für Maschinen ist (verteilte Writes, Offline‑Clients, Multi‑Region‑Systeme), sind globale Eindeutigkeit und Kollisionsvermeidung wichtiger.
Wenn Leute über "UUID vs ULID vs serial IDs" diskutieren, wählen sie im Grunde, wie jede Zeile ein einzigartiges Etikett bekommt. Dieses Etikett beeinflusst, wie einfach es ist, später einzufügen, zu sortieren, zusammenzuführen und Daten zu verschieben.
Eine serielle ID ist ein Zähler. Die Datenbank gibt 1, dann 2, dann 3 aus (oft als integer oder bigint gespeichert). Sie ist leicht zu lesen, kostet wenig Speicher und ist meist schnell, weil neue Zeilen am Ende des Index landen.
Eine UUID ist ein 128‑Bit‑Bezeichner, der zufällig aussieht, z. B. 3f8a.... In den meisten Setups kann sie erzeugt werden, ohne die Datenbank nach der nächsten Zahl zu fragen, sodass verschiedene Systeme IDs unabhängig erstellen können. Der Nachteil ist, dass zufällig wirkende Inserts Indizes stärker belasten und mehr Platz benötigen als ein simples bigint.
Eine ULID ist ebenfalls 128‑Bit, aber so gestaltet, dass sie grob zeitlich sortierbar ist. Neuere ULIDs sortieren normalerweise nach älteren, bleiben aber global eindeutig. Du bekommst oft einige der "überall erzeugbar"‑Vorteile von UUIDs mit freundlicherem Sortierverhalten.
Eine einfache Zusammenfassung:
Serielle IDs sind üblich für Single‑Database‑Apps und interne Tools. UUIDs tauchen auf, wenn Daten über mehrere Services, Geräte oder Regionen erstellt werden. ULIDs sind beliebt, wenn Teams verteilte ID‑Generierung wollen, aber Wert auf Sortierung, Pagination oder "neueste zuerst" legen.
Ein Primärschlüssel wird meist von einem Index (oft B‑Tree) unterstützt. Denk an diesen Index wie ein sortiertes Telefonbuch: Jede neue Zeile braucht einen Eintrag an der richtigen Stelle, damit Lookups schnell bleiben.
Bei zufälligen IDs (klassisches UUIDv4) landen neue Einträge überall im Index. Das bedeutet, die Datenbank berührt viele Indexseiten, es gibt öfter Page‑Splits und mehr Schreibaufwand. Mit der Zeit entsteht mehr Index‑Churn: mehr Arbeit pro Insert, mehr Cache‑Misses und größere Indizes als erwartet.
Bei überwiegend ansteigenden IDs (serial/bigint oder zeit‑sortierbare IDs wie viele ULIDs) kann die Datenbank neue Einträge meist ans Ende des Index anhängen. Das ist cache‑freundlicher, weil aktuelle Seiten warm bleiben, und Inserts sind bei höheren Schreibraten gleichmäßiger.
Die Schlüssellänge zählt, weil Indexeinträge nicht umsonst sind:
Größere Schlüssel bedeuten weniger Einträge pro Indexseite. Das führt oft zu tieferen Indizes, mehr Seiten, die pro Query gelesen werden müssen, und mehr RAM, um gut performant zu bleiben.
Wenn du eine "events"‑Tabelle mit konstanten Inserts hast, kann ein zufälliger UUID‑Primärschlüssel schneller spürbar langsamer werden als ein bigint, auch wenn einzelne Lookups noch akzeptabel sind. Bei hoher Schreiblast ist die Indexkosten oft der erste Unterschied, den man merkt.
Wenn du "Mehr laden" oder Infinite Scroll gebaut hast, kennst du den Schmerz von IDs, die nicht gut sortieren. Eine ID "sortiert gut", wenn die Reihenfolge nach ihr eine stabile, sinnvolle Reihenfolge ergibt (oft Erstellungszeit), sodass Pagination vorhersehbar ist.
Bei zufälligen IDs (wie UUIDv4) sind neuere Zeilen verstreut. Sortieren nach id entspricht nicht der Zeit, und Cursor‑Pagination wie "gib mir Elemente nach dieser id" wird unzuverlässig. Meist greift man auf created_at zurück, was in Ordnung ist, aber sorgfältig gemacht werden muss.
ULIDs sind so gestaltet, dass sie grob zeitlich sortierbar sind. Wenn du nach ULID sortierst (als String oder in binärer Form), kommen neuere Elemente tendenziell später. Das macht Cursor‑Pagination einfacher, weil der Cursor der zuletzt gesehene ULID sein kann.
ULID hilft bei natürlicher zeitlicher Reihenfolge für Feeds, einfacheren Cursorn und weniger zufälligen Insert‑Effekten als bei UUIDv4.
ULID garantiert jedoch keine perfekte Zeitfolge, wenn viele IDs in derselben Millisekunde auf mehreren Maschinen erzeugt werden. Wenn du exakte Reihenfolge brauchst, willst du weiterhin einen echten Zeitstempel.
created_at besser istNach created_at zu sortieren ist oft sicherer, wenn du Backfills machst, historische Datensätze importierst oder klare Tie‑Breaker brauchst.
Ein praktisches Muster ist, nach (created_at, id) zu ordnen, wobei id nur als Tie‑Breaker dient.
Sharding bedeutet, eine Datenbank in mehrere kleinere aufzuteilen, sodass jede Shard einen Teil der Daten hält. Teams tun das meist später, wenn eine einzelne DB schwer zu skalieren ist oder als Single Point of Failure zu riskant wird.
Deine ID‑Wahl kann Sharding entweder handhabbar oder schmerzhaft machen.
Bei sequentiellen IDs (Auto‑Increment serial/bigint) generiert jede Shard 1, 2, 3.... Dieselbe ID kann auf mehreren Shards existieren. Beim ersten Zusammenführen von Daten, Verschieben von Zeilen oder Aufbau von Cross‑Shard‑Features treten Kollisionen auf.
Du kannst Kollisionen mit Koordination vermeiden (zentraler ID‑Service oder Bereiche pro Shard), aber das fügt Komponenten hinzu und kann zum Flaschenhals werden.
UUIDs und ULIDs reduzieren die Notwendigkeit zur Koordination, weil jede Shard IDs unabhängig erzeugen kann mit extrem geringem Duplikationsrisiko. Wenn du denkst, dass du jemals Daten über Datenbanken hinweg aufteilen musst, ist das eines der stärksten Argumente gegen pure Sequenzen.
Ein gängiger Kompromiss ist, ein Shard‑Präfix hinzuzufügen und dann auf jeder Shard eine lokale Sequenz zu verwenden. Du kannst es als zwei Spalten speichern oder in einen Wert packen.
Es funktioniert, aber es erzeugt ein benutzerdefiniertes ID‑Format. Jede Integration muss es verstehen, die Sortierung verliert globale Zeitordnung ohne zusätzliche Logik, und das Verschieben von Daten zwischen Shards kann ein Umschreiben der IDs erfordern (was Referenzen bricht, wenn diese IDs geteilt werden).
Stelle früh eine Frage: Musst du jemals Daten aus mehreren Datenbanken zusammenführen und Referenzen stabil halten? Wenn ja, plane globale eindeutige IDs von Anfang an ein oder budgetiere eine Migration später.
Export und Import sind der Punkt, an dem die ID‑Wahl praktisch wird. In dem Moment, in dem du Prod nach Staging klonst, ein Backup wiederherstellst oder Daten aus zwei Systemen zusammenführst, merkst du, ob deine IDs stabil und portierbar sind.
Bei serial (Auto‑Increment) IDs kannst du Inserts normalerweise nicht sicher in eine andere Datenbank replayen und erwarten, dass Referenzen intakt bleiben, es sei denn, du bewahrst die Originalnummern. Wenn du nur einen Teil der Zeilen importierst (z. B. 200 Kunden und deren Bestellungen), musst du Tabellen in der richtigen Reihenfolge laden und exakt dieselben Primärschlüssel behalten. Wenn irgendetwas umnummeriert wird, brechen Foreign Keys.
UUIDs und ULIDs werden außerhalb der Datenbanksequenz erzeugt, daher lassen sie sich einfacher zwischen Umgebungen verschieben. Du kannst Zeilen kopieren, die IDs behalten und Beziehungen stimmen weiter. Das hilft beim Wiederherstellen von Backups, teilweisen Exporten oder beim Mergen von Datensätzen.
Beispiel: Exportiere 50 Accounts aus Production, um ein Problem in Staging zu debuggen. Mit UUID/ULID‑Primärschlüsseln kannst du diese Accounts plus zugehörige Zeilen (Projekte, Rechnungen, Logs) importieren und alles zeigt weiterhin auf die richtigen Eltern. Bei serial IDs baust du oft eine Übersetzungstabelle (old_id -> new_id) und schreibst Fremdschlüssel beim Import um.
Für Bulk‑Importe zählen die Basics mehr als der ID‑Typ:
Du kannst schnell eine solide Wahl treffen, wenn du dich auf das konzentrierst, was später weh tut.
Schreibe deine größten künftigen Risiken auf. Konkrete Ereignisse helfen: Aufteilen in mehrere DBs, Zusammenführen von Kundendaten, Offline‑Writes, häufige Datenkopien zwischen Umgebungen.
Entscheide, ob die ID‑Sortierung der Zeit entsprechen muss. Wenn du "neueste zuerst" ohne zusätzliche Spalten willst, passt ULID (oder eine ähnliche zeit‑sortierbare ID). Wenn created_at ausreicht, funktionieren UUIDs und serielle IDs beide.
Schätze die Schreiblast und Indexsensitivität ab. Bei hoher Insert‑Last und stark beanspruchtem Primärindex ist ein serial BIGINT meist am einfachsten für B‑Tree‑Indizes. Zufällige UUIDs verursachen meist mehr Churn.
Wähle eine Default‑Strategie und dokumentiere Ausnahmen. Halte es einfach: ein Default für die meisten Tabellen und eine klare Regel, wann du abweichst (meist: öffentliche IDs vs interne IDs).
Lass Raum für Änderungen. Vermeide, Bedeutungen in IDs zu kodieren, entscheide, wo IDs erzeugt werden (DB vs App), und halte Constraints explizit.
Die größte Falle ist, eine ID zu wählen, weil sie beliebt ist, und später festzustellen, dass sie zu deinem Abfrage‑, Skalierungs‑ oder Sharing‑Muster nicht passt. Die meisten Probleme tauchen erst Monate später auf.
Häufige Fehler:
123, 124, 125 zeigen, können Leute benachbarte Datensätze erraten und dein System sondieren.Warnsignale, die du früh angehen solltest:
Wähle einen Primärschlüsseltyp und bleibe größtenteils dabei. Das Mischen von Typen (bigint an einer Stelle, UUID an anderer) erschwert Joins, APIs und Migrationen.
Schätze die Indexgröße bei deiner erwarteten Skalierung. Breitere Schlüssel bedeuten größere Primärindizes und mehr Memory/IO.
Entscheide, wie du paginierst. Wenn du nach ID paginierst, stelle sicher, dass die ID vorhersehbare Reihenfolge hat (oder akzeptiere, dass sie das nicht tut). Wenn du nach Timestamp paginierst, indexiere created_at und nutze es konsequent.
Teste deinen Importplan mit produktionsähnlichen Daten. Verifiziere, dass du Datensätze ohne Fremdschlüsselbruch rekonstruieren kannst und dass Re‑Imports nicht still automatisch neue IDs erzeugen.
Schreibe deine Kollisionsstrategie auf. Wer erzeugt die ID (DB oder App) und was passiert, wenn zwei Systeme offline Datensätze erstellen und später synchronisieren?
Stelle sicher, dass öffentliche URLs und Logs keine Muster verraten, die dir wichtig sind (Anzahl der Datensätze, Erstellrate, interne Shard‑Hinweise). Bei serial IDs gehe davon aus, dass Leute benachbarte IDs erraten können.
Eine Solo‑Gründerin startet ein einfaches CRM: Kontakte, Deals, Notizen. Eine Postgres‑Datenbank, eine Web‑App, Ziel ist: schnell liefern.
Zuerst fühlt sich ein serial bigint Primärschlüssel perfekt an. Inserts sind schnell, Indizes bleiben ordentlich, in Logs ist es einfach zu lesen.
Ein Jahr später verlangt ein Kunde Quartals‑Exporte für ein Audit, und die Gründerin beginnt Leads aus einem Marketing‑Tool zu importieren. IDs, die vorher intern waren, tauchen jetzt in CSVs, E‑Mails und Support‑Tickets auf. Wenn zwei Systeme beide 1, 2, 3... verwenden, werden Merges kompliziert. Du fügst Quell‑Spalten, Mapping‑Tabellen hinzu oder schreibst IDs beim Import um.
Im zweiten Jahr kommt eine Mobile‑App hinzu. Sie muss offline Datensätze erstellen und später synchronisieren. Jetzt brauchst du IDs, die auf dem Client erzeugt werden können, ohne mit der DB zu sprechen, und ein geringes Kollisionsrisiko, wenn Daten aus verschiedenen Umgebungen zusammenlaufen.
Ein Kompromiss, der oft gut altert:
Wenn du zwischen UUID, ULID und serial IDs schwankst, entscheide anhand dessen, wie deine Daten sich bewegen und wachsen.
Ein‑Satz‑Empfehlungen für gängige Fälle:
bigint serial Primärschlüssel.Mischen ist oft die beste Antwort. Nutze serial bigint für interne Tabellen, die nie deine DB verlassen (Join‑Tabellen, Hintergrundjobs), und UUID/ULID für öffentliche Entities wie Benutzer, Organisationen, Rechnungen und alles, was du exportieren, synchronisieren oder von einem anderen Dienst referenzieren könntest.
Wenn du in Koder.ai (koder.ai) baust, lohnt es sich, dein ID‑Muster festzulegen, bevor du viele Tabellen und APIs generierst. Der Planungsmodus der Plattform und Snapshots/Rollbacks machen es einfacher, Schemaänderungen früh anzuwenden und zu validieren, solange das System noch klein genug ist, um Änderungen sicher durchzuführen.
Fang mit dem zukünftigen Schmerz an, den du vermeiden willst: langsame Inserts durch zufällige Index‑Schreibvorgänge, unpraktische Pagination, riskante Migrationen oder ID‑Kollisionen bei Importen und Merges. Wenn Daten zwischen Systemen wandern oder an mehreren Orten erstellt werden, wähle eine global eindeutige ID (UUID/ULID) und trenne Fragen zur Zeitordnung klar davon.
Serial bigint ist eine starke Standardwahl, wenn du eine Datenbank hast, viele Writes erwartest und IDs intern bleiben. Es ist kompakt, schnell für B‑Tree‑Indizes und leicht in Logs zu lesen. Der Nachteil: Zusammenführen von Daten später ist schwierig (Kollisionen), und öffentlich exponierte sequenzielle IDs verraten die Anzahl der Datensätze.
Wähle UUIDs, wenn Datensätze in mehreren Services, Regionen, Geräten oder offline erstellt werden und du sehr niedriges Kollisionsrisiko ohne Koordination willst. UUIDs eignen sich auch als öffentlich sichtbare IDs, weil sie schwer zu raten sind. Der tradeoff sind größere Indizes und zufälligere Insert‑Muster verglichen mit sequentiellen Schlüsseln.
ULIDs sind sinnvoll, wenn du IDs überall erzeugen willst und gleichzeitig eine grobe Zeitordnung möchtest. Das vereinfacht Cursor‑Pagination und reduziert das zufällige Einfügeproblem von UUIDv4. Behandle ULID nicht als perfekte Zeitangabe; wenn du strikte Reihenfolge brauchst, nutze created_at.
Ja — besonders bei UUIDv4‑Artiger Zufälligkeit auf stark beschriebenen Tabellen. Zufällige Inserts verteilen sich über den Primärschlüssel‑Index, verursachen mehr Page‑Splits, Cache‑Churn und größere Indizes über die Zeit. Meist fällt es zuerst bei der dauerhaft langsamen Einfügerate und erhöhtem Speicher/IO‑Bedarf auf, weniger bei Einzelabfragen.
Weil eine zufällige ID (z. B. UUIDv4) nicht der Erstellungszeit entspricht, stimmen "nach dieser id"‑Cursor oft nicht mit der Chronologie überein. Die verlässliche Lösung ist nach created_at zu paginieren und die ID als Tie‑Breaker hinzuzufügen, z. B. (created_at, id). Wenn du ausschließlich nach ID paginieren willst, ist eine zeit‑sortierbare ID wie ULID einfacher.
Sequentielle IDs kollidieren über Shards hinweg, weil jeder Shard 1, 2, 3... erzeugt. Du kannst Kollisionen mit Koordination (Shard‑Ranges oder einem zentralen ID‑Service) vermeiden, aber das erhöht die operative Komplexität und kann zum Bottleneck werden. UUIDs/ULIDs reduzieren den Koordinationsbedarf, weil jeder Shard IDs sicher selbst erzeugen kann.
UUIDs/ULIDs sind sicherer, weil du Zeilen exportieren, an anderer Stelle importieren und Beziehungen intakt lassen kannst, ohne Nummern neu zu vergeben. Bei serial IDs erfordern Teilimporte oft eine Übersetzungstabelle (old_id -> new_id) und das Umschreiben von Fremdschlüsseln — ein häufiger Fehlerpunkt. Wenn du oft Umgebungen klonst oder Datensätze zusammenführst, sparen global eindeutige IDs Zeit.
Ein gängiges Muster sind zwei IDs: ein kompakter interner Primärschlüssel (serial bigint) für Joins und Speicher‑Effizienz sowie eine unveränderliche öffentliche ID (ULID oder UUID) für URLs, APIs, Exporte und systemübergreifende Referenzen. Das hält die DB performant und macht Integrationen/Migrationen leichter. Wichtig ist, die öffentliche ID als stabil zu behandeln und niemals neu zu verwenden.
Plane es früh und wende es konsistent über Tabellen und APIs an. In Koder.ai (koder.ai) lege die Standard‑ID‑Strategie im Planungsmodus fest, bevor du viele Schemata und Endpunkte generierst. Nutze Snapshots/Rollbacks, um Änderungen zu validieren, solange das Projekt noch klein und leicht änderbar ist. Das Schwierigste ist nicht neue IDs zu erzeugen, sondern Fremdschlüssel, Caches, Logs und externe Payloads zu aktualisieren, die weiter auf alte IDs verweisen.