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 不會上傳。

實際使用流程:

  1. 開 supertools.tw/dev/uuid
  2. 選 v7(推薦給新專案)
  3. 設定數量 100
  4. 一鍵下載 CSV
  5. 匯入測試資料 / 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 個月):

  1. 加 id_v7 UUID 欄位
  2. 新資料同時寫 v4 + v7
  3. 背景 batch job 把舊資料補 v7(依 created_at 重算)
  4. 應用層讀寫切到 v7
  5. 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。

延伸閱讀: