在資料庫優化的世界裡,索引是提升查詢效能的關鍵武器。本文基於 Rick James 的 MySQL Indexing Cookbook 整理出 MySQL 索引設計的核心原則與實戰技巧。

為什麼索引如此重要?

想像一下在沒有目錄的百科全書中查找特定資訊 - 這就是沒有索引的資料表查詢。適當的索引可以將查詢時間從數秒縮短到毫秒級別,特別是在處理大量資料時。

索引設計的黃金法則

1. 三步驟索引設計演算法

建立有效索引的基本步驟:

步驟 1:優先處理等值條件 將 WHERE 子句中與常數比較的欄位放在索引最前面:

-- 查詢
SELECT * FROM users
WHERE status = 'active' AND department = 'IT';

-- 索引設計
INDEX(status, department)

步驟 2:加入範圍條件欄位 範圍查詢(BETWEEN、>、< 等)的欄位放在等值條件之後:

-- 查詢
SELECT * FROM orders
WHERE status = 'pending'
  AND created_date >= '2025-01-01';

-- 索引設計
INDEX(status, created_date)  -- 等值在前,範圍在後

步驟 3:考慮 GROUP BY 和 ORDER BY 如果沒有範圍條件,可以將 GROUP BY 或 ORDER BY 的欄位加入索引:

-- 查詢
SELECT department, COUNT(*)
FROM employees
WHERE company_id = 100
GROUP BY department;

-- 索引設計
INDEX(company_id, department)

2. 複合索引的藝術

複合索引(Composite Index)是包含多個欄位的索引,其設計原則:

欄位順序至關重要

-- 這兩個索引是不同的!
INDEX(last_name, first_name)   -- 適合 WHERE last_name = 'Chen'
INDEX(first_name, last_name)   -- 適合 WHERE first_name = 'Jack'

最左前綴原則

複合索引可以當作多個索引使用:

-- 擁有索引
INDEX(a, b, c)

-- 相當於同時擁有
INDEX(a)
INDEX(a, b)
INDEX(a, b, c)

3. 覆蓋索引(Covering Index)

當索引包含查詢所需的所有欄位時,MySQL 可以直接從索引取得資料,無需回表:

-- 查詢
SELECT user_id, username, email
FROM users
WHERE status = 'active';

-- 覆蓋索引
INDEX(status, user_id, username, email)

優點:

  • 大幅減少磁碟 I/O
  • 查詢速度提升數倍

限制:

  • 建議不超過 5 個欄位
  • 需要權衡索引大小與效能提升

實戰案例分析

案例 1:多對多關聯表優化

-- 文章標籤關聯表
CREATE TABLE article_tags (
    article_id INT,
    tag_id INT,
    PRIMARY KEY(article_id, tag_id),  -- 複合主鍵
    INDEX(tag_id, article_id)          -- 反向索引
);

-- 兩個索引支援雙向查詢
-- 1. 查詢文章的所有標籤:使用 PRIMARY KEY
-- 2. 查詢標籤的所有文章:使用 INDEX(tag_id, article_id)

案例 2:時間範圍查詢優化

-- 低效查詢
SELECT * FROM logs
WHERE created_at BETWEEN '2025-01-01' AND '2025-01-31'
  AND level = 'ERROR';

-- 原索引(效能差)
INDEX(created_at, level)

-- 優化後索引(效能好)
INDEX(level, created_at)
-- 因為 level 是等值條件,應該放在範圍條件 created_at 之前

案例 3:分頁查詢優化

-- 問題查詢:深度分頁
SELECT * FROM products
ORDER BY created_at DESC
LIMIT 10000, 20;  -- 跳過 10000 筆資料

-- 優化方案:使用覆蓋索引 + 子查詢
SELECT p.* FROM products p
INNER JOIN (
    SELECT id FROM products
    ORDER BY created_at DESC
    LIMIT 10000, 20
) AS tmp ON p.id = tmp.id;

