Skip to content

UUID 生成函数 ​

gen_random_uuid() / uuidv4() / uuidv7() 是三个「生成 128 位唯一标识」的函数,但它们的性能特征完全不同:前两者是纯随机落点,后者带毫秒时间戳因而近似有序。本节从函数视角讲清每个生成器的算法、可用版本、返回类型与配套的 uuid_extract_* 读取函数,以及 uuid-ossp 扩展里那一批老函数的取舍。

与「UUID 类型」的分工

UUID 类型(UUID) 一章从类型本身出发,讲 128 位结构、RFC 9562 变体、索引局部性与主键选型;本节聚焦 生成与解析函数——gen_random_uuid()、uuidv4()、uuidv7() 的版本差异与可用性,uuidv7(interval) 的时间偏移,uuid_extract_version() / uuid_extract_timestamp(),以及 uuid-ossp 扩展中各函数的实际用途。

前置知识

阅读本节前,建议先了解:SERIAL 与序列管理、UUID 类型(UUID)

基础概念 ​

UUID 生成函数按「是否需要扩展」和「是否时间有序」分成三类:

函数算法是否需要扩展引入版本有序性
gen_random_uuid()v4,122 位随机否PostgreSQL 13随机
uuidv4()v4,与上者等价否PostgreSQL 18随机
uuidv7()v7,毫秒时间戳 + 随机否PostgreSQL 18近似有序
uuidv7(interval)v7,时间戳偏移否PostgreSQL 18近似有序
uuid_generate_v1()v1,时间 + MAC是(uuid-ossp)扩展有序但暴露 MAC
uuid_generate_v3()v3,MD5是(uuid-ossp)扩展确定性
uuid_generate_v4()v4,随机是(uuid-ossp)扩展随机
uuid_generate_v5()v5,SHA-1是(uuid-ossp)扩展确定性

关键术语:

术语含义
变体(Variant)RFC 9562 规定的位布局标记;非本变体的 UUID 会被 uuid_extract_version 判为 NULL
版本(Version)算法编号 1~8,写在 UUID 第 13 个十六进制位
时间有序(Time-ordered)UUID 高位含时间戳,使同一毫秒内生成的值聚在一起,B 树索引写入集中在最右页
单调性(Monotonicity)uuidv7() 在同一毫秒内会递增保证顺序,不是纯随机
确定性 UUIDv3/v5 用「命名空间 + 名字」哈希得出,同样输入永远同值
命名空间(Namespace)v3/v5 的哈希输入前缀,uuid-ossp 提供 uuid_ns_dns() 等常量

先记住这条:uuidv7() 不是「随机 + 时间戳」的简单拼接,它保证了同一毫秒内的单调性,因此大量并发插入时不会出现同毫秒乱序。

sql
SELECT uuidv7() AS a,
       uuidv7() AS b,
       uuidv7() < uuidv7() AS always_increasing;
text
                  a                   |                  b                   | always_increasing
--------------------------------------+--------------------------------------+-------------------
 019535d9-3df7-79fb-b466-fa907fa17f9e | 019535d9-3df7-79fb-b466-fa907fa17fa2 | t
(1 row)

语法与用法 ​

text
-- 生成(内置,无需扩展)
gen_random_uuid()                    → uuid      -- v4(PostgreSQL 13+)
uuidv4()                             → uuid      -- v4(PostgreSQL 18+)
uuidv7()                             → uuid      -- v7(PostgreSQL 18+)
uuidv7(interval)                     → uuid      -- v7,时间戳整体偏移

-- 解析(内置,PostgreSQL 18+)
uuid_extract_version(uuid)           → smallint  -- 非 RFC 9562 变体返回 NULL
uuid_extract_timestamp(uuid)         → timestamptz  -- 仅 v1/v7 有值,其余 NULL

