Layerbit
Databases & Identifiers

UUID v4 vs v7 for Database Keys: Why Random Ids Get Slow

Random UUIDs are the default primary key in a great many schemas, and they work fine — until the table is large enough that inserts start to crawl and the index outgrows memory. The cause is not the UUID; it is the randomness. RFC 9562's time-ordered v7 keeps everything people like about UUIDs and removes the one property that hurts, and this guide explains the mechanism well enough that you can decide for your own tables.

The short version

  • A B-tree index stores keys in order. Random keys insert at random positions, splitting pages all over the tree; time-ordered keys append at the end.
  • UUID v7 puts a 48-bit millisecond timestamp first, then random bits. Ids created later sort later, and each one still has 74 random bits.
  • Use v7 (or a ULID) for new primary keys. Use v4 when the id must not reveal creation time — session tokens, invitation links.
  • An auto-increment integer is still the fastest key; UUIDs earn their place when ids are generated in many places or exposed outside the system.
  • Never generate ids with Math.random(). Use the platform's cryptographic generator.

What a random key does to a B-tree

Every mainstream database indexes a primary key with a B-tree, a structure that keeps keys sorted so it can find any one in a handful of page reads. Inserting a key means locating the page where it belongs in sort order and writing it there; when that page is full, it is split into two and the parent updated.

With a sequential key — an auto-increment integer, or anything that increases over time — every new row belongs at the far right of the tree. The same rightmost page fills and splits, the pages just before it are finished and never touched again, and the working set is a few hot pages that live comfortably in memory.

With a random key, every new row belongs somewhere different. At a hundred thousand rows the whole index still fits in RAM and nobody notices. At a hundred million rows it does not; a random insert now means reading a cold page from disk, splitting it, and writing two pages back, over and over, for every single row. Write throughput collapses, the index bloats because split pages average half-full, and the cache hit rate falls for reads too. MySQL's InnoDB and SQL Server, which cluster the table itself on the primary key, suffer most, because they are rearranging the rows and not just an index. PostgreSQL's heap tables are less affected but its B-tree indexes bloat the same way.

What v7 changes

A UUID v7, defined in RFC 9562 (May 2024), lays out its 128 bits as:

unix_ts_ms (48 bits) | ver (4) | rand_a (12) | var (2) | rand_b (62)

The first 48 bits are the Unix timestamp in milliseconds, which sorts ids by creation time to the millisecond. The rand_a field is either 12 more random bits or, in the better implementations, a counter that makes ids generated within the same millisecond monotonic. Everything after the variant bits is random — 62 bits, or 74 including rand_a, which is far more than enough to make collisions a non-issue.

The effect on the B-tree is exactly the sequential case above: new ids land at the right edge, the working set stays hot, pages fill completely before splitting. Benchmarks on large InnoDB tables commonly show insert throughput several times higher with v7 than v4, and the index noticeably smaller. You also get a creation timestamp for free, readable from the id without a query — Layerbit's UUID tool decodes it — which is handy in logs and support tickets.

The cost is that the timestamp is readable by anyone who sees the id. For a row id in a database that is usually irrelevant. For a token that gates access, it can matter, which is the one case where v4 remains the right choice.

Generate and decode UUIDs locally

Layerbit's UUID & ULID Generator makes v4, monotonic v7, namespace v5 and ULIDs in bulk, formats them for your database or language, and decodes any id to its version, variant and embedded timestamp — all with the browser's cryptographic random generator, nothing sent anywhere.

Open UUID Generator

The other versions, briefly

VersionBuilt fromSortableUse it for
v1100-ns timestamp since 1582, clock sequence, MAC addressNo (timestamp is stored low bits first)Legacy systems. Leaks the MAC address; superseded by v6 and v7.
v3 / v5MD5 / SHA-1 of a namespace UUID plus a nameNoDeterministic ids: the same name in the same namespace always gives the same UUID. v5 preferred.
v4122 random bitsNoAnything that must not reveal when it was created.
v6v1's fields reordered so the timestamp sortsYesMigrating a v1 system to sortable ids without changing the semantics.
v748-bit Unix ms timestamp plus random bitsYesNew database keys. The default for anything created after 2024.
ULIDSame layout as v7, written as 26 chars of Crockford base32Yes (as a string)Ids in URLs and logs where hyphens and mixed case are a nuisance.

