UUID通常用作数据库表主键。它们易于生成,易于在分布式系统之间共享并保证唯一性。
考虑到 UUID 的大小,这是否是一个正确的选择值得怀疑,但通常这不是由我们决定的。
本文的重点不是“ UUID 是否是键的正确格式”,而是如何有效地使用UUID作为PostgreSQL的主键。
用于 UUID 的 Postgres 数据类型
UUID 可以看作是一个字符串,因此很容易将其存储为字符串。Postgres 有一种用于存储字符串的灵活数据类型:文本,它经常被用作存储 UUID 值的主键。
这是一种正确的数据类型吗?肯定不是。
Postgres 有一种专门用于 UUID 的数据类型:uuid。UUID 是 128 位数据类型,因此存储单个值需要 16 个字节。
这些差异在小表中并不重要,但一旦开始存储数十万或数百万行,就会成为一个问题。
我做了一个实验,看看在实践中有什么区别。有两个表只有一列--作为主键的 id。第一个表使用文本,第二个表使用 uuid:
create table bank_transfer( id text primary key );
create table bank_transfer_uuid( id uuid primary key );
|
我没有指定主键索引的类型,因此 Postgres 使用了默认的 B 树索引。
然后,我使用 Spring 的 JdbcTemplate 中的 batchUpdate 向每个表插入了 10 000 000 条记录:
jdbcTemplate.batchUpdate("insert into bank_transfer (id) values (?)", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, UUID.randomUUID().toString()); } @Override public int getBatchSize() { return 10_000_000; } });
|
jdbcTemplate.batchUpdate("insert into bank_transfer_uuid (id) values (?)", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setObject(1, UUID.randomUUID()); }
@Override public int getBatchSize() { return 10_000_000; } });
|
我运行一个查询来查找表的大小和索引的大小:
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 | +------------------+-----------------------+----------+----------+
|
使用文本的表大 54%,索引大 85%。这也反映在 Postgres 用于存储这些表和索引的页数上:
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-Tree 索引。
要在 Java 中使用 UUID v7,我们需要一个第三方库,如 java-uuid-generator:
<dependency> <groupId>com.fasterxml.uuid</groupId> <artifactId>java-uuid-generator</artifactId> <version>5.0.0</version> </dependency>
UUID uuid = Generators.timeBasedEpochGenerator().generate();
|
从理论上讲,这应该能提高执行 INSERT 语句的性能。
UUID v7 如何影响 INSERT 性能
我创建了另一个表,与 bank_transfer_uuid 完全相同,但它只存储使用上述库生成的 UUID v7:
create table bank_transfer_uuid_v7( id uuid primary key );
|
然后,我在每个表中插入了 10 轮 10000 行,并测量了所需时间:
for (int i = 1; i <= 10; i++) { measure(() -> IntStream.rangeClosed(0, 10000).forEach(it -> { jdbcClient.sql("insert into bank_transfer (id) values (:id)") .param("id", UUID.randomUUID().toString()) .update(); }));
measure(() -> IntStream.rangeClosed(0, 10000).forEach(it -> { jdbcClient.sql("insert into bank_transfer_uuid (id) values (:id)") .param("id", UUID.randomUUID()) .update(); }));
measure(() -> IntStream.rangeClosed(0, 10000).forEach(it -> { jdbcClient.sql("insert into bank_transfer_uuid_v7 (id) values (:id)") .param("id", Generators.timeBasedEpochGenerator().generate()) .update(); })); }
|
结果看起来有点随意,尤其是在比较带有普通文本列和 uuid v4 的表格的时间时:
+-------+-------+---------+ | 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维护的[url=https://github.com/vladmihalcea/hypersistence-tsid]TSID[/url]。
但如果您必须或出于某种原因想要使用 UUID,请考虑我提到的优化。另请记住,此类优化对于大型数据集会产生影响。如果您存储数百甚至数千行,并且流量较低,您可能不会看到应用程序性能有任何差异。但是,如果您有可能拥有大型数据集或大流量 - 最好从一开始就这样做,因为更改主键可能是一个相当大的挑战。