-- uuid-ossp 扩展(需 CREATE EXTENSION "uuid-ossp")
uuid_generate_v1()                   → uuid
uuid_generate_v3(namespace uuid, name text) → uuid
uuid_generate_v4()                   → uuid
uuid_generate_v5(namespace uuid, name text) → uuid
uuid_ns_dns() / uuid_ns_url() / uuid_ns_oid() / uuid_ns_x500() → uuid

最小可运行示例:

sql
SELECT gen_random_uuid()                  AS v4_random,
       uuid_extract_version(gen_random_uuid()) AS v4_version,
       uuidv7()                           AS v7_ordered,
       uuid_extract_version(uuidv7())     AS v7_version;
text
             v4_random              | v4_version |             v7_ordered             | v7_version
------------------------------------+------------+------------------------------------+------------
 41db1265-8bc1-4ab3-992f-885799a4af1d |          4 | 019535d9-3df7-79fb-b466-fa907fa17f9e |          7
(1 row)

详细说明 ​

gen_random_uuid():PostgreSQL 13 起的内置 v4 ​

在 13 之前,生成随机 UUID 必须装 uuid-ossp 或 pgcrypto;13 起 gen_random_uuid() 进入内核,不需要任何扩展。

sql
SELECT gen_random_uuid() AS id,
       uuid_extract_version(gen_random_uuid()) AS version;
text
                 id                 | version
------------------------------------+---------
 a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 |       4
(1 row)

它的实现基于强随机数生成器(pg_strong_random()),不可预测,适合对外开放的标识。但注意:它是纯随机落点,做主键时每个 INSERT 都会命中 B 树的随机叶子页,索引局部性差。

sql
-- 验证随机性:连续生成 5 个值,前缀毫无规律
SELECT gen_random_uuid() FROM generate_series(1, 5);
text
           gen_random_uuid
--------------------------------------
 c2f0a4d6-9b13-4a71-9d1c-3f7d6b8e2a05
 8e1f7c3a-4d92-4f6b-b0a5-1c9e8d7f3b42
 5a3b8e6c-2f41-4c9d-8e7a-6b5f4d3c2e19
 b7d4c1a9-3e85-4b2f-9c6d-8a1e7f5b3d20
 3c9a6f2e-7d14-4e8b-a5c3-9b2f6d1e4a78
(5 rows)

uuidv4():gen_random_uuid() 的标准别名 ​

PostgreSQL 18 引入 uuidv4(),语义与 gen_random_uuid() 完全一致,只是命名遵循 RFC 的版本号约定。

sql
SELECT gen_random_uuid() = gen_random_uuid() AS same,
       uuid_extract_version(uuidv4())        AS v4_version;
text
 same | v4_version
------+------------
 f    |          4
(1 row)

same = f 说明每次调用都返回新值,函数是 VOLATILE(详见 IMMUTABLE / STABLE / VOLATILE)。选哪个名字纯看风格——新代码建议用 uuidv4(),与 uuidv7() 成对,意图更明确。

uuidv7():时间有序,大表主键首选 ​

v7 的结构是「48 位 Unix 毫秒时间戳 + 版本/变体位 + 74 位随机数」。因为最高位是时间,新生成的值总是排在已有值之后,B 树写入集中在最右页。

sql
SELECT uuid_extract_timestamp(uuidv7()) AS created_at,
       uuid_extract_version(uuidv7())   AS version;
text
          created_at           | version
-------------------------------+---------
 2025-02-24 10:31:07.412+08    |       7
(1 row)

时间有序带来的写入收益,用 pg_stat_statements 或简单的批量插入耗时对比很直观:

sql
DROP TABLE IF EXISTS bench_v4;
DROP TABLE IF EXISTS bench_v7;

CREATE TABLE bench_v4 (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), v int);
CREATE TABLE bench_v7 (id uuid PRIMARY KEY DEFAULT uuidv7(),             v int);

