UUID 通常用作数据库表主键。它们易于生成,易于在分布式系统之间共享并保证唯一性。
考虑到 UUID 的大小,这是否是一个正确的选择值得怀疑,但通常这不是由我们决定的。
本文的重点不是“UUID 是否是键的正确格式”,而是如何有效地使用 UUID 作为 PostgreSQL 的主键。
UUID 可以被视为一个字符串,并且可能很容易将它们存储为字符串。 Postgres 具有用于存储字符串的灵活数据类型: text ,并且通常用作存储 UUID 值的主键。
它是正确的数据类型吗?当然不。
Postgres 有一个专用于 UUID 的数据类型: uuid 。 UUID 是 128 位数据类型,因此存储单个值需要 16 个字节。 text 数据类型有 1 或 4 个字节的开销加上存储实际的字符串。
这些差异在小表中并不那么重要,但一旦开始存储数十万或数百万行,就会成为问题。
我进行了一个实验,看看实践中有何不同。有两个表只有一列 - id 作为主键。第一个表使用 text ,第二个表使用 uuid :
sqlcreate table bank_transfer(
id text primary key
);
create table bank_transfer_uuid(
id uuid primary key
);
我没有指定主键索引的类型,因此 Postgres 使用默认的 B 树。
向每个表插入 10 000 000 行:
我运行查询来查找表大小和索引大小:
sqlselect
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 用于存储这些表和索引的页数上:
sqlselect 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 树索引 - 并且 B 树索引是主键唯一可用的索引类型。
B 树索引最适合处理有序值 - 例如自动递增列或时间排序列。
UUID - 尽管看起来总是相似 - 有多种变体。 Java 的 UUID.randomUUID() - 返回 UUID v4 - 这是一个伪随机值。对我们来说,更有趣的是 UUID v7 - 它生成按时间排序的值。这意味着每生成一个新的UUID v7,它就有一个更大的值。这使得它非常适合 B 树索引。
python如何生成uuid v7:
pythonimport 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
我创建了另一个表,与 bank_transfer_uuid 完全相同,但它将仅存储使用上述库生成的 UUID v7:
sqlcreate 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 许可协议。转载请注明出处!