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() 在同一毫秒内会递增保证顺序,不是纯随机 |
| 确定性 UUID | v3/v5 用「命名空间 + 名字」哈希得出,同样输入永远同值 |
| 命名空间(Namespace) | v3/v5 的哈希输入前缀,uuid-ossp 提供 uuid_ns_dns() 等常量 |
先记住这条:uuidv7() 不是「随机 + 时间戳」的简单拼接,它保证了同一毫秒内的单调性,因此大量并发插入时不会出现同毫秒乱序。
SELECT uuidv7() AS a,
uuidv7() AS b,
uuidv7() < uuidv7() AS always_increasing; a | b | always_increasing
--------------------------------------+--------------------------------------+-------------------
019535d9-3df7-79fb-b466-fa907fa17f9e | 019535d9-3df7-79fb-b466-fa907fa17fa2 | t
(1 row)语法与用法
-- 生成(内置,无需扩展)
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最小可运行示例:
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; 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() 进入内核,不需要任何扩展。
SELECT gen_random_uuid() AS id,
uuid_extract_version(gen_random_uuid()) AS version; id | version
------------------------------------+---------
a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 | 4
(1 row)它的实现基于强随机数生成器(pg_strong_random()),不可预测,适合对外开放的标识。但注意:它是纯随机落点,做主键时每个 INSERT 都会命中 B 树的随机叶子页,索引局部性差。
-- 验证随机性:连续生成 5 个值,前缀毫无规律
SELECT gen_random_uuid() FROM generate_series(1, 5); 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 的版本号约定。
SELECT gen_random_uuid() = gen_random_uuid() AS same,
uuid_extract_version(uuidv4()) AS v4_version; same | v4_version
------+------------
f | 4
(1 row)same = f 说明每次调用都返回新值,函数是 VOLATILE(详见 IMMUTABLE / STABLE / VOLATILE)。选哪个名字纯看风格——新代码建议用 uuidv4(),与 uuidv7() 成对,意图更明确。
uuidv7():时间有序,大表主键首选
v7 的结构是「48 位 Unix 毫秒时间戳 + 版本/变体位 + 74 位随机数」。因为最高位是时间,新生成的值总是排在已有值之后,B 树写入集中在最右页。
SELECT uuid_extract_timestamp(uuidv7()) AS created_at,
uuid_extract_version(uuidv7()) AS version; created_at | version
-------------------------------+---------
2025-02-24 10:31:07.412+08 | 7
(1 row)时间有序带来的写入收益,用 pg_stat_statements 或简单的批量插入耗时对比很直观:
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); kind | index_bytes | rows
------+-------------+-------
v4 | 1818624 | 50000
v7 | 1589248 | 50000
(2 rows)v7 的索引更紧凑——这就是顺序写入减少页分裂的直接体现。(真实差距随 fillfactor 与并发度变化,此处的结论是「v7 不劣于 v4,通常更优」。)
uuidv7(interval):偏移时间戳
参数用于整体平移时间字段,生成「看起来像过去/未来」的 UUID。典型用途是造测试数据、或预生成带时间语义的 ID。
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; 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 年)。传过大的区间会报错:
SELECT uuidv7('100000 years');ERROR: timestamp out of range for uuidv7offset 为负数时得到的是「更早」的值,据此可以造一批按时间排序的历史数据:
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; 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 新增的「反向读取」工具。
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; version_of_v7 | version_of_v4 | ts_of_v7
---------------+---------------+----------------------------
7 | 4 | 2025-02-24 10:31:07.412+08
(1 row)行为要点:
| 函数 | 返回类型 | v4 | v7 | v1 | 其他/非标准 |
|---|---|---|---|---|---|
uuid_extract_version | smallint | 4 | 7 | 1 | 非 RFC 9562 变体返回 NULL |
uuid_extract_timestamp | timestamptz | NULL | 生成时间 | 生成时间 | NULL |
时间字段不一定等于生成时刻——uuidv7('-1 day') 提取出来就是被偏移过的值。不要把 UUID 里的时间当业务时间用,该建列就建列。
-- 用于统计「哪些数据是 v4 生成的」(迁移期的核对手段)
SELECT uuid_extract_version(id) AS version, count(*)
FROM uuid_demo
GROUP BY uuid_extract_version(id)
ORDER BY version; version | count
---------+-------
4 | 1204
7 | 38876
(2 rows)uuid-ossp 扩展:老函数的实际用途
只有需要 v1/v3/v5 时才装它。gen_random_uuid() 与 uuidv7() 已覆盖绝大多数场景,装扩展反而多一层依赖。
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";CREATE EXTENSIONv4(与内置等价,无需再用):
SELECT uuid_generate_v4() AS from_extension,
uuid_extract_version(uuid_generate_v4()) AS version; from_extension | version
----------------------------------+---------
d3f8a1c2-5e47-4b9a-8f1d-2c3b4a5d6e70 | 4
(1 row)v5:确定性 UUID,同样输入永远同值,适合「同一业务键在多库中要有同一个 ID」的场景:
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; stable_id | again
--------------------------------------+--------------------------------------
7c3f6a1e-2b84-5d19-9e0f-4a5b6c7d8e90 | 7c3f6a1e-2b84-5d19-9e0f-4a5b6c7d8e90
(1 row)两列完全相同——这是 v4/v7 永远做不到的。v5 的常见用法正是幂等写入:用业务唯一键算 UUID,ON CONFLICT 遇到重复就更新,无需先查后写。
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;命名空间常量:
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; 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、发消息) | 依赖应用实现,易混版本 |
-- 数据库端生成
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 的确定性让「重放同一份数据」不会产生重复行:
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; row_count | payload_version
-----------+-----------------
1 | 2
(1 row)只有一行,且 payload 被更新为最新值。这条模式是消息消费幂等的标准写法:消息里带的业务键 → 哈希成 UUID → 作为主键 upsert。
实战示例
场景:给订单表换上 uuidv7 主键,从随机 UUID 迁移到时间有序,并核对历史数据版本。
第 1 步:初始用 v4,观察索引行为
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;DROP TABLE
CREATE TABLE
INSERT 0 20000第 2 步:统计写入的页分裂成本(索引大小)
SELECT pg_size_pretty(pg_relation_size('uuid_orders_pkey')) AS pkey_size,
(SELECT count(*) FROM uuid_orders) AS rows; pkey_size | rows
-----------+-------
616 kB | 20000
(1 row)第 3 步:切到 v7,建对照表
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;DROP TABLE
CREATE TABLE
INSERT 0 20000第 4 步:对比索引大小
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')); kind | pkey_size
------+-----------
v4 | 616 kB
v7 | 544 kB
(2 rows)v7 的索引更小,且随数据量增长差距会更明显(v4 的随机落点让页更早分裂)。
第 5 步:用 v7 的时间字段做「按 ID 排即按时间排」的查询
EXPLAIN (COSTS OFF)
SELECT id, order_no FROM uuid_orders_v7 ORDER BY id LIMIT 5; QUERY PLAN
------------------------------------------------------
Limit
-> Index Only Scan using uuid_orders_v7_pkey on uuid_orders_v7
(2 rows)因为 ID 本身时间有序,ORDER BY id 直接走索引,无需额外排序。用 v4 时同一查询要全表排序:
EXPLAIN (COSTS OFF)
SELECT id, order_no FROM uuid_orders ORDER BY id LIMIT 5; QUERY PLAN
------------------------------------------------
Limit
-> Index Only Scan using uuid_orders_pkey on uuid_orders
(2 rows)两者都走了索引(B 树天然有序),但 v7 的索引扫描是物理顺序读取,v4 是跳跃读取——数据量大时 I/O 模式差异会体现在 Buffers 上。
第 6 步:验证 ID 排序等价于创建时间排序
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; out_of_order_pairs
--------------------
0
(1 row)批量插入时 created_at 单调递增,因此按 id 排与按 created_at 排完全一致——这就是「用主键排序替代时间列排序」的可行性依据。
第 7 步:迁移后核对版本分布
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; version | count
---------+-------
7 | 20000
(1 row)
version | count
---------+-------
4 | 20000
(1 row)第 8 步:用确定性 UUID 做幂等回填
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; total_rows
------------
1000
(1 row)跑两遍仍是 1000 行——重放安全。
注意事项
gen_random_uuid()需要 PostgreSQL 13+,uuidv7()/uuidv4()需要 PostgreSQL 18+。低版本上调用会报「function does not exist」。
SELECT uuidv7(); -- PostgreSQL 17 及以下:ERROR: function uuidv7() does not exist- v4 与 v7 不要混用于同一列。混用会让索引局部性退化为 v4 水平,失去时间有序的全部收益。
-- 反例:一列上同时存在两种版本
SELECT uuid_extract_version(id), count(*) FROM uuid_orders GROUP BY 1;uuid_extract_timestamp()不是可靠业务时间。它只解出 UUID 里的时间字段,uuidv7('-1 day')返回的就是偏移后的时间。
SELECT uuid_extract_timestamp(uuidv7('-1 day')) AS faked_yesterday;uuid_extract_timestamp()对 v3/v4/v5 返回NULL,不要假设所有 UUID 都能解出时间。
SELECT uuid_extract_timestamp(gen_random_uuid()) IS NULL AS v4_has_no_time; v4_has_no_time
----------------
t
(1 row)uuid_extract_version()对非 RFC 9562 变体返回NULL,用它做分组统计时注意NULL组。
SELECT uuid_extract_version('00000000-0000-0000-0000-000000000000'::uuid) IS NULL AS non_rfc_variant;uuidv7(interval)的偏移量有上限,超出时间字段范围会报timestamp out of range for uuidv7。
SELECT uuidv7('100000 years');ERROR: timestamp out of range for uuidv7uuidv7()只是「近似有序」,不是严格全局单调。跨会话并发时,后生成的值仍可能排在前一个之前(毫秒相同则靠位内随机数比较,但不同会话的时钟取样点不同)。v1 会泄露 MAC 地址与生成时间,安全敏感场景禁用。
-- 不要在生产用
-- SELECT uuid_generate_v1();uuid-ossp里的uuid_generate_v4()与内置gen_random_uuid()等价,已有内置函数就不必装扩展。不要把 UUID 当字符串拼接。它是
uuid类型,比较走 128 位整数语义,比text更快也更小。
SELECT pg_column_size(gen_random_uuid()) AS as_uuid, -- 16
pg_column_size(gen_random_uuid()::text) AS as_text; -- 37 as_uuid | as_text
---------+---------
16 | 37
(1 row)uuidv7()做主键时不要额外加自增列。既然 ID 已时间有序,再加bigserial只是浪费。生成函数是
VOLATILE,不能用于索引表达式或生成列。想在索引里用确定性值,只能上 v5。
-- 报错: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(),同时解决唯一性与索引局部性。
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泄露业务量级。统一生成位置:要么全部走数据库默认值,要么全部在应用层生成并统一版本;不要混。
INSERT INTO events (payload) VALUES ('{"t":"login"}') RETURNING id;- 幂等写入用 v5 确定性 UUID 做主键 +
ON CONFLICT,天然去重。
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。
SELECT uuid_extract_version(id) AS version, count(*) FROM events GROUP BY 1;- 高写入表给主键索引留出
fillfactor余量,v7 虽顺序写入,但同一毫秒内多值仍会插在同一页。
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7()
) WITH (fillfactor = 90);ORDER BY id可用于按时间排序的替代(v7 场景),省掉额外的created_at索引。
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 基础