UUID v4 vs v7 — which to use for database primary keys
On this page
You’ve already picked a UUID as the primary key. The remaining choice is v4 vs v7 — and it is not about uniqueness.
Both are 128-bit identifiers. Both are collision-safe for any workload you will actually run. What differs is where each new key lands in a B-tree. Random v4 keys scatter inserts across the index. Time-ordered v7 keys — standardized in RFC 9562 (May 2024) — append near the right edge, the way a bigint identity does. Collision math is a distraction. Timestamp leakage vs index locality is the real trade-off.
This page is the v4-vs-v7 primary-key comparison. For UUID vs auto-increment, see UUID as primary key. To mint a v7 in the browser, use the UUID v7 generator.
What actually differs in the bits (random vs time-ordered)
Same size. Different prefix.
UUID v4 fills about 122 bits from a CSPRNG and stamps the version/variant nibbles. There is no clock and no sequence. Two IDs generated a millisecond apart are unrelated. Lexicographic sort is random.
UUID v7 puts a 48-bit Unix millisecond timestamp in the most-significant bits, then version 7, then about 74 random bits (including the variant). New IDs share a time prefix. Lexicographic sort matches chronological sort. Same-millisecond uniqueness comes from the random tail — some libraries add a monotonic counter.
v4: [ 122 random bits | version | variant ]
v7: [ 48-bit Unix ms | ver | rand | var | rand ]
That 48-bit prefix is why a time-ordered UUID in PostgreSQL inserts like a sequence. It is also why anyone who sees a v7 can recover the creation millisecond. You cannot have sequential index locality without encoding something ordered — usually time.
Storage is the same 16 bytes if you use a native type:
-- PostgreSQL
id uuid PRIMARY KEY
-- MySQL / InnoDB
id BINARY(16) PRIMARY KEY
-- SQL Server
id uniqueidentifier PRIMARY KEY
Do not store either version as VARCHAR(36) or CHAR(36). You pay roughly 2× the bytes, lose type checks, and widen every index. The hyphenated string is a display format, not a column type.
Collision risk is not the deciding factor. v4’s 122 random bits and v7’s 74 random bits per millisecond are both far past the birthday-paradox threshold of any real table. Choosing v4 “to be safer” optimizes the wrong variable. The collision math will not pick your primary key.
Why B-tree indexes hate random inserts
Postgres, InnoDB, and SQL Server all keep a primary-key index in a B-tree (or B+tree). Inserts walk the tree to the leaf that owns that key.
Sequential keys — identity integers, and v7 values generated around “now” — always land on the rightmost leaf. That page stays in cache. A full leaf splits once and the new right page becomes the hotspot. WAL / redo stays compact. For a primary key, that insert hotspot is a good hotspot.
Random v4 keys land on a random leaf. On a large table that leaf is usually cold. The engine:
- Reads a random index page
- Inserts into the middle of a packed leaf
- Splits the page when it overflows
- Dirties two pages instead of one
- Writes extra WAL
- Leaves both pages half-full — index bloat
That is write amplification: one logical row insert becomes scattered reads, extra page splits, and more dirty pages flushed. The cache working set becomes “the whole index” instead of “the right edge.”
On a clustered index (InnoDB, SQL Server’s default) the table itself is stored in primary-key order. Random PK inserts physically scatter rows. Secondary indexes still point at the PK, so the pain shows up on every write.
Typical shape on a large PostgreSQL table — order of magnitude, not a lab claim:
| Primary key | Insert pattern | Relative insert rate | Index bloat |
|---|---|---|---|
bigint identity | rightmost leaf | 100% (baseline) | low |
| UUID v7 | rightmost leaf | ~90% | low |
| UUID v4 | random leaf | ~30–50% | high |
The old advice — “don’t use UUIDs as primary keys” — was about this v4 pattern. It was right. It does not apply to v7.
PostgreSQL BRIN indexes only help if physical order matches the key — v7 can use them, v4 cannot. For uuidv7() vs gen_random_uuid(), see the PostgreSQL UUID guide.
When v7 is the better default (new tables, write-heavy PKs)
Use v7 when:
- The column is a new table’s primary key and you want UUID benefits (no sequence coordination, mergeable IDs) without v4’s random-insert tax.
- The table is write-heavy — orders, events, messages, anything that inserts more than it updates the PK.
- You are on PostgreSQL and can call
uuidv7()(Postgres 18+) or generate v7 in the app and insert into a nativeuuidcolumn. A time-ordered UUID is the boring default for new UUID PKs. - You have many writers (services, offline clients) and do not want a central sequence.
- You are fine with the ID encoding when it was created. Most rows already have a
created_at.
You do not need to rewrite existing v4 rows. Change the default — or the app generator — and let new keys be v7. Mixed versions in one uuid column are valid; the version nibble is in the value. Skip v6 unless you are migrating off v1 — RFC 9562 includes both; v7 is the modern layout. See UUID versions.
When to keep v4 (public/opaque IDs, creation-time privacy)
Keep v4 when opacity matters more than insert locality.
- Public or shareable IDs where creation time must stay hidden. A v7 in a URL is a clock — volume and recency leak from a handful of IDs.
- Unpredictable public tokens (invite links, file shares) where you do not want “generated at 14:03:12” in the identifier. For session secrets, use a longer random token — not a UUID of either version.
- Hash-sharded or randomly partitioned stores where a time prefix would create a hot partition. v4’s scatter is a feature there.
- Existing v4 primary keys that are not on fire. Migrating a live PK rewrites every foreign key. Switching new tables is cheap; rewriting old ones is not.
A common split: v7 as the PK, plus a separate opaque public ID if the URL must not leak time. Two columns, two indexes — only when both constraints are real.
UUID v7 vs ULID
ULID solves the same time-ordered problem: a 48-bit millisecond prefix plus random bits, encoded as Crockford base32 (26 characters, no hyphens). Fine for string-native stores.
For a SQL primary key, prefer UUID v7. You get a native type, ORMs already know UUIDs, and switching from v4 is a generator change, not a column-type change. ULID’s extra random bits do not change the collision story or B-tree behavior — both designs share the time-prefix insert pattern.
How to generate either in the browser
Generate both versions on this site. The work stays in your browser — nothing is uploaded, there is no account, and the ID never hits our servers.
- UUID / GUID generator — defaults to v4; switch the version dropdown to v7
- UUID v7 generator — same tool, already set to time-ordered
- Validator / decoder — paste a value, read the version and (for v7) the timestamp
Choosing a primary key today: generate a v7, store it as a native UUID, and keep v4 where the clock in the ID would be a problem.