TECH

UUID 还是 BIGINT?一篇讲透 App 与系统设计里的 ID 选型本质

许多工程师最早接触数据库时,默认把自增数字 ID 当成标准答案。但当系统从“数据库中心”走向“边界协同中心”时,UUID 开始显得更合理。真正的问题不是谁更先进,而是谁在优化你当前最重要的约束。

topic: data-management type: 文章 origin: 原创 score: 8 addedAt: 2026-03-26

引子:一个真实项目里的认知冲突

最近我在用 AI Coding 开发一套 App,底层技术栈选的是 Supabase + Edge Functions。

之所以选这套体系,原因很现实:它把现代应用里最麻烦、但又绕不过去的几层基础设施一次性收拢了起来。

  • 身份认证和用户体系有现成能力
  • 数据库、存储、权限控制和 API 边界天然打通
  • RLS 让“数据权限跟数据库规则绑定”这件事更容易落地
  • 边缘函数适合承接轻量编排、异步处理和靠近请求侧的业务逻辑

对一个正在快速迭代、又希望尽快验证产品闭环的项目来说,这种优势非常直接:你可以少花很多时间在基础设施胶水层上,而把更多精力放在真实业务和交互设计上。

但也正是在这个过程中,一个看似很小的细节,反复让我停下来:系统里几乎所有 ID 都是 UUID。

这和我过去对数据库设计的直觉并不一致。在我原来的经验里,主键几乎默认就是自增数字,短、清晰、顺手,也更像“标准答案”。

所以当 AI 一路把模型和表结构推向 UUID 时,我第一反应不是接受,而是怀疑:到底是 AI 的判断错了,还是我过去的判断只适用于某一类系统?

顺着这个问题往下挖,我才发现,UUID 和 BIGINT 之争,表面上是字段类型选择,底层其实是在回答一个更大的问题:

你到底在优先优化数据库内部,还是在优先优化系统边界?

先说结论

如果你只想记住一句话,那就是:

BIGINT 更像数据库内部效率优先的选择,UUID 更像系统边界协同优先的选择。

这两者不是新旧之争,也不是“UUID 已经全面取代自增 ID”。它们优化的根本不是同一个问题。

  • 如果你的系统是单库、单体、后台管理、内部使用为主,优先考虑 BIGINT
  • 如果你的系统天然跨前后端、跨服务、跨异步任务、跨认证系统,优先考虑 UUID
  • 如果你既想要数据库内部效率,又想要对外资源标识的稳定性,最稳妥的方案通常是“双 ID”

也就是说,真正值得问的问题不是“哪种 ID 更高级”,而是:

你的系统到底是以数据库为中心,还是以系统边界协同为中心?

为什么这个问题会颠覆很多人的认知

很多工程师早期设计数据库时,接触的都是传统业务系统:

  • 一个中心数据库
  • 一个后端服务
  • 若干张 CRUD 表
  • 主键默认就是自增数字

在这种世界观里,BIGINT 几乎是天然正确的答案。它更短、更省空间、索引更小、查询更顺手,人看着也更直观。

但现代 App 和 Web 系统的约束已经变了。

今天你面对的常常不是“一个数据库给所有对象发号”,而是:

  • 浏览器端先创建对象,再异步落库
  • 移动端离线写入,稍后同步
  • 后端 worker、队列消费者、边缘函数同时生产数据
  • 认证系统、BaaS、第三方身份平台自带自己的 ID 体系
  • 对外 API、URL、分享链接不希望暴露连续编号

一旦系统从“数据库中心”变成“边界协同中心”,UUID 的价值就会突然变大。它不是在数据库内部更高效,而是在跨边界流转时摩擦更小。

所以让很多人“认知被颠覆”的,并不是 UUID 本身,而是系统设计的出发点已经变了。

BIGINT 和 UUID,到底分别在优化什么

BIGINT 优化的是数据库内部效率

BIGINT 的优势很纯粹,也很硬核:

  • 存储更小,通常是 8 bytes
  • 主键索引更小,外键索引也更小
  • 自增写入天然有序,B-Tree 局部性更好
  • 人类更容易阅读、排障和手工排查

换句话说,BIGINT 擅长的是:

  • 更省空间
  • 更好的缓存命中率
  • 更少的页分裂和索引膨胀
  • 更直接的运维体验

如果你的系统核心矛盾是“数据库吞吐、索引大小、join 成本、人工排障效率”,那 BIGINT 的优势非常真实。

UUID 优化的是系统边界协同

UUID 的优势并不主要发生在数据库内部,而是发生在数据库之外:

  • 可以在应用侧直接生成,不用先找数据库领号
  • 多个服务、多个节点、多个客户端可以并行创建对象
  • 更适合跨系统合并数据,不容易冲突
  • 对外暴露资源 ID 时不容易被枚举

所以 UUID 擅长的是:

  • 异步系统
  • 分布式系统
  • 前后端解耦
  • 多身份系统接入
  • 公共资源标识

它本质上是在用更多的存储和更差一点的索引局部性,换取系统边界上的低耦合。

底层原理:为什么它们会有这些差异

1. 存储大小不同,索引大小就不同

在常见数据库里:

  • BIGINT 通常是 8 bytes
  • UUID 通常是 16 bytes

主键变大,意味着:

  • 主键索引会更大
  • 所有引用这个主键的外键列会更大
  • 所有外键索引也会更大

这不是只影响一张表,而是会沿着关联关系向外扩散。系统表越多、关系越深,这个差异越明显。

所以如果你的系统是典型高频事务系统,海量主外键 join,BIGINT 的优势会不断放大。

2. 自增 ID 是顺序写,随机 UUID 更容易打散索引

大多数关系型数据库底层主索引都是 B-Tree 一类的结构。
自增 BIGINT 的写入位置天然向后推进,因此更接近顺序插入。

顺序插入的好处是:

  • 索引页局部性更好
  • 页分裂更少
  • cache 更容易命中
  • 磁盘写入模式更友好

而随机 UUID v4 的写入位置是分散的,它会把新记录打到索引树的不同位置,导致:

  • 页分裂增加
  • 索引更容易膨胀
  • 局部性变差
  • 写入吞吐通常不如顺序 ID

这也是为什么很多人说“UUID 性能差”。
更准确的说法应该是:

随机 UUID 在数据库索引局部性上通常不如自增 BIGINT。

注意,这里还可以继续细分。

  • 如果你用的是随机 UUID v4,问题最明显
  • 如果你用的是时间有序的 UUID v7、ULID 一类方案,写入局部性会好很多

所以,真正需要警惕的往往不是“UUID”三个字,而是“随机 UUID + 高频写入主索引”这个组合。

3. 生成时机不同,决定了系统耦合方式

BIGINT 通常依赖数据库的 sequence、identity 或 auto_increment。
这意味着对象必须先进入数据库,才能拿到正式 ID。

而 UUID 可以在这些位置生成:

  • 浏览器端
  • 移动端
  • 后端服务
  • worker
  • 边缘函数
  • 消息处理器

这个差别非常关键。

如果一个对象在进入数据库之前,就已经要被引用、传递、写入日志、绑定事件、放入任务队列,那么 UUID 的协同成本通常更低。

也就是说:

  • BIGINT 更像“数据库负责定义对象身份”
  • UUID 更像“系统边界先定义对象身份,数据库只是落库”

4. 对外暴露资源 ID 时,连续数字有天然副作用

如果你的 API 返回的是:

  • /orders/10001
  • /orders/10002
  • /orders/10003

外部观察者几乎立刻能推断出:

  • 订单大致规模
  • 新增速度
  • 是否能顺着枚举探测资源

这不一定构成真正的安全漏洞,因为真正的安全仍然取决于认证和授权,但它确实暴露了更多业务信息。

而 UUID 或独立的 public_id 在这方面更克制。它不是安全系统本身,却能显著降低“被顺手枚举”的风险。

还有一个经常被忽略的问题:语言运行时的边界

在 JavaScript / TypeScript 世界里,这个问题更微妙。

因为 JS 的 number 不能安全表达全部 64 位整数,所以很多数据库驱动、ORM 或 BaaS SDK 在处理 BIGINT 时会选择:

  • 直接返回字符串
  • 或要求你手工做 bigint 序列化

结果就是:你以为数据库里选的是数字,最后在前后端边界上依然要把它当字符串处理。

这时 UUID 反而显得“更自然”:

  • 在数据库里是 uuid
  • 在 API 里是字符串
  • 在前端状态里也是字符串

对于 TypeScript 前后端一体的系统,这是一个非常现实的工程收益。

