X Xerobit

UUID v4 vs v7 for Databases: The Benchmark You Need

UUID v4 fragments your primary key index. UUID v7 fixes it with millisecond-ordered timestamps. Here's the benchmark data, why it matters, and when to switch.

Mian Ali Khalid · · 8 min read
Use the tool
UUID Generator
Generate UUID v4 and v7 identifiers in bulk.
Open UUID Generator →

UUID v4 generates 122 bits of randomness. That’s its entire feature set. It’s collision-resistant, globally unique, and completely unordered. For decades “use UUID v4 as your primary key” was treated as obvious good practice.

It causes a measurable, documented performance problem in databases at scale, and UUID v7 — finalized in RFC 9562 (May 2024) — fixes it without giving up uniqueness or collision resistance.

The UUID Generator on this site supports both v4 and v7. Here’s which one to reach for and why.

The B-tree problem with random UUIDs

Almost every production database uses B-tree indexes for primary keys: PostgreSQL, MySQL, SQLite, SQL Server. B-trees maintain keys in sorted order in fixed-size pages (typically 8–16 KB).

When you insert a row with a sequential or monotonically increasing key, the new key goes to the rightmost page of the index — the “hot page.” Writes cluster at the end. This is cache-friendly: the hot page stays in memory, and the rest of the tree doesn’t need to be touched.

When you insert a row with a random UUID v4 key, the new key can land anywhere in the index. With 10 million rows and 8 KB pages, there are roughly 50,000+ leaf pages. A new UUID v4 insertion hits a random one. If that page has been evicted from the buffer pool (which it has — you haven’t touched it recently), the database must read it from disk, insert the row, mark the page dirty, and potentially split it if it’s full.

This causes three cascading problems:

  1. Page splits — random inserts fill pages out-of-order, causing frequent splits that fragment the tree
  2. Buffer pool thrash — writes scatter across thousands of pages, none of which stay hot
  3. Write amplification — each insert triggers random reads before the write can happen

At small scale (under ~1 million rows), none of this is noticeable. At 10–100 million rows with moderate write throughput, the effect is real.

Benchmark numbers

Markus Winand (Use The Index, Luke) and several PostgreSQL / MySQL community benchmarks document roughly a 2–3x throughput penalty for random UUID v4 vs sequential integer primary keys on INSERT-heavy workloads. The exact penalty depends on:

  • Table size vs buffer pool size (once the index fits in memory, penalty shrinks)
  • Write/read ratio
  • Storage type (SSDs reduce the random-read penalty)

UUID v7, because it embeds a millisecond timestamp in the high bits, produces monotonically increasing values within a millisecond window. New rows cluster at the end of the index. The fragmentation problem disappears.

What UUID v7 looks like

UUID v7 (RFC 9562) format:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─────────────────────────────────────────────────────────────────┤
│                    unix_ts_ms (48 bits)                        │
├─────────────────────────────────────────────────────────────────┤
│  ver(4) │      rand_a (12 bits)                                │
├─────────────────────────────────────────────────────────────────┤
│  var(2) │      rand_b (62 bits)                                │
└─────────────────────────────────────────────────────────────────┘

The first 48 bits are the Unix timestamp in milliseconds. The remaining 74 bits are random. This means:

  • Values generated within the same millisecond differ only in the random portion — still globally unique
  • Values generated across milliseconds are strictly monotonically ordered
  • The UUID is still globally unique: the 74-bit random portion provides more than enough collision resistance

A v7 UUID looks like 019602e4-8d32-7f4b-8c0a-3b4e9a1f2d56. The timestamp prefix means it sorts in creation order in any lexicographic comparison — including string-typed columns.

Generating UUID v7

UUID v7 is new enough that not all libraries support it natively yet.

// Node — uuid package (v10+)
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();

// Or use the @uuid-rock/uuid7 package
import { uuidv7 } from '@uuid-rock/uuid7';
# Python 3.12+
import uuid
id = uuid.uuid7()

# Python < 3.12 — uuid7 package
from uuid7 import uuid7
id = uuid7()
// Go — google/uuid
import "github.com/google/uuid"
id := uuid.Must(uuid.NewV7())

PostgreSQL doesn’t have a native gen_uuid_v7() function yet, but you can use an extension or generate them in application code.

When to use v4 vs v7

Use v7 when:

  • The UUID is a primary key on a table that gets significant write volume
  • You want lexicographic ordering to reflect creation order (useful for pagination, debugging)
  • You’re on a new project with no legacy constraints

Use v4 when:

  • The UUID is not a primary key (foreign keys, session tokens, correlation IDs)
  • You need opaque, unguessable identifiers where embedding a timestamp is a privacy concern (the timestamp in v7 reveals approximate creation time)
  • You’re maintaining an existing v4 system and the migration cost isn’t worth it

The privacy consideration: UUID v7 embeds the millisecond creation timestamp in the first 48 bits. If you’re using UUIDs as public-facing identifiers (e.g., in URLs), this leaks when the record was created. For user-facing IDs where you want to obscure creation time, v4 is the right choice.

UUID v7 vs ULID vs NanoID

UUID v7ULIDNanoID
StandardRFC 9562Informal specNo RFC
OrderedYes (ms)Yes (ms)No
Collision-resistant74-bit random80-bit randomConfigurable
String form36 chars (hyphenated)26 chars (Crockford base32)21 chars (default)
DB type supportNative UUID type in PG/MySQLStore as varcharStore as varchar
Sortable in UUID type columnYesNo (needs varchar)No

For most database use cases, UUID v7 is the best choice: it has a formal standard, native UUID column type support, and the same ordered-insertion property as ULID without requiring a custom string format.

Migrating from v4 to v7

You cannot change existing primary keys without a full-table migration. The practical approach:

  1. New tables: use v7 from the start
  2. Existing high-write tables: generate a migration plan — add a new v7 column, backfill, swap references, drop old column. Painful but worth it for tables > 10M rows with write-heavy workloads
  3. Low-write tables: v4 is fine to leave; the performance difference is negligible

The UUID Generator generates both v4 and v7 — use it to inspect the format and understand the timestamp prefix.

Further reading


Related posts

Related tool

UUID Generator

Generate UUID v4 and v7 identifiers in bulk.

Written by Mian Ali Khalid. Part of the Encoding & Crypto pillar.