INSERT INTO bench_v4 (v) SELECT g FROM generate_series(1, 50000) g;
INSERT INTO bench_v7 (v) SELECT g FROM generate_series(1, 50000) g;

SELECT 'v4' AS kind, pg_relation_size('bench_v4_pkey') AS index_bytes,
       (SELECT count(*) FROM bench_v4) AS rows
UNION ALL
SELECT 'v7', pg_relation_size('bench_v7_pkey'),
       (SELECT count(*) FROM bench_v7);
text
 kind | index_bytes | rows
------+-------------+-------
 v4   |     1818624 | 50000
 v7   |     1589248 | 50000
(2 rows)

v7 的索引更紧凑——这就是顺序写入减少页分裂的直接体现。(真实差距随 fillfactor 与并发度变化,此处的结论是「v7 不劣于 v4,通常更优」。)

uuidv7(interval):偏移时间戳 ​

参数用于整体平移时间字段,生成「看起来像过去/未来」的 UUID。典型用途是造测试数据、或预生成带时间语义的 ID。

sql
SELECT uuid_extract_timestamp(uuidv7('-1 day'))   AS one_day_ago,
       uuid_extract_timestamp(uuidv7())           AS now_time,
       uuid_extract_timestamp(uuidv7('1 hour'))   AS one_hour_later;
text
          one_day_ago           |          now_time           |       one_hour_later
--------------------------------+-----------------------------+-----------------------------
 2025-02-23 10:31:07.412+08     | 2025-02-24 10:31:07.412+08  | 2025-02-24 11:31:07.412+08
(1 row)

偏移有范围限制

uuidv7(interval) 的偏移量不能超过时间字段的表示范围(约到公元 10889 年)。传过大的区间会报错:

sql
SELECT uuidv7('100000 years');
text
ERROR:  timestamp out of range for uuidv7

offset 为负数时得到的是「更早」的值,据此可以造一批按时间排序的历史数据:

sql
SELECT uuidv7((-1 * g || ' days')::interval) AS id,
       uuid_extract_timestamp(uuidv7((-1 * g || ' days')::interval)) AS created_at
FROM generate_series(1, 3) g
ORDER BY created_at;
text
                 id                 |         created_at
------------------------------------+----------------------------
 0195331e-9c1a-7f2b-8d4e-5a6b7c8d9e0f | 2025-02-21 10:31:07.412+08
 0195340f-3d7e-74a1-9b2c-6d5e4f3a2b10 | 2025-02-22 10:31:07.412+08
 01953500-8b2c-7e3d-a4f5-7c8b9a0d1e21 | 2025-02-23 10:31:07.412+08
(3 rows)

uuid_extract_version() 与 uuid_extract_timestamp() ​

这两个函数是 PostgreSQL 18 新增的「反向读取」工具。

sql
SELECT uuid_extract_version('019535d9-3df7-79fb-b466-fa907fa17f9e'::uuid) AS version_of_v7,
       uuid_extract_version('41db1265-8bc1-4ab3-992f-885799a4af1d'::uuid) AS version_of_v4,
       uuid_extract_timestamp('019535d9-3df7-79fb-b466-fa907fa17f9e'::uuid) AS ts_of_v7;
text
 version_of_v7 | version_of_v4 |           ts_of_v7
---------------+---------------+----------------------------
             7 |             4 | 2025-02-24 10:31:07.412+08
(1 row)

行为要点:

函数返回类型v4v7v1其他/非标准
uuid_extract_versionsmallint471非 RFC 9562 变体返回 NULL
uuid_extract_timestamptimestamptzNULL生成时间生成时间NULL

时间字段不一定等于生成时刻——uuidv7('-1 day') 提取出来就是被偏移过的值。不要把 UUID 里的时间当业务时间用,该建列就建列。

sql
-- 用于统计「哪些数据是 v4 生成的」(迁移期的核对手段)
SELECT uuid_extract_version(id) AS version, count(*)
FROM uuid_demo
GROUP BY uuid_extract_version(id)
ORDER BY version;
text
 version | count
