TECH
UUID 还是 BIGINT?一篇讲透 App 与系统设计里的 ID 选型本质
许多工程师最早接触数据库时,默认把自增数字 ID 当成标准答案。但当系统从“数据库中心”走向“边界协同中心”时,UUID 开始显得更合理。真正的问题不是谁更先进,而是谁在优化你当前最重要的约束。
引子:一个真实项目里的认知冲突
最近我在用 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 bytesUUID通常是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 系统,我的默认顺序会是:
- 先判断这是数据库中心系统,还是边界协同系统
- 如果是内部后台和单库事务系统,优先
BIGINT - 如果对象要跨前后端、跨任务、跨系统流转,优先
UUID - 如果系统既重数据库效率,又重对外资源标识,优先双 ID
- 如果选择 UUID,优先考虑时间有序的方案,而不是随机
v4 - 不要机械地给每张表都塞一个独立主键
所以我的结论不是“以后都用 UUID”,也不是“传统自增 ID 已经过时了”。
更准确的结论是:
BIGINT 是数据库视角下的优解,UUID 是系统边界视角下的优解,而成熟系统往往会同时利用这两者。
最后一层理解:你到底在给谁设计身份系统
主键设计的本质,不只是数据库字段类型选择。
它背后真正回答的是:
- 对象的身份由谁定义?
- 是数据库定义,还是系统边界先定义?
- 这个身份是只给数据库内部 join 用,还是还要穿过 API、日志、任务、前端和外部链接?
如果你理解到了这一层,就不会再把 UUID 和 BIGINT 看成一道“标准答案选择题”。
它们都对。
只是它们各自忠于不同的系统现实。
在当前这个基于 Supabase 和 Edge Functions 的应用里,继续使用 UUID 是更务实、更一致的选择;真正值得做的,不是现在就把主键体系推倒重来,而是在保留 UUID 的前提下,先把展示层、表设计和后续性能观测做好,等出现真实瓶颈后再做局部优化。
评论