UUID v4 vs UUID v7: Which Should You Use for Database Primary Keys?

UUID v4 has been the default choice for distributed unique IDs for years. It is simple, collision-safe, and requires no coordination. But at table sizes above a few million rows, UUID v4 has a well-documented performance problem with B-tree indexes — the same randomness that makes it safe causes write amplification that slows inserts measurably.

UUID v7, finalised in RFC 9562 (May 2024), solves this by making the first 48 bits a Unix millisecond timestamp. New rows always sort near the end of the index, eliminating fragmentation — with no change to the UUID column type.

Generate UUID v4 values in your browser:

Open UUID Generator →

Why UUID v4 is slow in database indexes

How B-tree indexes work

Every relational database primary key is backed by a B-tree index. The index stays sorted at all times — each new row is inserted at the position its key belongs to, not appended to the end. When rows arrive in sorted order (like auto-increment integers), every insert appends to the rightmost leaf page. The hot page stays in cache, writes are sequential, and the index stays compact.

What UUID v4 does to that index

UUID v4 values are fully random. A new UUID is equally likely to belong anywhere in the index. This means:

  • Each insert reads a random leaf page from disk (cache miss)
  • If the page is full, it splits into two — doubling future lookup cost for that range
  • Over time the index fills with half-empty pages (index bloat)
  • VACUUM / OPTIMIZE TABLE has to work harder to recover that space

The practical result at scale: UUID v4 inserts are 30–50% slower than auto-increment at 10 million rows, and the gap widens as the table grows. Read performance degrades too because the index is larger and more fragmented than it needs to be.

Small tables do not notice

If your table has fewer than ~500,000 rows and fits entirely in the buffer pool, page splits stay in memory and the cost is negligible. UUID v4 is not a problem until the index exceeds available RAM.

What UUID v7 is and how it fixes the problem

The structure

UUID v7 is a 128-bit value formatted identically to any other UUID (xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx). The internal layout is:

  • Bits 0–47: Unix millisecond timestamp (48 bits)
  • Bits 48–51: Version field (7)
  • Bits 52–63: Random or sequence bits (12 bits)
  • Bits 64–127: Random bits (64 bits, including 2 variant bits)

The timestamp prefix means that UUIDs generated later sort after UUIDs generated earlier. New rows always insert at the end of the index — same behaviour as auto-increment — while the random suffix maintains global uniqueness with no coordination required.

v4 vs v7 side by side

# UUID v4 — fully random, inserts anywhere in the index
f47ac10b-58cc-4372-a567-0e02b2c3d479
3b3a8a5d-9f2e-4b1c-a8d7-1234567890ab
a1b2c3d4-e5f6-4789-abcd-ef0123456789

# UUID v7 — timestamp-prefixed, always inserts near the end
01960a3e-bf15-7c0a-9e12-4a3d9a2f1082
01960a3e-bf16-7f4b-8c23-5b4e0b3f2193  ← later timestamp
01960a3e-bf17-7a8c-ad34-6c5f1c4g3204  ← even later

Collision safety

UUID v7 retains approximately 74 random bits after the timestamp and version fields. The collision probability is comparable to UUID v4 in any realistic scenario — the timestamp prefix does not meaningfully reduce randomness at the scale of a single application.

Three-way comparison: auto-increment vs UUID v4 vs UUID v7

Factor Auto-increment UUID v4 UUID v7
Insert performance (large tables) Best Worst Near-equal to auto-increment
Works in distributed systems No (conflicts) Yes Yes
Sortable by creation time Yes No Yes (millisecond precision)
Reveals row count / sequence Yes No No
Reveals creation time Indirectly No Yes (ms precision)
Column storage 4–8 bytes 16 bytes 16 bytes
Native DB support Universal Universal PostgreSQL 17+, MySQL 8.4+

When to use each

Use UUID v4 when:

  • Your table is small (under ~500k rows) and insert performance is not a concern
  • Creation time must not be inferrable from the ID — e.g. user IDs where exposing account age is a privacy risk
  • You are maintaining an existing codebase where the migration cost exceeds the performance gain

Use UUID v7 when:

  • You have high insert rates or expect to exceed 1M rows
  • You need IDs that are sortable by creation time — event logs, audit tables, message queues, pagination cursors
  • You are building a new system and have no reason to prefer v4
  • You are on PostgreSQL 17+, MySQL 8.4+, or another database with native v7 support

Stay with auto-increment when:

  • Single-node database with no distributed writes or replication conflicts
  • Maximum insert throughput is the top constraint
  • Sequential exposure (row count, creation order) is not a concern

