UUID — 恰巧沒有重覆

0.000000000000000001

古哥
pgsql-tw
8 min readJul 29, 2021

--

本篇介紹 UUID (Universally Unique IDentifier)用於資料庫中 primary key 的情境,範例資料庫為 PostgreSQL。請注意,UUID 並不只用於資料庫領域唷!

唯一性

這應該是所有使用者在面對這個名詞時的第一個疑問,我認為也是最關鍵的突破點。所謂頭過身就過,理解並信任這點之後,其他的部份都只是水到渠成而已。

一般資料庫的唯一性是資料性的一致性,也就是建立一個索引,新資料透過比對的方式,確認了沒有出現過,所以每一筆資料就是唯一的。它的唯一性建立在每一筆資料的互相比較。邏輯上當然是絕對的唯一性確保,缺點只有檢查的流程可能稍長,也很難和其他資料庫一起比較。

UUID 提供了另一個方向的思維:如果我的 ID 即使不跟其他資料比較,也只有很低的機率會重覆,那也可以當作具備唯一性來使用嗎?更具體一點,如果這個機率低於 10^(-18),你可以接受嗎?作為對比,大樂透頭獎機率略高於 10^(-8)。

你可以得到什麼?

與其糾結在你會付出「巨大的」機率風險,不如想想你可以因此得到什麼,是不是足夠划算。

如果不需要跟其他資料比較,那麼:

  • 你的資料庫可以分散了!至少在 ID 這個概念的資料上,你可以拆分為多個 Table 或多個資料庫,甚至也不一定是單一主機,藉以分散存取的壓力。
  • 跨系統 ID 是可能的。由於 UUID 是一種標準,各種系統可以各自產生 ID 而不重覆,減少系統整合的困擾。
  • 替代複合資料欄位的唯一性索引。有些資料應用會產生龐大的唯一性索引,由於 UUID 也可以依據資料產生,長度固定,所以也可以應用於縮減唯一性索引的大小,進而增進效率。

有關機率的部份

UUID 的碰撞機率可以透過 Birthday Problem 來瞭解,但你也可以懶得瞭解,直接跳到一張表來得到結果。

Probability table

在 PostgreSQL 裡的 UUID 為 128 bit,例如你的資料是在 260 億筆的話,那麼碰撞機率大約會是 10^(-18)。現在可以回想你的 Table 有多少筆資料了。

你已經有勇氣嘗試了

你需要使用的是 uuid-ossp 延伸功能,使用前請先執行:

CREATE EXTENSION "uuid-ossp";

如果是 PostgreSQL 13 以上的話,還內建了 gen_random_uuid(),可以產生版本 4 的 UUID。資料型別皆為 uuid,可以索引,也支援比較運算子。

UUID 有多種版本的標準,建議先閱讀手冊和 wiki 上的說明,細節就不多贅述。

有 ID 就好 (version 4)

如果你的 ID 和其他資料皆無關,在版本 4 的 UUID 提供了完全以亂數產生的機制,速度是最快的。其中 gen_random_uuid() 又快於 uuid_generate_v4(),所以如果你只需要 version 4 UUID 的話,用內建函數即可。

postgres=# explain analyze select gen_random_uuid();
QUERY PLAN
-------------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=16) (actual time=0.019..0.022 rows=1 loops=1)
Planning Time: 0.071 ms
Execution Time: 0.064 ms
(3 rows)
postgres=# explain analyze select uuid_generate_v4();
QUERY PLAN
-------------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=16) (actual time=0.046..0.048 rows=1 loops=1)
Planning Time: 0.015 ms
Execution Time: 0.376 ms
(3 rows)

遞增 ID (version 1)

內容依據 Timestamp 和 MAC address 產生,而且可以解出原來的資訊(Timestamp, MAC)。時間是遞增的,其產生的 UUID 也是遞增的,所以也可以排序得出先後次序。

適合的情境就是需要有先後次序的情境,例如訊息記錄。

postgres=# select uuid_generate_v1();
uuid_generate_v1
--------------------------------------
c817082e-ed64-11eb-8212-dcf5054c98f0
(1 row)

如果要解出此 UUID 的時間,需要自訂一個函數:

然後就可以執行:

postgres=# select uuid_timestamp('c817082e-ed64-11eb-8212-dcf5054c98f0');
uuid_timestamp
-------------------------------
2021-07-26 00:24:27.439723+08
(1 row)

要注意的是你需要確定來源 UUID 是依 version 1 標準產生的 UUID,不然就會是奇怪的結果。

由於會參考主機時間,即使有對時,多少還是會有些誤差,你會需要根據你的應用確認容許誤差及其處理方法。單機的話可以考慮取代 sequence,INSERT 速度會變慢一些,但可以少維護一個物件,也保留延展服務到多主機的空間。

依據資料產生 ID (version 3, version 5)

在許多的實際情況中,直接指定某個欄位或多個欄位作為 primary key 或 unique index 並不會少見。尤其是多個欄位形成的複合欄位索引,大小超過資料表本身是很常見的,而這類索引會造成效能瓶頸也不會太意外。

version 3 使用的是 MD5,version 5 則是使用 SHA1,推薦自然是使用 version 5。總之它們都可以把資料內容對應到某一組 UUID,只要資料相同,就會得到相同的 UUID。所以可用於快速的資料內容比對。

先瞭解成本的部份,你會需要增加一個 UUID 的欄位,長度固定為 16 Bytes,通常複合的欄位資料串接起來也需要超過這個長度才會開始划算。

舉例來說,一般簡化後的情況:

CREATE TABLE test (a text, b text, c text, d text, primary key (a,b,c));

改用 UUID 的情況:

CREATE TABLE test (uuid uuid, a text, b text, c text, d text, primary key (uuid));
CREATE INDEX ON test (a);
CREATE INDEX ON test (b);
CREATE INDEX ON test (c);
CREATE OR REPLACE FUNCTION test_before_insert_update()
RETURNS trigger AS
$$
BEGIN
NEW.uuid=uuid_generate_v5(uuid_nil(),NEW.a||NEW.b||NEW.c);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;


CREATE TRIGGER t0_test_before_insert_update
BEFORE INSERT OR UPDATE
ON test
FOR EACH ROW
EXECUTE PROCEDURE test_before_insert_update();

因為原來的 primary key 還有索引的功能,所以用兩個單欄位索引來替代。

成本:

  • 多一個欄位 uuid,資料和索引都增加。
  • uuid 的內容需要搭配 Trigger 來自動填入。
  • INSERT 和 UPDATE 的速度可能會變慢,原 pkey 不大的時候。

優勢:

  • 原複合主鍵的內容長度加起來超過 16 Bytes 越多,節省效益越多。
  • 如果後續有新欄位要加入 pkey/unique index的話,成本不變。
  • 複合欄位索引只有依索引欄位順序的查詢條件有利,單欄位索引方便自動組合。
  • 有機會節省系統資源,增加效率。當原 pkey 處理耗用資料超過主機資源負載時,改用 uuid 後可能因為可用的記憶體資源增加,而提升處理速度(Trigger 仍然是成本)。例如記憶體僅 16GB,單一索引卻大過於 16GB 的情況。

UUID data type

記得要宣告為 UUID 這個資料型別,而不是 TEXT。它能在時間和空間上都提供更好的運作效率。UUID 也不一定要由資料庫產生,只要約定好產生的方法,各個程式語言和系統環境幾乎都能夠自行計算出 UUID 來使用。

--

--

古哥
pgsql-tw

解決不了問題,就解決提出問題的人