Migrating from UUID v4 to v7 does not require touching existing data. The uuid column type in PostgreSQL, MySQL, and SQLite stores any 128-bit value regardless of version. The migration is: change what generates new IDs, leave the rest alone. This guide covers how to do that safely in a live production system, database by database and language by language.
What you are not doing
You are not:
- Converting existing v4 UUIDs to v7
- Running a table rebuild or data migration
- Changing any column types
- Introducing downtime
You are changing one thing: the source of new UUID values. Existing rows keep their v4 IDs permanently.
Understanding the mixed-table state
After the migration, your table will have a mix of v4 IDs (old rows) and v7 IDs (new rows). This is intentional and correct:
id (uuid column)
--------------------------------------------
550e8400-e29b-41d4-a716-446655440000 ← v4, old row (random position in index)
6ba7b810-9dad-11d1-80b4-00c04fd430c8 ← v4, old row (random position in index)
019236a7-b4f2-7000-8d3e-9c1a2b3d4e5f ← v7, new row (appends to right side)
019236a8-1234-7000-abcd-ef0123456789 ← v7, new row (appends to right side)
The B-tree fragmentation from v4 rows is frozen in place. New inserts stop contributing to fragmentation immediately. Over months as old rows are deleted or archived, the index gradually heals.
PostgreSQL migration
PostgreSQL 18 (recommended)
Change the column default to the new built-in function:
ALTER TABLE users ALTER COLUMN id SET DEFAULT uuidv7();
That is the entire migration. One statement, no lock beyond the brief ALTER, no data movement.
PostgreSQL 13–17
Use gen_random_uuid() as a staging step while you update application code, or install the pgcrypto extension for v4 and generate v7 at the application layer:
-- Option A: generate v7 at application layer, remove DB default
ALTER TABLE users ALTER COLUMN id DROP DEFAULT;
-- Application now always provides the id explicitly
-- Option B: keep pgcrypto v4 default as fallback while migrating app code
-- (already the default in many PostgreSQL setups)
For PostgreSQL 13–17, the cleanest approach is application-layer generation — your application always provides the UUID v7 value and the database never generates it.
MySQL migration
MySQL has no native UUID type and no built-in v7 function. The migration is purely in application code.
If you have a database-level default using MySQL’s UUID() function (which generates v1), remove it and move generation to the application:
-- Remove the v1 database default
ALTER TABLE users ALTER id DROP DEFAULT;
Then update your application to supply UUID v7 values on every insert. With BINARY(16) storage:
-- Application generates v7 string, inserts as binary
INSERT INTO users (id, email) VALUES (UUID_TO_BIN(?), ?);
-- where ? = '019236a7-b4f2-7000-8d3e-9c1a2b3d4e5f'
Application layer migration
Node.js / TypeScript
// Before
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// After
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
For the uuid package, v7 was added in version 9.0.0. If you are on an older version:
npm install uuid@latest
# or
pnpm add uuid@latest
Python
# Before (Python 3.13+)
import uuid
id = str(uuid.uuid4())
# After
id = str(uuid.uuid7())
# Before (Python 3.9–3.12)
import uuid
id = str(uuid.uuid4())
# After
import uuid6
id = str(uuid6.uuid7())
Go
// Before
import "github.com/google/uuid"
id := uuid.New().String() // v4
// After
id, err := uuid.NewV7()
if err != nil {
return err
}
idStr := id.String()
uuid.NewV7() requires github.com/google/uuid v1.6.0+:
go get github.com/google/uuid@latest
ORM-specific changes
Prisma
// Before
model User {
id String @id @default(uuid())
}
// After
model User {
id String @id @default(uuid(7))
}
Run prisma migrate dev to record the schema change. No data migration is generated — the column type and existing data are unchanged.
Drizzle ORM
// Before
import { v4 as uuidv4 } from 'uuid';
id: uuid('id').primaryKey().$defaultFn(() => uuidv4()),
// After
import { v7 as uuidv7 } from 'uuid';
id: uuid('id').primaryKey().$defaultFn(() => uuidv7()),
Django
# Before
import uuid
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# After (Python 3.13+)
id = models.UUIDField(primary_key=True, default=uuid.uuid7, editable=False)
Verifying the migration
After deployment, confirm new rows are getting v7 values by checking the version digit (position 13 in the UUID string — should be 7):
-- PostgreSQL: check version digit of recently inserted rows
SELECT id, substring(id::text, 15, 1) AS version
FROM users
ORDER BY created_at DESC
LIMIT 10;
-- version column should show '7' for new rows
// JavaScript: verify version digit
const uuid = '019236a7-b4f2-7000-8d3e-9c1a2b3d4e5f';
const version = uuid[14]; // index 14 = position 15 (0-indexed), which is the version digit
console.log(version); // '7'
Rollback
If you need to roll back, the reverse is straightforward:
- Application layer: change
uuidv7()back touuidv4()in your generation code - PostgreSQL 18 database default:
ALTER TABLE users ALTER COLUMN id SET DEFAULT gen_random_uuid();
Rows inserted during the v7 period keep their v7 IDs — they remain valid and nothing needs to change. The rollback only affects new inserts going forward.
Further reading
- UUID v4 vs UUID v7 — full comparison of when each version is appropriate
- PostgreSQL 18 Ships Native UUID v7 — the new uuidv7() function
- UUID in Databases — storage types, index behaviour, migration paths
Frequently asked questions
Do I need to convert existing UUID v4 rows to v7 when migrating?
No. The uuid column type stores both v4 and v7 identically as 128-bit values — you never need to touch existing rows. The migration is just changing the source of new IDs: update your application code (or PostgreSQL 18 column default) to produce v7 for new inserts. Existing v4 rows remain valid permanently.
Does PostgreSQL 18 support UUID v7 natively?
Yes. PostgreSQL 18 ships a built-in uuidv7() function that generates RFC 9562-compliant UUID v7 values directly in SQL — no extensions or application-level generation required. The values store in the existing uuid column type unchanged.
Do UUIDs hurt database index performance?
UUID v4 does — its randomness causes every insert to land at a different leaf page, leading to page splits and poor cache locality. UUID v7 embeds a millisecond timestamp so consecutive inserts cluster together, behaving like an auto-increment integer for B-tree purposes.
Do I need to convert existing UUID v4 rows to v7?
No — and you should not. Existing v4 values are valid forever. The uuid column type stores both v4 and v7 identically as 128-bit values. The correct migration is to change the default for new inserts to produce v7 while leaving all existing rows untouched. A mixed table with v4 and v7 rows is perfectly valid.
Will mixed v4 and v7 UUIDs break my indexes?
No. The B-tree index stores all 128-bit values regardless of version. Mixed tables have a transitional period where new v7 inserts go to the right side of the index and old v4 rows stay scattered through it. The fragmentation stops growing the moment you switch to v7 — over time, as old rows are deleted or archived, the index becomes increasingly sequential.