UUIDv7 in Databases: PostgreSQL, MySQL & Performance Guide
How to use time-sortable UUIDs as primary keys for better database performance
TL;DR
- UUIDv7 as a primary key gives you globally unique, time-sortable IDs with near auto-increment insert performance
- PostgreSQL 17+ supports native generation via
gen_random_uuid_v7() - MySQL storage: use
BINARY(16)for 55% space savings overCHAR(36) - Performance: 2-3x faster inserts than UUIDv4 due to sequential B-tree ordering, with smaller indexes
- Migration: UUIDv7 is a drop-in replacement for UUIDv4 -- same 128-bit format, same column type
- Best for: distributed systems, microservices, event sourcing, and any table using UUID primary keys
Why UUIDv7 for Databases?
Traditional auto-increment integers are simple but break down in distributed systems. Random UUIDv4 solves the distribution problem but creates severe index fragmentation. UUIDv7 combines the best of both worlds -- globally unique IDs with sequential ordering that databases love.
Sequential Inserts
Time-ordered IDs append to the end of B-tree indexes, minimizing page splits
Smaller Indexes
Compact, non-fragmented indexes with better cache utilization
No Coordination
Generate IDs in any service, any region, without a central sequence
Built-in Timestamp
Extract creation time from the ID itself -- no extra column needed
PostgreSQL Implementation
PostgreSQL has native UUID support with a dedicated uuid column type that stores values efficiently as 16 bytes. PostgreSQL 17 added native UUIDv7 generation.
-- Create table with UUIDv7 primary key (PG 17+)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid_v7(),
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Insert without specifying ID
INSERT INTO users (email, name)
VALUES ('[email protected]', 'Alice');
-- IDs are naturally time-ordered
SELECT id, email FROM users ORDER BY id;
-- 01936b2e-1e85-7000-8000-4a6f7e3b2c1d | alice
-- 01936b2e-2f4a-7000-8000-9c3d5e7f1a2b | bob-- For PG versions before 17, generate UUIDv7
-- in your application code and pass it explicitly:
INSERT INTO users (id, email, name)
VALUES ('01936b2e-1e85-7000-8000-4a6f7e3b2c1d',
'[email protected]', 'Alice');
-- Or use a PL/pgSQL function (community recipes)
-- that constructs UUIDv7 from clock_timestamp()-- Extract creation timestamp from UUIDv7
-- Take first 12 hex chars → convert to ms epoch
SELECT
id,
to_timestamp(
('x' || lpad(replace(id::text, '-', ''), 12))
::bit(48)::bigint / 1000.0
) AS created_from_id
FROM users;MySQL Implementation
MySQL doesn't have a native UUID column type. You have two storage options, each with different trade-offs.
| Feature | BINARY(16) | CHAR(36) |
|---|---|---|
| Storage per row | 16 bytes | 36 bytes |
| Index size | Smaller (16 bytes) | Larger (36 bytes) |
| Comparison speed | Fast (binary) | Slower (string) |
| Human-readable | Needs conversion | Readable as-is |
| Recommendation | Production use | Development / debugging |
-- BINARY(16) storage with swap flag for sortability
CREATE TABLE orders (
id BINARY(16) PRIMARY KEY,
customer_id BINARY(16) NOT NULL,
total DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert: convert UUID string to binary
-- swap_flag=1 rearranges bytes for time-ordering
INSERT INTO orders (id, customer_id, total)
VALUES (
UUID_TO_BIN('01936b2e-1e85-7000-8000-4a6f7e3b2c1d', 1),
UUID_TO_BIN('01936b2e-0a12-7000-8000-1b2c3d4e5f6a', 1),
99.99
);
-- Query: convert binary back to readable UUID
SELECT BIN_TO_UUID(id, 1) AS id, total
FROM orders
ORDER BY id;-- CHAR(36) storage — simpler, human-readable
CREATE TABLE events (
id CHAR(36) PRIMARY KEY,
type VARCHAR(50) NOT NULL,
payload JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert: pass UUID string directly
INSERT INTO events (id, type, payload)
VALUES (
'01936b2e-1e85-7000-8000-4a6f7e3b2c1d',
'order.created',
'{"order_id": "12345"}'
);
-- UUIDv7 strings sort correctly as CHAR
-- (timestamp prefix ensures chronological order)
SELECT id, type FROM events ORDER BY id;Performance Benchmarks
The performance advantage of UUIDv7 over UUIDv4 comes from its sequential insertion pattern. Here's how different primary key types compare in typical database workloads.
| Metric | Auto-Increment | UUIDv7 | UUIDv4 |
|---|---|---|---|
| Insert throughput | Fastest | ~95% of auto-inc | ~40-50% of auto-inc |
| Index fragmentation | None | Minimal | High |
| Key size | 4-8 bytes | 16 bytes | 16 bytes |
| Time-sortable | Yes | Yes | No |
| Distributed generation | Requires coordination | No coordination | No coordination |
| Leaks ordering info | Yes (sequential) | Yes (timestamp) | No |
Key takeaway: UUIDv7 delivers insert performance within 5-10% of auto-increment integers while providing globally unique, coordination-free ID generation. The gap between UUIDv7 and UUIDv4 widens significantly as table size grows beyond 1 million rows due to increasing index fragmentation with random inserts.
Migration Strategies
From Auto-Increment to UUIDv7
-- Step 1: Add UUID column
ALTER TABLE users ADD COLUMN uuid UUID;
-- Step 2: Backfill (PG 17+)
UPDATE users SET uuid = gen_random_uuid_v7()
WHERE uuid IS NULL;
-- Step 3: Make it the primary key
ALTER TABLE users DROP CONSTRAINT users_pkey;
ALTER TABLE users ADD PRIMARY KEY (uuid);
ALTER TABLE users ALTER COLUMN uuid
SET DEFAULT gen_random_uuid_v7();From UUIDv4 to UUIDv7
This is the simplest migration -- no schema changes needed. Both formats use the same 128-bit UUID storage.
-- No schema change needed!
-- Just update the DEFAULT to use v7:
-- PostgreSQL 17+
ALTER TABLE users ALTER COLUMN id
SET DEFAULT gen_random_uuid_v7();
-- Or change your application code:
-- Before: uuid.uuid4() / crypto.randomUUID()
-- After: uuid7() / uuidv7()Frequently Asked Questions
Try Our UUIDv7 Tools
Generate UUIDv7 identifiers or extract timestamps from existing UUIDs.