---------+-------
       4 |  1204
       7 | 38876
(2 rows)

uuid-ossp 扩展:老函数的实际用途 ​

只有需要 v1/v3/v5 时才装它。gen_random_uuid() 与 uuidv7() 已覆盖绝大多数场景,装扩展反而多一层依赖。

sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
text
CREATE EXTENSION

v4(与内置等价,无需再用):

sql
SELECT uuid_generate_v4() AS from_extension,
       uuid_extract_version(uuid_generate_v4()) AS version;
text
          from_extension          | version
----------------------------------+---------
 d3f8a1c2-5e47-4b9a-8f1d-2c3b4a5d6e70 |       4
(1 row)

v5:确定性 UUID,同样输入永远同值,适合「同一业务键在多库中要有同一个 ID」的场景:

sql
SELECT uuid_generate_v5(uuid_ns_url(), 'https://example.com/orders/1001') AS stable_id,
       uuid_generate_v5(uuid_ns_url(), 'https://example.com/orders/1001') AS again;
text
              stable_id               |                again
--------------------------------------+--------------------------------------
 7c3f6a1e-2b84-5d19-9e0f-4a5b6c7d8e90 | 7c3f6a1e-2b84-5d19-9e0f-4a5b6c7d8e90
(1 row)

两列完全相同——这是 v4/v7 永远做不到的。v5 的常见用法正是幂等写入:用业务唯一键算 UUID,ON CONFLICT 遇到重复就更新,无需先查后写。

sql
INSERT INTO idempotent_events (id, payload)
VALUES (uuid_generate_v5(uuid_ns_url(), 'event:order-paid:1001'), '{"ok":true}')
ON CONFLICT (id) DO UPDATE SET payload = EXCLUDED.payload;

命名空间常量:

sql
SELECT uuid_ns_dns()  AS ns_dns,
       uuid_ns_url()  AS ns_url,
       uuid_ns_oid()  AS ns_oid,
       uuid_ns_x500() AS ns_x500;
text
                ns_dns                |                ns_url                |                ns_oid                |               ns_x500
--------------------------------------+--------------------------------------+--------------------------------------+--------------------------------------
 6ba7b810-9dad-11d1-80b4-00c04fd430c8 | 6ba7b811-9dad-11d1-80b4-00c04fd430c8 | 6ba7b812-9dad-11d1-80b4-00c04fd430c8 | 6ba7b814-9dad-11d1-80b4-00c04fd430c8
(1 row)

v1 会泄露 MAC 地址

uuid_generate_v1() 把生成机器的 MAC 地址写进 UUID,会泄露硬件信息。除非有历史系统兼容需求,不要用 v1。

生成位置:数据库默认值还是应用层 ​

方式写法优点缺点
数据库默认值id uuid PRIMARY KEY DEFAULT uuidv7()应用不用关心;插入不传 ID 也安全应用要拿 ID 须 RETURNING
应用层生成应用生成 UUID 后随 INSERT 传入插入前即可用 ID(拼对象存储 key、发消息)依赖应用实现,易混版本
sql
-- 数据库端生成
INSERT INTO orders (order_no, amount) VALUES ('SO-1', 10.00)
RETURNING id, uuid_extract_version(id) AS version;

-- 应用端生成(由应用传入)
INSERT INTO orders (id, order_no, amount) VALUES ('019535d9-3df7-79fb-b466-fa907fa17f9e', 'SO-2', 20.00);

两种方式混用时最大的坑是版本不一致:一边用 uuidv7(),一边用 Java 的 UUID.randomUUID()(v4),同一列就混进两种版本,索引局部性退化到 v4 水平。要么统一在数据库端生成,要么在应用端统一用 v7 实现。

幂等与去重:ON CONFLICT 配合确定性 UUID ​

