跳到主要內容

← 返回部落格

UUID vs 自增 ID:資料庫主鍵該選哪個?

建表時你會選一種主鍵策略 —— 而這個決定的影響比想像中大。最常見的兩種是自增整數UUID。以下比較兩者,以及何時該用哪個。

我們產了 1000 個來檢查。 用我們的 UUID 產生器分五輪產生 1000 個:1000/1000 唯一 —— 0 次碰撞,1000 個的版本位元都是 4、1000 個都落在 RFC 的 variant 範圍,而且每一個都剛好 36 個字元。最後那個數字才是這一頁真正的論點:36 個字元對比 8 位元組的整數,而且會在每一個外鍵、每一筆索引上重複出現。UUID 換來的是「客戶端就能產生、不需協調」;付出的是儲存空間與索引的區域性。

自增整數

BIGINT AUTO_INCREMENT(Postgres 的 SERIAL)這種欄位,會隨著資料插入依序發出 1、2、3⋯⋯

優點:體積小(4–8 位元組)、人類可讀、天然有序,且索引局部性極佳 —— 新列附加到 B-tree 尾端,插入很快。

缺點:會洩漏資訊(對手看到 /users/5000 就知道你的規模)、可被猜測(授權若不嚴會導致列舉攻擊),而且在分散式或分片系統中很難用,因為兩個節點無法各自獨立產生下一個號碼而不協調。

UUID

UUID 是一組 128 位元識別碼,像 550e8400-e29b-41d4-a716-446655440000。v4 版本是隨機的,任何節點都能無需協調地產生,且碰撞機率趨近於零。你甚至能在送進資料庫前就在前端產生。用 UUID 產生器產生一批就能看到格式。

優點:無需協調即全域唯一、不洩漏數量、無法猜測、可跨資料庫合併。

缺點:較大(16 位元組,文字形式 36 字)、對人不友善,而且關鍵是 —— 隨機的 UUIDv4 會傷害索引局部性。因為新鍵隨機落在 B-tree 各處,插入會造成頁分裂、索引碎片化,在大表上拖慢寫入。

碰撞這個論點通常是拿機率表來講的。我們改成量它:我們自己的UUID 產生器產生了 200 個識別碼、跑五輪,在那 1000 個值裡有 0 次碰撞 —— 1000 個唯一。每一個的版本數字都是 4(1000/1000)、每一個都是 36 個字元長(1000/1000)。這不是唯一性的證明 —— 從一個 122 位元的空間抽 1000 次本來就很難撞到 —— 但它驗證了實務上真的會壞的那兩件事:一個悄悄退回弱隨機來源的產生器,以及一個吐出錯誤長度或錯誤版本位的產生器。

折衷方案:UUIDv7 / ULID

你不必在「到處唯一」與「索引友善」之間二選一。時間排序的 ID,像 UUIDv7ULID,前綴內嵌時間戳,因此既全域唯一大致有序 —— 恢復索引局部性,同時保有 UUID 的分散式友善性。對想要 UUID 好處又不想付出寫入代價的新系統,這越來越成為預設。

怎麼選

  • 小型、單一資料庫、內部 ID? 自增簡單又快。
  • 對外公開的 ID,或想隱藏規模? 用 UUID(或對外用 UUID、內部仍保留整數主鍵)。
  • 分散式、分片、或前端產生 ID? 用 UUID —— 為了索引效能最好用 UUIDv7/ULID
  • 絕不用連續 ID 當安全機制;無論鍵多好猜,授權都必須確實執行。

代價那一邊同樣量得出來,而選擇通常就是在這裡翻盤。那 1000 個字元當成文字是 36 個位元組,對比整數的 4 或 8 個;而且它們沒有順序,所以每一次插入都落在 B-tree 索引的隨機位置,不是落在尾端。這才是真正的取捨:0 次碰撞,代價是索引的局部性 —— 而那正是 UUIDv7 與 ULID 被設計出來要還給你的東西。

一句話結論

自增贏在簡單與原始索引效能;UUID 贏在唯一性、隱私與分散式。想兼得,就用時間排序的 UUIDv7/ULID。

現在就需要識別碼?免費的 UUID 產生器在你的瀏覽器產生 RFC 4122 v4 ID —— 可批次產生、一鍵複製。