使用 UUID

本页目录

以本文讨论的 PostgreSQL 16(2023 年)为准,Postgres 根据 RFC 4122 提供 UUID v1–v5 的相关支持。下文关于内置函数和扩展的说明均基于这一版本背景。

使用 gen_random_uuid() 会生成一个UUID值,它生成的是 4 版本的 UUID (PG16 的源码):

PSQL
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 中,可以通过第二个连字符后的第一个字符来判断版本:

Shell
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 验证。

以下是一些解释性能方面的优秀资料:

对于尚未原生支持 UUID v7 的 PostgreSQL 版本,可以采用以下两种方式:

  1. 在客户端生成 UUID。
  2. 在 Postgres 中实现辅助函数。

对于第二种方法,这里提供了一个 SQL function (感谢 @DanielVerite)::

SQL
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(),它与带默认参数的新版函数会形成重载。请先核查依赖,在测试环境中处理旧函数,再替换定义,避免无参数调用产生歧义。

示例

PSQL
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)

几分钟之后:

PSQL
nik=# select uuid_generate_v7();
           uuid_generate_v7
--------------------------------------
 018c1be8-5002-70ab-96c0-c96ad5afa151
(1 row)

新版辅助函数也支持传入指定时间戳。下面是上游示例;随机部分每次调用会变化,因此不会总是返回相同的 UUID:

SQL
select uuid_generate_v7('2024-10-15 01:02:03+00'::timestamptz);

一些注意事项:

  1. 在 ORDER BY 中按 UUID 排序,可以保留其编码时间戳的先后关系。这个辅助函数在同一毫秒内使用随机部分区分值,因此不保证同一毫秒内的严格生成顺序。

  2. 在最初生成的三个值中 (几秒钟内生成),存在一个共同前缀 018c1be3-e。在稍后生成的最后一个值中,有一个共同前缀 018c1be。

  3. 注意所有值中第二个连字符后的 7:

    Shell
    018c1be3-e973-7... 👈 this means v7
    
  4. 该函数返回 UUID 类型的值,因此仍然是 16 字节 (而它的文本表示需要 36 个字符,包括连接符,这意味着加上 VARLENA header 的话,总共需要 40 个字节):

PSQL
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:

SQL
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 可还原为以下时间:

SQL
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 应用时最常遇到的数据库性能问题。社区也在实现中。

讨论与补充

在 GitHub 查看讨论
其他历史讨论(1)

支持作者

感谢支持 PostgreSQL Howto 的长期维护。

微信赞赏码

读者交流群

二维码若已过期,可通过公众号 PostgreSQL 学徒或邮件联系。

PostgreSQL 学徒读者交流群二维码