JsonMind
编码与文本11 分钟阅读更新于 2026年11月9日

UUID 和 ULID 怎么选:随机、有序与数据库索引的取舍

比较 UUID v4、UUID v7 和 ULID 的可排序性、索引友好度、URL 表示和分布式场景,给出新项目的选择建议。

JM作者:JsonMind Engineering最后复核: 2026年11月9日依据权威规范与引用来源复核。
快速答案

需要按时间排序、减少数据库索引碎片时,优先用 UUID v7 或 ULID;更看重随机性和生态兼容时用 UUID v4。两者都是 128 位,UUID 通常表示为带连字符的 36 字符,ULID 表示为 26 个 Crockford Base32 字符。

核心要点
  • UUID v4 是随机的;UUID v7 和 ULID 内嵌时间戳,近似按创建时间排序。
  • 时间有序 ID 能改善 B 树局部性,让最近写入和查询更快。
  • ULID 使用 Crockford Base32,大小写不敏感,文本更短。
  • 这些 ID 都不是密钥,不能把不可猜测当作授权。
  • 数据库尽量存二进制形式,接口层再暴露规范文本。

选择 ID 格式看起来是小事,直到表变大。随机主键会造成索引碎片,有序主键会暴露创建时间,文本表示又会影响 URL、日志和排查体验。

这篇文章从实际影响最大的几个维度比较 UUID v4、UUID v7 和 ULID。

格式对照

属性UUID v4UUID v7ULID
位数128128128
时间有序否是是
文本长度36(含连字符)36(含连字符)26
字母表十六进制十六进制Crockford Base32
大小写敏感否否否
标准RFC 9562RFC 9562ULID 规范
库支持非常广增长中较好

UUID v4 使用 122 位随机数。UUID v7 使用 48 位毫秒时间戳加随机位。ULID 使用 48 位毫秒时间戳加 80 位随机数,再用 Crockford Base32 编码。

位数分配直接决定取舍:

格式时间戳位随机或计数位文本长度
UUID v4012236
UUID v74874(12 位 rand_a + 62 位 rand_b)36
ULID488026

48 位毫秒时间戳到公元 10889 年才用完,实际项目不用担心溢出。同一毫秒内的碰撞概率由随机部分决定:UUID v7 有 74 位,ULID 有 80 位,都远远够用;就算每秒生成百万个 ID,生日碰撞概率也可以忽略。真正的差别在文本长度和生态支持,不在“够不够唯一”。

ULID 的第一个字符一定在 0 到 7 之间。这不是约定,而是编码结果:48 位时间戳占 10 个 Base32 字符(50 位),首位只有低 3 位有效。一个校验器如果接受 Z 开头的 ULID,它接受的就是一个解码不出合法 48 位时间戳的值。

为什么可排序很重要

B 树索引按顺序存储键。新键随机分布时,插入会落到树的各个位置,导致页分裂和写放大。新键近似顺序时,插入会追加到最右侧页面。

这是团队从 UUID v4 迁移到 UUID v7 或 ULID 的主要原因。收益最大的场景:

  • 每秒大量插入的表。
  • 聚簇索引,例如 MySQL InnoDB 主键。
  • 最近数据查询最频繁的时序类数据。

低写入表或使用独立自增键的表,收益较小。

机制值得说清楚,因为它决定了建议什么时候成立。B 树里插入一个随机键,要先找到一个可能已经满了的叶子页,分裂它,再更新所有祖先;新页散落在文件各处。顺序插入通常直接追加到最右侧叶子,树的其余部分不动。InnoDB 的聚簇主键让这个差异尤其明显:整行数据就住在主键 B 树里,随机键会同时搬动行数据、二级索引项和 undo 记录。PostgreSQL 的堆表和索引是分开的,主键索引仍然会碎,但行插入本身是追加的,影响真实存在,只是小一些。

应该用监控确认,而不是凭感觉。PostgreSQL 里 pg_stat_user_tables.n_dead_tup 和索引膨胀估算涨得比行数快,通常说明随机键在制造 churn;MySQL 里可以观察 INFORMATION_SCHEMA.INNODB_TABLESTATS,以及插入时 Innodb_buffer_pool_pages_free 压力的突然上升。在表的副本上做前后对比,比引用一篇博客里的百分比可靠。

