☰
UUID 还是自增 ID?别让 AI 替你做数据库设计
2026/10/27 2:05:48 网站建设 项目流程

自增 ID 不是落后的设计,UUID 也不是分布式系统的万能答案。真正成熟的数据库设计,应该从数据的生命周期、系统边界、索引成本和业务需求出发,而不是跟随某种技术潮流。

一、一个值得重新思考的问题

如果你使用过 MySQL 或 PostgreSQL,大概率写过这样的表:

CREATE TABLE orders ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, order_no VARCHAR(64) NOT NULL, status VARCHAR(32) NOT NULL );

这很自然。

id自动增长,查询简单,索引友好,调试时看一眼就知道哪条记录大概先创建。

但现在让 AI 生成一个 SaaS 系统、微服务项目,甚至一个简单的 CRUD 应用,它可能会给你这样的结构:

CREATE TABLE orders ( id UUID PRIMARY KEY, order_no VARCHAR(64) NOT NULL, status VARCHAR(32) NOT NULL );

为什么?

很多 AI 生成的项目默认采用 UUID,通常有几个原因:

  • 训练语料中有大量微服务、云原生和分布式系统的设计案例。

  • UUID 可以在数据库之外生成,方便 API、客户端和多个服务共同创建实体。

  • 不需要协调多个数据库的自增序列。

  • UUID 作为公开标识符,不会直接暴露连续的数据库编号。

这些理由都成立,但它们并不意味着 UUID 对所有项目都更好。

问题在于:AI 经常把“某些场景需要 UUID”简化成“新项目就应该使用 UUID”。

这是一个值得警惕的工程习惯。

数据库主键是基础设施决策。它会影响索引、外键、存储空间、数据迁移、接口设计和未来的系统集成,不应该仅仅因为某个技术方案更流行,就直接采用。

本文从数据库实现、性能、分布式架构、安全性和实际项目设计等方面,把这件事讲清楚。


二、首先明确:自增 ID 和 UUID 到底是什么?

1. 自增 ID

自增 ID 是由数据库的序列机制生成的数值标识符。

例如:

idorder_nostatus
1001ORD-001pending
1002ORD-002delivering
1003ORD-003completed
1004ORD-004pending

在 PostgreSQL 中,可以使用IDENTITY:

CREATE TABLE orders ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, order_no TEXT NOT NULL );

BIGINT是 64 位有符号整数,通常占用 8 字节。

数据库通过序列生成数字,应用不必自行协调编号。

需要注意,自增序列不保证绝对连续:事务回滚、并发插入等情况都可能造成编号间隙。

2. UUID

UUID 是一种 128 位标识符,标准文本形式通常为 36 个字符,包括连字符。

例如:

019a5d10-2a10-7c31-8f72-65c3a4d1b920

它的优势是可以在多个系统中独立生成,极大降低标识符碰撞的风险,而不必依赖同一个数据库序列。

PostgreSQL 原生支持 UUID 类型,不需要将其存储为 36 字符的文本。

CREATE TABLE orders ( id UUID PRIMARY KEY, order_no TEXT NOT NULL );

UUID 不是一种单一的生成算法。常见版本的行为差异很大:

  • UUIDv4:主要使用随机数据生成,生成过程简单,但插入位置通常较分散。

  • UUIDv7:在高位编码 Unix 毫秒时间戳,剩余位包含随机数据或可选的单调性辅助信息,整体上更适合按索引顺序插入。

UUIDv7 的规范见 RFC 9562。

这意味着,讨论 UUID 性能时,不能把 UUIDv4 和 UUIDv7 混为一谈。


三、从各个维度比较优劣

1. 数据库存储成本

先看最直接的区别。

对比项BIGINT 自增 IDUUID
原始字段大小8 字节16 字节
典型索引键大小较小较大
外键存储8 字节16 字节
人工阅读方便不方便
是否需要数据库参与生成通常需要不需要
多节点独立生成需要额外设计原生适合

UUID 的原始二进制长度是 BIGINT 的两倍。

而且,主键并不是只在主表中存储一次。它还会进入关联表、外键列以及相关索引。

假设一个系统有 1000 万条订单,每条订单平均被 4 条关联记录引用。

仅计算主键列和这 4 条引用列的原始数据:

  • BIGINT:(1000万 \times 5 \times 8) 字节,约 400 MB。

  • UUID:(1000万 \times 5 \times 16) 字节,约 800 MB。

这是一个简化的容量估算,不包含索引结构、页开销、对齐、其他字段和缓存成本。

