Skip to content

数据库主键选择总结

Posted on:October 20, 2025 at 06:42 AM

自增 id

model User {
  id        Int      @id @default(autoincrement()) // 主键,自动递增的整数
  email     String   @unique // 邮箱,必须唯一
  name      String? // 姓名,可选字段(末尾的 ? 表示可以为 null)
  password  String // 密码哈希,通常不存储明文密码
  createdAt DateTime @default(now()) // 记录创建时间,默认为当前时间
  updatedAt DateTime @updatedAt // 记录更新时间,每次更新时自动修改
}

优点 (Advantages)

方面描述
简单易用开发便利:数据库自动处理 ID 的生成和唯一性,开发者无需编写复杂逻辑来保证 ID 的唯一。这避免了业务属性与主键的耦合。
存储紧凑占用空间小:通常使用整数类型(如 INTBIGINT),相比字符串或 UUID 占用更少的存储空间。
查询高效索引有序性:自增 ID 是连续递增的,这使得数据在磁盘上是顺序存放的。这与 B+ 树索引结构非常匹配,能减少页分裂,提高插入速度和查询效率(特别是范围查询)。
避免热点插入效率高:新记录总是插入到索引的末尾,这减少了多个并发插入操作在索引中间节点上的竞争(锁),尤其在低并发场景下表现良好。

缺点 (Disadvantages)

方面描述
分布式挑战扩展性差:在需要水平拆分(分库分表)的分布式系统中,单个数据库的自增 ID 无法保证全局唯一性。需要额外的复杂机制(如分布式 ID 生成器)来解决。
性能瓶颈写入热点:在高并发写入场景下,所有插入操作都需要竞争同一个自增计数器(通常在数据库内部是串行操作),可能导致数据库实例成为性能瓶颈,形成写入“热点”。
信息泄露暴露数据量:由于 ID 是连续的,恶意用户可以轻易通过 ID 的增减来猜测数据库中的数据总量、每日新增量等敏感信息。
迁移困难数据导入/导出:在进行数据迁移或合并时,如果两个数据库都有自增 ID,可能会出现 ID 冲突,需要手动调整或重新映射 ID。
安全风险易于遍历:连续的 ID 使得恶意用户更容易通过简单地修改 URL 或请求中的 ID 参数来遍历或猜测其他记录(如订单、用户资料),存在安全隐患。

字符串作为主键

model User {
  id    String @id
  email String @unique // 邮箱,必须唯一
}

上面生成的 prisma 代码,在创建 user 的时候,需要自己在应用层生成 id 但是,prisma 也提供了一种叫做 attribute-functions 的机制,能帮我们自动生成 id

uuid

model User {
  id    String @id @default(uuid()) // 默认 UUID v4
  email String @unique // 邮箱,必须唯一
}
model User {
  id    String @id @default(uuid(7)) // 支持 UUID v7
  email String @unique // 邮箱,必须唯一
}
版本核心生成方式特点
v4完全随机每个 UUID 的 128 位都是随机生成,唯一性依赖随机数的质量。
v7基于时间戳 + 随机前 48 位是毫秒级时间戳,剩下部分是随机数。兼顾时间顺序与唯一性。
版本主键索引性能说明
v4随机值插入,B-tree 索引容易频繁分裂,写入性能较低。
v7时间连续,插入顺序接近自然顺序,索引维护成本低,写入性能更优。

cuid

model User {
  id    String @id @default(cuid())
  email String @unique
}
model User {
  id    String @id @default(cuid(2))
  email String @unique
}
特性CUID(v1)CUID2(v2)
安全性基本熵,存在可预测性增强安全性,使用 SHA-3 哈希和多重熵来源
冲突概率较低,但在分布式系统可能出现冲突极低,需要生成约 4.03 * 10¹⁸ 个 ID 才有 50% 冲突概率
熵来源时间、机器 ID、进程 ID、计数器时间、伪随机数、会话计数器、主机指纹
编码格式Base62(字母+数字)Base36(字母+数字)
长度~25 个字符24 个字符
可读性一般高,可读性好,开头为字母,无特殊字符
安全审核未进行加密安全审核使用符合 NIST 标准的加密哈希算法
适用场景内部系统公共 API、URL、分布式系统

ulid

model User {
  id    String @id @default(ulid())
  email String @unique
}

ULID(Universally Unique Lexicographically Sortable Identifier)

  1. 结构

    • 128 位
    • 前 48 位是 时间戳(毫秒级)
    • 后 80 位是 随机数
    • 使用 Base32 编码,长度固定为 26 个字符
  2. 特点

    • 可排序:因为前 48 位是时间戳,生成的 ID 按时间顺序排序
    • 时间可解析:可以从 ID 直接解析出生成时间
    • 高冲突抵抗:随机部分保证在同一毫秒内生成多个 ID 也不会轻易冲突
    • 公开安全性一般:ID 可直接暴露生成时间,不适合高安全需求的公共 API
  3. 适用场景

    • 日志系统、数据库主键、事件序列号
    • 需要按时间排序或分析数据的场景

总结

ID 类型优点缺点是否可排序是否可解析时间安全性存储/索引性能
自增 ID简单易用;存储空间小;索引开销低;自然排序可预测,不适合公开 API;分布式难生成全局唯一;合并数据库容易冲突;容量有限
UUID v4全球唯一;完全随机;分布式系统友好存储大(16B / 36 字符);索引碎片化;不按时间排序
UUID v7基于时间戳,可排序;全球唯一;分布式友好;可推算时间存储大;安全性一般;索引比自增 ID 慢
CUID v1分布式友好;可读性较好;时间可解析安全性一般;长度比自增 ID 长;索引性能中等
CUID v2高安全性;分布式友好;可读性好;冲突概率极低时间不可解析;长度比自增 ID 长;索引性能中等
ULID基于时间戳,可排序;全球唯一;可解析时间;分布式友好;URL-friendly安全性一般(时间暴露);长度比自增 ID 长;索引比自增 ID 慢

总计,在创建时间不是个敏感信息的情况下,ULID 作为主键是一个很好的方案。

参考链接

  1. https://www.prisma.io/docs/orm/reference/prisma-schema-reference#attribute-functions
  2. https://zenn.dev/kazu1/articles/e8a668d1d27d6b#%E6%83%85%E5%A0%B1%E6%BC%8F%E6%B4%A9%E3%81%AE%E3%83%AA%E3%82%B9%E3%82%AF
  3. https://zenn.dev/m0t0taka/articles/6a9f30f53e3558#%E5%85%AC%E9%96%8B-web-%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E5%90%91%E3%81%91