Primary Key 怎麼選?Auto Increment、UUID、與自然鍵設計比較
資料庫主鍵該用Auto Increment、UUID,還是自然鍵?本文整理各種主鍵設計方式的差異,並從效能、安全性與擴充性角度分析實務上的選擇。
剛開始設計資料庫時,很多人都會遇到同一個問題:主鍵到底該用什麼?
我們都知道主鍵必須唯一、不可重複,也因為這樣,很多人第一時間會想到一些「本來就不會重複」的欄位。
例如,身分證字號如果對每個人都是唯一的,那是不是就可以直接拿來當主鍵?
在 1995 年之前,因為身分證資料多為手寫建檔,曾經出現過重號的情況,所以嚴格來說,台灣的身分證字號並不是絕對唯一。
這其實是一個很值得討論的問題,因為「能不能當主鍵」和「適不適合當主鍵」,是兩件不同的事。
這篇就從這個角度出發,整理資料庫主鍵常見的幾種選擇,以及各自的優缺點。
人為產生的值適合當主鍵嗎?
先說結論:如果某個欄位真的能保證唯一,它當然可以被拿來當主鍵。
但在大多數情況下,人為產生的值其實不太適合直接當主鍵,主要原因通常有三個:效能、安全性,以及未來的擴充性。
1. 索引效能
像身分證字號這種字串欄位,不但長度較長,還會直接影響索引的大小與比較成本。
在資料庫常見的 B+ Tree 索引裡,整數型且遞增的值,通常會比字串型主鍵更有效率。這也是為什麼自增 ID 在效能上通常比較有優勢。
2. 隱私與安全性
主鍵往往會出現在很多地方,例如 URL、API 回應、系統 log、查詢條件,甚至是外部系統串接資料。
如果你把身分證字號、手機號碼、員工編號這種具業務意義或個資屬性的欄位直接當主鍵,那等於把敏感資訊暴露在系統最核心的位置。這在安全性與稽核角度都不太理想。
3. 擴充性
就算先不談效能與安全性,擴充性也常常會成為真正的大問題。
舉個簡單的例子:
假設你一開始把「一個身分證字號只能對應一個遊戲帳號」視為理所當然,因此直接把身分證字號設計成主鍵。
但如果未來業務改了,變成「一個身分證字號可以綁定三個遊戲帳號」,那原本的設計就會立刻卡住。
這時候你不只要改帳號表本身,如果這個主鍵早就被其他資料表當成外來鍵使用,整個資料模型都可能得跟著重構。這種情況下,改動成本通常非常高。
那什麼比較適合當主鍵?
如果不建議直接拿業務欄位來當主鍵,那實務上最常見的選擇,通常就是下面兩種:
- 自動增加的序號
- UUID
自動增加的序號
自增序號是最經典、也最常見的主鍵設計。
在 SQL Server 中常見的是 IDENTITY,在 PostgreSQL 中則通常搭配 SEQUENCE 或 GENERATED AS IDENTITY。
這種主鍵的優點很明顯:
- 長度短,索引效率好
- 遞增寫入,對 B+ Tree 索引較友善
- 通常由資料庫自動產生,不需要程式介入
如果你的系統是單體架構、單一資料庫,或者根本不需要在多系統間先產生 ID,自增序號通常會是非常穩健的選擇。
UUID
UUID 的優點在於,它不需要依賴資料庫遞增序號,可以在應用程式端或資料庫端產生,因此特別適合分散式系統、多服務架構,或需要離線先生成 ID 的場景。
UUID 可以由程式產生,也可以由資料庫產生,但不同資料庫提供的語法不太一樣。
UUID 常見的版本大致有以下三種:
- v1:基於時間戳與 MAC 位址產生。
- v4:基於隨機亂數產生,也是目前最常見的版本。
- v7:基於時間戳排序,對資料庫索引更友善。
不同資料庫產生 UUID 的方式也不太一樣,常見如下:
| 資料庫 | 產生 UUID 的語法 | 預設回傳格式 |
|---|---|---|
| PostgreSQL | gen_random_uuid() | 帶連字號的標準字串 |
| MySQL | UUID() | 帶連字號的標準字串 |
| SQL Server | NEWID() | 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 空間不夠大,而是隨機數來源出了問題。
常見原因包括:
- 隨機數種子重複
- 函式庫實作不佳
- 沒有使用足夠可靠的亂數來源
3. 如果真的擔心碰撞,實務上怎麼防呆?
如果你真的非常在意碰撞風險,例如金融、帳務或高可靠性系統,實務上至少可以做兩件事:
- 在資料庫層加上唯一約束(Unique Constraint)
- 視需求改用 UUID v7 或 ULID 這種更有序的方案
如果用了自增序號,該怎麼避免被猜出來?
很多人不用自增序號當主鍵,不是因為效能,而是擔心它太容易被猜。
例如一個網址是 /user/1001,使用者很容易就會去試 /user/1002、/user/1003。這個問題真正需要處理的,其實不是「主鍵不能遞增」,而是「系統不能只靠主鍵來做授權判斷」。
如果只是想降低外部直接看出序號規律的機會,可以考慮:
- 對外不要直接暴露資料表主鍵
- 另外提供一組公開用 ID
- 視情況使用 Hashids、ULID 或 UUID 當對外識別值
Hashids 是最經典,也是目前全世界最通用的函式庫, 幾乎所有程式語言(PHP, Python, JavaScript, Go, Java 等)都有對應版本, 主要是將整數 ID 透過一個 Salt(鹽值)編碼成短字串(如 1 -> jR)。 例如在電商系統裡,如果訂單編號直接用流水號對外暴露, 很容易就能從相鄰訂單號推測一天大概有多少訂單。 即使改成 Hashids,但如果沒有妥善使用 Salt,或樣本累積夠多, 仍然有可能被反推出原本的規律。
但要注意,這些做法主要是降低可讀性與可猜測性,不是真正的權限控管。真正的安全性仍然要靠後端授權檢查。
實務上該怎麼選?
如果要用一句話總結,大致可以這樣看:
- 單體系統、單一資料庫、追求效能:優先考慮自增序號
- 分散式系統、多服務架構、需要先生成 ID:考慮 UUID
- 不要直接把具業務意義或個資性質的欄位拿來當主鍵
主鍵最理想的狀態,是穩定、單純、沒有業務意義,而且不會因為需求變更就被迫重構。
很多時候,真正該保留業務意義的欄位,應該是獨立欄位加上唯一約束,而不是直接拿來當主鍵。