实际差距还可能受到索引结构和插入方式的影响。

对于只有几千或几万条数据的普通业务系统,这种差异通常不值得单独决定主键类型。

但对于拥有大量关联表、海量记录和严格内存预算的系统,键的宽度确实值得认真考虑。

自增 ID 在存储效率上有客观优势,不能因为 UUID 更现代就忽略这一点。

2. 索引性能:自增 ID 为什么通常更友好?

数据库中的主键一般会对应一个唯一索引。以 PostgreSQL 的 B-tree 索引为例,新记录插入的位置会受到主键值的影响。

对于自增 ID:

1001 1002 1003 1004 1005

新值通常大于之前生成的值,索引插入位置相对集中。

对于 UUIDv4:

c2f... 19a... e81... 47d...

新值可能分散在索引的不同位置。

随机插入可能带来:

  • 更差的缓存局部性。

  • 更多不同索引页的访问。

  • 页分裂及相关维护成本增加。

  • 更大的索引工作集。

这并不意味着每次 UUID 插入都会导致严重的页分裂,也不意味着 UUIDv4 一定会让系统变慢。实际影响取决于数据规模、缓存命中率、并发量、索引结构和硬件。

但是,从索引局部性来看,自增 ID 的确有优势。

UUIDv7 改善了什么?

UUIDv7 把时间戳放在高位,因此按 UUID 值排序时,通常会将相近时间生成的 UUID 排在相近位置。

它改善了 UUIDv4 的随机插入问题。

但要注意:

UUIDv7 具有时间排序特征,不等于它就是数据库自增序列。

同一毫秒生成多个 UUID 时,后续位可能是随机的,未必严格反映每次生成的先后顺序。多个节点的时钟也可能存在偏差。

因此可以粗略理解为:

  • 自增 BIGINT:通常有较好的索引局部性和单数据库序列顺序。

  • UUIDv4:没有天然的时间排序特征。

  • UUIDv7:有时间排序特征,通常比 UUIDv4 更适合数据库索引。

UUIDv7 的具体设计与排序行为,可以参考 RFC 9562 第 5.7 节及第 6.11 节。

3. 插入顺序:自增 ID 的优势究竟有多大?

这是你提出的一个非常实际的问题。

自增 ID 不仅能标识记录,还能在很多业务场景中近似反映编号分配顺序。

例如:

SELECT * FROM orders ORDER BY id DESC LIMIT 20;

在一个简单的单库业务系统中,这通常可以快速找到最近分配编号的一批记录。

但这里必须区分三个概念:

  1. 编号分配顺序:数据库序列分配 ID 的先后。

  2. 事务提交顺序:事务最终成功提交的先后。

  3. 业务发生顺序:订单实际创建、支付或配送等事件的发生顺序。

它们并不等价。

例如:

  • 事务 A 先获得 ID 1001,但迟迟没有提交。

  • 事务 B 后获得 ID 1002,却先提交。

  • 事务 A 最后提交。

此时 ID 顺序与事务提交顺序不同。

如果系统允许回填 ID、导入历史数据或调整序列,ID 与创建时间的关系还会更加复杂。

因此,自增 ID 可以帮助你近似观察编号分配顺序,却不能作为严格的业务时间线。

如果你需要知道订单什么时候创建、什么时候进入配送、什么时候完成,应该明确保存:

created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

如果需要准确追踪业务状态变更,则应记录独立的订单事件或状态历史表。

不要让主键承担时间戳、业务流水号和事件排序这三个不同职责。

4. 分布式系统:UUID 真正有价值的地方

这是 UUID 最有说服力的优势。

假设系统 A 和系统 B 各自有一个 PostgreSQL 数据库,两边都能独立创建订单。

系统 A 创建:

id = 1001

系统 B 也创建:

id = 1001

两个 ID 在各自数据库中都合法,但同步时会发生主键冲突。

你当然可以通过其他方式解决,例如:

  • A 使用奇数 ID,B 使用偶数 ID。

  • A 使用一个编号区间,B 使用另一个编号区间。

  • 使用带节点标识的分布式 ID 算法。

  • 设置独立的全局 ID 服务。

这些方案都能工作,但需要额外规则和维护成本。

如果双方独立生成 UUID:

系统 A:019a...a101 系统 B:019a...b902

双方几乎不可能生成相同的 UUID。无需为每条新记录向中心服务申请编号。

这正是 UUID 在分布式系统中的价值。

