Shaun Thomas:时间旅行者的主键

Shaun Thomas:时间旅行者的主键

💡 原文英文,约2500词,阅读约需10分钟。
📝

内容提要

Postgres 18 新增 UUID v7 函数,通过嵌入时间戳使键值有序,解决 v4 随机 UUID 导致的 B-tree 索引碎片化和性能下降问题。v7 索引更紧凑,减少存储占用,并支持提取时间戳。但需注意其泄露时间元数据及高并发下可能引发锁竞争,而 BIGINT 或 snowflake 等替代方案也值得考虑。

🔎

延伸解读

UUID v7 的索引优势与代价

文章通过实测对比显示,UUID v7 索引比 v4 小约 25%,叶节点密度从 71.53% 提升至 89.98%,碎片率从 49.89% 降至 0%。这是因为 v7 按时间排序,新键总落在索引右端,减少页分裂和缓存未命中。但代价是时间戳泄露和并发写入时的锁竞争,后者在分布式或高并发场景下可能成为瓶颈。

时间戳提取与元数据泄露风险

Postgres 18 提供 uuid_extract_timestamp() 函数,可直接从 UUID v7 中提取创建时间,使主键兼作 created_at 列。然而,这也意味着任何看到 UUID v7 的人都能推断出记录的创建时间和系统写入速率,可能泄露业务敏感信息。在需要隐藏时间信息的场景(如公开 API)中,UUID v4 的随机性反而成为优势。

替代方案:BIGINT 与 Snowflake

文章指出,对于单节点或无需分布式生成的场景,BIGINT 自增主键仍是更优选择,它不泄露时间戳且索引性能与 UUID v7 相当。而 pgEdge 的 snowflake 扩展将 BIGINT 重新利用,实现类似 v7 的有序性,但节点 ID 和时间的编码可能暴露集群拓扑,建议仅内部使用。选择哪种方案需权衡分布式需求、隐私和索引性能。

Q&A

Postgres 18 中新增的 UUID v7 函数有什么作用?

Postgres 18 新增了 uuidv7() 函数,用于生成 UUID v7 格式的标识符。UUID v7 在 UUID 的高位嵌入时间戳,使得生成的键值随时间有序递增,从而解决了 UUID v4 随机性导致的 B-tree 索引碎片化和性能下降问题。

为什么 UUID v4 会导致 B-tree 索引碎片化?

UUID v4 是随机生成的,插入时新值可能出现在索引的任何位置,导致每次插入都需要访问不同的叶子页,且经常需要分裂已满的页面,造成页面半空和碎片化,使索引膨胀,降低性能。

UUID v7 是如何实现有序性的?

UUID v7 的高 48 位存储 Unix 毫秒时间戳,并包含 12 位亚毫秒时间信息,使得新生成的 UUID 在时间上递增,从而在 B-tree 索引中按顺序插入,保持索引紧凑。

Postgres 18 中如何从 UUID v7 提取时间戳?

Postgres 18 提供了 uuid_extract_timestamp() 函数,可以从 UUID v7 中提取创建时间戳,例如:SELECT uuid_extract_timestamp(id) FROM table;

使用 UUID v7 有哪些潜在风险?

UUID v7 会泄露时间元数据,因为时间戳可被提取,可能暴露创建时间和插入速率;在高并发或分片环境中,有序插入可能导致索引最右页成为锁竞争热点。

与 UUID v7 相比,snowflake ID 有什么优缺点?

snowflake ID 使用 BIGINT 存储,索引更小,且不泄露时间戳(但可提取节点 ID)。缺点是节点 ID 位置可能导致跨节点插入时索引碎片化,且可能暴露集群拓扑。

在什么情况下应该使用 BIGINT 而不是 UUID?

当数据存储在单节点、主实例分配所有键、且键不会在应用或公开场景中流通时,使用 BIGINT GENERATED ALWAYS AS IDENTITY 更合适,因为它排序完美、不泄露元数据且索引紧凑。

UUID v7 相比 UUID v4 在索引大小上能节省多少?

根据文章中的测试,插入相同数据量,UUID v7 的索引大小为 30 MB,而 UUID v4 为 38 MB,节省约 25% 的索引空间。

🏷️

标签

➡️

继续阅读