避免 UUIDv4 主键
Source: Hacker News
Introduction
在过去的十年里,当使用 UUID Version 41 作为主键数据类型时,这些数据库通常表现不佳且 I/O 过大。
UUID 是 PostgreSQL 的原生数据类型,可以以二进制形式存储。RFC 中定义了多种版本。Version 4 大部分位是随机的,能够隐藏诸如创建时间或生成位置等信息。
自 PostgreSQL 13(2020 年发布)起,使用 gen_random_uuid()2 函数可以轻松生成 Version 4 UUID。
我发现围绕 UUID Version 4 存在一些误解,而这些误解有时是用户选择该数据类型的原因。
鉴于性能不佳、误解以及可用的替代方案,我的立场很简单:避免在主键中使用 UUID Version 4。
我更具争议的观点是整体上避免使用 UUID,但我也理解在没有实际替代方案的情况下,它们仍有一些合理的使用场景。
作为一名数据库爱好者,我想对这场经典的 “整数 vs. UUID” 争论给出一个明确的立场。
在数据库从业者之间,这个争论可能已经显得疲惫且陈词滥调。然而,从我的咨询工作来看,我仍然看到在 2024 年和 2025 年仍在使用 UUID v4 的数据库,而本文讨论的问题仍然相关。
让我们深入探讨。
UUID context for this post
- UUID(在 Microsoft 术语中称为 GUID)3 是长度为 36 个字符(32 位数字和 4 个连字符)的长字符串,使用 PostgreSQL 中的二进制
uuid数据类型存储为 128 位(16 字节)值。 - RFC 文档说明了这 128 位是如何设置的。
- UUID Version 4 的位大多是随机值。
- UUID Version 7 在前 48 位中包含时间戳,与随机值相比,在数据库索引中的表现要好得多。
虽然截至撰写时尚未正式发布,并且曾在 PostgreSQL 17 中被移除,UUID v7 已被纳入计划在 2025 年秋季发布的 PostgreSQL 184。
Scope of web app usage and their scale
本文所讨论的 Web 应用范围是使用 PostgreSQL 作为主要 OLTP 数据库的单体 Web 应用。典型类别包括社交媒体、电子商务、点击跟踪以及业务流程自动化。
讨论的性能问题与低效的存储和检索有关,因此适用于上述所有类别。
Randomness is the issue
UUID Version 4 的核心问题在于,122 位是 “随机或伪随机生成的值”1,而主键又依赖索引,这会影响插入以及从索引中检索单个项目或值范围的性能。
随机值不像整数那样具有自然排序,也不像字符字符串那样具有字典序排序。UUID v4 确实有 “字节顺序”,但这对它们的访问方式没有任何有用的意义。
Why choose UUIDs at all? Generating values from one or more client applications
使用 UUID 的一种场景是需要在客户端或多个服务中生成标识符,然后将其传递给 PostgreSQL 持久化。
在微服务架构中,每个服务都有自己的数据库,能够在不产生冲突的情况下生成标识符是 UUID 的一个用例。UUID 还能在以后标识来源数据库,而这点普通整数做不到。
为了避免冲突(参见 HN 讨论5),我们实际上无法用基于序列的整数提供同样的保证。虽然有一些技巧——例如在两个实例上使用奇偶整数,或在 int8 空间内分配不同的范围——但这些都会增加复杂度。
复合主键(CPK)等替代标识符也存在,但一对值仍可能无法在跨服务的情况下唯一标识特定行。
维基百科对冲突概率的描述如下:
为了达到 50 % 的冲突概率,需要生成的随机 version‑4 UUID 数量为:2.71 quintillion
以每秒 10 亿个 UUID 的速度生成,大约需要 86 年。
Misconceptions: UUIDs are secure
一种误解是认为 UUID 是安全的。RFC 明确指出它们不应被视为安全的 “能力”。
RFC 4122, §6 – Security Considerations
不要假设 UUID 难以猜测;它们不应作为安全能力使用。
Creating obfuscated values using integers
虽然 UUID v4 能够模糊其创建时间,但这些值无法排序以判断它们相对的创建先后。类似的属性可以通过整数并稍作处理来实现。
一种做法是从整数生成伪随机码,然后在对外暴露该值的同时仍在内部使用整数。
完整实现细节见 Short alphanumeric pseudo‑random identifiers in PostgreSQL6。简要步骤如下:
- 将十进制整数(例如
2)转换为二进制位(例如一个 4‑字节、32‑位整数:00000000 00000000 00000000 00000010)。 - 使用密钥对所有位执行异或(XOR)操作。
- 使用 Base62 字母表对得到的位进行编码。
模糊后的 ID 存储在生成列中。插入顺序的值可能是 01Y9I、01Y9L,随后是 01Y9K;按字母顺序,后两个会被颠倒(先是 01Y9I,然后是 01Y9K、01Y9L)。