不过要注意,UUID 的唯一性是基于生成算法及随机性等条件获得的工程保证,并不是数学意义上的绝对不碰撞。数据库仍然应该保留主键或唯一约束作为最后防线。

另外,UUID 只解决标识符碰撞问题,不解决业务数据冲突问题。

如果 A 将订单状态改成“已取消”,B 将同一订单改成“配送中”,即使 UUID 完全不同或正确匹配同一订单,系统仍然需要处理并发修改、状态机和业务裁决。

标识符唯一,不代表数据一致。

5. 系统扩展与数据迁移

如果未来只有一套系统、一个主数据库,自增 ID 通常非常自然。

如果未来可能出现以下情况,UUID 的优势就会逐渐明显:

  • 多套系统独立创建数据。

  • 手机客户端或离线设备需要先生成记录,再同步到服务端。

  • 服务拆分后,多个服务各自创建实体。

  • 数据库合并、跨区域同步、异步导入。

  • 需要在数据库事务提交前就确定一个稳定的实体标识符。

自增 ID 并非无法支持这些场景,只是需要设计编号协调机制。

UUID 的价值是把这部分协调成本降下来。

还有一个实际问题:如果系统现在使用自增 ID,未来才发现需要 UUID,能不能迁移?

可以,但不能简单地把所有主键字段从 BIGINT 改成 UUID 就结束了。

你需要考虑:

  • 所有外键及关联表。

  • API 和缓存中的 ID。

  • 消息队列和事件记录中的引用。

  • 历史数据和离线数据。

  • 其他系统已经保存的旧 ID。

因此,哪怕内部使用自增 ID,也可以从项目初期就设计一个稳定的外部标识符,为未来集成保留空间。

6. 安全性:UUID 不等于安全

这是另一个经常被误解的地方。

自增 ID:

/orders/1001 /orders/1002 /orders/1003

容易被猜测。

UUID:

/orders/019a5d10-2a10-7c31-8f72-65c3a4d1b920

不容易被顺序枚举。

但 UUID 并不是访问控制机制。

如果用户可以读取其他用户的订单,只要他拿到对应 UUID,照样可能访问。

真正的安全措施应该是:

  • 身份认证。

  • 对象级权限检查。

  • 租户隔离。

  • 数据访问策略。

  • API 请求的授权校验。

在多租户系统中,不能因为订单 ID 很难猜,就跳过tenant_id和资源权限检查。

UUID 可以减少简单的顺序枚举风险,但不能替代授权。

7. API 设计与开发体验

从开发者体验来看,自增 ID 很友好:

{ "id": 1001, "status": "pending" }

UUID 则更长:

{ "id": "019a5d10-2a10-7c31-8f72-65c3a4d1b920", "status": "pending" }

UUID 可能让日志、接口调试和人工沟通更麻烦。

例如:

订单 1001 出了问题

通常比:

订单 019a5d10-2a10-7c31-8f72-65c3a4d1b920 出了问题

更容易沟通。

当然,成熟的系统往往有order_no这样的业务单号,内部主键并不需要直接展示给用户。

这里最重要的不是让所有 ID 都易读,而是区分:

  • 数据库主键。

  • API 资源标识符。

  • 业务单号。

  • 事件 ID。

  • 请求追踪 ID。

它们可以使用不同的格式,不需要强行统一。

8. 数据库和语言生态

UUID 的使用成本也与技术栈有关。

PostgreSQL 原生支持 UUID 类型,Go 生态也有成熟的 UUID 库。

MySQL 等数据库同样支持 UUID,但存储形式、生成方式、索引行为和默认函数有所不同。

如果把 UUID 存成CHAR(36)或普通文本,通常会比原生 UUID 或紧凑二进制存储占用更多空间。

所以,不能简单地说“UUID 在所有数据库中都一样”。

使用 UUID 时,应优先使用数据库原生 UUID 类型或合适的紧凑二进制表示,而不是不加思考地存成 36 字符的字符串。


四、综合优劣对照表

维度自增 BIGINTUUIDv4UUIDv7
存储成本优较高较高
索引局部性通常很好通常较差通常较好
单库实现简单度优较简单较简单
多节点独立生成需要协调优优
近似时间排序编号分配顺序不支持支持时间排序
严格业务事件排序不支持不支持不支持
人工阅读优较差较差
客户端离线创建需要额外机制优优
顺序枚举防护较弱较强较强
数据库迁移与跨库合并需要编号策略更方便更方便

这里的“优”不是说其他方案不能实现,而是表示在该维度上更自然、更省成本。


