🧩 前端与全栈 · 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。