使用 UUID
本页目录
以本文讨论的 PostgreSQL 16(2023 年)为准,Postgres 根据 RFC 4122 提供 UUID v1–v5 的相关支持。下文关于内置函数和扩展的说明均基于这一版本背景。
- 文档:UUID Data Type
- 附加模块 uuid-ossp
使用 gen_random_uuid() 会生成一个UUID值,它生成的是 4 版本的 UUID (PG16 的源码):
nik=# select gen_random_uuid();
gen_random_uuid
--------------------------------------
c027497b-c510-413b-9092-8e6c99cf9596
(1 row)
nik=# select gen_random_uuid();
gen_random_uuid
--------------------------------------
08e63fed-f883-45d8-9896-8f087074bff5
(1 row)
在标准 UUID 中,可以通过第二个连字符后的第一个字符来判断版本:
08e63fed-f883-4 ... 👈 this means v4
这些值以"伪随机"顺序出现。这对性能有一定的负面影响:在 B-tree 索引中,插入发生在不同位置,这通常会影响写入性能以及 Top-N 读取 (选择最新的 N 行) 的性能。
原文写作时,社区正在推进 UUID v7 的标准化及 PostgreSQL 支持。UUID v7 在值中编码毫秒级时间戳,并结合随机位等信息,使排序具有时间局部性,有助于改善索引访问模式。
**上游更新:**UUID v7 已加入 PostgreSQL 18,参见原作者的更新说明。下文保留辅助函数方案,供没有原生支持的旧版本参考。
UUID 值占 16 字节。
译者校订:原文称 UUID 与
timestamp、timestamptz大小相同,这不准确;后两者均占 8 字节。可通过pg_column_size验证。
以下是一些解释性能方面的优秀资料:
- The effect of Random UUID on database performance by @hnasr (视频,大约 19 分钟)
- Identity Crisis: Sequence v. UUID as Primary Key by @brandur
对于尚未原生支持 UUID v7 的 PostgreSQL 版本,可以采用以下两种方式:
- 在客户端生成 UUID。
- 在 Postgres 中实现辅助函数。
对于第二种方法,这里提供了一个 SQL function (感谢 @DanielVerite)::
create or replace function uuid_generate_v7(
ts timestamptz default null
) returns uuid
as $$
-- use random v4 uuid as starting point (which has the same variant we need)
-- then overlay timestamp
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
select encode(
set_bit(
set_bit(
overlay(
uuid_send(gen_random_uuid())
placing substring(int8send(floor(extract(epoch from coalesce(ts, clock_timestamp())) * 1000)::bigint) from 3)
from 1 for 6
),
52, 1
),
53, 1
),
'hex')::uuid;
$$ language sql volatile;
译者校订:若此前已经创建了无参数的
uuid_generate_v7(),它与带默认参数的新版函数会形成重载。请先核查依赖,在测试环境中处理旧函数,再替换定义,避免无参数调用产生歧义。
示例
nik=# select uuid_generate_v7();
uuid_generate_v7
--------------------------------------
018c1be3-e485-7252-b80f-76a71843466a
(1 row)
nik=# select uuid_generate_v7();
uuid_generate_v7
--------------------------------------
018c1be3-e767-76b9-93dc-23c0c48be6c7
(1 row)
nik=# select uuid_generate_v7();
uuid_generate_v7
--------------------------------------
018c1be3-e973-7704-82ad-5967b79cf5c4
(1 row)
几分钟之后:
nik=# select uuid_generate_v7();
uuid_generate_v7
--------------------------------------
018c1be8-5002-70ab-96c0-c96ad5afa151
(1 row)
新版辅助函数也支持传入指定时间戳。下面是上游示例;随机部分每次调用会变化,因此不会总是返回相同的 UUID:
select uuid_generate_v7('2024-10-15 01:02:03+00'::timestamptz);
一些注意事项:
在
ORDER BY中按 UUID 排序,可以保留其编码时间戳的先后关系。这个辅助函数在同一毫秒内使用随机部分区分值,因此不保证同一毫秒内的严格生成顺序。在最初生成的三个值中 (几秒钟内生成),存在一个共同前缀
018c1be3-e。在稍后生成的最后一个值中,有一个共同前缀018c1be。注意所有值中第二个连字符后的 7:
018c1be3-e973-7... 👈 this means v7该函数返回
UUID类型的值,因此仍然是 16 字节 (而它的文本表示需要 36 个字符,包括连接符,这意味着加上VARLENAheader 的话,总共需要 40 个字节):
nik=# select pg_column_size(gen_random_uuid());
pg_column_size
----------------
16
(1 row)
nik=# select pg_column_size(uuid_generate_v7());
pg_column_size
----------------
16
(1 row)
从 UUID v7 提取时间戳
上游还补充了反向转换函数。UUID v7 的前 48 位保存 Unix 毫秒时间戳,下面将其转换为 UTC 时间,并返回不带时区的 timestamp:
create or replace function public.uuid_v7_to_ts(uuid_v7 uuid) returns timestamp
as $$
select
to_timestamp(
(
'x' || substring(
encode(uuid_send(uuid_v7), 'hex')
from 1 for 12
)
)::bit(48)::bigint / 1000.0
)::timestamptz at time zone 'UTC';
$$ language sql immutable;
例如,原文中的两个 UUID 可还原为以下时间:
select uuid_v7_to_ts('018c1be3-e485-7252-b80f-76a71843466a');
-- 2023-11-29 16:22:49.221
select uuid_v7_to_ts('01928db2-42f8-7539-8b09-47d9a00e668b');
-- 2024-10-15 01:02:03
该函数应只用于 UUID v7;其他版本的 UUID 不能用这种方式解释其前 48 位。
我见
关于 UUID 导致性能问题的案例,我也写过几篇:
为了规避前文所说的问题,你可以选择使用有序UUID:https://github.com/tvondra/sequential-uuids,以及UUID v7,v6 和 v7 ( https://github.com/fboulnois/pg_uuidv7 提供了v7的支持 ) 都有考虑可排序性,解决 UUID 应用时最常遇到的数据库性能问题。社区也在实现中。