UUID 完整指南:v1/v4/v7 差異、何時用哪個版本、線上產生器
寫給:每天
uuid.uuid4()卻不知道為什麼資料庫變慢的後端工程師、想知道 NanoID 跟 ULID 差別的架構師,以及好奇 GUID 跟 UUID 是不是同個東西的所有人。
UUID(Universally Unique Identifier)是 RFC 4122 定義的 128-bit 識別碼,幾乎是現代分散式系統的預設 ID 方案。它解決了「不需要協調就能產生全域唯一 ID」這個棘手問題,但代價是長、隨機、資料庫索引不友善。2024 年 RFC 9562 補上了 v6 / v7 / v8 三個新版本,徹底改變了 UUID 的設計權衡。本文 13 分鐘把 UUID 全家族、ULID、NanoID 一次講透。
1. UUID 是什麼
UUID 是 128 bit(16 byte)的數字,通常以 8-4-4-4-12 hex 格式呈現:
550e8400-e29b-41d4-a716-446655440000
它的設計目標:任何兩台機器、任何時候各自產生的 UUID,全宇宙範圍內碰撞機率趨近於零。沒錯,這聽起來像魔法,但 128 bit 空間實在太大(2^128 ≈ 3.4 × 10^38),即使一秒產 10 億個、產 100 年,碰撞機率仍小於中樂透二次。
GUID(Globally Unique Identifier)是微軟術語,與 UUID 完全等價,只是 hex 字母大小寫慣例不同:
- UUID:通常小寫
550e8400-e29b-... - GUID:通常大寫
{550E8400-E29B-...}加大括號
兩者可互轉。
1.1 版本欄位
UUID 第三組第 1 個字元(即第 13 個 hex char)標記版本:
xxxxxxxx-xxxx-Vxxx-Nxxx-xxxxxxxxxxxx
↑ ↑
版本 variant
V 可以是 1-8。最常用 v4,2024 年後 v7 快速普及。
2. 5 個核心觀念
2.1 八個版本一覽
| 版本 | 名稱 | 生成方式 | 是否時序 | 適用場景 |
|---|---|---|---|---|
| v1 | Time-based | 時間 + MAC | 是 | 老舊系統,洩漏 MAC |
| v2 | DCE Security | 同 v1 + POSIX UID | 是 | 幾乎沒用 |
| v3 | Name-based (MD5) | namespace + name → MD5 | 否 | deterministic ID |
| v4 | Random | 純隨機 | 否 | 預設選擇 |
| v5 | Name-based (SHA-1) | namespace + name → SHA-1 | 否 | deterministic ID(取代 v3) |
| v6 | Reordered v1 | v1 重排,時序在前 | 是 | DB 友善時序 ID |
| v7 | Unix Timestamp | ms timestamp + 隨機 | 是 | 2024 年新標準 |
| v8 | Custom | 自訂 | 自訂 | 特殊需求 |
2.2 v4:經典隨機
v4 結構:
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
↑ ↑
版本 variant (8/9/a/b)
其他位置 122 bit 全隨機
122 bit 隨機 = 5.3 × 10^36 種組合。
優點:
- 無需協調
- 不洩漏任何資訊
- 實作最簡單
缺點:
- 完全隨機 → DB B-tree 索引插入效能差(每次寫入位於索引隨機位置,導致 page split + cache miss)
- 字串排序 = 隨機排序,無法用 ID 取得時序
2.3 v7:時序友善的新王者
v7 結構:
xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx
↑ ↑
48-bit ms timestamp 74-bit 隨機
優點:
- timestamp 在前 → 字串排序 = 時序
- DB 索引插入永遠在尾端 → 效能接近自增 ID
- 仍有 74 bit 隨機 → 同毫秒碰撞機率仍極低(10^22 分之一)
缺點:
- 部分舊 lib 不支援
- 洩漏建立時間(部分場景是缺點)
2024 年後新專案首選 v7。Python uuid7 套件、Node uuid v9+ 都支援。
2.4 ULID(替代方案)
ULID(Universally Unique Lexicographically Sortable Identifier)2016 年由 Alizain Feerasta 提出:
01ARZ3NDEKTSV4RRFFQ69G5FAV
↑ ↑
48-bit 時間 80-bit 隨機
- 26 字元 Crockford Base32(去掉 ILOU 易混淆字元)
- 字串排序 = 時序
- 比 UUID 字串短(26 vs 36)
跟 v7 的差別:
| 屬性 | UUID v7 | ULID |
|---|---|---|
| 標準化 | RFC 9562 | 社群(無 RFC) |
| 字串長度 | 36 | 26 |
| 字元集 | hex | Crockford Base32 |
| 時序精度 | ms | ms |
ULID 在 v7 出來前是事實標準,但現在新專案建議直接用 v7(標準化更好)。
2.5 NanoID(短而隨機)
NanoID 設計目標:比 UUID 短但碰撞抗性接近。
import { nanoid } from "nanoid";
nanoid(); // "V1StGXR8_Z5jdHi6B-myT" (21 字元)
預設 21 字元、64 字元集(A-Za-z0-9_-)。產 10 億個碰撞機率仍小於 10^-12。
優點:
- 短(21 vs 36)
- URL safe(不含
+/) - 自訂字母集(可只用大寫、可改長度)
缺點:
- 非時序
- 非標準(無 RFC)
適合:URL slug、短連結、邀請碼。
3. 實戰範例
範例 1:Python 各版本生成
import uuid
# v4 隨機
u4 = uuid.uuid4()
print(u4, type(u4))
# 550e8400-e29b-41d4-a716-446655440000 <class 'uuid.UUID'>
# v1 時間+MAC(不推薦)
u1 = uuid.uuid1()
# v5 name-based
namespace = uuid.NAMESPACE_DNS
u5 = uuid.uuid5(namespace, "example.com")
# 同樣輸入永遠同樣 UUID
範例 2:v7 生成(Python 第三方)
# pip install uuid-utils
import uuid_utils
u7 = uuid_utils.uuid7()
print(u7)
# 0190a4e6-fc23-7xxx-yxxx-xxxxxxxxxxxx
# 字串排序 = 時序
ids = [str(uuid_utils.uuid7()) for _ in range(5)]
print(sorted(ids) == ids) # True
範例 3:Node.js 全套
import { v4 as uuidv4, v7 as uuidv7 } from "uuid";
import { ulid } from "ulid";
import { nanoid } from "nanoid";
console.log(uuidv4()); // 550e8400-...
console.log(uuidv7()); // 0190a4e6-...
console.log(ulid()); // 01ARZ3NDEKTSV4RRFFQ69G5FAV
console.log(nanoid()); // V1StGXR8_Z5jdHi6B-myT
範例 4:bash 一行產 UUID v4
# Linux / macOS
uuidgen | tr 'A-Z' 'a-z'
# 550e8400-e29b-41d4-a716-446655440000
# OpenSSL(無 uuidgen 時)
openssl rand -hex 16 | sed 's/\(........\)\(....\)\(....\)\(....\)\(.*\)/\1-\2-\3-\4-\5/'
# Python one-liner
python3 -c "import uuid; print(uuid.uuid4())"
範例 5:PostgreSQL 資料庫端生成
-- 開啟 extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- v4
SELECT uuid_generate_v4();
-- v7(PostgreSQL 17+ 內建)
SELECT uuidv7();
-- 表格 schema
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuidv7(),
name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
PostgreSQL 16 以下用 uuid_generate_v4()。
4. 進階技巧
4.1 DB 索引效能比較
實測 1000 萬筆 insert 到 PostgreSQL id PK,索引深度與 page split 次數:
| ID 類型 | 插入時間 | Index 大小 | Page Split |
|---|---|---|---|
| Serial (自增) | 12s | 220 MB | 100 |
| UUID v4 | 47s | 450 MB | 9000 |
| UUID v7 | 14s | 235 MB | 130 |
| ULID | 14s | 235 MB | 130 |
結論:時序 UUID 效能接近自增 ID,v4 是 v7 的 3.3 倍時間。大表格務必選 v7。
4.2 短字串呈現(base62 / base58)
UUID 36 字元太長,可編碼縮短:
import uuid, base64
u = uuid.uuid4()
short = base64.urlsafe_b64encode(u.bytes).decode().rstrip("=")
print(short) # VQ6EAOKbQdSnFkRmVUQAAA (22 字元)
# 還原
import base64
def decode_short(s):
s += "=" * (-len(s) % 4)
return uuid.UUID(bytes=base64.urlsafe_b64decode(s))
URL slug 變短一半。
4.3 v5 deterministic ID(idempotency key)
冪等 API call 需要 client 算出 deterministic ID:
import uuid
namespace = uuid.UUID("12345678-1234-1234-1234-123456789012")
def request_id(user_id, action, params):
name = f"{user_id}:{action}:{sorted(params.items())}"
return uuid.uuid5(namespace, name)
# 同樣參數永遠同樣 ID
print(request_id(100, "transfer", {"to": 200, "amount": 50}))
print(request_id(100, "transfer", {"to": 200, "amount": 50}))
# 兩次完全相同
Server 看到同樣 ID 直接 return cached response。
4.4 v7 從 ID 反推時間
import uuid_utils
u = uuid_utils.uuid7()
ts_ms = int(u.hex[:12], 16)
print(ts_ms) # 1717564800123
import datetime
print(datetime.datetime.fromtimestamp(ts_ms / 1000))
這個能力是優點(debug、分析),也是缺點(洩漏時間)。
4.5 跨資料庫遷移時保留 UUID
Postgres → MySQL → MongoDB 互轉時,UUID 在不同 DB 的儲存格式不同:
| DB | 儲存 | 索引效率 |
|---|---|---|
| PostgreSQL | 原生 UUID(16 byte) | 最佳 |
| MySQL 8 | BINARY(16) | 好 |
| MySQL 5 | CHAR(36) | 差(占 36 byte) |
| MongoDB | BinData(4) | 好 |
跨 DB 遷移時建議:應用層永遠用字串,存 DB 時轉成原生型別。
5. 常見錯誤與解法
錯誤 1:用 v4 當主鍵,DB 越來越慢
症狀:表格 100 萬筆後 insert 變慢,索引膨脹。 根因:v4 隨機,每次 insert 都在 B-tree 中間,page split 頻繁。 解法:改用 v7 或 ULID。
錯誤 2:v1 洩漏 MAC address
症狀:v1 UUID 後 12 位是 MAC 地址,可追蹤產生機器。 根因:v1 規範使然。 解法:除非真的需要時序、否則用 v4 / v7。
錯誤 3:自己 Math.random() 拼出 UUID
症狀:高負載下偶爾碰撞。
根因:Math.random() 不是密碼學等級隨機。
解法:用標準 lib(crypto.randomUUID() / uuid 套件)。
錯誤 4:UUID 當網址 slug
症狀:分享連結 36 字元太長。 根因:UUID 不是設計給人看的。 解法:用 NanoID(21 字元)或自己 base62 編碼。
錯誤 5:忘記 case 差異
症狀:API 比對 UUID 失敗,原來一邊大寫一邊小寫。 根因:UUID 文字表現大小寫敏感(雖然語意等價)。 解法:永遠 lowercase 後比較,或解析成 UUID 物件再比。
import uuid
uuid.UUID("550E8400-E29B-41D4-A716-446655440000") == uuid.UUID("550e8400-e29b-41d4-a716-446655440000")
# True
6. 用 SuperTools 線上版完成相同任務
需要快速產 UUID 不想開 REPL?打開 SuperTools UUID 產生器:
- 8 種類型:v1 / v3 / v4 / v5 / v6 / v7 / ULID / NanoID
- 批次模式:一次產 1000 個
- 格式選擇:標準 / 純 hex / 大寫 / 含大括號(GUID)
- NanoID 自訂:字母集 + 長度可調
- v5 namespace:DNS / URL / OID / X.500 一鍵切換
- 匯出:複製 / 下載 CSV / JSON
工具完全 client-side(瀏覽器 crypto API),ID 不會上傳。
實際使用流程:
- 開 supertools.tw/dev/uuid
- 選 v7(推薦給新專案)
- 設定數量 100
- 一鍵下載 CSV
- 匯入測試資料 / seed script
立即使用 SuperTools UUID 產生器:https://supertools.tw/dev/uuid
7. 其他語言實作
Python
import uuid
uuid.uuid4()
# v7 需 pip install uuid-utils
JavaScript
crypto.randomUUID(); // 瀏覽器原生 v4
import {v7} from "uuid";
v7();
Go
import "github.com/google/uuid"
uuid.New() // v4
uuid.NewV7() // v7(go 1.21+)
Rust
use uuid::Uuid;
Uuid::new_v4();
Uuid::now_v7(); // 需 uuid = { version = "1", features = ["v7"] }
Bash
uuidgen
python3 -c "import uuid; print(uuid.uuid4())"
8. FAQ
Q1:UUID 真的不會碰撞嗎? 理論上會但機率小於宇宙隨機事件。v4 一秒產 10 億個跑 100 年,碰撞機率仍 < 10^-9。
Q2:v7 已穩定可用嗎? RFC 9562 已於 2024 年正式發布。主流語言均已實作。生產可用。
Q3:MongoDB ObjectId 跟 UUID 哪個好? ObjectId 12 byte 含時序,但設計給 MongoDB 內部用。跨 DB / 跨服務用 UUID v7 更通用。
Q4:UUID 可以當 password 嗎? v4 是 122 bit 隨機,作為一次性 token 可用。但建議再加 expire + DB 黑名單(替代 JWT refresh)。
Q5:用 UUID 後資料庫變大很多怎辦? UUID 16 byte vs Int 4 byte 確實大 4 倍,但主鍵索引才比較痛。一般 row 內 UUID 對總體影響 < 5%。
9. 實戰案例研究
案例 1:1 億筆 UUID v4 主鍵 → v7 重建
某 fintech 用 UUID v4 當主鍵,1 億筆後遇到:
- INSERT 慢:B-tree 隨機 page split,每秒 5000 → 1500
- Index 膨脹:占用 78 GB
- Range query 慢:依時間排序需 ORDER BY created_at(無法用 ID)
遷移計畫(3 個月):
- 加
id_v7 UUID欄位 - 新資料同時寫 v4 + v7
- 背景 batch job 把舊資料補 v7(依 created_at 重算)
- 應用層讀寫切到 v7
- drop v4 PK,把 v7 設 PK
效果:
- INSERT 回到 5000/s
- Index 縮回 32 GB
- Range query 改用 ID(v7 含 timestamp)
教訓:百萬級表選 v4,後期遷移成本巨大。新表直接 v7。
案例 2:MongoDB ObjectId vs UUID
某新創用 MongoDB ObjectId 當 user ID。要擴展到 cross-region 多主寫入時遇到:
- ObjectId 包含 machine_id + counter,跨 region 設定複雜
- 想匯出到 PostgreSQL analytics,schema 不對齊
改用 UUID v7(兩端都支援):
const { v7 } = require("uuid");
const user = { _id: v7(), ... };
教訓:跨 DB 系統用 UUID 比 DB-specific ID 通用。
案例 3:邀請碼用 NanoID 替代 UUID
某 SaaS 邀請碼原本用 UUID v4:
https://app.example.com/invite/550e8400-e29b-41d4-a716-446655440000
問題:URL 太長、user 念出來困難。
改 NanoID 8 字元(加長字母集到 70 字元 + 排除易混 IO0l1):
https://app.example.com/invite/x7Kp3Mn2
碰撞機率:70^8 = 5.7 × 10^14。每秒產 10 萬個跑 100 年仍碰撞機率 < 10^-6。對邀請碼足夠。
教訓:人類要讀的 ID 不要用 UUID,用 NanoID 自訂字母集。
10. 結論 + 延伸閱讀
UUID 不是「越新越好」也不是「全用 v4」。新專案資料庫主鍵選 v7、URL slug 選 NanoID、deterministic 需求選 v5,遺留系統可繼續 v4。掌握五個版本的權衡後,你可以針對場景選對 ID 方案,省下未來索引重建的痛。三條黃金法則:DB 主鍵走 v7、人類念的走 NanoID、idempotent 走 v5。
延伸閱讀: