19 February 2025 · 6 min read
UUID v4 or v7: which identifier to reach for
Random identifiers are easy to reason about and hard on your database index. Time-ordered ones fix that, with a trade-off.
A UUID is 128 bits, conventionally written as 36 characters. Which version you generate decides how those bits are chosen, and that turns out to matter a great deal once the identifiers are primary keys.
v4: random
Version 4 fills 122 of the 128 bits with random data. The collision probability is negligible — you would need to generate billions per second for a lifetime to worry — and it leaks nothing about when or where the identifier was made.
Its weakness is ordering. Consecutive v4 UUIDs are scattered uniformly across the keyspace, so inserting them into a B-tree index touches a different page every time.
Why random keys hurt
A B-tree index performs best when new rows land at the end. Sequential inserts append to the same page, which stays in memory and is written once. Random inserts land anywhere, so each write pulls a cold page from disk, dirties it, and pushes something useful out of the buffer pool.
The effect compounds. Pages split more often, the index fragments, and it grows larger than it should. On InnoDB, where the primary key is the physical row order, a random primary key means the table itself is written in random order.
This is invisible at ten thousand rows and painful at fifty million. By then the primary key is referenced by every foreign key you have.
v7: time-ordered
Version 7 puts a 48-bit Unix millisecond timestamp in the leading bits and fills the rest with randomness. Two consequences follow: identifiers generated close together sort close together, and lexicographic order matches creation order.
- Inserts append rather than scatter, so index locality returns.
- Sorting by ID approximates sorting by creation time without a separate column.
- Range queries over a time window become index scans.
- It stays a valid UUID, so existing columns and libraries keep working.
What you give up
A v7 identifier discloses when it was created, to the millisecond. If IDs are public, that leaks signups per hour, order volume, and whether two records were created in the same request. For most systems that is uninteresting; for some it is a competitive disclosure.
It also reduces randomness from 122 bits to about 74. Still far beyond guessable, but worth knowing if you were relying on the identifier being unguessable as a security control — which you should not be.
The alternatives
- ULID: the same idea, encoded in 26 characters of Crockford base32. Shorter and case-insensitive, but not a UUID, so a uuid column will reject it.
- Nano ID: 21 URL-safe characters, purely random. Excellent for public-facing short IDs, no ordering.
- Auto-increment integers: the smallest and fastest option, but they leak volume, are guessable, and are awkward to generate across shards.
Choosing
Use v7 for database primary keys, where insert locality is worth more than opacity. Use v4 when the identifier is exposed and the creation time should not be. Use Nano ID when it appears in a URL and length matters. And keep the internal key separate from the public one when the two goals conflict — they usually do.
Tools mentioned