跳至內容
Toova
所有工具

UUID v4 vs v7 — 差異與何時使用何者

Toova

UUID v4 已是分散式主鍵的預設選擇超過十年。產生隨機 128 位元值、格式化為八個十六進位群組,完成。無需協調、碰撞機率實質為零、處處可用。那麼為什麼 UUID v7 — 在 2024 年於 RFC 9562 中標準化 — 在 2026 年的生產系統中快速普及?

簡短答案:資料庫效能。UUID v4 摧毀了 B-tree 索引本地性。UUID v7 修正了這點,同時保留開發者熱愛 UUID 的一切。本文解釋每個版本實際是什麼、為何差異在規模上重要、何時選擇何者,以及如何在不停機的情況下遷移。

現在就需要產生 UUID 嗎?試試 Toova UUID 產生工具,它批次支援 v4 與 v7。若需其他隨機識別碼,隨機字串產生工具密碼產生工具涵蓋較短的令牌。

UUID v4 — 純粹隨機

UUID 版本 4 使用密碼學安全的偽隨機數產生器(CSPRNG)填入 128 位元中的 122 位元。其餘 6 個位元為固定:4 個位元編碼版本(0100),2 個位元編碼變體(10)。結果看起來像這樣:

f47ac10b-58cc-4372-a567-0e02b2c3d479
              ^^^^
              版本 = 4(隨機)

隨機性即為功能。兩個獨立系統能在無需協調的情況下產生 UUID 且永不碰撞 — 在 2.71 quadrillion 個 v4 UUID 集合中發生碰撞的機率約為 50%,這代表對任何實務應用而言風險可忽略。你不需要集中式 ID 伺服器、資料庫序列或分散式鎖。

v4 如何產生

import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// => 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

uuid 函式庫(JavaScript)、Python 的 uuid.uuid4()、Go 的 google/uuid 與所有主要語言執行環境,都會委派給作業系統 CSPRNG — Linux 上的 /dev/urandom、Windows 上的 CryptGenRandom。產生成本實質為零。

問題:隨機插入會摧毀 B-tree 索引

B-tree 索引讓資料保持已排序。當你插入新列時,資料庫會找出新鍵在已排序順序中的位置並放入。如果每個新鍵都是隨機的,它會落在索引的隨機位置 — 這代表每次插入都需要將不同的頁面從磁碟載入緩衝區。在低流量下這不可見。在高流量下(數百萬列、高 INSERT 率)會產生一種稱為索引碎裂的模式:索引填滿半空頁面,因為每次插入都打到不同位置,且緩衝區持續轉動,因為熱工作集橫跨整個索引,而非可預測的近期切片。

在生產環境中的症狀:INSERT 延遲增加、autovacuum 額外開銷上升(PostgreSQL)、檢查點壓力增長,而近期紀錄的讀取效能下降,因為「近期」不再對應索引中的任何本地性。這並非假設 — 它是具 UUID v4 主鍵的大型 PostgreSQL 與 MySQL 資料表上一個有充分文件記錄的痛點。

UUID v7 — 具時間順序的隨機性

UUID v7 為解決 B-tree 碎裂問題而明確設計。它在最重要的位元中編碼 48 位元 Unix 毫秒時間戳,後接版本位元、12 個隨機位元、變體位元,以及另外 62 個隨機位元。總隨機性為 74 位元 — 仍遠超過防止碰撞所需。

018f4e6b-a23c-7d45-9abc-0e02b2c3d479
 ^^^^^^^^^^^^^^
 48 位元 Unix 時間戳(毫秒精度)
                   ^
                   版本 = 7

由於時間戳佔據高位元,稍後產生的 UUID 排在較早產生的 UUID 之後。插入永遠附加到索引的右邊緣。資料庫只需在緩衝區保持最近的索引頁面為熱,而非整個索引。在高 INSERT 率下,光是這點就能將寫入延遲削減 30–60%,並在具數千萬列的資料表上將 I/O 減少一個數量級。

