编辑
2024-07-16
💻数据库
00
请注意,本文编写于 338 天前,最后修改于 225 天前,其中某些信息可能已经过时。

目录

UUID 的 Postgres 数据类型
UUID 和 B 树索引
UUID v7 如何影响 INSERT 性能
概括

UUID 通常用作数据库表主键。它们易于生成,易于在分布式系统之间共享并保证唯一性。

考虑到 UUID 的大小,这是否是一个正确的选择值得怀疑,但通常这不是由我们决定的。

本文的重点不是“UUID 是否是键的正确格式”,而是如何有效地使用 UUID 作为 PostgreSQL 的主键。

UUID 的 Postgres 数据类型

UUID 可以被视为一个字符串,并且可能很容易将它们存储为字符串。 Postgres 具有用于存储字符串的灵活数据类型: text ,并且通常用作存储 UUID 值的主键。

它是正确的数据类型吗?当然不。

Postgres 有一个专用于 UUID 的数据类型: uuid 。 UUID 是 128 位数据类型,因此存储单个值需要 16 个字节。 text 数据类型有 1 或 4 个字节的开销加上存储实际的字符串。

这些差异在小表中并不那么重要,但一旦开始存储数十万或数百万行,就会成为问题。

我进行了一个实验,看看实践中有何不同。有两个表只有一列 - id 作为主键。第一个表使用 text ,第二个表使用 uuid :

sql
create table bank_transfer( id text primary key ); create table bank_transfer_uuid( id uuid primary key );

我没有指定主键索引的类型,因此 Postgres 使用默认的 B 树。

向每个表插入 10 000 000 行:

我运行查询来查找表大小和索引大小:

sql
select relname as "table", indexrelname as "index", pg_size_pretty(pg_relation_size(relid)) "table size", pg_size_pretty(pg_relation_size(indexrelid)) "index size" from pg_stat_all_indexes where relname not like 'pg%'; +------------------+-----------------------+----------+----------+ |table |index |table size|index size| +------------------+-----------------------+----------+----------+ |bank_transfer_uuid|bank_transfer_uuid_pkey|422 MB |394 MB | |bank_transfer |bank_transfer_pkey |651 MB |730 MB | +------------------+-----------------------+----------+----------+

使用 text 的表增大了 54%,索引大小增大了 85%。这也反映在 Postgres 用于存储这些表和索引的页数上:

sql
select relname, relpages from pg_class where relname like 'bank_transfer%'; +-----------------------+--------+ |relname |relpages| +-----------------------+--------+ |bank_transfer |83334 | |bank_transfer_pkey |85498 | |bank_transfer_uuid |54055 | |bank_transfer_uuid_pkey|50463 | +-----------------------+--------+

更大的表、索引和更多的表意味着 Postgres 必须执行插入新行和获取行的工作 - 特别是当索引大小大于可用 RAM 内存时,Postgres 必须从磁盘加载索引。

UUID 和 B 树索引

随机 UUID 不太适合 B 树索引 - 并且 B 树索引是主键唯一可用的索引类型。

B 树索引最适合处理有序值 - 例如自动递增列或时间排序列。

UUID - 尽管看起来总是相似 - 有多种变体。 Java 的 UUID.randomUUID() - 返回 UUID v4 - 这是一个伪随机值。对我们来说,更有趣的是 UUID v7 - 它生成按时间排序的值。这意味着每生成一个新的UUID v7,它就有一个更大的值。这使得它非常适合 B 树索引。

python如何生成uuid v7:

python
import uuid def generate_uuid_v7(): return uuid.uuid7() if __name__ == "__main__": # 生成 UUID v7 uuid_v7 = generate_uuid_v7() print(f"生成的 UUID v7: {uuid_v7}")

运行上述代码后,输出可能类似于:

生成的 UUID v7: 7b2fa7a6-0dd8-7e43-b27e-9e735f8d5e60

UUID v7 如何影响 INSERT 性能

我创建了另一个表,与 bank_transfer_uuid 完全相同,但它将仅存储使用上述库生成的 UUID v7:

sql
create table bank_transfer_uuid_v7( id uuid primary key );

然后,我运行 10 轮,向每个表插入 10000 行,并测量需要多长时间:

结果看起来有点随机,尤其是在比较具有常规 text 列和 uuid v4 的表的时间时:

sql
+-------+-------+---------+ | text | uuid | uuid v7 | +-------+-------+---------+ | 7428 | 8584 | 3398 | | 5611 | 4966 | 3654 | | 13849 | 10398 | 3771 | | 6585 | 7624 | 3679 | | 6131 | 5142 | 3861 | | 6199 | 10336 | 3722 | | 6764 | 6039 | 3644 | | 9053 | 5515 | 3621 | | 6134 | 5367 | 3706 | | 11058 | 5551 | 3850 | +-------+-------+---------+

但我们可以清楚地看到,插入 UUID v7 比插入常规 UUID v4 快约 2 倍。

概括

正如一开始提到的 - 由于 UUID 长度 - 即使进行了所有这些优化,它也不是主键的最佳类型。如果您可以选择,请查看由 Vlad Mihalcea 维护的 TSID。

但如果您必须或出于某种原因想要使用 UUID,请考虑我提到的优化。另请记住,此类优化对于大型数据集会产生影响。如果您存储数百甚至数千行,并且流量较低,您可能不会看到应用程序性能有任何差异。但是,如果您有可能拥有大型数据集或大流量 - 最好从一开始就这样做,因为更改主键可能是一个相当大的挑战。

深入阅读

本文作者:Eric

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!