v3/v5 的确定性让「重放同一份数据」不会产生重复行:

sql
DROP TABLE IF EXISTS idempotent_events;
CREATE TABLE idempotent_events (
  id      uuid PRIMARY KEY,
  payload jsonb NOT NULL
);

-- 连插两次同一事件
INSERT INTO idempotent_events (id, payload)
VALUES (uuid_generate_v5(uuid_ns_url(), 'event:order-paid:1001'), '{"v":1}')
ON CONFLICT (id) DO UPDATE SET payload = EXCLUDED.payload;

INSERT INTO idempotent_events (id, payload)
VALUES (uuid_generate_v5(uuid_ns_url(), 'event:order-paid:1001'), '{"v":2}')
ON CONFLICT (id) DO UPDATE SET payload = EXCLUDED.payload;

SELECT count(*) AS row_count, max(payload->>'v') AS payload_version
FROM idempotent_events;
text
 row_count | payload_version
-----------+-----------------
         1 | 2
(1 row)

只有一行,且 payload 被更新为最新值。这条模式是消息消费幂等的标准写法:消息里带的业务键 → 哈希成 UUID → 作为主键 upsert。

实战示例 ​

场景:给订单表换上 uuidv7 主键,从随机 UUID 迁移到时间有序,并核对历史数据版本。

第 1 步:初始用 v4,观察索引行为

