索引存什麼,決定了它是「資料本身」還是「指路牌」。Clustered index 把整列資料放在葉節點(資料即索引);Secondary index 葉節點只放指標,要拿完整列得再跳一次(回表)。這一刀切下去,決定了寫入成本、回表代價,與「該不該把欄位塞進索引」。
🧠 Intuition
TIP
一句話分辨:clustered index 是「書的內文本身按章節排好」——找到位置就是內容;secondary index 是「書末的索引頁」——找到的是頁碼,還得翻回去那一頁。
- Clustered index:表的資料列物理上依某個 key(通常是主鍵)排序,並直接存在 B-Tree 葉節點。InnoDB 主鍵就是這樣——表本身就是一棵以主鍵為 key 的 B-Tree。
- Secondary index:另一棵獨立 B-Tree,葉節點存的是「索引欄位值 → 定位資訊」。InnoDB 存的是主鍵值;heap-based 系統(PostgreSQL、SQL Server heap)存的是實體位置(heap 的 ctid / RID)。
- 回表 (bookmark lookup):secondary index 只給你定位資訊,要取其餘欄位得拿著主鍵/位置回 clustered index 或 heap 再查一次。每筆命中多一次隨機 I/O。
- Heap table vs Index-Organized Table (IOT):heap 把資料無序丟在堆上、所有索引都是 secondary;IOT/clustered 把資料塞進主索引葉節點。Oracle 稱 IOT,SQL Server 稱 clustered index,InnoDB 強制 IOT。
⚖️ Tradeoffs
Clustered (IOT) — 主鍵查詢與範圍掃描極快
- 以主鍵點查或範圍掃描時,找到葉節點就拿到整列,零回表。
- 主鍵相鄰的列物理相鄰,range scan 是順序 I/O。
- 代價:每個 secondary index 都要存主鍵當指標,主鍵越肥、所有次級索引越肥。所以 InnoDB 主鍵宜窄、宜單調遞增(避免頁分裂)。
Heap table — 寫入便宜,但讀靠回表
- 資料無序追加進 heap,插入不必維持物理順序,寫入路徑單純。
- 所有索引一律是 secondary,葉節點存實體位置(RID/ctid),命中後直接定位、不必經主索引。
- 代價:沒有「資料即索引」的免費午餐,任何非覆蓋查詢都要回 heap;PostgreSQL 還因 MVCC 更新搬移列而需 HOT / index-only scan 搭配 visibility map 來緩解。
回表 (Bookmark Lookup) — secondary index 的隱藏成本
- 每筆命中 = 一次索引存取 + 一次回表,後者通常是隨機 I/O。
- 命中列數一多,optimizer 可能判定「回表太貴」而放棄索引、改全表掃描(tipping point)。
- 解法是讓索引「覆蓋」查詢所需欄位(見覆蓋索引),把回表整個消掉。
InnoDB 指標 = 主鍵 vs Heap 指標 = 實體位置
- InnoDB secondary index 存主鍵值(邏輯指標):列搬家(頁分裂)不必更新 secondary index,但回表要再走一次主鍵 B-Tree(兩次 B-Tree 查找)。
- Heap 系統存實體位置(物理指標):回表一步到位,但列實體搬移時需維護指標(PostgreSQL 用 MVCC + HOT 規避,SQL Server heap 用 forwarding pointer,會造成 fragmentation)。
- 取捨本質:邏輯指標換「搬家免維護」,物理指標換「回表更快」。
🔑 Takeaways
- Clustered index 葉節點 = 整列資料(資料即索引),主鍵點查/範圍掃描零回表;secondary index 葉節點只存指標。
- 回表 (bookmark lookup) 是 secondary index 的固有成本,命中過多時 optimizer 會改走全表掃描。
- InnoDB 是強制 IOT、secondary 存主鍵;heap 系統(PG/SQL Server heap)所有索引皆 secondary、存實體位置——邏輯指標 vs 物理指標的取捨。
- 推論:InnoDB 主鍵要窄且單調遞增,因為它被每個次級索引複製,且影響頁分裂與寫放大。
2026-07-01
- Q: 既然 clustered index 主鍵查那麼快,為什麼不把所有重要欄位都當主鍵?
- A: 因為主鍵在 InnoDB 裡會被每一個 secondary index 當作指標複製進去。主鍵越寬,所有次級索引的葉節點越胖,磁碟與快取佔用全面上升、寫入也更慢。這就是為什麼最佳實務是用窄的、單調遞增的代理鍵(如 BIGINT auto-increment)當主鍵,把語意上的唯一鍵改用 unique secondary index 表達——讓 clustered key 保持精瘦。