從根兒上理解MySQL | B+樹索引

目錄

一個簡單的索引方案

InnoDB中的索引方案

聚簇索引

二級索引

聯合索引

B+樹索引的使用

B+樹索引使用的條件

回表的代價

使用索引的注意事項


一個簡單的索引方案

在上節中我們知道,爲了快速定位一條記錄在頁中的位置而設立了頁目錄,那麼爲了快速定位記錄所在的數據頁,我們也可以建立一個目錄,這個目錄也就是索引:

  • 下一個數據頁中用戶記錄的主鍵值必須大於上一個頁中用戶記錄的主鍵值
  • 給所有的頁建立一個目錄項,目錄項包括兩個部分:頁的用戶記錄中最小的主鍵值和頁號

image_1caba0afo11fa1cli1nu070m16bg1j.png-119.1kB

在上圖中,比如我們想查找主鍵值爲20的記錄,具體查找過程分兩步:

  1. 先從目錄項中根據二分法快速確定出主鍵值爲20的記錄在目錄項3中(因爲 12 < 20 < 209),它對應的頁是頁9;
  2. 再根據上節說的在頁中查找記錄的方式去頁9中定位具體的記錄。

InnoDB中的索引方案

上邊之所以稱爲一個簡易的索引方案,是因爲我們爲了在根據主鍵值進行查找時使用二分法快速定位具體的目錄項而假設所有目錄項都可以在物理存儲器上連續存儲,但實際上存儲所有的目錄項需要非常大的存儲空間,並且我們對記錄的增刪容易導致牽一髮而動全身。所以在InnoDB中,複用了之前存儲用戶記錄的數據頁來存儲目錄項,爲了和用戶記錄做一下區分,我們把這些用來表示目錄項的記錄稱爲目錄項記錄。目錄項記錄record_type值爲1,並且只有主鍵值和頁的編號兩個列,另外,只有在存儲目錄項記錄的頁中的主鍵值最小的目錄項記錄min_rec_mask值爲1,其他別的記錄的min_rec_mask值都是0。

如上圖所示,不論是存放用戶記錄的數據頁,還是存放目錄項記錄的數據頁,我們都把它們存放到B+樹這個數據結構中了。從圖中可以看出,實際用戶記錄其實都存放在B+樹的葉子節中,而目錄項記錄均存放在非葉子節點中

InnoDB中規定最下邊的那層爲第0層,假設所有存放用戶記錄的葉子節點代表的數據頁可以存放100條用戶記錄,所有存放目錄項記錄的內節點代表的數據頁可以存放1000條目錄項記錄,那麼如果B+樹有2層,最多能存放1000×100=100000條記錄;如果B+樹有4層,最多能存放1000×1000×1000×100=100000000000條記錄。實際上我們的記錄並不會有這麼多,所以一般情況下B+樹不會超過四層,也就是說通過主鍵值去查找某條記錄最多隻需要做4個頁面內的查找(查找3個目錄項頁和一個用戶記錄頁),然後在頁面內也可以通過二分法實現快速定位記錄。

聚簇索引

上面所說的B+樹本身就是一個索引,聚簇索引具備兩個特點:

  1. 使用記錄主鍵值的大小進行記錄和頁的排序:頁內的記錄是按照主鍵的大小順序排成一個單向鏈表;每一層的頁與頁之間根據用戶/目錄項記錄的主鍵大小順序排成一個雙向鏈表
  2. B+樹的葉子節點存儲的是完整的用戶記錄

InnoDB存儲引擎中,聚簇索引就是數據的存儲方式(所有的用戶記錄都存儲在了葉子節點),InnoDB會自動創建聚簇索引。

二級索引

聚簇索引只能在搜索條件是主鍵值時才能發揮作用,如果我們想以別的列作爲搜索條件,可以爲該列建一棵B+樹:

  1. 使用記錄索引列的大小進行記錄和頁的排序;
  2. B+樹的葉子節點存儲的是索引列和主鍵值
  3. 目錄項記錄只有索引列、主鍵值和頁號(主鍵值是爲了保證除了頁號外字段的唯一性)。

如果我們爲c2列建立索引,需要查找c2列的值爲某個值的記錄,查找過程爲:確定目錄項記錄頁 → 通過目錄項記錄頁確定用戶記錄真實所在的頁 → 在真實存儲用戶記錄的頁中定位到具體的記錄 → 根據主鍵值去聚簇索引中再查找一遍完整的用戶記錄(回表)。這種按照非主鍵列建立的B+樹需要一次回表操作纔可以定位到完整的用戶記錄,所以這種B+樹也被稱爲二級索引。

聯合索引

