索引原理與優化#
索引是資料庫系統裡面最重要的概念之一。簡單來說,索引的出現是為了提高資料查詢的效率,就像書的目錄一樣。
索引的常見模型#
可以用於提高讀寫效率的資料結構很多,這裡介紹三種常見模型:
哈希表#
哈希表是一種以鍵-值(key-value)存儲資料的結構,通過哈希函數把 key 換算成陣列位置。
flowchart LR
H1["ID_card_n1"] -->|hash| U1["User1"]
H2["ID_card_n2"] -->|hash| U2["User2"] --> U4["User4"]
H3["ID_card_n3"] -->|hash| U3["User3"]
style U4 fill:#fff3e0哈希衝突時使用鏈表解決
特點:
- 增加新記錄很快,只需往後追加
- 不適合範圍查詢(必須全表掃描)
哈希表適用於只有等值查詢的場景,比如 Memcached 及其他 NoSQL 引擎。
有序數組#
flowchart LR
subgraph Array["有序數組"]
direction LR
I1["ID1"] --- I2["ID2"] --- I3["ID3"] --- I4["ID4"] --- I5["ID5"] --- I6["ID6"]
end
BS["二分查找<br/>O(log N)"] -.-> I1
style BS fill:#e3f2fd特點:
- 等值查詢和範圍查詢性能都優秀
- 插入成本太高(需要挪動後面所有記錄)
有序陣列只適用於靜態存儲引擎,比如歷史資料歸檔。
N 叉樹(B+ 樹)#
二叉樹查詢效率高 O(log N),但 100 萬節點的平衡二叉樹,樹高 20,每次查詢要訪問 20 個資料塊。
為什麼用 N 叉樹?
flowchart TD
A[根節點<br/>常駐內存] --> B[分支]
A --> C[分支]
A --> D[分支]
B --> E[葉子<br/>實際資料]
B --> F[葉子<br/>實際資料]
B --> G[葉子<br/>實際資料]- InnoDB 的整數索引 N 約等於 1200
- 樹高 4 時,可存 1200³ = 17 億條記錄
- 查找一個值最多訪問 3-4 次磁盤
N 叉樹(B+ 樹)由於適配磁盤的訪問模式,已被廣泛應用在資料庫引擎中。
InnoDB 的索引模型#
在 InnoDB 中,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱為索引組織表。
兩種索引類型#
CREATE TABLE T (
id INT PRIMARY KEY,
k INT NOT NULL,
name VARCHAR(16),
INDEX (k)
) ENGINE=InnoDB;| 索引類型 | 別名 | 葉子節點內容 |
|---|---|---|
| 主鍵索引 | 聚簇索引 (Clustered Index) | 整行資料 |
| 非主鍵索引 | 二級索引 (Secondary Index) | 主鍵的值 |
回表#
查詢:SELECT * FROM T WHERE k=5
步驟 1: 搜索 k 索引樹,找到 k=5,得到 ID=500
步驟 2: 搜索 ID 索引樹,找到 ID=500 的完整記錄 ← 這就是「回表」基於非主鍵索引的查詢需要多掃描一棵索引樹,因此應用中應該盡量使用主鍵查詢。
索引維護#
頁分裂與頁合併#
B+ 樹為了維護索引有序性,在插入新值時需要做必要的維護:
| 操作 | 情況 | 影響 |
|---|---|---|
| 順序插入 | 只需在最後追加 | 效率高 |
| 亂序插入 | 需要挪動後面的資料 | 效率低 |
| 頁分裂 | 資料頁滿了,需要申請新頁 | 效能下降,空間利用率降低約 50% |
| 頁合併 | 相鄰頁利用率低 | 合併資料頁 |
為什麼推薦自增主鍵#
效能角度:
- 自增主鍵的插入模式是追加操作
- 不涉及挪動其他記錄
- 不會觸發葉子節點的分裂
存儲角度:
- 每個二級索引的葉子節點都存主鍵值
- 主鍵越小,二級索引占用空間越小
| 主鍵類型 | 占用空間 |
|---|---|
| INT | 4 字節 |
| BIGINT | 8 字節 |
| 身份證號 | 約 20 字節 |
適合用業務字段做主鍵的場景:只有一個索引,且必須是唯一索引(典型的 KV 場景)。
索引優化技術#
覆蓋索引#
如果查詢的字段已經在索引中,就不需要回表。
-- 需要回表
SELECT * FROM T WHERE k BETWEEN 3 AND 5;
-- 覆蓋索引,不需要回表
SELECT ID FROM T WHERE k BETWEEN 3 AND 5;覆蓋索引可以減少樹的搜索次數,顯著提升查詢效能,是常用的效能最佳化手段。
案例:身份證號和姓名的聯合索引
假設有高頻請求:根據身份證號查詢姓名。
方案一:只建 (id_card) 索引
- 查詢時需要回表取 name
方案二:建 (id_card, name) 聯合索引
- 覆蓋索引,不需要回表
- 缺點是索引占用更多空間
權衡:如果這是高頻請求,聯合索引的收益大於成本。
最左前綴原則#
B+ 樹索引可以利用索引的「最左前綴」來定位記錄。
-- 聯合索引 (name, age)
-- 索引項按 name 排序,name 相同時按 age 排序
-- 可以用上索引
WHERE name = '張三'
WHERE name LIKE '張%'
-- 無法使用索引
WHERE age = 10聯合索引的字段順序原則:
- 複用原則:如果通過調整順序可以少維護一個索引,優先採用
- 空間原則:如果 a 和 b 都需要單獨索引,把大字段放聯合索引,小字段單獨建索引
索引下推 (Index Condition Pushdown)#
MySQL 5.6 引入的優化,在索引遍歷過程中對索引中包含的字段先做判斷,減少回表次數。
-- 聯合索引 (name, age)
SELECT * FROM tuser WHERE name LIKE '張%' AND age = 10;| 版本 | 行為 | 回表次數 |
|---|---|---|
| MySQL 5.6 之前 | 找到所有「張」開頭的記錄,逐一回表判斷 age | 多 |
| MySQL 5.6+ | 在索引中就判斷 age,過濾不符合的 | 少 |
索引設計原則#
建索引的考量#
| 考量點 | 說明 |
|---|---|
| 選擇性 | 區分度高的字段更適合建索引 |
| 查詢頻率 | 高頻查詢字段優先 |
| 更新頻率 | 頻繁更新的字段,索引維護成本高 |
| 空間成本 | 索引也占用存儲空間 |
重建索引#
索引可能因為刪除或頁分裂導致數據頁有空洞,重建索引可以讓索引更緊湊。
-- 重建二級索引(可以)
ALTER TABLE T DROP INDEX k;
ALTER TABLE T ADD INDEX(k);
-- 重建主鍵索引(不合理,會重建整個表兩次)
ALTER TABLE T DROP PRIMARY KEY;
ALTER TABLE T ADD PRIMARY KEY(id);
-- 正確做法:重建表
ALTER TABLE T ENGINE=InnoDB;本章小結#
| 概念 | 要點 |
|---|---|
| 索引模型 | 哈希表(等值查詢)、有序數組(靜態)、B+ 樹(通用) |
| InnoDB 索引 | 聚簇索引存數據,二級索引存主鍵 |
| 回表 | 二級索引查詢需要額外搜索主鍵樹 |
| 覆蓋索引 | 查詢字段都在索引中,避免回表 |
| 最左前綴 | 聯合索引可部分使用 |
| 索引下推 | 5.6+ 在索引層面提前過濾 |
設計建議:一般情況下使用自增主鍵;利用覆蓋索引減少回表;聯合索引注意字段順序;定期評估索引使用情況。