How to generate UUID v7 in code

JavaScript / Node.js (uuid package v9+)

import { v7 as uuidv7 } from 'uuid';

const id = uuidv7();
console.log(id);
// "01960a3e-bf15-7c0a-9e12-4a3d9a2f1082"

Note: crypto.randomUUID() generates v4 only. There is no native browser v7 API yet.

Python

# pip install uuid-utils
import uuid_utils

id = uuid_utils.uuid7()
print(str(id))
# "01960a3e-bf15-7c0a-9e12-4a3d9a2f1082"

Go

// go get github.com/google/uuid
import "github.com/google/uuid"

id := uuid.New()  // v4
id7 := uuid.Must(uuid.NewV7())  // v7 (google/uuid v1.6+)
fmt.Println(id7.String())

PostgreSQL 17+ (native)

-- Generate a UUID v7
SELECT uuidv7();

-- Use as column default
CREATE TABLE events (
  id   uuid DEFAULT uuidv7() PRIMARY KEY,
  name text NOT NULL,
  created_at timestamptz DEFAULT now()
);

PostgreSQL pre-17 (extension workaround)

-- Install pg_uuidv7 extension
CREATE EXTENSION IF NOT EXISTS "pg_uuidv7";
SELECT uuid_generate_v7();

MySQL 8.4+

-- Native UUID v7
SELECT UUID_V7();

-- Store efficiently as BINARY(16)
CREATE TABLE events (
  id   BINARY(16) DEFAULT (UUID_TO_BIN(UUID_V7(), 0)) PRIMARY KEY,
  name VARCHAR(255) NOT NULL
);

Migrating an existing table from UUID v4 to UUID v7

Is the migration worth it?

Run this check first: if your table has fewer than 1M rows or insert rate is under a few hundred per second, the measurable benefit is small. Migrations carry risk — only proceed if the performance gain is justified.

No-downtime migration steps

-- Step 1: add new column with v7 default (does not block reads or writes)
ALTER TABLE orders
  ADD COLUMN id_v7 uuid DEFAULT uuidv7();

-- Step 2: backfill in batches (run during off-peak hours)
UPDATE orders SET id_v7 = uuidv7()
WHERE id_v7 IS NULL
LIMIT 10000;
-- repeat until complete

-- Step 3: switch application code to write/read id_v7

-- Step 4: verify no references remain to old column

-- Step 5: drop old column
ALTER TABLE orders DROP COLUMN id;

What you do not need to change

  • Column type — both v4 and v7 are stored as uuid (PostgreSQL) or BINARY(16) (MySQL)
  • Application code that reads UUIDs — a UUID is a UUID regardless of version
  • Foreign key constraints — references to the column work unchanged
  • Client libraries — any UUID string is valid in any UUID field

UUID v7 vs ULID — what is the difference?

ULID (Universally Unique Lexicographically Sortable Identifier) was created in 2016 with the same core idea: timestamp prefix + random suffix. The differences:

  • Format: ULID uses Crockford Base32 — 26 characters, no hyphens, shorter than UUID's 36-character string
  • Standard: UUID v7 is an IETF RFC. ULID has no formal standard body.
  • DB support: UUID v7 fits natively in uuid columns. ULID needs a TEXT or BINARY column and custom tooling.
  • Ecosystem: UUID v7 works in any library or ORM that already handles UUIDs

Recommendation: prefer UUID v7 for new systems. Choose ULID only if you are already using it or need the shorter string representation for user-visible IDs.

Frequently Asked Questions

Is UUID v7 collision-safe?

Yes. UUID v7 retains approximately 74 random bits. The probability of generating two identical UUID v7 values in the same millisecond is approximately 1 in 2⁷⁴ — negligible in any real-world application.

Does UUID v7 reveal when a record was created?

Yes — the first 48 bits encode a Unix millisecond timestamp. Anyone who can read the UUID can extract the approximate creation time. For most data (events, orders, log entries) this is fine or even useful. For user-facing IDs where creation time is sensitive, consider keeping UUID v4 or a separate opaque ID.

Can I mix UUID v4 and v7 in the same table?

Yes. Both are valid UUID format values. A uuid column accepts either without schema changes. You may see slightly reduced index benefit if the table has a large proportion of v4 rows, but it works correctly.

Does Hibernate / JPA support UUID v7?

Yes, from Hibernate 6.2 onwards. Use @UuidGenerator(style = UuidGenerator.Style.TIME) on your ID field.

Generate UUID v4 values instantly — single or bulk — with the free UUID Generator →