对于 2026 年新建的数据库主键,默认选择 UUID v7;只有当标识符会公开暴露、且其创建时间必须保密时,才考虑 v4。
如果你曾经眼看着一张繁忙表的插入延迟不断攀升,最后追查到根源是主键每次都落在索引的不同角落,那么这个问题你一定不陌生。而修复方案远比一次 schema 迁移轻量得多。两种格式都是同一份 RFC 中标准化的 128 位 UUID,所以选择与兼容性或碰撞安全性无关。真正的差别只在一个属性上:你的 ID 是否按创建顺序排序。v7 的时间有序特性解决了随机 v4 主键在写密集型表上造成的索引碎片问题,代价是每个值中都嵌入了一个可读的时间戳。本文将剖析二者的结构差异、v7 写入性能背后的数据库索引机制、隐私权衡、当前的库支持情况,以及一个明确的默认选项。
核心要点
- UUID v4 和 v7 都是在 RFC 9562(2024 年 5 月)中标准化的 128 位、16 字节标识符;v7 将前导 48 位替换为 Unix 毫秒时间戳,因此其值按创建顺序排序,而 v4 则完全随机。
- 随机的 v4 主键会散布在整棵 B-tree 索引中,引发页分裂、缓存抖动和碎片化;v7 的时间有序前缀使插入操作落在索引末端附近,行为上更接近顺序主键。
- v7 ID 会以毫秒精度泄露自身的创建时间,而 v4 不会。对于公开标识符、邀请链接,或任何必须隐藏增长速度的场景,请使用 v4。
- v7 已获得一流支持:PostgreSQL 18 内置了原生
uuidv7(),Python 3.14 新增了uuid.uuid7(),JavaScript 的uuid包(v14.x)也导出了v7()。 - 任何 UUID 都不是密钥:身份验证请使用专门的 256 位随机令牌,UUID 只用于标识身份。
UUID v4 和 v7 的区别是什么?
UUID v4 和 v7 都是 RFC 9562 中标准化的 128 位、16 字节标识符。该 RFC 于 2024 年 5 月作为标准跟踪(Standards-Track)文档发布,取代了较早的 RFC 4122。二者唯一的结构差异在于这些位从何而来。UUID v4 由 122 位随机数构成,另有 6 位保留给版本和变体标记,因此它完全随机且无序。UUID v7 将前导 48 位替换为以毫秒为单位的 Unix 时间戳,剩余约 74 位填充随机数(再加上版本和变体标记),因此 v7 的值在字典序和字节序上都按创建顺序排序,而 v4 不具备这一特性。
UUID v4: [ 122 random bits ...................... ] + version/variant
UUID v7: [ 48-bit ms timestamp ][ ~74 random bits ] + version/variant
这一处改动就是全部决策依据。二者的碰撞概率数学在随机部分上是一致的,占用同样的列类型,并且都由同一份标准覆盖。
Discover how at OpenReplay.com.
为什么 UUID v7 在数据库主键上更胜一筹?
v7 作为主键优于 v4,是因为它的时间戳前缀为插入操作带来了随机主键所破坏的索引局部性。随机的 v4 主键会落在 B-tree 索引的随机位置,由于每次插入都指向树的不同部分,从而引发页分裂、缓存抖动和碎片化。这正是 RFC 9562 列出的定义 v7 的理由:当标识符不携带时间顺序时,每一行新数据都必须写入其随机值恰好落到的位置;而在时间有序方案下接连生成的值,在索引中最终会成为邻居。v7 的单调递增前缀意味着新行会追加到索引末端附近,页分裂因此变得罕见,插入吞吐量逼近顺序整数主键,同时仍保留 UUID 的碰撞安全性和分布式生成能力。
这一开销在聚簇索引引擎上最为严重。在 MySQL InnoDB 和 SQL Server 中,表在物理上按主键排序,因此随机插入会导致整个结构中各处的页被重写。PostgreSQL 将行存储在堆(heap)中并使用独立索引,对主键随机性不那么敏感,但其索引在随机 v4 主键下仍会失去缓存局部性。影响程度因引擎、工作负载和硬件而异,因此对来源不明的博客基准测试给出的具体百分比应持怀疑态度,请在自己的表上实测。但机制本身并无争议。
时间有序性在突发插入下同样成立。各实现都会加入亚毫秒级计数器,使同一毫秒内生成的 ID 仍能正确排序:Python 的 uuid.uuid7() 预留了 42 位作为计数器,使单个毫秒内生成的值保持顺序;PostgreSQL 18 的 uuidv7() 则由毫秒级 Unix 时间戳、亚毫秒级小数部分和随机位共同构建每个值。
什么时候 UUID v4 仍是正确选择
当 ID 会对外暴露且其创建时间属于敏感信息时,请选择 v4,因为 v7 ID 以毫秒精度嵌入了自身的创建时间。任何能读到该 ID 的人都能读出记录的创建时刻,这使 v7 不适合面向公众的标识符。Aiven 团队也得出了相同结论:一旦主键通过外部应用或 API 交到最终用户手中,v7 就不再是明智之选,因为该标识符会泄露记录的创建时间。对于邀请令牌、分享链接,或任何竞争对手可能通过 ID 区间推断你增长速度的场景,请使用 v4。
有一点告诫对两个版本同样适用:任何 UUID 都不是安全令牌。v7 中的非随机位是可预测的,而 v4 的随机性质量取决于具体实现,因此两者都不应用于把守身份验证。请为密钥生成专门的、至少 256 位的加密安全随机字符串,UUID 仅用于标识身份。
今天就开始生成 v7,并渐进式迁移
v7 的支持在数据库和语言层面已相当广泛,不过具体版本很关键:
| 平台 | v7 生成方式 | 版本 |
|---|---|---|
| PostgreSQL | uuidv7()(原生) | PostgreSQL 18 |
| Python | uuid.uuid7()(标准库) | Python 3.14 |
| JavaScript / Node | uuid 包中的 v7() | uuid v14.x |
PostgreSQL 18 将 uuidv7() 作为原生函数提供,并为已有的 gen_random_uuid() 增加了 uuidv4() 别名;在 PostgreSQL 17 及更早版本上,你需要借助扩展或应用侧库。Python 是在 3.14 中将 uuid.uuid7() 加入标准库的,而不是 3.12——在 3.12 中该调用会抛出 AttributeError。在 JavaScript 中,uuid 包通过 ESM 具名导入暴露 v7;根据其更新日志,该包在 v12 中已放弃对 CommonJS 的支持:
// Node.js, uuid v14.x
import { v7 as uuidv7 } from "uuid";
const id = uuidv7();
-- PostgreSQL 18
SELECT uuidv7();
如果你想在确定列类型之前先看看差异,可以各生成一批并排比较。OpenReplay 的 UUID 生成器可在浏览器中生成 v4 或 v7 值,单次最多 500 个,并提供大写、去连字符和加引号输出等开关,方便你直接粘贴到 SQL 或 JSON 测试数据中。把一列 v7 值排序,它们会按创建顺序排列;对 v4 做同样的操作,结果则是散乱的。这些值来自你自己标签页中的 Web Crypto API,因此不会被发送到任何地方。
迁移无需重写。v4 和 v7 共用同一个 16 字节的 uuid 列类型,因此你可以保留现有的 v4 行,并在同一张表中以 v7 生成新行。版本信息编码在值内部,无需回填。新插入的数据会聚集在索引末端,随着页面逐渐被重写,碎片化会缓慢缓解;这是一个渐进过程,而非即时的碎片整理。
结论:该选哪一个?
新建数据库主键、日志和事件流默认选择 UUID v7;当不可预测性至关重要时选择 v4。v7 让你在获得顺序主键写入性能的同时,保有 UUID 的碰撞安全性和分布式生成能力,而且它现在已在大多数团队正在运行的平台上获得原生支持。将 v4 留给那些公开暴露、且创建时间必须保密的标识符。
还有两个备选方案可以完善这一决策。ULID 用 Crockford base32 编码了同样的”时间戳 + 随机数”思路,得到更短的 26 字符、URL 友好的字符串,但它不是 IETF 标准,也没有原生的 uuid 列类型。对于永远不需要分布式 ID 生成的小型单节点系统,普通的 bigint 自增仍是体积最小、速度最快的选择。
如果你今天正在建一张新表,并且已经因 v4 主键而感受到索引方面的痛苦,那就把新插入切换为 v7,保留旧数据不动,让索引自行沉淀。这一收益几乎不需要任何迁移成本。
常见问题
能否从 UUID v7 值中提取创建时间戳?
可以。由于 UUID v7 在前导位中存储了 48 位 Unix 毫秒时间戳,你可以解码出该值的生成时间。PostgreSQL 18 正是为此提供了 uuid_extract_timestamp(),并将其扩展为支持版本 7 的值。这对调试和时间范围查询是一项特性,但也正是 v7 会泄露创建时间的原因——在时间信息敏感的公开标识符场景中不应使用它。
UUID v7 和 v4 的碰撞风险相同吗?
不相同,但实际差异微不足道。UUID v4 携带 122 位随机数,而 v7 在为时间戳预留 48 位以及版本和变体标记之后,保留约 74 位随机数。v7 的随机位更少,但碰撞只可能发生在同一毫秒内生成的 ID 之间,而各实现都在该时间窗口内加入了单调递增计数器。对于真实工作负载,在任何现实的生成速率下,两者实际上都可视为无碰撞。
UUID v7 能提升读查询性能,还是只对插入有帮助?
v7 主要改善写入侧性能和范围扫描,因为它让行在索引中彼此靠近排列,从而减少页分裂并提升缓存局部性。不要把大幅的读性能提升归因于 v7 本身。PostgreSQL 18 官方宣称的"最高 3 倍"读性能提升来自其新的异步 I/O 子系统,这是一项与 uuidv7() 无关的特性。v7 性能收益的准确范围是索引局部性和插入吞吐量。
我应该把现有的 UUID v4 主键迁移到 v7 吗?
通常不需要完整迁移。v4 和 v7 共用同一个 16 字节 uuid 列类型,且版本编码在值内部,因此你可以让现有 v4 行保持原样,在同一张表中以 v7 生成新行,无需回填。新插入的数据会聚集在索引末端,随着页面被重写,碎片化会逐渐缓解。只有当碎片化已经造成可测量的实际痛点时,重写旧数据才值得考虑。