v7 如何產生

import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// => '018f4e6b-a23c-7d45-9abc-0e02b2c3d479'

相同的 uuid 函式庫在版本 10 中加入了 v7 支援。Python 標準函式庫在 3.14 中加入。PostgreSQL 17 內建 uuidv7() 函數。如果你使用較舊的技術堆疊,有數個小型函式庫提供無相依套件的 v7 產生。

次毫秒單調性

當兩個 v7 UUID 在同一毫秒內產生會發生什麼?RFC 9562 允許實作在隨機位元中遞增單調計數器,以確保同一毫秒內的順序。uuid 函式庫預設這麼做。結果:即使在一毫秒內產生 10,000 個 ID,它們仍能正確排序。

B-tree 索引碎裂問題的細節

為理解為何此事重要,考慮在一個已具 1 億現有列的資料表上,以 UUID v4 主鍵每秒 10,000 次插入會發生什麼:

  • 每次插入都會產生隨機的 128 位元鍵,落在 1 億現有項目間的隨機位置。
  • 資料庫必須將包含該位置的特定 B-tree 頁面載入緩衝區。
  • 有 1 億列與 8 KB 頁面,索引涵蓋約 100,000 頁。每秒需要 10,000 個不同頁面 — 遠超典型 8 GB shared_buffers 僅為此索引能持有的容量。
  • 每次快取未命中都會造成磁碟讀取。在每秒 10,000 次插入下,這可能每秒產生數千次隨機磁碟讀取,讓大型資料表上即使是 SSD 也會飽和。

使用 UUID v7,所有每秒 10,000 次插入都落在最右側的葉頁面(或近期少數頁面)。緩衝區只需保持那少數頁面為熱。寫入的快取命中率接近 100%。寫入放大大幅下降。

相同好處適用於範圍掃描:對 v4 資料表執行 WHERE created_at BETWEEN x AND y 需要完整索引掃描或獨立的時間戳索引。在 v7 資料表上,主鍵本身就是時間戳索引 — 查詢能直接尋找到正確範圍。

如何在 PostgreSQL 中使用 UUID v7

-- PostgreSQL 16+ 中原生支援 UUID 型別
CREATE TABLE events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4(隨機)
  -- 透過擴充功能或應用端產生切換到 UUIDv7
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 使用 UUIDv7 時:時間戳已嵌入,
-- 因此這個獨立的 created_at 欄位通常是多餘的。

PostgreSQL 17 原生提供 uuidv7()。對於 PostgreSQL 14–16,pg_uuidv7 擴充功能提供相同函數。UUID 資料型別以相同方式儲存 v4 與 v7 — 16 位元組,無額外開銷。唯一差別是決定排序順序的位元模式。

一個有用的結果:由於時間戳已嵌入,許多資料表不再需要獨立的 created_at 欄位作為排序或顯示之用。你可以用一次函數呼叫從 v7 UUID 中擷取時間戳。這降低 schema 複雜度,並消除每次插入的一次寫入。

UUID v4 vs v7 — 取捨

特性 UUID v4 UUID v7
隨機性位元 122 74
排序順序 隨機 時序
B-tree INSERT 效能 差(碎裂) 極佳(連續)
時間戳洩漏 有(毫秒精度)
RFC 標準 RFC 4122(2005)、RFC 9562(2024) RFC 9562(2024)
函式庫支援 普及 快速擴展中(2024–2026)
嵌入 created_at

遷移指南 — v4 至 v7

將具 UUID v4 主鍵的現有資料表遷移到 v7 是多步驟操作。關鍵限制:你不能在不更新所有引用資料表的情況下,改變被外鍵引用的主鍵值。請規劃維護視窗或採用雙寫做法。

-- 1. 新增新欄位
ALTER TABLE orders ADD COLUMN id_v7 UUID;

