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(<font>"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(<font>"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 <font>"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(<font>"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 倍。
概括<strong>
正如一開始提到的 - 由於 UUID 長度 - 即使進行了所有這些最佳化,它也不是主鍵的最佳型別。如果您有選擇,請檢視由Vlad Mihalcea維護的[url=https://github.com/vladmihalcea/hypersistence-tsid]TSID[/url]。
但如果您必須或出於某種原因想要使用 UUID,請考慮我提到的最佳化。另請記住,此類最佳化對於大型資料集會產生影響。如果您儲存數百甚至數千行,並且流量較低,您可能不會看到應用程式效能有任何差異。但是,如果您有可能擁有大型資料集或大流量 - 最好從一開始就這樣做,因為更改主鍵可能是一個相當大的挑戰。