面試官:為什么 MySQL 索引要用 B+ 樹?
數據庫是后端工程師繞不開的核心技術,而索引,則是這核心中的核心。在日常工作中,我們每天都在和索引打交道,索引問題也是高級工程師面試中,面試官最喜歡考察的地方。
很多開發者在面試中談及索引時,往往只能給出一些零散、機械的記憶性答案,比如“B+樹查詢快”、“索引能提速”。這樣的回答很難在面試官心中留下深刻印象。真正體現一個工程師技術深度的,是對索引背后設計原理的系統性理解,以及在復雜場景下進行選型和優化的能力。

今天,我們就從一個經典的面試問題“MySQL為什么選擇B+樹?”切入,層層深入,不僅講解索引的“是什么”,更聚焦于“為什么”和“怎么做”。希望通過本文,你能構建起一個體系化的索引知識框架,讓你在未來的面試和工作中,都能游刃有余。
一、B+樹的核心結構
B+樹是一種精巧的多叉樹結構。對于一棵m階的B+樹,它遵循以下規則:
- 樹中的每個節點最多可以擁有m個子節點。
- 除了根節點,其他每個節點至少有 ?m/2? 個子節點。根節點若非葉子,則至少有兩個子節點。
- 擁有k個子節點的非葉子節點,必定包含k個關鍵字(可以理解為索引列的值)。
定義可能有些枯燥,記不住沒關系,但下面這兩個核心特征,你必須牢牢記住:
- 數據只存在于葉子節點:所有非葉子節點(可以看作是“目錄頁”)僅存儲關鍵字信息,用于指引查找方向。真正的數據記錄,全部存放在樹最底層的葉子節點中。
- 葉子節點構成有序鏈表:葉子節點之間通過雙向指針(在InnoDB中)相互連接,形成一個天然的、支持雙向遍歷的有序序列。

二、B+樹做索引的三大優勢
正是基于上述特性,B+樹為數據庫查詢帶來了三個優勢,每個優勢都給性能帶來了極大的提升。
1. 高效的磁盤I/O
數據庫的數據和索引通常存儲在磁盤上,而磁盤I/O的耗時是內存操作的上萬倍,是系統性能的主要瓶頸。查詢過程的本質,就是不斷地進行磁盤I/O來讀取數據頁(Page,InnoDB默認為16KB)。
B+樹的“多路”特性,意味著每個節點可以擁有成百上千個子節點(階數m非常大)。這使得整棵樹的結構呈現出“矮而胖”的形態,高度極低。
我們來做一個簡單的估算:假設主鍵是BIGINT類型(8字節),指針大小在InnoDB中是6字節,那么一個16KB的數據頁作為非葉子節點,大概可以存放 16KB / (8B + 6B) ≈ 1170 個(關鍵字+指針)組合。如果樹的高度為3,那么它可以索引的數據量大約是 1170 * 1170 * (葉子節點可容納的行數)。假設一行數據為1KB,葉子節點能放16行,那么總共可以索引 1170 * 1170 * 16 ≈ 2190萬 條數據。查詢這2000多萬條數據中的任意一條,最多只需要3次磁盤I/O!相比之下,傳統的二叉樹,在存儲同樣量級數據時,會形成一個“高瘦”的結構,查詢深度巨大,將導致災難性的磁盤I/O次數。
下圖是數量級與樹高的一個關系圖,可以看出B+樹對比二叉樹性能優勢非常明顯:

2. 范圍查詢效率高
在業務場景中,范圍查詢極其普遍,例如“查詢某個用戶在九月份的所有訂單”。B+樹的葉子節點通過鏈表串聯的設計,簡直是為范圍查詢量身定做的。
當執行一個范圍查詢,如 SELECT * FROM orders WHERE order_id BETWEEN 120 AND 230; 時,數據庫引擎會進行如下操作:
- 通過B+樹從根節點開始,快速定位到order_id = 120所在的葉子節點。
- 讀取該節點的數據。
- 然后,無需再從根節點開始查找121、122..,而是直接通過葉子節點的鏈表指針,順序向后遍歷,直到找到order_id > 230的記錄為止。
這個查找過程非常高效,找到第一個節點數據后,由于葉子結點的數據都是排好序的,直接找到第二個臨界數據,去中間的數據就完事了。

