UUID v4 vs v7 vs ULID:如何为数据库选择合适的标识符
你正在启动一个新服务,第一个决定看似简单实则暗藏玄机:主键应该长什么样?自增整数会泄露行数、暴露创建顺序,而且在需要跨库合并数据时就会出问题。UUID 解决了这些问题——但现在你得选择用哪种 UUID。
UUID v4 作为默认选项已经超过十年。UUID v7 于 2024 年 5 月在 RFC 9562 中正式标准化,通过时间排序来提升数据库性能。而 ULID 则是 2016 年由社区推动的替代方案,以更紧凑的格式提供类似的优势。三者各有取舍,影响着性能、隐私和长期可维护性。
本指南将全面对比这三种方案——附带真实基准测试数据、实用的迁移建议,以及 2026 年清晰的选型框架。
什么是 UUID?(以及为什么”GUID”和它是同一回事)
UUID(通用唯一标识符)是一个 128 位的值,无需中央权威机构即可保证唯一性。标准格式为 32 个十六进制字符加连字符:550e8400-e29b-41d4-a716-446655440000。
如果你用过 .NET 或 Windows,你可能见过它被称为 GUID(全局唯一标识符)。它们其实是同一个东西——GUID 是微软对 UUID 标准的叫法。位布局、生成算法和存储要求完全一致。
UUID 经历了多个版本。如今值得关注的有:
| 版本 | 策略 | 标准化 | 是否仍在使用? |
|---|---|---|---|
| v1 | 时间戳 + MAC 地址 | RFC 4122 (2005) | 大部分已被 v7 取代 |
| v4 | 随机 | RFC 4122 (2005) | 是——当前的默认选项 |
| v5 | 基于名称(SHA-1 哈希) | RFC 4122 (2005) | 仅限特定场景 |
| v7 | 时间戳 + 随机 | RFC 9562 (2024) | 是——新的推荐方案 |
RFC 9562 对方向有明确表态:“实现方应尽可能使用 UUIDv7 来替代 UUIDv1 和 UUIDv6。“
UUID v4:随机标准
UUID v4 从 122 位密码学安全随机数生成标识符。这意味着约 5.3 × 10³⁶ 个可能的值——你需要每秒生成十亿个 UUID,持续 86 年,才能达到 50% 的碰撞概率。
工作原理: 总共 128 位,其中 6 位保留给版本号(4)和变体标记。剩余 122 位来自 CSPRNG(如 crypto.getRandomValues())。
示例: f47ac10b-58cc-4372-a567-0e02b2c3d479
简单即是它最大的优势。无需协调、不泄露时间戳、不维护状态。每种主流语言和数据库都内置了 v4 支持。
问题所在:随机 UUID 会导致数据库索引碎片化
纯随机性是有代价的。当你将随机 UUID 插入 B 树索引——PostgreSQL、MySQL 和大多数关系数据库的默认索引类型——新行会落在树的任意位置。这会导致:
- 页分裂: 数据库不断拆分和重组索引页,而不是追加到末尾。
- 写放大: EnterpriseDB 的基准测试显示,随机 UUID 产生的 WAL(预写日志)是顺序方案的 8 倍——同样的负载下 20GB vs 2.5GB。
- 吞吐量骤降: 当数据集超过内存容量时,随机 UUID 插入的吞吐量仅为顺序 UUID 的 20-30%。
- 空间浪费: PlanetScale 报告称,随机 UUID 导致 MySQL 的 B+ 树页利用率降至约 50%,而顺序键可达 94%。
这不是理论问题。Buildkite 在切换到时间排序的 UUID 后,主数据库 WAL 速率降低了 50%。Shopify 在将支付系统幂等键从 UUID v4 改为时间排序标识符后,INSERT 耗时减少了 50%。
对于小表、低频写入,或 UUID 不作为主键的场景,UUID v4 完全够用。但对于高吞吐系统,碎片化带来的开销会随时间不断累积。
UUID v7:时间排序,对数据库友好
UUID v7 通过将时间戳放在最前面来解决碎片化问题。最高 48 位编码了毫秒级 UNIX 时间戳,后面跟着约 74 位密码学安全随机数。
工作原理:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤
| unix_ts_ms (48 bits) |
├─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤
| unix_ts_ms | ver | rand_a (12 bits) |
├─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤
|var| rand_b (62 bits) |
├─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤─┤
由于同一毫秒内生成的 UUID 共享时间戳前缀,它们会在 B 树索引中聚集在一起。新插入的数据追加到树的末端附近,而不是随机分散——这与自增整数高效的原因一样。
示例: 019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91——前 12 个十六进制字符编码了创建时间。
RFC 9562 带来了什么变化
在 RFC 9562 之前,时间排序的 UUID 只存在于非正式提案中。该 RFC 于 2024 年 5 月发布,使 UUID v7 成为具有明确位布局的 IETF 官方标准。它列举了 16 种不同的非标准时间排序 ID 实现——包括 ULID、Twitter 的 Snowflake 和 Instagram 的 ShardId——正是它们推动了 UUID v7 的诞生。
实际影响:库开发者现在有了统一的规范可以遵循,数据库也在添加原生支持。PostgreSQL 18(2025 年底发布)内置了 uuidv7() 函数,不再需要扩展或在应用层生成。
PostgreSQL 18 原生 uuidv7()
PostgreSQL 18 于 2025 年底发布,新增了两个函数:
-- 生成时间排序的 UUID v7
SELECT uuidv7();
-- 结果: 019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91
-- 生成随机 UUID v4(gen_random_uuid() 的别名)
SELECT uuidv4();
PostgreSQL 的实现增加了 12 位亚毫秒时间戳分片,在单个后端进程内提供更好的单调递增排序。这意味着即使是同一毫秒内插入的行也能保持创建顺序——对于高吞吐表非常重要。
对于更早版本的 PostgreSQL,你可以在应用层生成 UUID v7,或使用 pg_idkit 等扩展。
UUID v4 vs v7:正面对比
| 属性 | UUID v4 | UUID v7 |
|---|---|---|
| 格式 | 128 位,36 字符十六进制 | 128 位,36 字符十六进制 |
| 随机位数 | 122 | 约 74 |
| 按创建时间排序 | 否 | 是(毫秒精度) |
| B 树索引性能 | 大规模下较差 | 优秀 |
| 时间戳泄露 | 无 | 有(毫秒精度) |
| RFC 标准 | RFC 4122 / RFC 9562 | RFC 9562(2024 年 5 月) |
| 数据库原生支持 | 所有主流数据库 | PostgreSQL 18+,逐步扩展中 |
| 库支持 | 全面 | 广泛且持续增长中 |
| 存储大小 | 16 字节(二进制) | 16 字节(二进制) |
| 列类型兼容性 | uuid / BINARY(16) | uuid / BINARY(16) ——同一列,完全兼容 |
关键发现:v4 和 v7 列类型完全兼容。它们使用相同的 uuid 数据类型、相同的 16 字节二进制存储、相同的 36 字符字符串表示。你可以在同一列中混合存储两者,这让渐进式迁移变得简单。
自己试试: UUID 生成器——在浏览器中即时生成 UUID v4 标识符,支持批量生成和一键复制。
UUID v7 vs ULID:还需要 ULID 吗?
ULID(通用唯一字典排序标识符)于 2016 年推出,旨在解决 UUID v7 现在同样解决的问题:时间排序的全局唯一标识符。以下是两者的对比:
| 属性 | UUID v7 | ULID |
|---|---|---|
| 编码 | 36 字符十六进制带连字符 | 26 字符 Crockford Base32 |
| 时间戳 | 48 位毫秒级时间戳 | 48 位毫秒级时间戳 |
| 随机位数 | 约 74 | 80 |
| 标准 | IETF RFC 9562 | 社区规范(无 RFC) |
| 单调排序 | 取决于实现 | 规范定义了同毫秒内的递增 |
| 数据库类型 | 原生 uuid 列 | 需要 CHAR(26) 或 BINARY(16) |
| URL 安全 | 否(含连字符) | 是(Base32) |
| 时间戳有效期 | 公元 10889 年 | 公元 10889 年 |
UUID v7 的优势
-
标准化。 UUID v7 拥有 IETF RFC。每个数据库、ORM 和语言运行时都已支持 UUID 类型。而 ULID 在大多数生态中需要自定义解析。
-
数据库原生支持。 ULID 无法直接存入 PostgreSQL 的原生
uuid列,除非转换。你要么以字符串存储(浪费空间),要么转为二进制(丢失 Crockford 编码)。UUID v7 可以直接存入现有的uuid列。 -
生态势头。 Bytebase 预测业界将”逐步放弃定制方案,转向以 UUIDv7 作为大多数场景下的主键”。PostgreSQL 18 原生
uuidv7()加速了这一趋势。
ULID 仍有意义的场景
-
更紧凑的表示。 26 个字符 vs 36 个字符,ULID 在 URL 和 API 中更短。如果字符串长度对你的场景很重要,ULID 更紧凑。
-
略多的随机性。 ULID 提供 80 个随机位,而 UUID v7 约 74 位。实际上两者的碰撞概率都极低,但 ULID 的随机部分稍大一些。
-
已有 ULID 基础设施。 如果你的系统已经在使用 ULID 且迁移成本不低,没有紧迫的理由去切换。两者提供类似的数据库性能,因为它们共享同样的 48 位时间戳前缀。
对于 2026 年的新项目,UUID v7 是更安全的选择。它有标准化支持、数据库原生支持,也不需要你的团队维护 ULID 解析逻辑。
数据库性能:真实基准测试
随机标识符与时间排序标识符之间的性能差异,在生产系统中已有充分记录:
PostgreSQL
| 指标 | UUID v4 | UUID v7 / 顺序方案 |
|---|---|---|
| INSERT 吞吐量(大数据集) | 基准值的 20-30% | 100% 基准值 |
| WAL 数据量 | 约 20 GB | 约 2.5 GB |
| 缓存命中率 | 85% | 99% |
来源:EnterpriseDB 基准测试。数据集超过可用内存后,差距进一步扩大。
MySQL / InnoDB
MySQL 的 InnoDB 引擎使用聚簇索引,主键决定了物理行的存储顺序。随机 UUID 的代价尤其高:
- B+ 树页利用率降至约 50%(顺序方案为 94%)
- UUID 以
CHAR(36)存储时,大小是 32 位整数的 9 倍 - 推荐使用
BINARY(16)存储——虽然是整数的 4 倍,但对大多数负载来说足够紧凑
来源:PlanetScale 分析。
何时继续使用自增主键
UUID 并非万能药。在以下场景中,自增整数仍然是正确的选择:
- 只有单个数据库且无分片计划
- 写入吞吐量比全局唯一性更重要
- 行数不属于敏感信息
- 不需要在数据库外部生成 ID
对于分布式系统、微服务或任何需要在应用层生成 ID 的架构,UUID v7 既能提供顺序键的性能,又能兼顾去中心化生成的灵活性。
如何生成 UUID v7
JavaScript / TypeScript
// Using the 'uuid' package (v10+)
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7(); // '019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91'
Python
# Python 3.x with uuid7 package
from uuid_extensions import uuid7
id = str(uuid7()) # '019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91'
Go
// Using github.com/gofrs/uuid/v5
import "github.com/gofrs/uuid/v5"
id, _ := uuid.NewV7()
fmt.Println(id.String()) // "019078e5-d2c7-7b3a-8f1e-4a6d5c8b2e91"
PostgreSQL 18+
-- Native function, no extension needed
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
customer_id uuid NOT NULL,
created_at timestamptz DEFAULT now()
);
其他语言
UUID v7 库适用于 Rust(uuid crate v1.4+)、Java(uuid-creator)、C#(UUIDNext)和 PHP(symfony/uid)。
从 UUID v4 迁移到 v7
已经在生产环境中使用 UUID v4 了?好消息是:迁移不需要一刀切式的大改造。
v4 和 v7 可以共存
两个版本共享相同的 uuid 列类型和 16 字节二进制表示。你可以为新行生成 v7,同时保留现有的 v4 行不变。查询、连接和索引的工作方式完全一样——数据库并不关心 UUID 是哪个版本。
唯一的行为差异:按主键排序时,新的 v7 行会排在旧的 v4 行之后,因为 v7 的时间戳前缀使其在字典序中靠后。如果你的应用依赖 UUID 排序(使用 v4 时不应该这样做),请验证这个假设。
迁移清单
- 更新 ID 生成方式——将应用层的 UUID 生成切换为 v7。对于 PostgreSQL 18+,将列默认值改为
uuidv7()。 - 更新 ORM——大多数 ORM 将 UUID 生成委托给应用或数据库。更新生成器,而非列类型。
- 监控索引性能——切换后,新的插入将是顺序的。随着时间推移,v7 行占比增加,索引效率会自然提高。
- 不要回填——将现有 v4 值转换为 v7 既不必要又会破坏外键引用。让 v4 和 v7 共存即可。
隐私考量:ID 中的时间戳
UUID v7 以毫秒精度编码创建时间。任何能看到 UUID 的人都可以提取它的创建时间。对于内部数据库键,这通常不是问题。但需要注意以下场景:
- 面向公众的 API——如果你的 API 暴露了实体 ID,客户端可以判断资源的创建时间。这可能泄露业务模式(订单量、用户增长率)。
- 会话令牌和 API 密钥——对于密钥,请优先使用 UUID v4。v7 中的时间戳对令牌毫无价值,还会不必要地暴露时间信息。
- 法规合规——根据你所在的司法管辖区,嵌入标识符中的创建时间戳如果能关联到用户,可能构成个人数据。
务实的做法:数据库主键(内部使用)用 UUID v7,对外可见的令牌和 API 密钥用 UUID v4。
自己试试: UUID 生成器——需要一批用于测试的 UUID?一次最多生成 25 个,支持单独复制或下载为文本文件。
常见问题
两个 UUID 会重复吗?
理论上会,但实际上不会。UUID v4 的 122 个随机位提供了 5.3 × 10³⁶ 的密钥空间。你需要每秒生成十亿个 UUID,持续 86 年才能达到 50% 的碰撞概率。UUID v7 的随机位数更少(约 74 位),但同一毫秒内碰撞的概率仍然微乎其微。
UUID 应该存为字符串还是二进制?
二进制。UUID 以 CHAR(36) 存储占 36 字节;以 BINARY(16) 存储仅占 16 字节——不到一半的存储空间。PostgreSQL 的原生 uuid 类型自动以 16 字节二进制存储。在 MySQL 中,建议使用 BINARY(16) 并在应用层转换。PlanetScale 指出 CHAR(36) 是 32 位整数的 9 倍大,因此大表必须使用二进制存储。
UUID v7 和 Twitter Snowflake 相比如何?
两者都是时间排序的分布式标识符,但服务于不同需求。Snowflake ID 是 64 位的(更小、比较更快),但需要中央协调服务来分配 worker ID。UUID v7 是 128 位的,无需协调——任何节点都可以独立生成。RFC 9562 明确将 Snowflake 列为推动 UUID v7 诞生的 16 种非标准实现之一。
PostgreSQL 18 之前如何生成 UUID v7?
在原生支持之前,你有几个选择:使用 pg_idkit 扩展、在应用层生成(在你的应用代码中生成并传递给数据库),或编写 PL/pgSQL 函数手动拼接时间戳和随机字节。如果基础设施允许,升级到 PostgreSQL 18 是最干净的方案。
UUID v7 适合用作 API 密钥吗?
不适合——密钥请使用 UUID v4。UUID v7 的时间戳会暴露密钥的创建时间,这在安全场景中是不必要的信息。对于 API 密钥和会话令牌,你需要最大化的熵且不包含元数据。UUID v4 来自 CSPRNG 的 122 位纯随机数是合适的,不过专用的令牌格式可能提供额外功能如内置过期机制。
该选哪个?决策框架
选择 UUID v4 当:
- 你需要最大化随机性(令牌、API 密钥、会话 ID)
- 创建时间必须保密
- 你在小表、低写入量的场景添加 UUID,索引碎片化不是问题
选择 UUID v7 当:
- UUID 用作数据库主键
- 你需要按时间排序而不想额外加
created_at列 - 大规模写入吞吐量很重要
- 你想获得顺序键的优势,同时不依赖中央权威
选择 ULID 当:
- 你需要更短的字符串表示(26 字符 vs 36 字符)
- 你的系统已有 ULID 基础设施
- 你需要无需编码即可在 URL 中安全使用的标识符
对于 2026 年的大多数新项目,UUID v7 是默认推荐。它结合了 UUID v4 的去中心化生成与顺序键的数据库性能,有 IETF 标准支撑,数据库原生支持也在不断增长。向 v7 的收敛已经开始——PostgreSQL 18 内置的 uuidv7() 函数就是最明确的信号。
无论你选择哪种方案,都可以使用 UUID 生成器 即时生成 UUID v4 标识符,或使用 JSON 格式化工具 格式化包含 UUID 的 API 响应。