索引原理與優化#

索引是資料庫系統裡面最重要的概念之一。簡單來說,索引的出現是為了提高資料查詢的效率,就像書的目錄一樣。

索引的常見模型#

可以用於提高讀寫效率的資料結構很多,這裡介紹三種常見模型:

哈希表#

哈希表是一種以鍵-值(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%
頁合併相鄰頁利用率低合併資料頁

為什麼推薦自增主鍵#

效能角度:

  • 自增主鍵的插入模式是追加操作
  • 不涉及挪動其他記錄
  • 不會觸發葉子節點的分裂

存儲角度:

  • 每個二級索引的葉子節點都存主鍵值
  • 主鍵越小,二級索引占用空間越小
主鍵類型占用空間
INT4 字節
BIGINT8 字節
身份證號約 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

聯合索引的字段順序原則:

  1. 複用原則:如果通過調整順序可以少維護一個索引,優先採用
  2. 空間原則:如果 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+ 在索引層面提前過濾

設計建議:一般情況下使用自增主鍵;利用覆蓋索引減少回表;聯合索引注意字段順序;定期評估索引使用情況。