五、为什么 AI 经常推荐 UUID?

我认为,这不仅是技术问题,也与 AI 生成代码的方式有关。

1. AI 倾向于给出可迁移的通用方案

UUID 可以同时适用于单体系统、多服务系统、离线客户端和跨库同步。

对 AI 来说,推荐 UUID 是一种容易解释、容易复用的默认选择。

但是,通用性不等于最优性。

对于一个单体应用,所有订单都在同一个数据库创建,未来也没有跨系统同步需求,UUID 可能只是增加了存储和调试成本。

2. AI 更容易识别架构层面的收益,而忽略数据库内部成本

“可以在多个系统独立生成”是非常容易描述的优势。

但索引页局部性、关联表键宽度、缓存工作集、查询计划和实际性能,需要结合数据库实现和负载分析。

因此,生成式代码很容易优先选择架构上看起来灵活的设计,却忽略了更基础的成本。

3. 现代技术案例对 AI 的选择有影响

近年来,微服务、SaaS、云原生和事件驱动架构的技术文章大量讨论 UUID。

这些案例会让 UUID 成为 AI 容易采用的方案。

但 AI 的推荐并不是性能测试的结论,也不等于某种设计已经成为所有数据库项目的最佳实践。

AI 应该帮助我们降低设计成本,而不是替代工程判断。

让 AI 生成数据库表结构时,最好明确要求它比较主键方案,并说明:

  • 数据是否跨数据库同步。

  • 是否需要离线创建。

  • 数据规模和写入频率。

  • 是否需要公开资源标识符。

  • 数据库类型和索引特点。

  • 是否有既定的业务单号。

如果这些信息都没有给出,直接让 AI 生成完整数据库结构,本身就存在设计不充分的问题。


六、实战方案:为什么我更倾向于“双标识符”

现在讨论一种更实用的设计。

假设我们正在开发一个 SaaS 系统,内部有订单、用户、组织、配送任务等多张表。

我不一定会要求所有表都以 UUID 作为主键。

我更愿意考虑:

  • 内部使用BIGINT自增主键。

  • 额外使用 UUID 作为全局稳定标识符。

  • API、跨系统同步和事件引用使用 UUID。

  • 内部关联和高频数据库查询使用 BIGINT。

例如:

CREATE TABLE orders ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, public_id UUID NOT NULL UNIQUE, order_no VARCHAR(64) NOT NULL UNIQUE, status VARCHAR(32) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );

这里有三个不同的标识符:

字段职责
id数据库内部主键
public_id跨系统稳定标识符、API 资源标识符
order_no面向业务人员的订单编号

这种设计的好处是,内部数据库可以保留紧凑的数值主键,同时拥有一个可以在系统之间独立生成的稳定标识符。

如果业务需要两个独立系统创建同一类订单,那么public_id应在创建订单时生成,并随着订单同步到另一端。不能让另一端重新生成一个新的 UUID,否则双方就无法通过它识别同一笔订单。

1. 外键应该引用哪个字段?

如果使用双标识符,可以让内部关联继续引用id:

CREATE TABLE order_items ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(id), product_name TEXT NOT NULL );

而跨系统事件使用public_id:

{ "event_type": "order.updated", "order_id": "019a5d10-2a10-7c31-8f72-65c3a4d1b920", "status": "delivering" }

接收端根据public_id找到自己的本地订单,再更新对应记录。

这样,内部关联仍然使用 BIGINT,外部系统不需要知道本地自增 ID。

不过,双标识符也有代价:

  • 每张需要外部标识符的表增加一个字段。

  • 增加唯一索引。

  • 创建和查询逻辑需要明确区分两种 ID。

  • 数据迁移和导入必须保证public_id稳定。

  • 开发规范必须防止误把内部 ID 当作跨系统 ID。

所以双标识符并非没有代价。对于很小的应用,如果不需要跨系统引用,直接使用自增 ID 通常更简单。

2. 两套独立系统同步订单时,怎么选?

这是一个非常典型的场景。

如果系统 A 和系统 B 都能独立创建订单,我会考虑以下方案:

方案 A:UUID 作为订单主键。

两边直接共享订单 UUID。架构简单,数据库不需要再维护额外的外部标识符。

适合数据规模不大、系统之间需要频繁共享实体标识符的项目。

方案 B:BIGINT 内部主键 + UUID 全局标识符。

两边各自生成本地自增 ID,同时创建一个全局 UUID。

例如:

系统本地 idpublic_id
A1001019a...a101
B2056019a...a101

