UUID v7生成器
Generate time-ordered UUID v7 for database primary keys
…
格式化
什么是 UUID v7?
UUID v7 是在 RFC 9562(2024 年 5 月)中标准化的下一代 UUID 格式。它在最高有效位中编码 48 位 Unix 毫秒时间戳,随后是版本和变体标记,其余位填充加密安全随机数据。
由于时间戳占据高位,UUID v7 值按时间顺序排序——无论是字典序还是数字序。这使它们非常适合作为数据库主键,而随机 UUID(v4)会导致 B 树索引碎片化。
为什么随机 UUID 会使数据库索引碎片化
B 树索引——被 PostgreSQL、MySQL、SQLite 和大多数其他数据库使用——按键值排序保存行。当你插入新行时,数据库必须将其放置在索引中正确的排序位置。
使用 UUID v4(完全随机),每次新插入都会落在索引树中本质上随机的位置。这迫使数据库不断读取和重写内部索引页,分裂满页并让其他页半空。结果是一个碎片化、膨胀的索引,随着表的增长,读写速度都会降低。
UUID v7 位布局
UUID v7 宽 128 位,分为六个字段:
| 位数 | 字段 | 用途 |
|---|---|---|
| 48 | unix_ts_ms | 48 位 Unix 毫秒时间戳——占据整个高半部分 |
| 4 | ver | 版本号——始终为 0111(十进制 7) |
| 12 | rand_a | 12 位加密安全随机数据 |
| 2 | var | 变体标记——始终为 10(RFC 4122 变体) |
| 62 | rand_b | 62 位加密安全随机数据 |
时间戳精度为 1 毫秒。在同一毫秒内,UUID v7 值按随机后缀排序——不保证亚毫秒级单调递增,但它们是 k-sortable(k 可排序)的:时间上相近生成的 ID 在索引中也会相近排列。
UUID v7 vs UUID v1
UUID v1 和 UUID v7 都嵌入了时间戳,但设计上有显著差异:
| 特性 | UUID v7 | UUID v1 |
|---|---|---|
| 纪元 / 时间基准 | Unix 纪元(1970 年 1 月 1 日) | 格里高利纪元(1582 年 10 月 15 日) |
| 时间精度 | 1 毫秒 | 100 纳秒 |
| 可排序 | 是——按设计 k 可排序 | 否——时间字段在 UUID 布局中被打乱 |
| 隐私 | 不泄露主机信息 | 嵌入生成主机的 MAC 地址 |
| 数据库索引性能 | 极佳——顺序插入,最小碎片化 | 差——尽管有时间戳但非顺序 |
| 标准 | RFC 9562(2024 年) | RFC 4122(2005 年) |
| 原生浏览器支持 | 暂无(无 crypto.randomUUID v7) | 原生不可用 |
对于任何需要时间排序 UUID 的新项目,优先选择 UUID v7 而非 UUID v1。UUID v1 是遗留格式且会泄露主机信息。
UUID v7 vs ULID
ULID(通用唯一字典排序标识符)解决与 UUID v7 类似的问题。以下是两者的对比:
- 遵循 RFC 9562 UUID 标准——与所有 UUID 工具兼容
- 连字符十六进制格式——通用识别
- 原生数据库 UUID 列支持
- 总计 128 位
- Crockford Base32 编码——26 个字符,稍微更紧凑
- 不区分大小写,避免模糊字符(I、L、O、U)
- 一眼看去更易于人类阅读
- 需要库——没有原生平台支持
如果你已经在 UUID 生态系统中(PostgreSQL uuid 列、返回 UUID 的 REST API),请使用 UUID v7。如果你从零开始且更喜欢更友好的人类编码,ULID 是合理的替代方案。
在数据库中使用 UUID v7
大多数数据库尚未原生生成 UUID v7,但它可以存储在标准 UUID 列中,并在应用程序代码或通过扩展生成:
uuid 列中。pg-uuidv7 扩展添加了 uuid_generate_v7() 服务端函数,如果你需要数据库生成的 ID。BINARY(16) 或 CHAR(36) 列中。在应用程序代码中生成。MySQL 8.0+ 通过 UUID_TO_BIN(UUID(), 1) 支持 v1 的有序 UUID,但 v7 需要应用层生成。TEXT(36 个字符)或 BLOB(16 字节)。在应用程序代码中生成。TEXT 上的字典排序正确,因为 UUID v7 使用固定宽度的时间戳前缀。代码示例
UUID v7 尚无法通过 crypto.randomUUID() 获得。在原生支持到来之前,使用 uuidv7(npm)等库:
function generateUuidV7() {
const buf = new Uint8Array(16)
crypto.getRandomValues(buf)
const ms = BigInt(Date.now())
// Embed 48-bit Unix ms timestamp
buf[0] = Number((ms >> 40n) & 0xFFn)
buf[1] = Number((ms >> 32n) & 0xFFn)
buf[2] = Number((ms >> 24n) & 0xFFn)
buf[3] = Number((ms >> 16n) & 0xFFn)
buf[4] = Number((ms >> 8n) & 0xFFn)
buf[5] = Number(ms & 0xFFn)
// Set version 7 (0111xxxx)
buf[6] = (buf[6] & 0x0F) | 0x70
// Set variant (10xxxxxx)
buf[8] = (buf[8] & 0x3F) | 0x80
const hex = [...buf].map(b => b.toString(16).padStart(2, '0')).join('')
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`
}
// Node.js 20+ built-in
// import { randomUUID } from 'node:crypto' // v4 only — no v7 yet in stdlib# pip install uuid7 import uuid_extensions uid = uuid_extensions.uuid7() print(uid) # e.g. 018e2b3d-1a2b-7000-8000-abc123456789 print(uid.time) # Unix ms timestamp embedded in the UUID # Or as a plain string from uuid_extensions import uuid7str print(uuid7str())
-- PostgreSQL 13+ extension-free implementation
CREATE OR REPLACE FUNCTION uuid_generate_v7()
RETURNS uuid
LANGUAGE sql
AS $$
SELECT encode(
set_bit(
set_bit(
overlay(
uuid_send(gen_random_uuid())
PLACING substring(int8send(floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint) FROM 3)
FROM 1 FOR 6
),
52, 1
),
53, 1
),
'hex'
)::uuid;
$$;
-- Usage as a default primary key
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
payload jsonb,
created_at timestamptz DEFAULT now()
);function extractTimestamp(uuid: string): Date {
const hex = uuid.replace(/-/g, '')
const ms = parseInt(hex.slice(0, 12), 16) // first 48 bits = ms timestamp
return new Date(ms)
}
const uid = '018e2b3d-1a2b-7000-8000-abc123456789'
console.log(extractTimestamp(uid).toISOString())
// → "2024-03-15T10:22:05.259Z"