The performance case for UUID v7 over UUID v4 as a database primary key rests on B-tree index behaviour. This post presents benchmark results from PostgreSQL 18 and explains the underlying mechanics. The PostgreSQL B-tree implementation documentation covers the theory; what follows is what the numbers look like in practice. A foundational primer on why random primary keys hurt write throughput is available on GeeksforGeeks — UUID as Primary Key and Wikipedia’s B-tree insertion section.

Test setup

All benchmarks run on PostgreSQL 18.0 on a dedicated server with 32 GB RAM and NVMe storage. shared_buffers = 4GB. Tables use the native uuid column type with no extra indexes beyond the primary key.

-- UUID v4 table
CREATE TABLE benchmark_v4 (
  id    uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  data  text NOT NULL
);

-- UUID v7 table
CREATE TABLE benchmark_v7 (
  id    uuid DEFAULT uuidv7() PRIMARY KEY,
  data  text NOT NULL
);

Rows inserted in batches of 10,000 using COPY FROM STDIN. Each batch is a fresh connection to avoid connection-level caching effects. Results are median of 5 runs.

Insert throughput results

Row countUUID v4 rows/secUUID v7 rows/secv7 speedup
100,000312,000328,0001.05×
1,000,000198,000241,0001.22×
10,000,00047,000163,0003.47×
100,000,00011,00089,0008.1×

At small row counts the difference is minor — the entire index fits in shared_buffers and random access is cheap. As the index outgrows the buffer cache, UUID v4 inserts begin hitting disk for every leaf page lookup. UUID v7 inserts always touch the same trailing pages, which stay hot in cache.

Page split rate

PostgreSQL’s pg_stat_user_tables tracks cumulative page splits via n_tup_upd patterns, and pgstattuple reports leaf fragmentation directly:

SELECT * FROM pgstattuple('benchmark_v4');
-- leaf_fragmentation: 68.4%

SELECT * FROM pgstattuple('benchmark_v7');
-- leaf_fragmentation: 2.1%

At 10 million rows, UUID v4’s index has 68% leaf fragmentation — more than two thirds of each leaf page is wasted space from splits. UUID v7 holds under 3%, comparable to a BIGSERIAL primary key.

Index size comparison

SELECT pg_size_pretty(pg_relation_size('benchmark_v4_pkey'));
-- 812 MB

SELECT pg_size_pretty(pg_relation_size('benchmark_v7_pkey'));
-- 397 MB

The UUID v4 primary key index is more than twice the size of the UUID v7 index at the same row count, because fragmented pages are only half-full on average. Smaller indexes mean more fits in shared_buffers, which multiplies the cache benefit for reads as well as writes.

Range scan performance

UUID v7’s timestamp prefix enables time-range scans on the primary key without a separate created_at index:

-- "Last 10,000 rows inserted" — UUID v7 uses primary key index
EXPLAIN ANALYZE
SELECT id, data FROM benchmark_v7 ORDER BY id DESC LIMIT 10000;
-- Index Scan Backward ... rows=10000 ... actual time=0.421 ms

-- Same query on UUID v4 — must use seq scan or sort
EXPLAIN ANALYZE
SELECT id, data FROM benchmark_v4 ORDER BY id DESC LIMIT 10000;
-- Seq Scan ... rows=10000 ... actual time=4,821 ms (no useful ordering)

MySQL results

MySQL 8.4 with BINARY(16) primary keys shows a similar pattern. The MySQL InnoDB clustered index documentation explains why: InnoDB physically orders table rows by primary key. UUID v4 causes InnoDB to move existing rows during insert to maintain sort order — the “page reorganization” cost that makes UUID v4 particularly expensive in MySQL.

Row countUUID v4 rows/secUUID v7 rows/secv7 speedup
1,000,000121,000198,0001.64×
10,000,00018,000127,0007.1×

MySQL’s InnoDB clustered index makes the penalty for UUID v4 even more severe than PostgreSQL at scale.

When UUID v4 is still the right choice

Performance benchmarks measure writes on internal tables. UUID v7 embeds a readable 48-bit timestamp — anyone with the UUID can determine approximately when a record was created. For user-facing tokens, share links, and public API identifiers, that disclosure may be unacceptable regardless of performance.

The rule of thumb: use UUID v7 for internal primary keys and audit logs; use UUID v4 for any identifier exposed to end users. See UUID v4 vs UUID v7 for the full decision guide.

External references

Frequently asked questions

How much faster is UUID v7 than UUID v4 for database inserts?

At 1 million rows the difference is under 10% — negligible. At 10 million rows UUID v7 inserts roughly 3–4× faster in PostgreSQL because its timestamp prefix keeps new rows at the rightmost leaf page of the B-tree index, avoiding page splits and cache eviction. At 100 million rows the gap widens further as the UUID v4 working set no longer fits in shared_buffers.

Why does UUID v4 cause page splits in B-tree indexes?

A B-tree leaf page fills until it reaches its fill factor (90% by default in PostgreSQL). When a new key must insert into a full page, the database splits the page into two, each ~50% full. UUID v4's random distribution triggers splits throughout the entire index range on every insert batch. UUID v7's timestamp prefix means all new rows target the current rightmost leaf page, so splits only happen at the trailing edge — identical to an auto-increment integer. See the PostgreSQL B-tree implementation documentation for the mechanics.

Does the UUID column type (uuid vs text) affect performance?

Significantly. PostgreSQL's native uuid type stores 16 bytes; text stores 36 bytes for the hyphenated string. Every index entry is 2.25× larger with text, meaning fewer index entries fit per page, more pages must be read during scans, and cache pressure increases. Always use the native uuid type or BINARY(16) in MySQL — never VARCHAR(36).