3. 輕量的非葉子節點
由于非葉子節點不存儲真實數據,只存儲關鍵字和指針,使得它們占用的空間非常小。這意味著,在系統內存允許的情況下,我們通常可以將索引樹的非葉子節點(也就是那幾層“目錄索引”)完整地加載到內存中,并長期駐留。
當一個查詢請求到來時,大部分的索引尋址過程(從根節點到葉子節點的上層路徑)都在飛快的內存中完成,只有最后需要讀取真實數據時,才需要進行那一次或幾次不可避免的磁盤I/O來訪問葉子節點。這進一步將磁盤I/O的次數壓縮到了極致。

B+樹的這三大優勢,環環相扣,共同保證里了B+樹查詢方面的高性能。
三、常見索引分類
理解了B+樹的底層原理,我們再把視野拉回應用層,看看在MySQL中,索引是如何基于B+樹衍生出五花八門的類型的,以及它們在實踐中是如何影響我們的系統性能的。
1. 聚簇索引與非聚簇索引
這是索引分類中最重要的一個維度,也是接下來我們將重點分析的一種索引分類,因為它直接關系到數據的物理存儲方式。
- 聚簇索引(Clustered Index):其葉子節點直接存儲了完整的數據行。在InnoDB中,數據表本身就是按主鍵組織的一棵B+樹,這棵樹就是聚簇索引。因此,一張表只能有一個聚簇索引。
- 非聚簇索引(Non-Clustered Index):也常被稱為二級索引或輔助索引。它的葉子節點存儲的不是完整數據行,而是對應行的主鍵值。除了主鍵索引外的其他索引,如普通索引、唯一索引等,都是非聚簇索引。

(1) 回表查詢
上面說了主要的索引分類,就不得不提一個非聚簇索引的性能問題了。那就是回表查詢。當我們使用非聚簇索引進行查詢時,如果查詢所需的數據列不完全包含在該索引中,數據庫就會經歷一個兩步走的過程:
- 索引查找:首先在非聚簇索引(某個非主鍵字段建立的索引樹)樹上查找到滿足條件的記錄,并從中獲取到主鍵值。
- 主鍵查找(回表):再用這個獲取到的主鍵值,去聚簇索引樹(按主鍵id排序的樹)上進行一次查找,最終定位到完整的行數據。

例如,我們有一個用戶表employees,主鍵是id,在employee_no(員工編號)上建了一個非聚簇索引。當我們執行 SELECT * FROM employees WHERE employee_no = '86'; 時,就需要先通過employee_no索引找到主鍵id(比如是50),然后再根據id=50去聚簇索引中找到完整的員工信息。這個拿著主鍵再去查一次的過程,就是“回表”。它至少會增加一次額外的樹查找和磁盤I/O,在高并發場景下,性能損耗也是不小的
(2) 覆蓋索引
如何避免回表操作帶來的性能損耗呢?答案就是 覆蓋索引。覆蓋索引并不是一種獨立的索引類型,而是指在一個查詢中,索引本身已經覆蓋了所有需要查詢的字段,因此數據庫引擎無需再回到主表(聚簇索引)去獲取數據的一種狀態。
還是上面的例子,如果我們為employees表建立一個 (employee_no, name) 的聯合索引。現在,我們的查詢變為 SELECT employee_no, name FROM employees WHERE employee_no = '86';。此時,查詢所需的所有列(employee_no 和 name)的值,都已存在于這個聯合索引的葉子節點上。數據庫可以直接從該索引的葉子節點提取數據并返回,完全避免了回表操作,性能大幅提升。