-- 2. 為現有列回填(保留原始 created_at 作為
--    時間戳來源;在你的應用中使用 UUIDv7 函式庫函數)
UPDATE orders SET id_v7 = generate_uuidv7(created_at);

-- 3. 驗證沒有 NULL 值殘留
SELECT COUNT(*) FROM orders WHERE id_v7 IS NULL;

-- 4. 交換欄位(需要簡短維護視窗)
ALTER TABLE orders ALTER COLUMN id_v7 SET NOT NULL;
ALTER TABLE orders ALTER COLUMN id_v7 SET DEFAULT generate_uuidv7(now());
ALTER TABLE orders RENAME COLUMN id TO id_v4_old;
ALTER TABLE orders RENAME COLUMN id_v7 TO id;
ALTER TABLE orders ADD PRIMARY KEY (id);

對於高流量資料表的零停機遷移,建議做法為:

  1. 新增具伺服器預設的 id_v7 欄位,該預設會為新進列產生 v7 UUID。
  2. 在低流量期間以批次回填舊列,使用現有的 created_at 時間戳作為 v7 時間戳部分的種子。
  3. 更新引用資料表中的所有外鍵欄位以指向 id_v7
  4. 在簡短維護視窗中重新命名欄位並移除舊主鍵約束。

回填步驟最長。以每批次 10,000 列、批次間 50 ms 睡眠的速度,1 億列的資料表約需 8 小時。請及早開始。

現實世界影響

多個工程團隊發表了在生產規模資料集上比較 UUID v4 與 v7 的基準測試。一致的發現是:

  • INSERT 吞吐量:從 v4 切換到 v7 時,在具 5,000 萬以上列的資料表上有 2–5 倍改善,隨著資料表大小增加,改善幅度也增加。
  • 寫入延遲 p99:在相同硬體上,從數百毫秒(v4,負載下)下降到個位數毫秒(v7)。
  • 索引大小:v7 欄位上的 B-tree 索引比相同資料的等效 v4 索引小 15–30%,因為碎裂留下較少半空頁面。
  • 緩衝區效率:主鍵索引的 shared buffers 命中率從約 40%(v4,大型資料表)提升到約 99%(v7),因為只有近期頁面需要保持為熱。

在約 100 萬列以下時,獲益可忽略。如果你的資料表保持小型,為求簡單請維持 v4。在具任何顯著 INSERT 率的 1,000 萬列以上,v7 是較佳的預設。

何時使用何者

使用 UUID v7 的時機:

  • 你在設計新 schema,且資料表會變大(1,000 萬列以上)。
  • 資料表有高 INSERT 率 — 事件、日誌、訂單、訊息、通知。
  • 你想要兼作建立時間戳的主鍵(可省略一個欄位)。
  • 你使用 PostgreSQL 17、MySQL 8.0+,或支援 v7 產生的現代 ORM。
  • 依 ID 排序在語意上等同於依建立時間排序 — 對多數附加密集型資料表都會如此。

使用 UUID v4 的時機:

  • 識別碼暴露給使用者且不能透露建立時間(邀請碼、分享連結、計費控制碼)。
  • 資料表小型且穩定 — 沒有效能好處能合理化遷移成本。
  • 你在建立時間敏感的情境中產生 ID(在以成長外觀競爭的產品中的私人使用者紀錄)。
  • 你需要某種一次性憑證或令牌 — 請使用專用祕密產生器,而非任何 UUID 版本。

總結

UUID v4 是隨機、私人且廣泛支援。UUID v7 具時間順序、對資料庫友善,且現在已是主要函式庫與資料庫的標準。對於具大型或快速成長資料表的新 schema,v7 是較佳的預設。對於暴露給使用者且時間戳洩漏是疑慮的 ID,v4 仍是正確選擇。

使用 Toova UUID 產生工具即時產生兩種版本 — 無需帳戶。若需較短令牌,隨機字串產生工具能以任意長度產出英數 ID。