文章

Primary Key 怎麼選?Auto Increment、UUID、與自然鍵設計比較

資料庫主鍵該用Auto Increment、UUID,還是自然鍵?本文整理各種主鍵設計方式的差異,並從效能、安全性與擴充性角度分析實務上的選擇。

Primary Key 怎麼選?Auto Increment、UUID、與自然鍵設計比較

剛開始設計資料庫時,很多人都會遇到同一個問題:主鍵到底該用什麼?

我們都知道主鍵必須唯一、不可重複,也因為這樣,很多人第一時間會想到一些「本來就不會重複」的欄位。

例如,身分證字號如果對每個人都是唯一的,那是不是就可以直接拿來當主鍵?

在 1995 年之前,因為身分證資料多為手寫建檔,曾經出現過重號的情況,所以嚴格來說,台灣的身分證字號並不是絕對唯一。

這其實是一個很值得討論的問題,因為「能不能當主鍵」和「適不適合當主鍵」,是兩件不同的事。

這篇就從這個角度出發,整理資料庫主鍵常見的幾種選擇,以及各自的優缺點。

人為產生的值適合當主鍵嗎?

先說結論:如果某個欄位真的能保證唯一,它當然可以被拿來當主鍵。

但在大多數情況下,人為產生的值其實不太適合直接當主鍵,主要原因通常有三個:效能、安全性,以及未來的擴充性。

1. 索引效能

像身分證字號這種字串欄位,不但長度較長,還會直接影響索引的大小與比較成本。

在資料庫常見的 B+ Tree 索引裡,整數型且遞增的值,通常會比字串型主鍵更有效率。這也是為什麼自增 ID 在效能上通常比較有優勢。

2. 隱私與安全性

主鍵往往會出現在很多地方,例如 URL、API 回應、系統 log、查詢條件,甚至是外部系統串接資料。

如果你把身分證字號、手機號碼、員工編號這種具業務意義或個資屬性的欄位直接當主鍵,那等於把敏感資訊暴露在系統最核心的位置。這在安全性與稽核角度都不太理想。

3. 擴充性

就算先不談效能與安全性,擴充性也常常會成為真正的大問題。

舉個簡單的例子:

假設你一開始把「一個身分證字號只能對應一個遊戲帳號」視為理所當然,因此直接把身分證字號設計成主鍵。

但如果未來業務改了,變成「一個身分證字號可以綁定三個遊戲帳號」,那原本的設計就會立刻卡住。

這時候你不只要改帳號表本身,如果這個主鍵早就被其他資料表當成外來鍵使用,整個資料模型都可能得跟著重構。這種情況下,改動成本通常非常高。

那什麼比較適合當主鍵?

如果不建議直接拿業務欄位來當主鍵,那實務上最常見的選擇,通常就是下面兩種:

  1. 自動增加的序號
  2. UUID

自動增加的序號

自增序號是最經典、也最常見的主鍵設計。

在 SQL Server 中常見的是 IDENTITY,在 PostgreSQL 中則通常搭配 SEQUENCEGENERATED AS IDENTITY

這種主鍵的優點很明顯:

  1. 長度短,索引效率好
  2. 遞增寫入,對 B+ Tree 索引較友善
  3. 通常由資料庫自動產生,不需要程式介入

如果你的系統是單體架構、單一資料庫,或者根本不需要在多系統間先產生 ID,自增序號通常會是非常穩健的選擇。

UUID

UUID 的優點在於,它不需要依賴資料庫遞增序號,可以在應用程式端或資料庫端產生,因此特別適合分散式系統、多服務架構,或需要離線先生成 ID 的場景。

UUID 可以由程式產生,也可以由資料庫產生,但不同資料庫提供的語法不太一樣。

UUID 常見的版本大致有以下三種:

  • v1:基於時間戳與 MAC 位址產生。
  • v4:基於隨機亂數產生,也是目前最常見的版本。
  • v7:基於時間戳排序,對資料庫索引更友善。

不同資料庫產生 UUID 的方式也不太一樣,常見如下:

資料庫產生 UUID 的語法預設回傳格式
PostgreSQLgen_random_uuid()帶連字號的標準字串
MySQLUUID()帶連字號的標準字串
SQL ServerNEWID()UNIQUEIDENTIFIER 型別
Oracle (舊)SYS_GUID()RAW(16),無連字號
Oracle (新 23ai)UUID()符合標準的 RAW(16)

在 Oracle 中,為了效能,我們通常會選用 RAW(16) 儲存 UUID 而非 VARCHAR2(36),因為它能減少超過 50% 的儲存空間並提升索引效率。

UUID 真的不會重複嗎?

簡單來說:理論上會,但現實中幾乎不可能。

可以從三個角度來看這件事。

1. 機率有多低?

以最常見的 UUID v4 為例,它大約有 $2^{122}$ 種可能組合,約等於 $5.3 \times 10^{36}$。

這個數字大到什麼程度?即使你每秒產生 10 億個 UUID,持續產生 100 年,碰撞機率仍然低到幾乎可以忽略。

2. 現實中如果重複,通常真正的原因是什麼?

如果真的出現 UUID 重複,問題通常不是 UUID 空間不夠大,而是隨機數來源出了問題。

常見原因包括:

  1. 隨機數種子重複
  2. 函式庫實作不佳
  3. 沒有使用足夠可靠的亂數來源

3. 如果真的擔心碰撞,實務上怎麼防呆?

如果你真的非常在意碰撞風險,例如金融、帳務或高可靠性系統,實務上至少可以做兩件事:

  1. 在資料庫層加上唯一約束(Unique Constraint)
  2. 視需求改用 UUID v7 或 ULID 這種更有序的方案

如果用了自增序號,該怎麼避免被猜出來?

很多人不用自增序號當主鍵,不是因為效能,而是擔心它太容易被猜。

例如一個網址是 /user/1001,使用者很容易就會去試 /user/1002/user/1003。這個問題真正需要處理的,其實不是「主鍵不能遞增」,而是「系統不能只靠主鍵來做授權判斷」。

如果只是想降低外部直接看出序號規律的機會,可以考慮:

  1. 對外不要直接暴露資料表主鍵
  2. 另外提供一組公開用 ID
  3. 視情況使用 Hashids、ULID 或 UUID 當對外識別值

Hashids 是最經典,也是目前全世界最通用的函式庫, 幾乎所有程式語言(PHP, Python, JavaScript, Go, Java 等)都有對應版本, 主要是將整數 ID 透過一個 Salt(鹽值)編碼成短字串(如 1 -> jR)。 例如在電商系統裡,如果訂單編號直接用流水號對外暴露, 很容易就能從相鄰訂單號推測一天大概有多少訂單。 即使改成 Hashids,但如果沒有妥善使用 Salt,或樣本累積夠多, 仍然有可能被反推出原本的規律。

但要注意,這些做法主要是降低可讀性與可猜測性,不是真正的權限控管。真正的安全性仍然要靠後端授權檢查。

實務上該怎麼選?

如果要用一句話總結,大致可以這樣看:

  1. 單體系統、單一資料庫、追求效能:優先考慮自增序號
  2. 分散式系統、多服務架構、需要先生成 ID:考慮 UUID
  3. 不要直接把具業務意義或個資性質的欄位拿來當主鍵

主鍵最理想的狀態,是穩定、單純、沒有業務意義,而且不會因為需求變更就被迫重構。

很多時候,真正該保留業務意義的欄位,應該是獨立欄位加上唯一約束,而不是直接拿來當主鍵。

本文章以 CC BY 4.0 授權