這給我們帶來了兩個極其重要的SQL優化啟示:
- 精準查詢:杜絕SELECT *,只查詢你真正需要的列,這是利用覆蓋索引的第一步。
- 為高頻查詢場景設計合適的覆蓋索引:分析業務中最核心的查詢,為其建立合適的聯合索引以實現覆蓋。
(3) 最左前綴匹配原則
在上面我們提到用索引覆蓋可以有效解決非聚簇索引的回表問題,這里我們還必須了解組合索引的生效規則,否則即使我們在一些字段上建立了組合索引,最終也不會生效。
對于組合索引,MySQL查詢優化器會嚴格遵循“最左前綴匹配”原則。這是組合索引最重要的,也是最容易被誤解的規則。
假設我們有一個訂單表orders,并建立了一個聯合索引 idx_reg_stat_date ON orders (region, status, order_date)。這個索引的B+樹在物理上是這樣排序的:首先按region排序,在region相同的情況下再按status排序,在前兩者都相同的情況下最后按order_date排序。
這種結構決定了索引的查找方式,必須從索引的最左邊的列開始,并且不能跳過中間的列。
- WHERE region = '華東' AND status = '已支付':能使用索引。優化器會用'華東'定位到特定范圍,再在這個范圍內用'已支付'繼續定位。
- WHERE region = '華東':能使用索引。只使用了索引的第一個列。
- WHERE status = '已支付' AND order_date = '2025-09-07':無法使用索引。因為查詢條件跳過了最左邊的region列,索引無法定位。
- WHERE region = '華東' AND order_date = '2025-09-07':只能使用索引的region部分。當region確定后,由于status未知,order_date是無序的,所以order_date條件無法利用索引進行快速查找,只能在所有region為'華東'的記錄中逐條掃描。
- WHERE region = '華東' AND status > '待發貨' AND order_date = '2025-09-07':只能使用region和status部分。當遇到范圍查詢(>、<、BETWEEN等)時,該列后續的索引列將無法再用于精確匹配。因為status大于'待發貨'的是一個范圍,在這個范圍里,order_date是無序的。
這里有一個簡化的口訣可以幫助記憶:等值匹配,從左到右;范圍中斷,跳過失效。
2. 其他常見索引分類
除了上述所說的聚簇索引和非聚簇索引的分類外,Mysql還有以下幾種分類,不過這些分類都不復雜,我們只需了解其對應的概念即可。
- 唯一索引(Unique Index):顧名思義,索引列的值必須唯一,但允許有空值(NULL)。主鍵是一種特殊的唯一索引,但不允許有空值。
- 組合索引(Composite Index):即聯合索引,由多個列共同組成的索引。它在實際業務中非常常用,是實現覆蓋索引和遵循最左前綴原則的關鍵。
- 前綴索引(Prefix Index):當索引列是很長的字符串時,為了節約索引空間和提高查詢效率,我們可以只取字符串的前一部分作為索引。例如,對一個varchar(255)的URL列,我們可以只索引其前50個字符。
- 全文索引(Full-Text Index):主要用于在大量文本中進行關鍵詞搜索,類似于搜索引擎的功能。它有自己的特殊語法(如 MATCH() AGAINST()),通常用于文章、評論等字段。
四、索引的維護成本
索引在極大提升查詢性能的同時,也帶來了不可忽視的成本,它并不是多多益善的“銀彈”。
- 空間成本:每一個索引都是一棵B+樹,需要實實在在地占用磁盤空間。索引越多,占用的空間就越大。
- 時間成本(維護成本):當對表中的數據進行INSERT、UPDATE、DELETE操作時,數據庫系統不僅要修改數據行,還必須同步地去修改每一棵相關的索引樹,以保證其數據的正確性和有序性。這個過程可能涉及到B+樹節點的頁分裂、合并等復雜操作,會引入額外的性能開銷。

因此,索引的設計是一門權衡的藝術。為一張表創建過多不必要的索引,尤其是在寫操作頻繁的場景下,反而會嚴重拖累系統的整體性能。
五、面試實戰指南
掌握了以上知識,你已經能應對80%的索引問題。但想在面試中真正征服面試官,還需要準備一些更具深度和廣度的話題。這里再考察索引問題的時候,有經驗的面試官很可能對以下三個問題深挖。
1. B+樹對比其他數據結構
如果這是考察你B+樹的數據結構,這只是一個初級的八股問題。有經驗的面試官一般會這樣問:
B+樹雖然好,但是其他數據結構同樣有著不錯的查詢性能,那為什么不用B樹、紅黑樹、跳表呢?”
這是一個絕佳的展示你知識廣度和深度的機會,體現你對技術選型背后思考的理解。你可以這樣回答:
(1) 對比B樹:B樹的非葉子節點也存儲數據。這導致了兩個致命缺陷:第一,由于非葉子節點變“胖”了,單個節點能容納的關鍵字和指針就少了,導致樹的高度更高,查詢時磁盤I/O次數更多。第二,范圍查詢時,B樹可能需要進行復雜的跨層級回溯遍歷,效率遠不如B+樹葉子節點之間簡單的鏈表遍歷。

(2) 對比紅黑樹/平衡二叉樹:這類數據結構都是為內存設計的,它們的查詢深度與數據量成對數關系(logN)。在動輒千萬、上億條記錄的數據庫場景下,由于二叉樹的每個節點最多只能有兩個孩子結點,所以樹的高度依然會非常高,會導致大量的磁盤I/O,這在數據庫的設計中是無法接受的。

