PostgreSQL 18: UUIDv7, 更优的主键标识列

John Doe 十一月 3, 2025

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

image

什么是 UUID?

UUID(通用唯一标识符)是 128 位的值,可用于标识各类对象(从交易记录到企业信息均可)。其设计目标是实现 “跨空间与时间的唯一性”,能以高效、高并发的方式生成,且无需依赖中心化服务。

传统上,关系型数据库使用自增类型(如SERIALidentity)生成唯一标识符。这种方式在单节点环境下效率较高(尽管仍有缺陷),但当需要横向扩展时,就必须有一种能在所有节点间保证唯一性的标识符生成方案。

在以下数据库场景中,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 官方文档。

这个特性对开发者应该会很有用处。感谢社区的所有相关人员。