>_
🧩 前端与全栈 · 3 分钟阅读

数据库主键用 UUID v4 还是 v7?一次讲清楚

自增 ID、UUID v4、UUID v7 各有什么优缺点?为什么随机 UUID 会拖慢写入,v7 又解决了什么问题,以及存储时要注意的细节。

设计表结构时,主键用什么类型是绕不开的问题。最常见的三个选择是:自增整数、UUID v4,以及 2024 年随 RFC 9562 正式标准化的 UUID v7。

先看自增 ID

自增 ID 简单、紧凑、有序,对数据库索引最友好。但它也有明显的问题:

  • 暴露业务信息:/orders/10235 让别人很容易猜出订单量,也方便遍历;
  • 分布式不友好:多个库、多个服务各自生成时容易冲突,合并数据也麻烦;
  • 必须先写库才能拿到 ID,客户端无法预先生成。

UUID 就是为了解决这些问题:128 位,几乎不可能重复,任何地方都可以独立生成。

UUID v4:完全随机

v4 除了少数版本位,其余全部是随机数,例如:

3f6c1e2a-9b4d-4c8e-a1f2-7d5e9c0b2a14

它的问题出在数据库索引上。B+ 树索引希望新数据按顺序追加在末尾,而随机 UUID 会让每次插入落在索引的随机位置:

  • 需要频繁地把旧的索引页读回内存;
  • 页写满后要分裂,产生碎片;
  • 在 MySQL InnoDB 这种按主键组织数据(聚簇索引)的引擎里,影响尤其明显。

数据量小的时候感觉不到,表到几千万行以后,写入性能和缓存命中率的差距就会显现出来。

UUID v7:按时间排序

v7 的前 48 位是毫秒级 Unix 时间戳,后面是随机数:

019a0f3c-7b21-7c4e-9a3d-5f1e2b8c6d40
└─ 时间戳 ─┘

因为开头是时间,新生成的 ID 总是比旧的大,插入时基本都追加在索引末尾,兼顾了 UUID 的全局唯一和自增 ID 的有序。另外:

  • 按主键排序基本等于按创建时间排序;
  • 从 ID 里就能读出生成时间,排查问题时很方便。

可以在 UUID 生成器 里批量生成 v7,观察前几位是怎么随时间递增的;把前 12 位十六进制转成十进制,就是一个毫秒时间戳,可以放进 时间戳转换 里验证。

那 v7 有什么缺点

  • 暴露创建时间:如果 ID 会出现在公开的 URL 里,而创建时间属于敏感信息,就要考虑这一点;
  • 依赖时钟:多台机器时钟不同步时,顺序只是“大致”有序,不能当作严格的时间线使用;
  • 同一毫秒内生成多个 ID 时,顺序取决于具体实现(RFC 9562 提供了可选的计数器方案)。

存储时的细节

无论用哪个版本,都别把 UUID 存成 VARCHAR(36):

  • PostgreSQL:使用原生的 uuid 类型(16 字节)。PostgreSQL 18 起内置了 uuidv7() 函数,旧版本可以在应用层生成;
  • MySQL:使用 BINARY(16),配合 UUID_TO_BIN() / BIN_TO_UUID() 转换。注意 MySQL 自带的 UUID() 函数生成的是 v1;
  • 应用层生成:主流语言都有支持 v7 的库,也可以在应用层统一生成,让 ID 在写库之前就确定下来。

怎么选

场景建议
单库、内部系统、ID 不对外暴露自增 ID 依然是简单可靠的选择
需要对外暴露 ID、分布式生成、数据量大UUID v7
ID 不能泄露任何时间信息UUID v4(接受一定的写入性能代价)
已经大量使用 v4、目前没有性能问题不必为了换而换,新表可以考虑 v7

一句话总结:新项目需要 UUID 时,默认选 v7。

本文用到的工具