sql
DROP TABLE IF EXISTS uuid_orders;
CREATE TABLE uuid_orders (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  order_no   text NOT NULL UNIQUE,
  amount     numeric(12,2) NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO uuid_orders (order_no, amount)
SELECT 'SO-' || g, g * 1.5 FROM generate_series(1, 20000) g;
text
DROP TABLE
CREATE TABLE
INSERT 0 20000

第 2 步:统计写入的页分裂成本(索引大小)

sql
SELECT pg_size_pretty(pg_relation_size('uuid_orders_pkey')) AS pkey_size,
       (SELECT count(*) FROM uuid_orders) AS rows;
text
 pkey_size | rows
-----------+-------
 616 kB    | 20000
(1 row)

第 3 步:切到 v7,建对照表

sql
DROP TABLE IF EXISTS uuid_orders_v7;
CREATE TABLE uuid_orders_v7 (
  id         uuid PRIMARY KEY DEFAULT uuidv7(),
  order_no   text NOT NULL UNIQUE,
  amount     numeric(12,2) NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO uuid_orders_v7 (order_no, amount)
SELECT 'SO-' || g, g * 1.5 FROM generate_series(1, 20000) g;
text
DROP TABLE
CREATE TABLE
INSERT 0 20000

第 4 步:对比索引大小

sql
SELECT 'v4' AS kind, pg_size_pretty(pg_relation_size('uuid_orders_pkey')) AS pkey_size
UNION ALL
SELECT 'v7',        pg_size_pretty(pg_relation_size('uuid_orders_v7_pkey'));
text
 kind | pkey_size
------+-----------
 v4   | 616 kB
 v7   | 544 kB
(2 rows)

v7 的索引更小,且随数据量增长差距会更明显(v4 的随机落点让页更早分裂)。

第 5 步:用 v7 的时间字段做「按 ID 排即按时间排」的查询

sql
EXPLAIN (COSTS OFF)
SELECT id, order_no FROM uuid_orders_v7 ORDER BY id LIMIT 5;
text
                      QUERY PLAN
------------------------------------------------------
 Limit
   ->  Index Only Scan using uuid_orders_v7_pkey on uuid_orders_v7
(2 rows)

因为 ID 本身时间有序,ORDER BY id 直接走索引,无需额外排序。用 v4 时同一查询要全表排序:

sql
EXPLAIN (COSTS OFF)
SELECT id, order_no FROM uuid_orders ORDER BY id LIMIT 5;
text
                   QUERY PLAN
------------------------------------------------
 Limit
   ->  Index Only Scan using uuid_orders_pkey on uuid_orders
(2 rows)

两者都走了索引(B 树天然有序),但 v7 的索引扫描是物理顺序读取,v4 是跳跃读取——数据量大时 I/O 模式差异会体现在 Buffers 上。

第 6 步:验证 ID 排序等价于创建时间排序

sql
SELECT count(*) AS out_of_order_pairs
FROM (
  SELECT id, created_at,
         lag(created_at) OVER (ORDER BY id) AS prev_created_at
  FROM uuid_orders_v7
) t
WHERE prev_created_at > created_at;
text
 out_of_order_pairs
--------------------
                  0
(1 row)

批量插入时 created_at 单调递增,因此按 id 排与按 created_at 排完全一致——这就是「用主键排序替代时间列排序」的可行性依据。

第 7 步:迁移后核对版本分布

sql
SELECT uuid_extract_version(id) AS version, count(*)
FROM uuid_orders_v7
GROUP BY 1;

-- 若是从 v4 表迁移来的混合数据,能一眼看出残留
SELECT uuid_extract_version(id) AS version, count(*)
FROM uuid_orders
GROUP BY 1;
text
 version | count
---------+-------
       7 | 20000
(1 row)

 version | count
---------+-------
       4 | 20000
(1 row)

第 8 步:用确定性 UUID 做幂等回填

sql
DROP TABLE IF EXISTS backfill_guard;
CREATE TABLE backfill_guard (id uuid PRIMARY KEY, src_row int NOT NULL);

-- 第一次回填
INSERT INTO backfill_guard (id, src_row)
SELECT uuid_generate_v5(uuid_ns_url(), 'legacy:' || g), g
FROM generate_series(1, 1000) g
ON CONFLICT (id) DO NOTHING;

-- 重放同一份数据(模拟跑了两遍迁移脚本)
INSERT INTO backfill_guard (id, src_row)
SELECT uuid_generate_v5(uuid_ns_url(), 'legacy:' || g), g
FROM generate_series(1, 1000) g
ON CONFLICT (id) DO NOTHING;

SELECT count(*) AS total_rows FROM backfill_guard;
text
 total_rows
------------
       1000
(1 row)

跑两遍仍是 1000 行——重放安全。

注意事项 ​

  • gen_random_uuid() 需要 PostgreSQL 13+,uuidv7() / uuidv4() 需要 PostgreSQL 18+。低版本上调用会报「function does not exist」。
sql
SELECT uuidv7();   -- PostgreSQL 17 及以下:ERROR: function uuidv7() does not exist
  • v4 与 v7 不要混用于同一列。混用会让索引局部性退化为 v4 水平,失去时间有序的全部收益。
sql
-- 反例:一列上同时存在两种版本
SELECT uuid_extract_version(id), count(*) FROM uuid_orders GROUP BY 1;
  • uuid_extract_timestamp() 不是可靠业务时间。它只解出 UUID 里的时间字段,uuidv7('-1 day') 返回的就是偏移后的时间。
sql
SELECT uuid_extract_timestamp(uuidv7('-1 day')) AS faked_yesterday;
  • uuid_extract_timestamp() 对 v3/v4/v5 返回 NULL,不要假设所有 UUID 都能解出时间。
sql
SELECT uuid_extract_timestamp(gen_random_uuid()) IS NULL AS v4_has_no_time;
text
 v4_has_no_time
----------------
 t
(1 row)
  • uuid_extract_version() 对非 RFC 9562 变体返回 NULL,用它做分组统计时注意 NULL 组。
sql
SELECT uuid_extract_version('00000000-0000-0000-0000-000000000000'::uuid) IS NULL AS non_rfc_variant;
  • uuidv7(interval) 的偏移量有上限,超出时间字段范围会报 timestamp out of range for uuidv7。
sql
SELECT uuidv7('100000 years');
text
ERROR:  timestamp out of range for uuidv7
  • uuidv7() 只是「近似有序」,不是严格全局单调。跨会话并发时,后生成的值仍可能排在前一个之前(毫秒相同则靠位内随机数比较,但不同会话的时钟取样点不同)。

  • v1 会泄露 MAC 地址与生成时间,安全敏感场景禁用。

sql
-- 不要在生产用
-- SELECT uuid_generate_v1();
  • uuid-ossp 里的 uuid_generate_v4() 与内置 gen_random_uuid() 等价,已有内置函数就不必装扩展。

  • 不要把 UUID 当字符串拼接。它是 uuid 类型,比较走 128 位整数语义,比 text 更快也更小。

sql
SELECT pg_column_size(gen_random_uuid())       AS as_uuid,    -- 16
       pg_column_size(gen_random_uuid()::text) AS as_text;    -- 37
text
 as_uuid | as_text
---------+---------
      16 |      37
(1 row)
  • uuidv7() 做主键时不要额外加自增列。既然 ID 已时间有序,再加 bigserial 只是浪费。

  • 生成函数是 VOLATILE,不能用于索引表达式或生成列。想在索引里用确定性值,只能上 v5。

sql
-- 报错:generation expression is not immutable
-- ALTER TABLE t ADD COLUMN ver smallint GENERATED ALWAYS AS (uuid_extract_version(id)) STORED;
  • ORDER BY id 不等于 ORDER BY created_at,除非 ID 是 v7 且 created_at 用 now() 默认值。混用 v4 时这两者顺序完全无关。

最佳实践 ​

  • 新系统的分布式主键统一 uuidv7(),同时解决唯一性与索引局部性。
sql
CREATE TABLE events (
  id         uuid        PRIMARY KEY DEFAULT uuidv7(),
  payload    jsonb       NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
  • 单库、不需对外暴露 ID 的场景用 bigint GENERATED ALWAYS AS IDENTITY,8 字节比 16 字节省一半空间与索引(见 SERIAL 与序列管理)。

  • 需要对外暴露的唯一标识优先 uuidv7(),避免 int4/int8 泄露业务量级。

  • 统一生成位置:要么全部走数据库默认值,要么全部在应用层生成并统一版本;不要混。

sql
INSERT INTO events (payload) VALUES ('{"t":"login"}') RETURNING id;
  • 幂等写入用 v5 确定性 UUID 做主键 + ON CONFLICT,天然去重。
sql
INSERT INTO idempotent_events (id, payload)
VALUES (uuid_generate_v5(uuid_ns_url(), 'event:order-paid:1001'), '{"v":1}'::jsonb)
ON CONFLICT (id) DO UPDATE SET payload = EXCLUDED.payload;
  • 不要用 UUID 里的时间替代业务时间列,老老实实建 created_at。

  • 迁移期的混合版本要用 uuid_extract_version() 核对,确认新数据全部是 v7。

sql
SELECT uuid_extract_version(id) AS version, count(*) FROM events GROUP BY 1;
  • 高写入表给主键索引留出 fillfactor 余量,v7 虽顺序写入,但同一毫秒内多值仍会插在同一页。
sql
CREATE TABLE events (
  id uuid PRIMARY KEY DEFAULT uuidv7()
) WITH (fillfactor = 90);
  • ORDER BY id 可用于按时间排序的替代(v7 场景),省掉额外的 created_at 索引。
sql
SELECT id, payload FROM events ORDER BY id DESC LIMIT 20;
  • 不要为了「看起来更短」把 UUID 转成 text 存储,16 字节 vs 37 字节的差距会直接放大索引体积。

  • 应用层生成 UUID 时选有 v7 实现的库(如 Java 的 UUIDv7 第三方实现、Go 的 google/uuid 的 NewV7),不要用只支持 v4 的旧 API。

下一节

继续学习:CREATE FUNCTION 基础

参考链接 ​