由 John Doe 十一月 3, 2025
你在设计表结构的时候,习惯使用 UUID 值作为主键吗?现在,PostgreSQL 提供了内置的顺序 UUID 生成函数。

什么是 UUID?
UUID(通用唯一标识符)是 128 位的值,可用于标识各类对象(从交易记录到企业信息均可)。其设计目标是实现 “跨空间与时间的唯一性”,能以高效、高并发的方式生成,且无需依赖中心化服务。
传统上,关系型数据库使用自增类型(如SERIAL或identity)生成唯一标识符。这种方式在单节点环境下效率较高(尽管仍有缺陷),但当需要横向扩展时,就必须有一种能在所有节点间保证唯一性的标识符生成方案。
在以下数据库场景中,UUID 作为主键具有显著优势:
1. 分布式数据库中生成唯一 ID
尽管许多分布式数据库支持自增(identity)列,但这类列存在局限性与性能问题,而 UUID 可轻松规避这些问题。
2. 不可猜测的公开标识符
若 UUID 生成方式得当,其值无法被猜测、预测,也不会泄露系统信息。例如,若用自增 ID 作为客户标识,攻击者可能扫描所有已存在的标识、尝试盗用,或通过猜测下一个标识、估算客户总数来获取敏感信息,而 UUID 可避免这些风险。
3. 允许客户端自主生成标识符
使用 UUID 后,客户端无需与服务器协调即可生成标识符,这在移动应用、无服务器(Serverless)环境中尤为实用,能大幅减少与服务器的通信开销。
凭借这些优势,UUID 被广泛用作许多数据库的主键。但使用 UUID 也存在三大顾虑:
- 排序性:UUID 的值不具备有意义的排序规则。
- 索引局部性:新生成的 UUID 在索引中分布零散,插入操作会随机命中索引的不同位置,可能导致索引膨胀等性能问题(详细情况可参考 PostgreSQL 中的顺序 UUID 生成器)。
- 存储空间:UUID 为 128 位值,而多数开发者默认使用 32 位的
INT或 64 位的BIGINT作为主键。对于包含大量小记录的表,UUID 会带来额外的存储开销。
为何选择 UUIDv7?
UUIDv7 同时解决了 “排序性” 与 “索引局部性” 两大痛点:它将 Unix 时间戳作为最高 48 位,其余 74 位用于存储随机值(部分位用于标识版本与变体)。这种结构使 UUID 可按时间顺序排序,同时保证唯一性。标准还允许在 UUID 中包含毫秒级时间戳和精心初始化的计数器(如需在 1 秒内进一步保证有序性)。因此,UUIDv7 非常适合作为数据库主键,既保证全局唯一,又支持排序,还能提供良好的索引局部性。
在 PostgreSQL 18 之前,数据库不原生支持 UUIDv7:内置函数gen_random_uuid()仅能生成 UUIDv4;而常用扩展uuid-ossp虽支持更多 UUID 变体,但也仅限于 RFC 4122 定义的类型。
PostgreSQL 18 新增了uuidv7()函数,用于生成 UUIDv7 值。PostgreSQL 的实现中,在时间戳后额外添加了 12 位的 “亚毫秒级时间戳片段”(这一设计符合标准但非强制要求),可确保同一 PostgreSQL 会话(同一后端进程)生成的所有 UUIDv7 值具备单调性(即后生成的值始终大于先生成的值)。
为保持命名一致性,PostgreSQL 18 还将uuidv4()作为gen_random_uuid()的别名。
示例
调用uuidv7()时,会生成以 “当前时间” 为时间戳的 UUIDv7 值;若需为特定时间生成 UUIDv7,可向函数传递一个可选的interval(时间间隔)类型的参数。
PostgreSQL 中用于提取 UUID 时间戳与版本的现有函数,也已更新以支持 UUIDv7。以下是新函数的使用示例:
SELECT uuidv7();
uuidv7
--------------------------------------
0196ea4a-6f32-7fd0-a9d9-9c815a0750cd
(1 row)
SELECT uuidv7(INTERVAL '1 day');
uuidv7
--------------------------------------
0196ef74-8d09-77b0-a84b-5301262f05ad
(1 row)
SELECT uuid_extract_version(uuidv4());
uuid_extract_version
----------------------
4
(1 row)
SELECT uuid_extract_version(uuidv7());
uuid_extract_version
----------------------
7
(1 row)
SELECT uuid_extract_timestamp(uuidv7());
uuid_extract_timestamp
----------------------------
2025-05-19 20:50:40.381+00
(1 row)
SELECT uuid_extract_timestamp(uuidv7(INTERVAL '1 hour'));
uuid_extract_timestamp
----------------------------
2025-05-19 21:50:59.388+00
(1 row)
SELECT uuid_extract_timestamp(uuidv7(INTERVAL '-1 day'));
uuid_extract_timestamp
----------------------------
2025-05-18 20:51:15.774+00
(1 row)
将uuidv7()用作表的主键非常简单;结合时间戳提取功能,可轻松将 UUID 作为可排序键,甚至直接查看记录的创建时间:
CREATE TABLE test (
id uuid DEFAULT uuidv7() PRIMARY KEY,
name text
);
INSERT INTO test (name) VALUES ('foo');
INSERT INTO test (name) VALUES ('bar');
-- this will be sorted to the beginning of the list since we are making it 1h older than the other two
INSERT INTO test (id, name) VALUES (uuidv7(INTERVAL '-1 hour'), 'oldest');
SELECT uuid_extract_timestamp(id), name FROM test ORDER BY id;
uuid_extract_timestamp | name
----------------------------+--------
2025-05-19 19:55:43.87+00 | oldest
2025-05-19 20:55:01.304+00 | foo
2025-05-19 20:55:01.305+00 | bar
(3 rows)
所有上述函数的用法均可查询 PostgreSQL 官方文档。
这个特性对开发者应该会很有用处。感谢社区的所有相关人员。