两边的id不同,但public_id相同,代表同一笔订单。

对于你的双向同步场景,这种设计很值得考虑:内部保留自增 ID 的便利,跨系统同步则使用 UUID。

需要注意,如果订单最初在 A 创建,B 只是接收它,那么 B 应保留 A 生成的public_id,而不是重新生成。

如果 A、B 独立创建的是两笔不同订单,则它们应该拥有不同的public_id。


七、如果选择 UUID,应该使用哪个版本?

如果确定需要 UUID,我一般不会在没有理由的情况下继续默认使用 UUIDv4 作为数据库主键。

如果数据库和语言生态支持,UUIDv7 通常是更值得考虑的选择。

原因很简单:

  1. 可以在数据库之外生成。

  2. 不依赖单个数据库的自增序列。

  3. 具有时间排序特征。

  4. 相比随机 UUIDv4,通常有更好的索引局部性。

但也要注意:

  • UUIDv7 的时间排序不等于严格的全局生成顺序。

  • 系统时钟不准确或回拨会影响时间排序特征。

  • 同一毫秒生成的多个 UUID 不一定严格按生成顺序排列。

  • 如果需要严格的业务事件顺序,应该使用独立的事件序列或明确的排序机制。

另外,如果数据库内只需要一个主键,使用 UUIDv7 并不自动意味着需要再增加一个 BIGINT。

双标识符方案的价值在于把内部数据库标识和外部稳定标识分开,而不是因为 UUID 性能不好,就一定要同时保存两个 ID。


八、什么时候我会直接选择自增 ID?

以下场景,我会毫不犹豫地考虑BIGINT自增主键:

  • 单体业务系统,主要使用一个数据库。

  • 后端服务统一创建数据。

  • 不需要离线客户端预先创建实体。

  • 不需要跨数据库合并相同实体。

  • 数据量较大,内部关联和索引成本值得优化。

  • 业务单号与内部主键已经明确分离。

例如,一个内部管理系统,所有订单都由服务端创建,订单通过业务编号对外展示,那么自增 ID 就是非常合理的默认方案。

不需要为了“未来可能分布式”提前为每一张表增加 UUID。

未来真的需要分布式能力时,可以为需要跨系统标识的实体增加外部 ID,而不是从第一天就让整个数据库承担额外成本。


九、什么时候我会优先选择 UUID?

以下场景,我更倾向于 UUID:

  • 多个独立系统可以创建同一类实体。

  • 需要跨库、跨服务共享标识符。

  • 客户端或离线设备需要在同步前生成 ID。

  • 事件、消息或资源需要在数据库事务之外被引用。

  • 不希望多个数据库协调自增序列。

  • 需要让实体在迁移和合并时保持稳定的全局标识。

例如,跨系统订单同步、离线任务创建、多个服务共同创建业务对象等场景。

这时 UUID 带来的简化可能远大于多出的几个字节。


十、最后的结论

我不赞成把 UUID 和自增 ID 看成先进与落后的关系。

它们解决的是不同的工程问题:

  • 自增 ID:用很低的存储成本和简单的数据库机制,提供稳定的本地行标识。

  • UUID:用更宽的标识符,换取跨系统独立生成和共享标识的便利。

  • UUIDv7:在 UUID 的分布式生成能力之外,改善了时间排序和数据库索引局部性。

  • 双标识符:把数据库内部主键与外部稳定标识分离,适合同时重视内部效率和跨系统集成的项目。

如果是一个简单的单库 CRUD 系统,我会优先考虑自增 BIGINT。

如果是两套独立系统双向同步订单,我会优先考虑 UUID,或者内部 BIGINT 加外部 UUID。

如果是一个规模较大、内部关联密集、同时有明确分布式标识需求的系统,我会认真评估双标识符,而不是简单地让 UUID 替代所有自增主键。

最后,还有一个原则比选哪种 ID 更重要:

不要让主键承担它不该承担的职责。

主键负责标识记录;业务单号负责业务识别;创建时间负责时间记录;事件序号负责严格的事件排序;权限系统负责访问控制。

把这些职责分清楚,才是真正成熟的数据库设计。

而在 AI 时代,这一点反而更重要:代码生成可以越来越快,但系统设计不能因为生成得快,就省略必要的判断。


参考资料

  1. PostgreSQL 官方文档:UUID 数据类型

  2. PostgreSQL 官方文档:Identity Columns

  3. IETF:RFC 9562 — Universally Unique IDentifiers (UUIDs)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询