可排序也不是万能。如果查询本来就按 ID 随机读,时间有序键不会让查找更快;表小到能全部放进内存时,页分裂很便宜;应用已经按独立的 created_at 索引排序时,主键顺序只影响插入成本。时间有序键还可能把写入集中到一个热点页,在极高插入速率下变成争用点。带单调计数器的 UUID v7 能缓解这个问题,但代价是随机性下降。

随机性的价值

时间有序 ID 会暴露创建时间。ID 公开后,任何人都能估算记录创建时间。通常可以接受,但要看场景:

  • 公开订单号可能暴露业务量。
  • 连续 ID 会暴露记录总数。
  • 时间有序 ID 会暴露大致创建时间。

UUID v4 能隐藏这些信息。如果时间戳敏感,应该用 v4,或者另外生成一个不透明的公开 ID。

UUID v7 和 ULID 会把创建时间暴露到毫秒。拿到一个公开 ID,就能推出记录创建的具体秒数;拿到一串 ID,还能估算注册速率、订单量,以及哪些账号是在同一时间段创建的。截断时间戳再编码不是标准操作,也会破坏格式的唯一性保证,所以实际做法是再加一个标识:时间有序值继续做内部主键,另外生成随机 public_id 暴露在 URL 和 API 里。代价是每行多 16 字节和一个唯一索引,通常比泄露业务指标或让跨租户数据可关联便宜得多。

这些格式都不是密钥。ID 不是授权凭证。无论用哪种格式,服务端都必须做访问控制。

文本表示和 URL

ULID 更短、更好读:

UUID v4: 550e8400-e29b-41d4-a716-446655440000
UUID v7: 018f4f8e-7c2a-7c3e-9b1a-2f6d8e4a1b3c
ULID:    01JCH6X8Y4Q2V7M3N9K5R1T6B8

ULID 使用 Crockford Base32,去掉了容易混淆的 I、L、O、U,并且大小写不敏感。复制粘贴、电话支持和人工转录都更友好。

UUID 更常见,库、数据库和第三方系统支持更广。如果集成很多外部服务,UUID 是更稳妥的默认值。

Crockford Base32 有一个容易把校验器写错的细节。编码时排除了 I、L、O、U,但解码时应该把 i、l 当成 1,把 o 当成 0,因为这些正是人工转录时最容易看错的字符。ULID 大小写不敏感,01jch6x8... 和 01JCH6X8... 是同一个值;但把两种写法都存进大小写敏感的列,就会出现“看起来重复”的行和失效的等值查询。写入时统一转大写,必须存文本时用二进制排序规则;大小写和重音都不敏感的排序规则会让 A 和 a 比较相等,对 UUID 来说方便,但也会掩盖“规范形式没有被保留”这件事。

校验应该拒绝整个值,而不是修补它。ulid 包的 decodeTime("not-a-ulid") 抛 ULIDError: Malformed ULID (DEC_TIME_MALFORMED),isValid 则返回 false;两种都比一个宽松的正则好——正则可能放过 26 个字符,等到后面才失败。UUID 的规范 v4/v7 字符串是 36 个字符、连字符位置固定;32 位十六进制串很多解析器也接受,但它不是规范形式。SQL Server 的 uniqueidentifier 排序字节序和规范字符串顺序不同,按列排序和按文本排序结果会不一致。边界上统一规范化,不要拿不同来源的原始字符串直接比较。

URL 里的占用差异比字符数看起来更大。UUID 要 36 个字符,放进路径还有编码开销;ULID 只要 26 个,没有连字符,双击选中和 CSV 导出都更省事。两者都不够短,不能替代人类可读的 slug,也都不应该拿来当密码重置 Token。

数据库存储

两种格式都是 128 位。可以存成:

  • 数据库原生 BINARY(16) 或 UUID 类型。
  • 没有 128 位类型时,用两个 BIGINT。
  • 只有在便利性明显大于存储和索引成本时,才存文本。

文本存储会让体积增加两到三倍,比较也更慢。如果存文本,使用固定长度类型并统一大小写。