所以很多现代 Web 项目选择 UUID,并不只是为了分布式,也因为它和 JS/TS 的边界更顺手。

如果是 App 和现代业务系统,我会怎么选

场景一:单库单体、后台系统、内部使用为主

这类系统我会优先选 BIGINT。

原因很简单:

  • 数据库就是系统绝对中心
  • 没有太多跨边界预生成 ID 的需求
  • 人工排障、运营查询、导数纠错很频繁
  • 性能和运维体验比“分布式优雅”更重要

这是 BIGINT 的舒适区。

场景二:现代 App、前后端分离、异步任务较多

这类系统我更倾向选 UUID,尤其是顶层业务对象。

比如:

  • 用户侧创建会话
  • 客户端预生成草稿
  • 多个服务协作写入订单、消息、事件
  • 系统接入外部身份平台
  • URL、分享链接、调试链路都需要稳定对象 ID

这时 UUID 的优势不在 SQL 跑得更快,而在于系统边界更顺。

场景三:BaaS / Supabase / Firebase 一类栈

如果你本来就建立在一套外部认证与平台能力之上,我通常更倾向于继续跟随平台原生 ID 风格。

原因不是“平台永远是对的”,而是:

  • 身份系统本身就可能使用 UUID
  • 平台生成的类型、SDK、权限模型已经围绕它设计好了
  • 你硬要换成另一套主键体系,常常会把简单问题变成映射问题

这种场景里,统一性往往比理论上的最佳存储效率更重要。

真正更成熟的方案,往往不是二选一

很多系统最后会走向第三条路:

双 ID 方案

id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
public_id UUID NOT NULL UNIQUE

这个方案很值得认真考虑。

它把两个世界的优点拆开了:

  • 数据库内部 join、索引、外键都走 BIGINT
  • 对外接口、URL、日志追踪、跨系统传递走 public_id

这样做的好处是:

  • 内部高效
  • 外部稳定
  • 不暴露连续编号
  • 未来系统边界变化时更从容

如果你今天做的是一个可能长大的 App,这种设计通常比“全表纯 UUID”或者“全表纯 BIGINT”都更稳。

还有一个更深一层的建议:不是每张表都需要独立主键

很多团队一上来会陷入另一个误区:

“既然要规范,那每张表都加一个 id,而且全部统一成 UUID。”

这并不一定是最优设计。

有些表更适合:

  • 直接复用父表主键做 1:1 关系
  • 使用复合唯一键表达真实业务身份
  • 用自然键或业务键而不是额外 surrogate key

也就是说,比“UUID 还是 BIGINT”更重要的一层问题其实是:

这张表真的需要一个独立的 surrogate key 吗?

如果答案是否定的,那么争论 UUID 还是 BIGINT,本身就偏题了。

我现在的默认方案

如果今天让我从零开始设计一个现代 App / Web 系统,我的默认顺序会是:

  1. 先判断这是数据库中心系统,还是边界协同系统
  2. 如果是内部后台和单库事务系统,优先 BIGINT
  3. 如果对象要跨前后端、跨任务、跨系统流转,优先 UUID
  4. 如果系统既重数据库效率,又重对外资源标识,优先双 ID
  5. 如果选择 UUID,优先考虑时间有序的方案,而不是随机 v4
  6. 不要机械地给每张表都塞一个独立主键

所以我的结论不是“以后都用 UUID”,也不是“传统自增 ID 已经过时了”。

更准确的结论是:

BIGINT 是数据库视角下的优解,UUID 是系统边界视角下的优解,而成熟系统往往会同时利用这两者。

最后一层理解:你到底在给谁设计身份系统

主键设计的本质,不只是数据库字段类型选择。

它背后真正回答的是:

  • 对象的身份由谁定义?
  • 是数据库定义,还是系统边界先定义?
  • 这个身份是只给数据库内部 join 用,还是还要穿过 API、日志、任务、前端和外部链接?

如果你理解到了这一层,就不会再把 UUID 和 BIGINT 看成一道“标准答案选择题”。

它们都对。
只是它们各自忠于不同的系统现实。

在当前这个基于 Supabase 和 Edge Functions 的应用里,继续使用 UUID 是更务实、更一致的选择;真正值得做的,不是现在就把主键体系推倒重来,而是在保留 UUID 的前提下,先把展示层、表设计和后续性能观测做好,等出现真实瓶颈后再做局部优化。

评论