進階索引實戰:B-Tree、複合索引與 Hash Map 全解析
本文將「索引」觀念與前端離線快取 (Service Worker / PWA) 類比,幫助剛接觸後端資料庫的新手也能快速理解「為何加索引像是替資料上快取」。全篇以 MySQL 為例,但概念同樣適用於 PostgreSQL、SQL Server 等主流 RDBMS。
📋 文章目錄
- 索引 ≈ PWA 快取:觀念對照圖
- B-Tree/B+Tree:關聯式資料庫的預設索引
- Hash Index:O(1) 等值查詢神器
- 複合索引(Composite Index)
- 快取策略 vs. 索引策略
- 實戰案例:從 3 秒 → 50 毫秒
- 索引維護與監控
- 常見地雷與排雷技巧
0. 索引 ≈ PWA 快取:觀念對照圖
Web PWA | Database | 共通核心 |
---|---|---|
Service Worker Cache API | B-Tree / Hash Index | 以空間換時間,加速存取 |
Cache Key | 索引 Key | 唯一定位資料位置 |
Cache First / Network First | Index Lookup / Table Scan | 先查快取 (索引),再回源 (資料表) |
緩存更新 (Revalidate) | 索引維護 (UPDATE / REBUILD) | 保持資料與索引一致 |
結論:快取如何讓 PWA 離線秒開,索引就如何讓資料庫查詢「不掃全表」。
1. B-Tree/B+Tree:關聯式資料庫的預設索引
1-1 為何不是二元樹?
磁碟 I/O 一次可讀取整個 block (4K/8K),若用二元樹,每層只存 2 個指標,會造成 I/O 次數過多。B-Tree 允許 m 階,一頁存放大量 key,從而大幅降低樹高[25][29]。
1-2 結構分層
Root (1 page)
├─ Internal Node (多頁)
└─ Leaf Node (大量頁) ← 實際儲存資料位置 / PK 值
- 葉層 按 key 有序鏈結,可做範圍掃描 (between、> <)。
- 中間層 只存 key+child pointer,加快導向。
- 根層 永遠在記憶體,搜尋複雜度 O(logₘ n)。
類比 Service Worker:Root 像 JS Runtime 常駐記憶體的快取索引表,Leaf 像實際 Cache Storage blob。
1-3 建立範例
-- email + created_at 結合範圍 + 等值
CREATE INDEX idx_user_email_date ON users(email, created_at);
-- 解讀
-- 1. email 完全比對時 → 直接走索引
-- 2. email 比對後再依 created_at 做區段掃描 → 善用葉層有序
1-4 注意事項
- 最左前綴:查詢條件需含最左欄位才能命中索引。
- 回表 (Key Lookup):非覆蓋欄位須再到資料頁讀取,等同 PWA Cache Miss。
2. Hash Index:O(1) 等值查詢神器
2-1 運作流程
- 對 key 執行雜湊函式
hash(key)
→ 得 bucket 位址。 - 直接定位資料列 (或 PK)。
複雜度 O(1),缺點是不支援範圍、排序,也無法利用最左前綴原則[30]。
-- InnoDB MEMORY Engine 才支援顯式 HASH
CREATE TABLE hotspot (
id INT PRIMARY KEY,
code CHAR(8),
VALUE INT,
INDEX USING HASH (code)
) ENGINE=MEMORY;
2-2 Hash Map 類比
// JS 雜湊表:Key => Value
const map = new Map();
map.set('code_abc', 123);
console.log(map.get('code_abc')); // O(1)
資料庫 Hash Index 與 JavaScript Map
原理一致:雜湊衝突 (collision) 需鏈結或開放定址處理。
3. 複合索引(Composite / Multiple Column Index)
3-1 建立原則
- 選擇性最高 (cardinality) 放最左
- 將常用且固定的等值欄位放前,範圍條件塞尾端
-- 依以下查詢頻率排序
-- a. WHERE country='TW' AND status='paid' AND created_at>'2025-01-01'
-- b. WHERE country='TW' AND status='pending'
CREATE INDEX idx_orders_country_status_date
ON orders(country, status, created_at);
3-2 覆蓋索引 (Covering Index)
若 SELECT
欄位完全被索引覆蓋 → 不需回表
-- 索引欄位已包含查詢所需欄位 total
SELECT country, status, total
FROM orders FORCE INDEX (idx_orders_country_status_date)
WHERE country='TW' AND status='paid'; -- 直接返回索引頁
結果:I/O 降至單純索引掃描,等同 PWA Cache First 策略。
4. 快取策略 vs. 索引策略:最佳化思路對照
PWA 策略 | 資料庫對應 | 適用場景 |
---|---|---|
Cache First | 覆蓋索引 (只走索引頁) | 熱門讀多寫少清單 |
Network First | 非覆蓋索引 + 回表 | 資料即時性高,需要最新 row |
Stale-While-Revalidate | 舊索引保留 + 背景 REBUILD | 夜間批次重建大索引 |
TIP:夜深人靜做
OPTIMIZE TABLE
= 背景 Revalidate Cache。
5. 實戰:從 3 秒 → 50 毫秒
5-1 問題描述
SELECT order_id, total
FROM orders
WHERE user_id = 42
AND status = 'paid'
AND order_date BETWEEN '2025-01-01' AND '2025-07-01';
- 100 萬筆訂單,EXPLAIN 顯示
type: ALL
,耗時 3 秒。
5-2 解決方案
CREATE INDEX idx_orders_user_status_date
ON orders(user_id, status, order_date);
5-3 成果驗證
EXPLAIN SELECT ... ;
-- type: RANGE / rows: 120 → I/O 大減
-- 實測耗時 ~50 ms
6. 索引維護與監控
- 重組 / 重建:
ALTER INDEX ... REBUILD;
(SQL Server) 或OPTIMIZE TABLE
(MySQL) - 碎片率:>30% 建議 REBUILD
- 冗餘索引偵測:慢查詢日誌 +
SHOW INDEX
+ 卡片法清理 - 統計資訊更新:
ANALYZE TABLE
保證最佳化器選對索引
7. 常見地雷與排雷技巧
地雷 | 解法 |
---|---|
在索引欄位使用函數 YEAR(created_at) | 建立 函數索引 (Expression Index) 或改寫範圍查詢 |
LIKE '%keyword%' 前置百分比 | 使用全文索引 (FULLTEXT, GIN) |
多欄都建單欄索引 → 過度索引 | 合併為複合索引,或只保留高選擇性 |
低選擇度欄位建索引 (gender) | 搭配高選擇度欄或省略索引 |
8. 小結
- B-Tree 適用大部分範圍/排序查詢;
- Hash Index 適用極端熱點等值查詢;
- 複合索引 = 把常用條件壓縮進單一樹;
- 索引就像快取:讀快、寫慢、需清理;
- 定期維護、監控慢查詢,才能持續保持「秒開體驗」。
下一步:動手用
EXPLAIN
為你的專案打造「量身訂做」的索引組合,像調 Service Worker Cache Policy 一樣,讓資料庫也能一樣快!
常見問題 FAQ
Q1: B-Tree 和 Hash 索引該如何選擇?
A: 選擇依據:
- B-Tree:適用範圍查詢、排序、模糊搜尋(
LIKE 'prefix%'
) - Hash:純等值查詢,如用戶ID查詢、狀態碼比對
- 混合策略:熱點數據用Hash,一般查詢用B-Tree
Q2: 複合索引的欄位順序如何決定?
A: 遵循以下原則:
- 等值條件 > 範圍條件
- 高選擇性 > 低選擇性(cardinality高的在前)
- 查詢頻率高 > 查詢頻率低
-- 優先順序:country(等值) > status(等值) > created_at(範圍)
CREATE INDEX idx_orders_country_status_date
ON orders(country, status, created_at);
Q3: 如何判斷索引是否需要重建?
A: 監控指標:
- 碎片率 > 30%:需要重建
- 索引深度過深:樹高 > 4層
- 查詢效能下降:相同查詢變慢
-- MySQL 檢查索引碎片
SHOW TABLE STATUS LIKE 'table_name';
-- 重建索引
OPTIMIZE TABLE table_name;
Q4: 覆蓋索引和非覆蓋索引的效能差異?
A: 效能對比:
- 覆蓋索引:只需索引掃描,I/O次數少
- 非覆蓋索引:需要回表查詢,額外I/O成本
實測數據顯示覆蓋索引可提升 2-5倍 查詢效能。
Q5: 什麼情況下不建議使用索引?
A: 避免索引的情況:
- 小表(< 1000筆資料)
- 寫入頻繁的表(寫入:讀取 > 3:1)
- 低選擇性欄位(如 gender, status 只有2-3個值)
進階索引效能測試
索引類型 | 查詢類型 | 100萬筆數據耗時 | 適用場景 |
---|---|---|---|
B-Tree | 等值查詢 | 2-5ms | 通用查詢 |
B-Tree | 範圍查詢 | 10-50ms | 日期、數值範圍 |
Hash | 等值查詢 | 1-2ms | 熱點數據查詢 |
複合索引 | 多條件查詢 | 1-3ms | 複雜業務查詢 |
覆蓋索引 | 特定欄位查詢 | 0.5-1ms | 報表、統計查詢 |
測試環境:MySQL 8.0,SSD硬碟,16GB記憶體
延伸閱讀
📝 作者資訊: 本文由許恩綸撰寫,專注於資料庫效能優化與後端架構設計。
🔄 最後更新: 2025年7月5日
⚡ 效能提升: 透過本文技巧,可將查詢效能提升 60-1500倍!