并且二叉搜索樹在插入,刪除節點的時候可能出現樹極度不平衡的情況,出現樹退化成鏈表。這個時候就需要進行左旋右旋來維持樹的平衡:在滿足二叉搜索樹的條件下,要求任何節點的兩個子樹高度差不超過1。更新的時間復雜度也是 O(log(N)),這個性能損耗同樣也不可忽視。
(3) 對比跳表:跳表是一種非常優秀的內存數據結構(Redis的zset就是用了它),查詢效率的期望值也很高。但它的平衡性依賴于隨機性,查詢性能存在微小的波動,而數據庫作為底層系統,需要的是一種高度穩定、可預期的查詢性能。
更重要的還是IO次數太多的問題,跳表是鏈表結構,一條數據一個結點,如果最底層要存放2kw數據,且每次查詢都要能達到二分查找的效果,2kw大概在2的24次方左右,所以,跳表大概高度在24層左右。最壞情況下,這24層數據會分散在不同的數據頁里,也即是查一次數據會經歷24次磁盤IO。

綜合來看數據庫索引的技術選型,本質上還是在磁盤I/O次數、查詢效率穩定性和特定查詢場景(如范圍查詢)的友好度這幾個關鍵指標之間做出的最優工程權衡。
2. 索引失效
面試官:“有了索引,查詢就一定快嗎?或者說,什么情況下MySQL會放棄使用索引?”
這個問題考察的是你對MySQL查詢優化器工作原理的理解。答案是:不一定。優化器在某些情況下會認為全表掃描比走索引更快,從而放棄使用索引。一般情況下有以下這么幾種,在面試的時候,你只要能夠說出一些基本上不會有什么大的問題:
- 查詢條件不滿足最左前綴原則:這是最常見的情況,前面已經詳述。
- 在索引列上進行任何操作:包括計算、函數調用或隱式類型轉換。例如 WHERE YEAR(order_date) = 2025 或者 WHERE phone_number = 13812345678(如果phone_number是字符串類型,這里發生了隱式類型轉換),都會導致索引失效。
- 使用 != 或 <> 操作符:通常無法有效利用索引,因為它們過濾掉的數據太少。
- LIKE 查詢以通配符 % 開頭:例如 LIKE '%關鍵詞'。這種情況索引無法定位起始點,只能全表掃描。而 LIKE '關鍵詞%' 是可以走索引的。
- 數據區分度低(選擇性差):比如在一個gender(性別)列上建索引,當查詢 WHERE gender = '男' 時,優化器通過統計信息發現符合條件的數據可能占了全表的近一半,它會認為“走索引(一次索引查找+大量回表)”的總成本,甚至高于“直接全表掃描”的成本,于是果斷放棄索引。
以上只是一些常見的索引失效情況,在實踐中,我們必須通過 EXPLAIN 命令來分析和驗證索引的實際使用情況,而不是靠主觀猜測。
3. NULL值索引
面試官:“我對含有NULL值的列建了索引,它會起作用嗎?”
這又是一個常見的知識誤區,也是一個很好的加分項。很多人認為索引列不能為NULL,或者對NULL值的查詢無法使用索引。
在MySQL(特別是InnoDB)中,這個說法并不完全準確:
- 索引可以包含NULL值:索引會專門處理NULL值,WHERE column IS NULL 和 WHERE column IS NOT NULL 的查詢條件是可以正常利用到索引的。
- 唯一索引的特殊處理:在唯一索引列中,你可以插入多個NULL值。這是因為在SQL標準中,NULL 和任何值(包括另一個 NULL)進行比較的結果都是未知的(unknown),所以多個 NULL 不被視為違反唯一性約束。
盡管如此,還是強烈建議為字段設置 NOT NULL 約束并提供默認值。這樣做主要是為了保證數據的明確性、避免業務邏輯中出現復雜的NULL值判斷。
六、小結
數據庫索引是一個龐大而精深的領域。在面試中,我們不需要追求面面俱到,但必須展現出結構化的知識體系和由表及里的深入思考。你需要重點掌握B+樹的數據結構、聚簇索引與非聚簇索引的區別、回表的概念,以及最左匹配原則的運作機制。
要想在面試中脫穎而出,你需要從以下幾個方面下功夫,展現你的思考深度:
- MySQL為何選擇B+樹? 能否從I/O成本、范圍查詢、內存效率等多個維度,對比分析不同數據結構的優劣。
- 數據庫為何會放棄使用索引? 能否說出幾種常見的索引失效場景,以及優化思路。
- 索引與NULL的關系? 能否澄清常見的誤區,并給出最佳實踐建議。




























