---
layout: post
title: "Primary Key 怎麼選？Auto Increment、UUID、與自然鍵設計比較"
description: "資料庫主鍵該用Auto Increment、UUID，還是自然鍵？本文整理各種主鍵設計方式的差異，並從效能、安全性與擴充性角度分析實務上的選擇。"
author: Mark_Mew
categories: [database]
tags: [primary key, uuid, Database]
keywords: [資料庫主鍵, primary key, 資料庫主鍵設計, primary key design, 自增 ID, auto increment, UUID, 自然鍵, surrogate key, natural key, 資料庫設計]
date: 2026-4-13
---

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

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

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

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

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

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

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

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

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

### 1. 索引效能

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

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

### 2. 隱私與安全性

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

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

### 3. 擴充性

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

舉個簡單的例子：

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

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

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

## 那什麼比較適合當主鍵？

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

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

### 自動增加的序號

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

在 SQL Server 中常見的是 `IDENTITY`，在 PostgreSQL 中則通常搭配 `SEQUENCE` 或 `GENERATED AS IDENTITY`。

這種主鍵的優點很明顯：

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

如果你的系統是單體架構、單一資料庫，或者根本不需要在多系統間先產生 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% 的儲存空間並提升索引效率。
{: .prompt-warning}

### 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，或樣本累積夠多，
> 仍然有可能被反推出原本的規律。
{: .prompt-info}

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

## 實務上該怎麼選？

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

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

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

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