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 v4UUID v7
格式128 位,36 字符十六进制128 位,36 字符十六进制
随机位数122约 74
按创建时间排序是(毫秒精度)
B 树索引性能大规模下较差优秀
时间戳泄露有(毫秒精度)
RFC 标准RFC 4122 / RFC 9562RFC 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 v7ULID
编码36 字符十六进制带连字符26 字符 Crockford Base32
时间戳48 位毫秒级时间戳48 位毫秒级时间戳
随机位数约 7480
标准IETF RFC 9562社区规范(无 RFC)
单调排序取决于实现规范定义了同毫秒内的递增
数据库类型原生 uuid需要 CHAR(26)BINARY(16)
URL 安全否(含连字符)是(Base32)
时间戳有效期公元 10889 年公元 10889 年

UUID v7 的优势

  1. 标准化。 UUID v7 拥有 IETF RFC。每个数据库、ORM 和语言运行时都已支持 UUID 类型。而 ULID 在大多数生态中需要自定义解析。

  2. 数据库原生支持。 ULID 无法直接存入 PostgreSQL 的原生 uuid 列,除非转换。你要么以字符串存储(浪费空间),要么转为二进制(丢失 Crockford 编码)。UUID v7 可以直接存入现有的 uuid 列。

  3. 生态势头。 Bytebase 预测业界将”逐步放弃定制方案,转向以 UUIDv7 作为大多数场景下的主键”。PostgreSQL 18 原生 uuidv7() 加速了这一趋势。

ULID 仍有意义的场景

  1. 更紧凑的表示。 26 个字符 vs 36 个字符,ULID 在 URL 和 API 中更短。如果字符串长度对你的场景很重要,ULID 更紧凑。

  2. 略多的随机性。 ULID 提供 80 个随机位,而 UUID v7 约 74 位。实际上两者的碰撞概率都极低,但 ULID 的随机部分稍大一些。

  3. 已有 ULID 基础设施。 如果你的系统已经在使用 ULID 且迁移成本不低,没有紧迫的理由去切换。两者提供类似的数据库性能,因为它们共享同样的 48 位时间戳前缀。

对于 2026 年的新项目,UUID v7 是更安全的选择。它有标准化支持、数据库原生支持,也不需要你的团队维护 ULID 解析逻辑。

数据库性能:真实基准测试

随机标识符与时间排序标识符之间的性能差异,在生产系统中已有充分记录:

PostgreSQL

指标UUID v4UUID 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 库适用于 Rustuuid crate v1.4+)、Javauuid-creator)、C#UUIDNext)和 PHPsymfony/uid)。

从 UUID v4 迁移到 v7

已经在生产环境中使用 UUID v4 了?好消息是:迁移不需要一刀切式的大改造。

v4 和 v7 可以共存

两个版本共享相同的 uuid 列类型和 16 字节二进制表示。你可以为新行生成 v7,同时保留现有的 v4 行不变。查询、连接和索引的工作方式完全一样——数据库并不关心 UUID 是哪个版本。

唯一的行为差异:按主键排序时,新的 v7 行会排在旧的 v4 行之后,因为 v7 的时间戳前缀使其在字典序中靠后。如果你的应用依赖 UUID 排序(使用 v4 时不应该这样做),请验证这个假设。

迁移清单

  1. 更新 ID 生成方式——将应用层的 UUID 生成切换为 v7。对于 PostgreSQL 18+,将列默认值改为 uuidv7()
  2. 更新 ORM——大多数 ORM 将 UUID 生成委托给应用或数据库。更新生成器,而非列类型。
  3. 监控索引性能——切换后,新的插入将是顺序的。随着时间推移,v7 行占比增加,索引效率会自然提高。
  4. 不要回填——将现有 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 响应。