自增 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 的唯一。这避免了业务属性与主键的耦合。 |
| 存储紧凑 | 占用空间小:通常使用整数类型(如 INT 或 BIGINT),相比字符串或 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)
-
结构
- 128 位
- 前 48 位是 时间戳(毫秒级)
- 后 80 位是 随机数
- 使用 Base32 编码,长度固定为 26 个字符
-
特点
- 可排序:因为前 48 位是时间戳,生成的 ID 按时间顺序排序
- 时间可解析:可以从 ID 直接解析出生成时间
- 高冲突抵抗:随机部分保证在同一毫秒内生成多个 ID 也不会轻易冲突
- 公开安全性一般:ID 可直接暴露生成时间,不适合高安全需求的公共 API
-
适用场景
- 日志系统、数据库主键、事件序列号
- 需要按时间排序或分析数据的场景
总结
| ID 类型 | 优点 | 缺点 | 是否可排序 | 是否可解析时间 | 安全性 | 存储/索引性能 |
|---|---|---|---|---|---|---|
| 自增 ID | 简单易用;存储空间小;索引开销低;自然排序 | 可预测,不适合公开 API;分布式难生成全局唯一;合并数据库容易冲突;容量有限 | ✅ | ❌ | 低 | 高 |
| UUID v4 | 全球唯一;完全随机;分布式系统友好 | 存储大(16B / 36 字符);索引碎片化;不按时间排序 | ❌ | ❌ | 高 | 低 |
| UUID v7 | 基于时间戳,可排序;全球唯一;分布式友好;可推算时间 | 存储大;安全性一般;索引比自增 ID 慢 | ✅ | ✅ | 中 | 中 |
| CUID v1 | 分布式友好;可读性较好;时间可解析 | 安全性一般;长度比自增 ID 长;索引性能中等 | ❌ | ✅ | 中 | 中 |
| CUID v2 | 高安全性;分布式友好;可读性好;冲突概率极低 | 时间不可解析;长度比自增 ID 长;索引性能中等 | ❌ | ❌ | 高 | 中 |
| ULID | 基于时间戳,可排序;全球唯一;可解析时间;分布式友好;URL-friendly | 安全性一般(时间暴露);长度比自增 ID 长;索引比自增 ID 慢 | ✅ | ✅ | 中 | 中 |
总计,在创建时间不是个敏感信息的情况下,ULID 作为主键是一个很好的方案。
参考链接
- https://www.prisma.io/docs/orm/reference/prisma-schema-reference#attribute-functions
- 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
- 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