具体数字能让代价更直观。BINARY(16) 每个值 16 字节;InnoDB 里 utf8mb4 的 CHAR(36) 最多预留 144 字节,索引项也按同样宽度计算。一千万行的表,光多出来的索引数据就接近 1.4 GB,还没算页开销。文本列无法避免时,CHAR(36) CHARACTER SET ascii COLLATE ascii_bin 是比较合理的折中:ASCII 字符每个只占 1 字节,二进制排序规则也能给出确定的比较结果。

数据库的具体行为比通用建议更重要。MySQL 的 UUID_TO_BIN(uuid, 1) 会交换 time-low 和 time-high 字段,让二进制形式按时间排序;不加交换标志时,二进制顺序和 UUID v1 的文本顺序不一致。到了 UUID v7,时间戳本来就在最前面,再做交换反而会破坏想要的时间顺序。PostgreSQL 18 增加了原生 uuidv7() 和 uuidv4(),生成时间有序 ID 不必再绕到应用层。SQLite 里应该存 16 字节的 BLOB,或者存规范文本并加 CHECK(length(id) = 26) 约束;一个没有约束、大小写混用的 TEXT 列,迟早会积累出不一致的数据。

索引类型也要一起考虑。InnoDB 的 BINARY(16) 主键是聚簇的,这 16 字节会重复出现在每个二级索引里;换成 36 字节文本主键,重复的就是 36 字节。行数上千万、二级索引又有好几个时,索引体积和缓存压力的差距会比单行算术大得多。

生成 ID

// UUID v4(随机)
const id = crypto.randomUUID();

// ULID(时间有序),使用库
import { ulid } from "ulid";
const id = ulid();

UUID v7 应该使用实现 RFC 9562 的库,不要自己拼时间戳位。位布局出错时,生成的值看起来合法,但排序会错。

UUID/ULID 生成工具 可以生成两种格式用于测试。GUID 生成工具 适合需要 Windows GUID 格式的系统。

生成环节有几个运维细节。浏览器里的 crypto.randomUUID() 只在安全上下文可用,局域网 HTTP 页面会抛 TypeError: crypto.randomUUID is not a function;要么用库,要么用 uuid 包的降级实现。大多数运行时的 randomUUID() 生成的是 v4,不是 v7,所以“用内置的”并不能满足时间有序需求。需要 v7 时用 uuid 的 v7() 之类的库,需要 ULID 时用 monotonicFactory():它会处理位布局,并在时钟没有前进时递增随机部分,让同一进程产生的 ID 仍然可排序。

单调性只在本进程、本时钟内成立。两台应用服务器的时钟差 500 毫秒,生成的 ID 顺序就可能和真实时间相反;ID 仍然唯一,但“最新记录”查询和游标分页可能漏行或重复行。NTP 能减小偏差,不能消除偏差;时钟回拨还会产生比已经发出的值更早的 v7。如果严格的全局顺序是正确性要求,就不要从墙上时钟 ID 推导顺序,改用中心化序列、数据库生成值,或者服务器分配的时间戳加稳定的 tiebreaker。

什么时候用哪种

用 UUID v4:

  • 需要最大化的库和数据库兼容性。
  • 不想暴露创建时间。
  • 表写入量低,或使用独立自增键。
  • 第三方系统只接受 UUID。

用 UUID v7:

  • 想要时间有序插入,同时保持 UUID 兼容。
  • 数据库或 ORM 支持 v7 生成。
  • 需要标准定义的位布局。

用 ULID:

  • 想要紧凑、大小写不敏感的文本 ID。
  • 能控制完整技术栈并引入 ULID 库。
  • 想要时间有序且人类友好的表示。

决策树很短。系统已经在说 UUID、表又有大量插入时,选 UUID v7;能控制完整技术栈、文本形式对人类重要时,选 ULID;更看重不可预测性,或者生态只接受 UUID,就用 v4。都不合适时,可以换一个标识符家族:

格式大小有序适合场景
自增 BIGINT8 字节是单库内部键
Snowflake 类 ID8 字节是有 worker ID 的分布式系统
KSUID20 字节是27 字符 Base62、按时间排序
NanoID约 21 字符否UI 或文件名用的短随机 ID
UUID v716 字节是标准工具链 + 索引局部性
ULID16 字节是能端到端控制的紧凑文本 ID