Do you need a UUID at all?

An auto-increment BIGINT is 8 bytes instead of 16, sorts perfectly, and is the fastest key any database offers. UUIDs justify their extra size in three situations. When ids are minted in several places — mobile clients working offline, sharded services, event producers — and must not collide without coordination. When ids are exposed in URLs or APIs and a sequential integer would let anyone enumerate every record or read your growth rate off the counter. And when data from separate systems will later be merged, and integer ids from two databases would clash.

If none of those apply, an integer key with a separate public UUID column is a reasonable compromise: fast joins internally, opaque ids externally. If they do apply, v7 gives you the coordination-free generation and the opacity while keeping most of the integer's index behaviour.

Storing them properly

The 36-character string form is for humans. In the database, store the 16 raw bytes: PostgreSQL's native uuid type, MySQL's BINARY(16), SQL Server's uniqueidentifier. A VARCHAR(36) key is more than twice the size, compares character by character, and defeats much of the benefit. Two database-specific traps: MySQL's UUID() function returns a v1, which does not sort, so use v7 generated in the application or UUID_TO_BIN(id, 1) to reorder it; and SQL Server sorts uniqueidentifier by byte groups in an unusual order, so a v7 is not automatically sequential there — NEWSEQUENTIALID() or a byte-swapped layout is needed.

Where the random bits come from

The uniqueness of a v4 or v7 rests entirely on the randomness of its bits. JavaScript's Math.random(), Java's java.util.Random and similar general-purpose generators are predictable: an attacker who sees a few outputs can compute the next ones, which turns an "unguessable" invitation link into a guessable one. Use crypto.randomUUID() or crypto.getRandomValues() in the browser and Node, uuid.uuid4() in Python (which uses os.urandom), UUID.randomUUID() in Java (which uses SecureRandom). The same applies to the random part of a v7: the timestamp makes it sortable, but the random bits are still what makes it unique and unguessable within a millisecond.

Choose by what the id has to hide

A primary key has to be unique and cheap to index; it does not have to be secret, and it usually does not matter that its creation time is readable. That describes v7. A capability token — a password-reset link, a share URL — has to be unguessable, and its creation time is one fewer thing to leak. That describes v4. Most schemas have far more of the first kind than the second, which is why v7 is the right default for new work and why the random-key slowdown, once you have seen it explained, is an easy problem to avoid.

Frequently asked questions

For a database primary key, yes. Its leading millisecond timestamp means new ids sort after existing ones, so inserts append to the end of the B-tree index instead of splitting random pages, which keeps large tables fast and indexes compact. It still contains 74 random bits, so collisions are not a concern. The only reason to prefer v4 is when the id's creation time must stay secret.

In principle; in practice, no. A v4 has 122 random bits, so you would need to generate about 2.7 quintillion ids for a 50% chance of a single duplicate. A v7 generated within the same millisecond has 74 random bits, still far beyond any realistic volume. Collisions that do occur in the wild come from broken random number generators or copied virtual machine images, not from the format.

Yes. The first 48 bits are the creation time in milliseconds, and anyone with the id can read it. For most database rows this is harmless or even useful. For an id that acts as a secret, such as a password-reset token, use v4 instead so the id carries no information beyond its randomness.

Binary. Use PostgreSQL's uuid type, MySQL's BINARY(16) or SQL Server's uniqueidentifier. The 36-character text form is more than twice the size, slows comparisons and joins, and bloats every index that references the key. Convert to text only when displaying or exposing the id.

They share the same 128-bit idea: a 48-bit millisecond timestamp followed by random bits. A ULID is written as 26 characters of Crockford base32 with no hyphens and is case-insensitive, which suits URLs and logs. A v7 uses the standard UUID text form and sets the UUID version and variant bits, so it works with every existing UUID column and library. Pick v7 for databases and ULID where the string form matters.