UUID v4 vs UUID v7: Which Should You Use for Database Keys?
UUIDs solve a real problem: any client can mint a globally unique identifier without asking a central database for the next integer. Version 4 has been the default for two decades because it is pure randomness and trivial to generate. Version 7, standardised in RFC 9562 in May 2024, keeps the same 128-bit shape but leads with a timestamp, and that one change fixes the main operational complaint about v4: random keys make B-tree indexes slow and large. This article covers the layout, the index behaviour, the collision arithmetic, and how to generate and store both versions.
In short
UUID v7, standardized in RFC 9562, should be preferred for database primary keys: its leading 48-bit timestamp keeps values roughly sortable, so B-tree inserts append at the right edge instead of scattering across random pages. UUID v4 is still fine for values that must reveal nothing about when they were created, such as public API tokens.
What the 36 characters encode: version and variant bits
A UUID is 128 bits, written as 32 hexadecimal digits in an 8-4-4-4-12 grouping. Two small fields are fixed by the specification. The first hex digit of the third group is the version: 4 for random, 7 for time-ordered, 1 for the original timestamp-plus-MAC-address form. The first hex digit of the fourth group encodes the variant; RFC-compliant UUIDs set its top two bits to 10, so that digit is always 8, 9, a or b.
Every other bit belongs to the version's own layout. That is why 'a v4 UUID has 122 bits of randomness' is the accurate description (128 minus 4 version bits minus 2 variant bits), and why the version of any UUID can be read at character 15 of the string, immediately after the second hyphen.
v7: 01a0893e-3c06-71f7-8552-ae98c33654b2
|------------| | | | |
48-bit Unix ms timestamp (big-endian)
| | |
version nibble = 7
| |
12 bits rand_a (random, or a sub-ms counter)
|
variant nibble: 8, 9, a or b (top bits 10)
remaining 62 bits: rand_b (random)
v4: 7741efb4-e0e2-46a0-af93-7c597611caca
^ ^
version = 4 variant — all 122 other bits are randomv1, v4 and v7 side by side
v1 packs a 60-bit timestamp counted in 100-nanosecond ticks since 15 October 1582, a 14-bit clock sequence and a 48-bit node identifier that was originally the machine's MAC address. It sorts badly because the low bits of the timestamp come first, and it reveals which host generated it. MySQL's UUID() function still returns v1.
v4 is 122 random bits from a cryptographically secure generator. It is stateless, needs no clock and no coordination, and carries no structure an attacker or an index can exploit. v7 replaces the first 48 bits with a big-endian Unix timestamp in milliseconds (enough to last until the year 10889), then the version nibble, 12 bits the RFC calls rand_a (random by default, or a counter for ordering within a millisecond), the variant bits and 62 random bits. Two v7 values generated a millisecond apart sort in generation order as strings, as bytes and as 128-bit integers. RFC 9562 also defines v6 (v1's fields reordered so they sort) and v8 (application-defined layouts), but v7 is the one new systems adopt.
| v1 | v4 | v7 | |
|---|---|---|---|
| Timestamp | 60-bit, 100 ns ticks | None | 48-bit Unix ms, big-endian |
| Random bits | 14-bit clock sequence | 122 | 74 (12 rand_a + 62 rand_b) |
| Node ID | 48-bit MAC or random | None | None |
| Sortable | No (low bits first) | No | Yes, by creation time |
| Reveals | Host + creation time | Nothing | Creation time (to the ms) |
v1 v4 v7
timestamp 60-bit, 100 ns ticks none 48-bit Unix milliseconds, big-endian
random bits 14-bit clock sequence 122 74 (12 rand_a + 62 rand_b)
node id 48-bit MAC or random none none
sortable no (low bits first) no yes, by creation time
reveals host + creation time nothing creation time (to the millisecond)
generate uuid.v1() crypto.randomUUID() uuid.v7() / 15 lines of codeWhy v4 primary keys fragment a B-tree, and how v7 fixes it
A B-tree stores keys in sorted order across fixed-size pages: 8 KB in Postgres, 16 KB in InnoDB. Keys that arrive in increasing order are appended to the rightmost leaf; that page stays in cache, fills completely, then splits once. Uniformly random keys land on a random leaf every time. Each insert touches a page that is probably not cached, and when the page is full it splits in the middle, leaving two half-empty pages. Random insertion leaves B-tree pages about 70% full on average (the classic ln 2 result), against 90% or better for append-only inserts, so the index is roughly a third larger, the working set is the whole index rather than its right edge, and every batch of inserts dirties far more pages.
The cost is larger in InnoDB, where the table itself is the primary-key B-tree, so random keys scatter the rows as well as the index entries. In Postgres, the first modification of any page after a checkpoint writes the entire page to WAL, so random keys also inflate WAL volume. v7 keys are increasing at millisecond granularity, which restores the append-at-the-right-edge pattern: new rows cluster together on disk and in cache, a scan over recent rows reads consecutive pages, and splits go back to being rare. You keep v4's decentralised generation; what you give up is the pretence that the key carries no information.
-- Postgres 18: both generators are built in
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7(), -- gen_random_uuid() for v4
kind text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- With v7 the primary-key index already orders rows by creation time,
-- so "latest 100 events" needs no separate created_at index:
SELECT id, kind FROM events ORDER BY id DESC LIMIT 100;Collision probability, with the arithmetic
The birthday bound gives the probability that at least one pair collides among n values drawn uniformly from N possibilities: p ≈ n² / 2N. For v4, N = 2^122 ≈ 5.3 × 10^36. Generating one billion v4 UUIDs gives p ≈ (10^9)² / (2 × 5.3 × 10^36) ≈ 9.4 × 10^-20; a trillion gives 9.4 × 10^-14. Reaching a 50% chance of a single collision takes about 2.7 × 10^18 UUIDs, which is a billion per second for 86 years. This is why applications never check v4 keys for uniqueness before inserting: the database's unique constraint is a formality.
v7 has 74 random bits, but they only need to be unique among UUIDs created in the same millisecond, because the timestamp separates everything else. With N = 2^74 ≈ 1.9 × 10^22, a million UUIDs minted inside one millisecond collide with probability 2.6 × 10^-11. A fleet that sustained that rate around the clock (a billion per second) would expect its first collision after roughly 1.2 years. Nobody generates at that rate, and a node that needs strict ordering can put a counter in rand_a, which removes same-node collisions entirely. In practice the real collision risk for either version is a bad random source (a forked process that duplicated its PRNG state, or Math.random), never the arithmetic.
// birthday bound: p ≈ n² / (2 · 2^bits)
const p = (n, bits) => (n * n) / (2 * 2 ** bits);
p(1e9, 122) // 9.40e-20 one billion v4 UUIDs
p(1e12, 122) // 9.40e-14 one trillion v4 UUIDs
Math.sqrt(2 * 2 ** 122 * Math.LN2) // 2.72e18 number of v4 UUIDs for a 50% chance
p(1e6, 74) // 2.65e-11 a million v7 UUIDs inside one millisecondGenerating UUIDs in JavaScript: v4 built in, v7 in fifteen lines
Our UUID generator produces both versions in bulk, but crypto.randomUUID() returns a v4 UUID directly in code. It has been in Node's crypto module since 14.17, is a global in Node 19 and later, and is available in every current browser in a secure context (https or localhost). There is no built-in v7 yet; the uuid package on npm added v7() in version 10, and the implementation is short enough to write yourself. The function below fills 16 bytes from the CSPRNG, overwrites the first six with the big-endian millisecond timestamp, and forces the version and variant nibbles. The values shown are its actual output, and the timestamp parses back to the moment it ran.
Two details matter in production. Use crypto.randomBytes or crypto.getRandomValues, never Math.random, which is neither uniform enough nor unpredictable. And if you need strict ordering for UUIDs generated inside the same millisecond on one node, fill the 12 rand_a bits with a counter (RFC 9562 describes two methods) instead of random data; the version below accepts a small chance that two same-millisecond values sort out of generation order.
const { randomBytes, randomUUID } = require("node:crypto");
randomUUID(); // v4 → "7741efb4-e0e2-46a0-af93-7c597611caca"
function uuidv7() {
const b = randomBytes(16); // in the browser: crypto.getRandomValues(new Uint8Array(16))
const ts = BigInt(Date.now()); // 48-bit millisecond timestamp
for (let i = 0; i < 6; i++) b[i] = Number((ts >> BigInt(8 * (5 - i))) & 0xffn);
b[6] = (b[6] & 0x0f) | 0x70; // version 7
b[8] = (b[8] & 0x3f) | 0x80; // variant 10xx
const h = b.toString("hex");
return h.slice(0, 8) + "-" + h.slice(8, 12) + "-" + h.slice(12, 16) + "-" + h.slice(16, 20) + "-" + h.slice(20);
}
const a = uuidv7(), c = uuidv7();
a // "01a0893e-3c06-71f7-8552-ae98c33654b2"
c // "01a0893e-3c06-7ada-b536-f18275839206"
a < c // true — later generation sorts later
new Date(parseInt(a.replace(/-/g, "").slice(0, 12), 16)).toISOString()
// "2026-09-10T02:55:52.326Z" — the embedded creation timeStoring UUIDs in Postgres and MySQL
Postgres has a native uuid type: 16 bytes, compared as bytes, indexed efficiently. Use it. Storing UUIDs as text or varchar(36) costs 37 bytes per value, compares slower, and lets malformed values in. gen_random_uuid() (v4) has been built in since Postgres 13 and uuidv7() since Postgres 18; on older versions, generate v7 in the application or install the pg_uuidv7 extension. Because Postgres tables are heaps rather than clustered on the primary key, the locality argument applies to the index rather than the rows, which is still a large win for insert throughput and index size.
MySQL, and MariaDB before 10.7, have no UUID type. Store them in BINARY(16) and convert at the boundary with UUID_TO_BIN() and BIN_TO_UUID(). The optional swap flag, UUID_TO_BIN(uuid, 1), reorders the time fields of a v1 UUID so that it sorts chronologically; do not use it with v7, whose bytes are already in time order, because the swap would scramble them. InnoDB clusters the table on the primary key, so on MySQL the v4-versus-v7 choice affects row placement as well, and the insert-performance gap is correspondingly larger.
-- MySQL 8: 16-byte binary column, convert at the edges
CREATE TABLE orders (
id BINARY(16) PRIMARY KEY,
total_cents INT NOT NULL
);
INSERT INTO orders (id, total_cents)
VALUES (UUID_TO_BIN('01a0893e-3c06-71f7-8552-ae98c33654b2'), 4200);
SELECT BIN_TO_UUID(id) AS id, total_cents
FROM orders ORDER BY id DESC LIMIT 10; -- newest first, straight from the PK
-- UUID_TO_BIN(x, 1) rearranges v1 time fields; leave the flag off for v4 and v7When not to expose a UUID
A v7 UUID tells anyone who sees it when the row was created, to the millisecond. In a URL that is usually harmless (an order placed at 14:02:17 is not a secret), but it can matter: a 'private' document whose ID shows it was created minutes after a particular meeting, or a medical record whose creation time is itself personal data. v1 is worse, exposing the MAC address or a stable node identifier of the generating machine. v4 exposes nothing.
Time-ordered IDs also leak a little about volume. Two public IDs a day apart do not reveal how many rows sit between them (the random bits hide that), but a batch created in the same millisecond is visibly a batch. When any of this matters, keep v7 as the internal primary key and expose a separate v4 or opaque token in URLs and APIs. Whatever the version, never let possession of an ID stand in for authorisation: even unguessable v4 IDs end up in logs, referrer headers and screenshots.
Sources
Frequently asked questions
Is UUID v7 unique across multiple servers without coordination?
Yes. Each value carries 74 random bits alongside the timestamp, so two servers generating in the same millisecond collide with probability n² / 2^75 for n values in that millisecond. At a million per millisecond fleet-wide that is about 2.6 × 10^-11, and no coordination or node ID is needed.
Should I just use auto-increment integers instead?
Integers are 8 bytes rather than 16, a little faster to compare, and human-readable. They also expose row counts and are guessable, require a round trip to the database before a client knows the key, and are painful to merge across shards or regions. If you need client-side generation or plan to merge data, UUID v7 gives most of the index behaviour of an integer without those problems.
Can I sort v4 UUIDs by creation time?
No. A v4 UUID contains no timestamp, so ordering by id is random. Keep a created_at column and index it if you need chronological queries, or switch new rows to v7, where the id itself orders correctly.
How is UUID v7 different from ULID?
Both put a 48-bit millisecond timestamp first. ULID follows it with 80 random bits and is written as 26 Crockford base32 characters; v7 uses the standard UUID hex layout with 74 random bits and version/variant markers, so it fits any existing uuid column, driver and validator. v7 is the standardised choice; ULID remains useful where a shorter, case-insensitive string is preferred.
How do I tell which version a UUID is?
Look at the character immediately after the second hyphen (position 15 in the string). It is the version digit: 1, 4 or 7 for the versions discussed here. The character after the third hyphen should be 8, 9, a or b for any RFC-compliant UUID.
Generate v4 or v7 UUIDs in bulk, in the case and format you need, and copy them straight into fixtures or SQL.
Generate UUIDsKeep working
Related tools
UUID Generator
Random v4 or sortable, time-ordered v7 — generated in bulk with your browser's secure random source, formatted exactly how your codebase wants them.
Hash Generator
Every common digest at once — type text or drop a file and read MD5 through SHA-512 side by side, with HMAC mode and one-paste checksum verification.
Epoch Converter
Paste a timestamp in seconds or milliseconds — or a date string — and get every form of the same instant at once, without ever consulting the clock.