UUID 解决的是全局唯一标识问题,而数据库主键解决的是数据定位、索引组织和存储效率问题。两者并不是同一个概念。
因此,“UUID 是否适合作为主键”不能简单回答“适合”或“不适合”,需要结合数据库、UUID 版本、数据规模和写入模式判断。
UUID 做主键的好处
-
可以独立生成
不同服务、不同节点可以独立生成 UUID,不需要依赖数据库自增序列。
-
适合分布式系统
不同数据库实例生成的 UUID 可以保持极低的碰撞概率,因此更适合分库分表、数据合并以及跨系统数据同步。
-
降低 ID 的可枚举性
相比:
/users/10001
/users/10002
/users/10003随机 UUID 更难通过连续 ID 推测其他资源。
不过这并不意味着 UUID 本身具有安全属性。UUIDv7 还包含时间信息,因此仍可能泄露部分元数据。 -
更容易作为跨系统标识
当数据需要在多个数据库、服务或者系统之间传递时,UUID 不依赖某个数据库实例的自增序列,因此更容易保持全局唯一。
UUID 做主键的代价
UUID 本身并不会因为生成成本高而明显拖慢数据库,真正影响性能的是它作为索引键之后的存储和写入方式。
-
UUID 的存储空间需要 16 字节(MySQL
BINARY(16)/ PostgreSQLUUID),相较于 bigint 8 字节,存储成本要更大。
不仅意味着主键本身更大,还会增加索引空间和缓存压力。
对于包含多个二级索引的表,这种影响会进一步放大。以 InnoDB 为例,二级索引的叶子节点还需要保存主键值,因此主键越大,二级索引通常也越大。 -
不具有时间顺序的 UUID(v3/v4/v5)不适合作为高频写入表的主键。
- 插入位置分散;新生成的 ID 无法像自增 BIGINT 一样持续写入索引末端。
- B+Tree 更容易发生 Page Split;
- 数据页利用率和缓存局部性较差;
- 随着数据量增长,随机访问带来的 I/O 压力更加明显。
这个问题在 MySQL/InnoDB 上更加明显。InnoDB 使用聚簇索引(Clustered Index)组织表数据,主键索引采用 B+Tree 结构,因此主键不仅用于唯一标识,还直接影响表数据的组织和写入方式。
UUIDv7:改善 UUID 的写入特性
UUIDv7 在高位使用 Unix 时间戳,使生成的 UUID 具有时间趋势顺序。
因此,与 UUIDv4 相比,UUIDv7 能够让索引插入更加集中,更适合数据库写入场景。
不过,UUIDv7 的顺序是时间趋势有序,而不是严格递增。例如同一毫秒内生成多个 UUID 时,后续的随机部分或计数部分仍可能导致最终 UUID 的排序与实际生成顺序不同。
PostgreSQL
PostgreSQL 的普通表采用 Heap Table 存储,主键通常通过唯一 B-tree 索引实现。
因此,UUID 主键不会像 InnoDB 那样直接决定整张表的数据组织顺序。UUIDv4 仍然会带来 B-tree 索引的随机插入和维护成本,但对表数据本身的影响相对较小。
因此,UUID 作为 PostgreSQL 主键是非常正常的设计。
PostgreSQL 还提供原生 UUID 类型:
id UUID PRIMARY KEY
不需要像 MySQL 使用 BINARY(16) 来保存 UUID,也不需要程序自行进行编码和解码。
思考:是否应该坚持数据库主键和业务标识应当职责分离?
《阿里巴巴 Java 开发手册》的 MySQL 数据库规约确实明确规定:表必须有 id,且 id 为主键,类型为 unsigned bigint,单表时自增,步长为 1。
这种设计实际上将数据库内部主键与业务标识分开。比如:
id BIGINT
order_no UUIDv7 / Snowflake
数据库主键主要服务于数据存储、索引和关联查询,而 order_no 则作为业务层面的唯一标识,可以独立生成并对外使用。
这样做的主要好处是:数据库可以使用更小、更加适合索引和写入的 BIGINT 作为主键,同时业务标识又可以使用 UUIDv7、Snowflake 等方案满足分布式生成、全局唯一等需求。
代价是增加一个字段,以及相应的唯一索引,同时数据模型和部分查询逻辑会略微复杂。
我个人觉得,统一约定的价值并不一定在于技术上最优,而是让团队保持一致,降低学习、沟通、决策和维护成本,同时减少踩坑概率,便于工具和基础设施建设。
尤其在大团队中,一套稳妥的默认方案很有必要。技术最佳实践追求的是“特定场景下最优”,工程规范追求的是“多数场景下足够好,并且大家都采用同一种方式”。
因此,除非有明确的业务或技术原因,否则应遵循团队规范;确有必要时,再允许例外。数据库设计不仅是性能问题,统一约定本身也是工程效率的一部分。
UUIDv7 与雪花算法
UUIDv7 是标准化的 128 位 UUID,通常占用 16 字节。
它使用 Unix 时间戳作为高位字段,其余空间使用随机数或计数器,因此具有时间趋势有序的特性。
UUIDv7 不需要中心化发号服务,也不依赖机器 ID,节点之间可以独立生成。
它最大的优势是标准化和通用性,跨语言、跨数据库、跨系统使用都比较方便。
缺点是体积较大,作为数据库主键时会增加索引和存储成本。
Snowflake 通常使用 64 位整数,实际占用 8 字节,一般由时间戳、机器/节点 ID、序列号等部分组成。
它同样能够分布式生成,并且具有时间趋势递增的特点。
由于只有 8 字节,相比 UUIDv7 更节省存储空间,对 MySQL InnoDB 这类依赖主键组织数据的数据库更加友好。
缺点是需要规划节点 ID,并需要处理时钟回拨、节点冲突等问题,同时不同实现之间的格式并不统一。
因此,两者可以理解为不同方向的取舍:
| 对比 | UUIDv7 | Snowflake |
|---|---|---|
| 长度 | 16 字节 | 8 字节 |
| 全局唯一 | 是 | 是 |
| 分布式生成 | 是 | 是 |
| 时间趋势有序 | 是 | 是 |
| 标准化 | 高 | 各实现不同 |
| 节点 ID | 不需要 | 需要 |
| 存储成本 | 较高 | 较低 |
| 时钟回拨 | 主要依赖实现 | 需要重点处理 |