我們也可以同時爲多個列建立索引,以多個列的大小爲排序規則建立的B+樹稱爲聯合索引,以爲c2c3列建立索引爲例:

  1. B+樹葉子節點處的用戶記錄由c2c3和主鍵c1列組成;
  2. 每條目錄項記錄都由c2c3頁號這三個部分組成,各條記錄先按照c2列的值進行排序,如果記錄的c2列相同,則按照c3列的值進行排序。

B+樹索引的使用

B+樹索引使用的條件

索引雖然很好,但一個表上索引建的越多,就會佔用越多的存儲空間,在增刪改記錄的時候性能就越差。爲了能建立又好又少的索引,我們需先知道索引在哪些條件下起作用的。這裏先創建一個表:

CREATE TABLE person_info(
    id INT NOT NULL auto_increment,
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    PRIMARY KEY (id),
    KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
);
  • 全值匹配

全值匹配即搜索條件中的列和索引列一致,注意WHERE子句中的幾個搜索條件的順序並不影響執行過程

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';
  • 匹配左邊的列

搜索語句中可以不包含全部聯合索引中的列,只包含左邊的就行,比如:

SELECT * FROM person_info WHERE name = 'Ashburn';

但需要特別注意的是,如果我們想使用聯合索引中儘可能多的列,搜索條件中的各個列必須是聯合索引中從最左邊連續的列,比如下面的這個語句就用不到B+樹索引:

SELECT * FROM person_info WHERE name = 'Ashburn' AND phone_number = '15123983239';
  • 匹配列前綴

對於字符串類型的索引列來說,只匹配它的前綴也是可以快速定位記錄的,比如:

SELECT * FROM person_info WHERE name LIKE 'As%';
  • 匹配範圍值

查找索引列的值在某個範圍內的記錄,比如:

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';

上邊語句的查詢過程其實是:找到name值爲Asa的記錄 → 找到name值爲Barlow的記錄 → 由於所有記錄都是由鏈表連起來的,他們之間的記錄可以很容易地取出來 → 找到這些記錄的主鍵值,再到聚簇索引中回表查找完整的記錄。

但如果對多個列同時進行範圍查找的話,只有對索引最左邊的那個列進行範圍查找的時候才能用到B+

  • 精確匹配某一列並範圍匹配另一列

對於同一個聯合索引來說,雖然對多個列都進行範圍查找時只能用到最左邊那個索引列,但是如果左邊的列是精確查找,則右邊的列可以進行範圍查找,比如:

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone_number > '15100000000';
  • 用於排序

如果ORDER BY子句裏使用到了索引列,就有可能省去在內存或文件中排序的步驟,比如:

SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;

在聯合索引中需要注意的是,ORDER BY子句後邊的列的順序必須要按照索引列的順序給出。

  • 用於分組

有時候爲了方便統計表中的一些信息,會把表中的記錄按照某些列進行分組,如果分組順序又和B+樹中的索引列的順序是一致的,就可以直接使用B+樹索引進行分組,比如:

SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number;

回表的代價

由上面的內容我們知道,對於二級索引,如果我們需要查找用戶的完整記錄,需要進行回表操作。但是需要回表的記錄越多,使用二級索引的性能就越低,甚至讓某些查詢寧願使用全表掃描也不使用二級索引。一般情況下,通過Limit語句來限制查詢獲取較少的記錄數,這樣可以讓優化器更傾向於選擇使用二級索引 + 回表的方式進行查詢。

另外,爲了徹底告別回表操作帶來的性能損耗,我們最好在查詢列表裏只包含索引列,也就是覆蓋索引。

使用索引的注意事項

  • 只爲用於搜索、排序或分組的列創建索引;
  • 考慮列的基數(某一列中不重複數據的個數):最好爲那些列的基數大的列建立索引,爲基數太小列的建立索引效果可能不好
  • 索引列的類型(類型表示的數據範圍的大小)儘量小:節省存儲空間,加快查詢效率;
  • 索引字符串值的前綴;
  • 讓索引列在比較表達式中單獨出現:如果索引列比較表達式中是以某個表達式,或者函數調用形式出現的話,是用不到索引的
  • 爲了儘可能少的讓聚簇索引發生頁面分裂和記錄移位的情況,建議讓主鍵擁有AUTO_INCREMENT屬性;
  • 定位並刪除表中的重複和冗餘索引;
  • 儘量使用聚簇索引進行查詢,避免回錶帶來的性能損耗

聲明:本博客純粹爲讀書筆記,如想詳細瞭解MySQL相關知識請訪問《MySQL是怎麼運行的:從根兒上理解MySQL》原作者撰寫資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章