Database•Jun 2026•3 min read

Sequential Ids vs Uuid

Auto-incrementing integer keys versus UUIDs for primary keys. One is tight, fast, and leaky. The other is distributed-friendly and bulky. Here's the decisive call.

The short answer

Uuid over Sequential Ids for most cases. Use UUIDv7 as your default primary key.

  • Pick Sequential Ids if run a single Postgres/MySQL node, will never shard, expose IDs only behind auth, and want the tightest possible indexes and join performance with zero ceremony
  • Pick Uuid if shard, replicate multi-master, generate IDs client-side, expose IDs in URLs/APIs, or want any of those options open later — reach for UUIDv7 specifically
  • Also consider: This is not all-or-nothing. Keep a sequential bigint surrogate for internal joins and storage efficiency, and expose a UUID (or hashed/encoded public ID) externally. Many serious schemas run both columns deliberately.

— Nice Pick, opinionated tool recommendations

The enumeration problem is real

Sequential IDs broadcast information you didn't mean to ship. /invoices/41 tells anyone that invoice 42 exists, and that you wrote roughly N invoices total — a free competitor metric and a scraping roadmap. The German Tank Problem isn't academic; people have estimated startup signup counts, order volumes, and user growth straight off incrementing IDs in URLs. Worse is the IDOR: developers lean on guessable keys, forget an authorization check on one endpoint, and now id+1 is someone else's data. UUIDs don't make you secure — authorization does that — but they remove the 'just guess the next number' attack surface entirely and stop leaking business intelligence by accident. If your IDs ever touch a URL, an email link, or a public API response, sequential integers are quietly working against you. That alone disqualifies them as a default for anything user-facing.

Distributed systems hate a global counter

An auto-increment column needs one authority handing out the next number. On a single node that's free. The moment you shard, run multi-master replication, or merge two databases after an acquisition, that single authority becomes the fight. You end up with offset/step tricks (node A does even, node B odd), centralized ID services like Snowflake, or post-hoc renumbering that breaks every foreign key and cached link. UUIDs sidestep the entire problem: any node, any client, even an offline mobile app can mint a globally-unique key with no round trip and no collision worth worrying about. Insert two databases' worth of rows into one table and nothing clashes. This is the structural reason large and growing systems default to UUIDs — not fashion, but the refusal to bolt a coordination bottleneck onto every single INSERT for the rest of the product's life.

The performance gap closed — use UUIDv7

The classic case against UUIDs was storage and index pain: 16 bytes versus 4–8, and random UUIDv4 values scattered across the B-tree, fragmenting indexes, blowing out the buffer cache, and slowing inserts as the table grew. That critique was correct in 2015 and is mostly dead now. UUIDv7 embeds a millisecond timestamp in its high bits, so new keys sort near each other and inserts stay append-mostly — the same locality that made sequential ints fast. You still pay the bytes: bigger keys mean fatter indexes and wider foreign keys, which matters at billions of rows and on hot join paths. But store them as native uuid/BINARY(16), never as 36-char text (that's where people self-inflict the real bloat). Net: minor, bounded cost — not the order-of-magnitude penalty UUIDv4 deserved.

When sequential still wins outright

I won't pretend ints are obsolete. For a single-node OLTP database that will genuinely never shard — an internal tool, a back-office system, a bounded SaaS that fits one big Postgres box — a bigint identity column is smaller, faster to join, trivially sortable, and human-debuggable. 'Go look at order 90210' beats reading a hyphenated hex string aloud over a call. Analytics and warehouse tables that never leave your perimeter are fine on sequential keys too; the enumeration risk only bites when IDs are exposed. The honest move when you choose ints: keep them internal. Don't put the raw auto-increment in a URL — encode it, hash it, or carry a separate public token. Do that and the security objection mostly evaporates. So sequential IDs remain a legitimate, even superior, choice for closed internal systems. They are simply the wrong default for anything that might face users or grow.

Quick Comparison

FactorSequential IdsUuid
Enumeration / data leakageReveals record counts and growth, invites IDOR attacks via guessable next IDUnguessable, leaks nothing about volume or order
Distributed / sharded insertsNeeds a global counter; offset tricks or central ID service requiredAny node or client mints unique keys with no coordination
Storage & index size4–8 bytes, tightest possible indexes and foreign keys16 bytes native; fatter indexes, real cost at billions of rows
Insert locality / fragmentationPerfectly sequential, append-onlyUUIDv7 time-ordered ≈ sequential; UUIDv4 fragments badly
Human debuggability'order 90210' — readable, sortable, easy to referenceHyphenated hex, painful to read aloud or eyeball

The Verdict

Use Sequential Ids if: You run a single Postgres/MySQL node, will never shard, expose IDs only behind auth, and want the tightest possible indexes and join performance with zero ceremony.

Use Uuid if: You shard, replicate multi-master, generate IDs client-side, expose IDs in URLs/APIs, or want any of those options open later — reach for UUIDv7 specifically.

Consider: This is not all-or-nothing. Keep a sequential bigint surrogate for internal joins and storage efficiency, and expose a UUID (or hashed/encoded public ID) externally. Many serious schemas run both columns deliberately.

Sequential Ids vs Uuid: FAQ

Is Sequential Ids or Uuid better?

Uuid is the Nice Pick. Use UUIDv7 as your default primary key. It kills the enumeration leak, lets clients generate IDs offline, survives sharding and multi-master merges without a coordination dance, and — critically — its time-ordered prefix keeps B-tree inserts sequential, erasing the old random-UUID index-fragmentation penalty. Sequential ints are tighter and read marginally faster, but you buy that with a global counter, a security smell, and a painful migration the day you outgrow one node. Pay the 16 bytes now.

When should you use Sequential Ids?

You run a single Postgres/MySQL node, will never shard, expose IDs only behind auth, and want the tightest possible indexes and join performance with zero ceremony.

When should you use Uuid?

You shard, replicate multi-master, generate IDs client-side, expose IDs in URLs/APIs, or want any of those options open later — reach for UUIDv7 specifically.

What's the main difference between Sequential Ids and Uuid?

Auto-incrementing integer keys versus UUIDs for primary keys. One is tight, fast, and leaky. The other is distributed-friendly and bulky. Here's the decisive call.

How do Sequential Ids and Uuid compare on enumeration / data leakage?

Sequential Ids: Reveals record counts and growth, invites IDOR attacks via guessable next ID. Uuid: Unguessable, leaks nothing about volume or order. Uuid wins here.

Are there alternatives to consider beyond Sequential Ids and Uuid?

This is not all-or-nothing. Keep a sequential bigint surrogate for internal joins and storage efficiency, and expose a UUID (or hashed/encoded public ID) externally. Many serious schemas run both columns deliberately.

🧊
The Bottom Line
Uuid wins

Use UUIDv7 as your default primary key. It kills the enumeration leak, lets clients generate IDs offline, survives sharding and multi-master merges without a coordination dance, and — critically — its time-ordered prefix keeps B-tree inserts sequential, erasing the old random-UUID index-fragmentation penalty. Sequential ints are tighter and read marginally faster, but you buy that with a global counter, a security smell, and a painful migration the day you outgrow one node. Pay the 16 bytes now.

Related Comparisons

Disagree? nice@nicepick.dev