「沒有索引的資料庫查詢,就像在一本沒有目錄的書裡找一個章節—— 只能從第一頁翻到最後一頁。」
索引是資料庫效能優化最直接、最有效的工具。但「加了索引就快」是個危險的迷思—— 索引有維護成本,加錯地方反而拖慢寫入。這篇帶你從原理出發,學會判斷「什麼時候該加索引、加在哪裡」。
索引是什麼?Full Table Scan 的代價
沒有索引時,資料庫找資料的方式是全表掃描(Full Table Scan): 從第一列讀到最後一列,逐一比對條件。資料量小時無感,資料量大時慘不忍睹。
無索引:全表掃描
複雜度:O(N),N=資料筆數
有索引:B-Tree 查詢
複雜度:O(log N),百萬筆只需 ~20 步
B-Tree 索引的運作原理
PostgreSQL 和 MySQL 預設的索引結構都是 B-Tree(Balanced Tree)。 它是一棵「平衡」的多叉樹,確保從根節點到任何葉節點的距離都相同—— 這保證了查詢的時間複雜度穩定在 O(log N)。
B-Tree 結構示意(以 email 欄位建索引)
alice → row 2
bob → row 7
joseph → row 1
kevin → row 5
mary → row 9
zoe → row 3
葉節點儲存索引值 + 指向實際資料列的指標(row pointer)
B-Tree 擅長的查詢
- ✅ 等值查詢:WHERE email = 'x@x.com'
- ✅ 範圍查詢:WHERE age BETWEEN 20 AND 30
- ✅ 前綴查詢:WHERE name LIKE 'Jo%'
- ✅ 排序:ORDER BY email(可避免額外排序步驟)
B-Tree 無法優化的查詢
- ❌ 後綴查詢:WHERE name LIKE '%Chen'
- ❌ 函數計算:WHERE YEAR(created_at) = 2026
- ❌ 否定:WHERE status != 'active'(回傳大量資料時)
- ❌ OR 跨欄位(需要多個索引)
建立與管理索引
⚠️ 生產環境建索引:一定要加 CONCURRENTLY
普通的 CREATE INDEX 會鎖住整張表,期間所有寫入都被阻塞。 百萬列的表可能鎖幾分鐘——生產環境中這等同於服務中斷。 加上 CONCURRENTLY 讓索引在背景建立,不阻塞讀寫。
-- ❌ 生產環境禁用(會鎖表)
CREATE INDEX idx_users_email ON users(email);
-- ✅ 生產環境必用(背景建立,不阻塞)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- 注意:CONCURRENTLY 不能在交易(BEGIN...COMMIT)內使用
-- 若中途失敗,需手動清除殘留的 INVALID 索引
💡 主鍵(PRIMARY KEY)自動建索引
定義 PRIMARY KEY 時,PostgreSQL 會自動建立一個唯一索引。 不需要、也不應該再手動對 id 欄位建索引。 FOREIGN KEY 欄位(如 user_id)則不會自動建索引,需要手動加——這是常見遺漏。
複合索引的欄位順序:最重要的細節
複合索引(Composite Index)可以同時索引多個欄位,但欄位的順序決定了索引的效用。 這是最多人搞錯的地方。
規則:最左前綴原則(Leftmost Prefix Rule)
索引 (A, B, C) 可以服務以下查詢,但只能從左邊開始連續使用:
WHERE A = 1✅ 使用索引(A)WHERE A = 1 AND B = 2✅ 使用索引(A, B)WHERE A = 1 AND B = 2 AND C = 3✅ 使用索引(A, B, C)WHERE B = 2❌ 無法使用索引(跳過了 A)WHERE A = 1 AND C = 3⚠️ 只能使用 A 部分,C 無法走索引EXPLAIN:看穿查詢的執行計畫
EXPLAIN 是診斷慢查詢最重要的工具。它顯示資料庫「打算怎麼執行你的 SQL」—— 有沒有走索引、掃了多少列、花了多少成本。
看懂 EXPLAIN 輸出的關鍵欄位
Seq Scan全表掃描,通常是問題所在Index Scan走索引,好的跡象Index Only Scan只讀索引不讀資料列,最快cost=X..YX=取第一列的成本,Y=總成本(越小越好)rows=N預估回傳的列數(與 actual 差很多表示統計過期)actual time=X..Y實際執行時間(ms),ANALYZE 才有⚠️ 為什麼索引有時不被使用?
- • 資料量太少:幾百列時全表掃描比走索引更快(有 overhead)
- • 選擇性太低:欄位值重複率高(如性別),全掃比索引跳轉更划算
- • 函數包裹欄位:WHERE LOWER(email) = 'x',索引在 email 上,LOWER() 使其失效
- • 隱式型別轉換:欄位是 VARCHAR,查詢傳 INTEGER,型別不符導致索引失效
索引的代價:不是加越多越好
每個索引都有維護成本
💾
儲存空間
每個索引都需要額外的磁碟空間。一張表索引多了,可能比資料本身還大。
✏️
寫入效能
INSERT / UPDATE / DELETE 時,所有相關索引都需要同步更新。寫多讀少的表慎加索引。
🔒
鎖定範圍
索引更新需要獲取鎖,高並發寫入時多餘的索引會加劇鎖競爭。
✅ 索引最佳實踐
- • WHERE、JOIN ON、ORDER BY 的欄位是索引的第一候選
- • FK 欄位(如 user_id)幾乎都該加索引——外鍵查詢非常頻繁
- • 選擇性高的欄位優先(email 勝過 gender)
- • 先用 EXPLAIN ANALYZE 確認問題,再加索引——不要猜測
- • 定期用
pg_stat_user_indexes檢查索引使用率,刪除從未被使用的索引