这套建议在两种情况下是错的。第一,ID 必须不可预测且创建时间敏感:v7 和 ULID 都会泄露时间,正确做法是另加随机公开标识,而不是换一种时间戳编码。第二,数据库就是单个 PostgreSQL 实例,用 BIGINT 序列、没有分布式写入者:64 位整数比任何 128 位格式都更小、更快、更简单。按你实际拥有的系统选,而不是按演讲里的架构选。

常见错误

  • 应用生成 UUID,又用另一个可能漂移的时间戳排序。
  • 把 UUID 当密码重置 Token 或 API Key。应该使用专门的随机 Token,并且只存哈希。
  • UUID 按大小写敏感文本存储,却用不同大小写比较。
  • 假设时间有序 ID 严格单调。时钟调整会产生乱序;部分 ULID 实现有单调模式,但不应依赖它保证正确性。
  • 公开连续或时间有序 ID,却不做访问控制。

其中两个错误有很具体的症状。大小写不敏感存储 + 大小写敏感比较,会产生很难复现的工单:用户从邮件里复制了大写 ID,API 却按小写查,行明明存在,却报找不到。集成测试里用两种大小写各查一次。单调性假设会在游标分页里暴露:两行落在同一毫秒,随机 tiebreaker 又不稳定时,WHERE id > $cursor ORDER BY id 可能漏掉第二行,也可能把它返回两次。用 (id, created_at) 这样的确定 tiebreaker,或者同时按两列分页。

第三个错误是把 ID 当秘密。随机 UUID v4 确实猜不到,但如果它同时是唯一的访问检查,链接一泄露就等于永久授权。公开 ID 要配合鉴权、过期和撤销;标识符格式不是安全控制。

实用建议

新项目如果使用现代数据库,UUID v7 是很好的默认值:标准化、时间有序、兼容 UUID 工具链。想要更短的文本 ID 并能控制技术栈时,ULID 是不错的替代。兼容性或不可预测性是首要要求时,用 UUID v4。

无论选哪种,都要写清楚格式,尽量存二进制,并把授权和 ID 生成分开。

把已有表从 v4 迁到 v7 不是改个格式,而是重建索引。先加新列,按旧 created_at 排序分批回填,让新键基本顺序;再灰度切换读写,确认稳定后重建主键,最后删掉旧列。迁移期间表和索引都会膨胀,要在维护窗口或用在线 DDL 工具做。如果回填时生成的 v7 是乱序的,换格式本来想要的索引局部性就没了。

落地前用一张短清单验证:生成一千万个 ID,确认无重复且顺序符合预期;插入生产表的副本,测量索引大小、写入吞吐和 EXPLAIN 计划;确认所有消费方接受文本长度和字母表;检查大小写归一化和输入校验;记录时间戳范围和时钟假设。如果实测收益在噪音范围内,就选更简单的格式。

常见问题

ULID 比 UUID 更好吗?

没有绝对的好坏。ULID 和 UUID v7 都提供时间有序 ID。UUID 的库和数据库支持更广,ULID 的文本更短且大小写不敏感。应该根据生态选择。

UUID v4 能按创建时间排序吗?

不能。UUID v4 是随机的,字典序和创建时间没有关系,必须另建 created_at 字段排序。UUID v7 和 ULID 内嵌时间戳,可以近似按时间排序。

UUID 可以安全地作为公开 ID 吗?

可以作为公开 ID,但它不是密钥。公开 UUID 不会暴露连续数量,但也不能代替权限校验。服务端必须做访问控制,不能把不可猜测的 ID 当作授权凭证。

UUID v4 的碰撞概率高吗?

实际系统中可以忽略。UUID v4 有 122 位随机数。ULID 和 UUID v7 把时间戳和随机位组合,只要随机部分足够大,碰撞概率同样可以忽略。

UUID 适合做数据库主键吗?

可以,但随机 UUID 可能造成聚簇索引碎片。UUID v7 和 ULID 因为近似顺序,碎片更少。应该在自己的数据库上测量影响,而不是假设。

参考资料

把指南用起来

浏览全部工具

打开浏览器工具。输入内容保留在浏览器中,不会上传到业务服务器。

相关指南

全部指南