-- 需要索引
INDEX(created_at, id)  -- 覆蓋索引

常見錯誤與陷阱

1. 低基數索引陷阱

-- 錯誤:為布林欄位建立單獨索引
INDEX(is_deleted)  -- 只有 true/false 兩個值

-- 正確:結合其他欄位建立複合索引
INDEX(is_deleted, created_at)

原因: MySQL 優化器對於選擇性低的索引(如性別、狀態等)通常會選擇全表掃描。

2. 索引過多的問題

-- 錯誤:為每個欄位都建立索引
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    email VARCHAR(100),
    phone VARCHAR(20),
    address TEXT,
    INDEX(username),
    INDEX(email),
    INDEX(phone),
    INDEX(address(100))  -- 更糟糕的是索引 TEXT 欄位
);

問題:

  • 降低 INSERT/UPDATE/DELETE 效能
  • 增加儲存空間
  • 維護成本高

建議: 每個表保持在 6 個索引以內

3. 索引失效的情況

-- 索引失效案例 1:函數操作
WHERE YEAR(created_at) = 2025  -- 索引失效
-- 改為
WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'

-- 索引失效案例 2:隱式類型轉換
WHERE phone = 123456789  -- phone 是 VARCHAR,索引失效
-- 改為
WHERE phone = '123456789'

-- 索引失效案例 3:LIKE 前綴匹配
WHERE username LIKE '%jack%'  -- 索引失效
-- 如果可能,改為
WHERE username LIKE 'jack%'  -- 可以使用索引

進階優化技巧

1. 前綴索引(Prefix Index)

對於長字串欄位,可以只索引前 N 個字元:

-- 原始欄位很長
ALTER TABLE articles ADD INDEX(title(50));  -- 只索引前 50 個字元

-- 選擇合適的前綴長度
SELECT
    COUNT(DISTINCT LEFT(title, 10)) / COUNT(DISTINCT title) AS sel10,
    COUNT(DISTINCT LEFT(title, 20)) / COUNT(DISTINCT title) AS sel20,
    COUNT(DISTINCT LEFT(title, 50)) / COUNT(DISTINCT title) AS sel50
FROM articles;
-- 選擇接近 1.0 的最小長度

2. 索引合併(Index Merge)

MySQL 可以同時使用多個索引:

-- 查詢
SELECT * FROM users
WHERE username = 'jack' OR email = '[email protected]';

-- 需要的索引
INDEX(username)
INDEX(email)
-- MySQL 會使用 Index Merge 優化

3. 使用 EXPLAIN 分析查詢

EXPLAIN SELECT * FROM users WHERE status = 'active';

重點關注:

  • type:最好是 const、eq_ref、ref
  • key:實際使用的索引
  • rows:掃描的行數(越少越好)
  • Extra:Using index(覆蓋索引)是好訊號

索引維護最佳實踐

1. 定期分析索引使用情況

-- 查看未使用的索引
SELECT * FROM sys.schema_unused_indexes;

-- 查看重複索引
SELECT * FROM sys.schema_redundant_indexes;

2. 監控慢查詢

-- 啟用慢查詢日誌
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;  -- 2 秒以上的查詢

3. 定期優化表

-- 重建索引,回收空間
OPTIMIZE TABLE users;

-- 更新統計資訊
ANALYZE TABLE users;

總結與檢查清單

設計索引時的檢查清單:

  • WHERE 條件的欄位都在索引中嗎?
  • 等值條件欄位在範圍條件之前嗎?
  • 是否可以建立覆蓋索引?
  • 索引的選擇性夠高嗎(> 10%)?
  • 複合索引的欄位順序正確嗎?
  • 是否有重複或冗餘的索引?
  • 更新頻繁的欄位是否真的需要索引?
  • 使用 EXPLAIN 驗證索引是否被使用?

參考資源

記住:索引不是越多越好,而是越精準越好。每個索引都要有明確的使用場景,並且要定期檢視和優化。