深入理解MySQL索引之B+Tree

正確的創建合適的索引,是提升數據庫查詢性能的基礎。在正式講解之前,對後面舉例中使用的表結構先簡單看一下:

create table user
(
    id     bigint  not null comment 'id' primary key,
    name   varchar(200) null comment 'name',
    age    bigint       null comment 'age',
    gender int          null comment 'gender',
    key (name)
);

1 索引是什麼及工作機制?

索引是爲了加速對錶中數據行的檢索而創建的一種分散存儲的數據結構。其工作機制如下圖:
在這裏插入圖片描述

上圖中,如果現在有一條sql語句 select * from user where id = 40,如果沒有索引的條件下,我們要找到這條記錄,我們就需要在數據中進行全表掃描,匹配id = 13的數據。

如果有了索引,我們就可以通過索引進行快速查找,如上圖中,可以先在索引中通過id = 40進行二分查找,再根據定位到的地址取出對應的行數據。

2. MySQL數據庫爲什麼要使用B+TREE作爲索引的數據結構?

2.1 二叉樹爲什麼不可行

對數據的加速檢索,首先想到的就是二叉樹,二叉樹的查找時間複雜度可以達到O(log2(n))。下面看一下二叉樹的存儲結構:
在這裏插入圖片描述

二叉樹搜索相當於一個二分查找。二叉查找能大大提升查詢的效率,但是它有一個問題:二叉樹以第一個插入的數據作爲根節點,如上圖中,如果只看右側,就會發現,就是一個線性鏈表結構。如果我們現在的數據只包含1, 2, 3, 4,就會出現以下情況:在這裏插入圖片描述

如果我們要查詢的數據爲4,則需要遍歷所有的節點才能找到4,即,相當於全表掃描,就是由於存在這種問題,所以二叉查找樹不適合用於作爲索引的數據結構。

2.2 平衡二叉樹爲什麼不可行

爲了解決二叉樹存在線性鏈表的問題,會想到用平衡二叉查找樹來解決。下面看看平衡二叉樹是怎樣的:
在這裏插入圖片描述
平衡二叉查找樹定義爲:節點的子節點高度差不能超過1,如上圖中的節點20,左節點高度爲1,右節點高度0,差爲1,所以上圖沒有違反定義,它就是一個平衡二叉樹。保證二叉樹平衡的方式爲左旋,右旋等操作,至於如何左旋右旋,可以自行去搜索相關的知識。

如果上圖中平衡二叉樹保存的是id索引,現在要查找id = 8的數據,過程如下:

  1. 把根節點加載進內存,用8和10進行比較,發現8比10小,繼續加載10的左子樹。
  2. 把5加載進內存,用8和5比較,同理,加載5節點的右子樹。
  3. 此時發現命中,則讀取id爲8的索引對應的數據。

索引保存數據的方式一般有兩種:

  • 數據區保存id 對應行數據的所有數據具體內容。
  • 數據區保存的是真正保存數據的磁盤地址。

到這裏,平衡二叉樹解決了存在線性鏈表的問題,數據查詢的效率好像也還可以,基本能達到O(log2(n)), 那爲什麼mysql不選擇平衡二叉樹作爲索引存儲結構,他又存在什麼樣的問題呢?

  1. 搜索效率不足。一般來說,在樹結構中,數據所處的深度,決定了搜索時的IO次數(MySql中將每個節點大小設置爲一頁大小,一次IO讀取一頁 / 一個節點)。如上圖中搜索id = 8的數據,需要進行3次IO。當數據量到達幾百萬的時候,樹的高度就會很恐怖。
  2. 查詢不不穩定。如果查詢的數據落在根節點,只需要一次IO,如果是葉子節點或者是支節點,會需要多次IO纔可以。
  3. 存儲的數據內容太少。沒有很好利用操作系統和磁盤數據交換特性,也沒有利用好磁盤IO的預讀能力。因爲操作系統和磁盤之間一次數據交換是以頁爲單位的,一頁大小爲 4K,即每次IO操作系統會將4K數據加載進內存。但是,在二叉樹每個節點的結構只保存一個關鍵字,一個數據區,兩個子節點的引用,並不能夠填滿4K的內容。倖幸苦苦做了一次的IO操作,卻只加載了一個關鍵字。在樹的高度很高,恰好又搜索的關鍵字位於葉子節點或者支節點的時候,取一個關鍵字要做很多次的IO。

那有沒有一種結構能夠解決二叉樹的這種問題呢?有,那就是多路平衡查找樹。

2.3 多路平衡查找樹(Balance Tree)

B Tree 是一個絕對平衡樹,所有的葉子節點在同一高度,如下圖所示:
在這裏插入圖片描述
上圖爲一個2-3樹(每個節點存儲2個關鍵字,有3路),多路平衡查找樹也就是多叉的意思,從上圖中可以看出,每個節點保存的關鍵字的個數和路數關係爲:關鍵字個數 = 路數 – 1。

假設要從上圖中查找id = X的數據,B TREE 搜索過程如下:

  1. 取出根磁盤塊,加載40和60兩個關鍵字。
  2. 如果X等於40,則命中;如果X小於40走P1;如果40 < X < 60走P2;如果X = 60,則命中;如果X > 60走P3。
  3. 根據以上規則命中後,接下來加載對應的數據, 數據區中存儲的是具體的數據或者是指向數據的指針。

爲什麼說這種結構能夠解決平衡二叉樹存在的問題呢?

B Tree 能夠很好的利用操作系統和磁盤的交互特性, MySQL爲了很好的利用磁盤的預讀能力,將頁大小設置爲16K,即將一個節點(磁盤塊)的大小設置爲16K,一次IO將一個節點(16K)內容加載進內存。這裏,假設關鍵字類型爲 int,即4字節,若每個關鍵字對應的數據區也爲4字節,不考慮子節點引用的情況下,則上圖中的每個節點大約能夠存儲(16 * 1000)/ 8 = 2000個關鍵字,共2001個路數。對於二叉樹,三層高度,最多可以保存7個關鍵字,而對於這種有2001路的B樹,三層高度能夠搜索的關鍵字個數遠遠的大於二叉樹。

這裏順便說一下:在B Tree保證樹的平衡的過程中,每次關鍵字的變化,都會導致結構發生很大的變化,這個過程是特別浪費時間的,所以創建索引一定要創建合適的索引,而不是把所有的字段都創建索引,創建冗餘索引只會在對數據進行新增,刪除,修改時增加性能消耗。

B樹確實已經很好的解決了問題,我先這裏先繼續看一下B+Tree結構,再來討論BTree和B+Tree的區別。

先看看B+Tree是怎樣的,B+Tree是B Tree的一個變種,在B+Tree中,B樹的路數和關鍵字的個數的關係不再成立了,數據檢索規則採用的是左閉合區間,路數和關鍵個數關係爲1比1,具體如下圖所示:
在這裏插入圖片描述
如果上圖中是用ID做的索引,如果是搜索X = 1的數據,搜索規則如下:

  1. 取出根磁盤塊,加載1,28,66三個關鍵字。
  2. X <= 1 走P1,取出磁盤塊,加載1,10,20三個關鍵字。
  3. X <= 1 走P1,取出磁盤塊,加載1,8,9三個關鍵字。
  4. 已經到達葉子節點,命中1,接下來加載對應的數據,圖中數據區中存儲的是具體的數據。

2.4 B TREE和B+TREE區別是什麼?

  1. B+Tree 關鍵字的搜索採用的是左閉合區間,之所以採用左閉合區間是因爲他要最好的去支持自增id,這也是mysql的設計初衷。即,如果id = 1命中,會繼續往下查找,直到找到葉子節點中的1。

  2. B+Tree 根節點和支節點沒有數據區,關鍵字對應的數據只保存在葉子節點中。即只有葉子節點中的關鍵字數據區纔會保存真正的數據內容或者是內容的地址。而在B樹種,如果根節點命中,則會直接返回數據。

  3. 在B+Tree中,葉子節點不會去保存子節點的引用。

  4. B+Tree葉子節點是順序排列的,並且相鄰的節點具有順序引用的關係,如上圖中葉子節點之間有指針相連接。

2.5 MySQL爲什麼最終要去選擇B+Tree?

  1. B+Tree是B TREE的變種,B TREE能解決的問題,B+TREE也能夠解決(降低樹的高度,增大節點存儲數據量)

  2. B+Tree掃庫和掃表能力更強。如果我們要根據索引去進行數據表的掃描,對B TREE進行掃描,需要把整棵樹遍歷一遍,而B+TREE只需要遍歷他的所有葉子節點即可(葉子節點之間有引用)。

  3. B+TREE磁盤讀寫能力更強。他的根節點和支節點不保存數據區,所以根節點和支節點同樣大小的情況下,保存的關鍵字要比B TREE要多。而葉子節點不保存子節點引用,能用於保存更多的關鍵字和數據。所以,B+TREE讀寫一次磁盤加載的關鍵字比B TREE更多。

  4. B+Tree排序能力更強。上面的圖中可以看出,B+Tree天然具有排序功能。

  5. B+Tree查詢性能穩定。B+Tree數據只保存在葉子節點,每次查詢數據,查詢IO次數一定是穩定的。當然這個每個人的理解都不同,因爲在B TREE如果根節點命中直接返回,確實效率更高。

3 MySQL B+Tree具體落地形式

這裏主要講解的是MySQL根據B+Tree索引結構不同的兩種存儲引擎(MYISAM 和 INNODB)的實現。

首先找到MySQL保存數據的文件夾,看看MySQL是如何保存數據的:

mysql> show variables like '%datadir%';
+---------------+------------------------+
| Variable_name | Value                  |
+---------------+------------------------+
| datadir       | /usr/local/mysql/data/ |
+---------------+------------------------+

進入到這個目錄下,這個目錄下保存的是所有數據庫,再進入到具體的一個數據庫目錄下。就能夠看到MySQL存儲數據和索引的文件了。

這裏我創建了兩張表,user_innod和user_myisam,分別指定索引爲innodb和myisam。對於每張表,MySQL會創建相應的文件保存數據和索引,具體如下:

-rw-rw----. 1 mysql mysql      8652 May  3 21:11 user_innodb.frm
-rw-rw----. 1 mysql mysql 109051904 May  7 21:26 user_innodb.ibd
-rw-rw----. 1 mysql mysql      8682 May 16 18:27 user_myisam.frm
-rw-rw----. 1 mysql mysql         0 May 16 18:27 user_myisam.MYD
-rw-rw----. 1 mysql mysql      1024 May 16 18:27 user_myisam.MYI

從圖中可以看出:

  • MYISAM存儲引擎存儲數據庫數據,一共有三個文件:
    Frm:表的定義文件。
    MYD:數據文件,所有的數據保存在這個文件中。
    MYI:索引文件。

  • Innodb存儲引擎存儲數據庫數據,一共有兩個文件(沒有專門保存數據的文件):
    Frm文件: 表的定義文件。
    Ibd文件:數據和索引存儲文件。數據以主鍵進行聚集存儲,把真正的數據保存在葉子節點中。

3.1 MyISAM存儲引擎

說明:爲了畫圖簡便,下面部分圖使用在線數據結構工具進行組織數據,組織的B+Tree爲右閉合區間,但不影響理解存儲引擎數據存儲結構。

在MYISAM存儲引擎中,數據和索引的關係如下:

在這裏插入圖片描述

如何查找數據的呢?
如果要查詢id = 40的數據:先根據MyISAM索引文件(如上圖左)去找id = 40的節點,通過這個節點的數據區拿到真正保存數據的磁盤地址,再通過這個地址從MYD數據文件(如上圖右)中加載對應的記錄。

如果有多個索引,表現形式如下:
在這裏插入圖片描述
所以在MYISAM存儲引擎中,主鍵索引和輔助索引是同級別的,沒有主次之分。

3.2 Innodb存儲引擎

Innodb主鍵索引爲聚集索引,首先簡單理解一下聚集索引的概念:數據庫錶行中數據的物理順序和鍵值的邏輯順序相同。

Innodb以主鍵索引來聚集組織數據的存儲,下面看看Innodb是如何組織數據的。

在這裏插入圖片描述
如上圖中,葉子節點的數據區保存的就是真實的數據,在通過索引進行檢索的時候,命中葉子節點,就可以直接從葉子節點中取出行數據。mysql5.5版本之前默認採用的是MyISAM引擎,5.5之後默認採用的是innodb引擎。

在innodb中,輔助索引的格式如下圖所示?
在這裏插入圖片描述

如上圖,主鍵索引的葉子節點保存的是真正的數據。而輔助索引葉子節點的數據區保存的是主鍵索引關鍵字的值。

假如要查詢name = C 的數據,其搜索過程如下:

  1. 先在輔助索引中通過C查詢最後找到主鍵id = 9.
  2. 在主鍵索引中搜索id爲9的數據,最終在主鍵索引的葉子節點中獲取到真正的數據。

所以通過輔助索引進行檢索,需要檢索兩次索引。

之所以這樣設計,一個原因就是:如果和MyISAM一樣在主鍵索引和輔助索引的葉子節點中都存放數據行指針,一旦數據發生遷移,則需要去重新組織維護所有的索引。

把Innodb 和 MYISAM區別放在一張圖中看,就如下所示:

在這裏插入圖片描述

4 創建索引的幾大原則

4.1 列的離散型

離散型的計算公式:count(distinct column_name):count(*),就是用去重後的列值個數比個數。值在 (0,1] 範圍內。離散型越高,選擇型越好。

如下表中各個字段,明顯能看出Id的選擇性比gender更高。

mysql> select * from user;
+----+--------------+------+--------+
| id | name         | age  | gender |
+----+--------------+------+--------+
| 20 | 君莫笑       |   15 |      1 |
| 40 | 蘇沐橙       |   12 |      0 |
| 50 | 張楚嵐       |   25 |      1 |
| 60 | 諸葛青       |   27 |      1 |
| 61 | 若有人兮     |   38 |      0 |
| 64 | 馮寶寶       |   18 |      0 |
+----+--------------+------+--------+

爲什麼說離散型越高,選擇型越好?
因爲離散度越高,通過索引最終確定的範圍越小,最終掃面的行數也就越少。

4.2 最左匹配原則

對於索引中的關鍵字進行對比的時候,一定是從左往右以此對比,且不可跳過。之前講解的id都爲int型數據,如果id爲字符串的時候,如下圖:
在這裏插入圖片描述
當進行匹配的時候,會把字符串轉換成ascll碼,如abc變成97 98 99,然後從左往右一個字符一個字符進行對比。所以在sql查詢中使用like %a 時候索引會失效,因爲%表示全匹配,如果已經全匹配就不需要索引,還不如直接全表掃描。

4.3 最少空間原則

前面已經說過,當關鍵字佔用的空間越小,則每個節點保存的關鍵字個數就越多,每次加載進內存的關鍵字個數就越多,檢索效率就越高。創建索引的關鍵字要儘可能佔用空間小。

5 聯合索引

單列索引:節點中的關鍵字[name]
聯合索引:節點中的關鍵字[name, age]

可以把單列索引看成特殊的聯合索引,聯合索引的比較也是根據最左匹配原則。

5.1 聯合索引列的選擇原則

  • 經常用的列優先(最左匹配原則)
  • 離散度高的列優先(離散度高原則)
  • 寬度小的列優先(最少空間原則)

5.2 實例分析

下面簡單舉例平時經常會遇到的問題:
如,平時經常使用的查詢sql如下:
select * from users where name = ?
select * from users where name = ? and age = ?

爲了加快檢索速度,爲上面的查詢sql創建索引如下:
create index idx_name on users(name)
create index idx_name_age on users(name, age)

在上面解決方案中,根據最左匹配原則,idx_name爲冗餘索引, where name = ?同樣可以利用索引idx_name_age進行檢索。冗餘索引會增加維護B+TREE平衡時的性能消耗,並且佔用磁盤空間。

6. 覆蓋索引

如果查詢的列,通過索引項的信息可直接返回,則該索引稱之爲查詢SQL的覆蓋索引。覆蓋索引可以提高查詢的效率。
在這裏插入圖片描述
如上圖,如果通過name進行數據檢索:
select * from users where name = ?
需要需要在name索引中找到name對應的Id,然後通過獲取的Id在主鍵索引中查到對應的行。整個過程需要掃描兩次索引,一次name,一次id。

如果我們查詢只想查詢id的值,就可以改寫SQL爲:
select id from users where name = ?
因爲只需要id的值,通過name查詢的時候,掃描完name索引,我們就能夠獲得id的值了,所以就不需要再去掃面id索引,就會直接返回。

當然,如果你同時需要獲取age的值:
select id,age from users where name = ?
這樣就無法使用到覆蓋索引了。

知道了覆蓋索引,就知道了爲什麼sql中要求儘量不要使用select *,要寫明具體要查詢的字段。其中一個原因就是在使用到覆蓋索引的情況下,不需要進入到數據區,數據就能直接返回,提升了查詢效率。在用不到覆蓋索引的情況下,也儘可能的不要使用select *,如果行數據量特別多的情況下,可以減少數據的網絡傳輸量。當然,這都視具體情況而定,通過select返回所有的字段,通用性會更強,一切有利必有弊。

7 總結

  • 索引列的數據長度滿足業務的情況下能少則少。

  • 表中的索引並不是越多越好,冗餘或者無用索引會佔用磁盤空間並且會影響增刪改的效率。

  • Where 條件中,like 9%, like %9%, like%9,三種方式都用不到索引。後兩種方式對於索引是無效的。第一種9%是不確定的,決定於列的離散型,結論上講可以用到,如果發現離散情況特別差的情況下,查詢優化器覺得走索引查詢性能更差,還不如全表掃描。

  • Where條件中IN可以使用索引, NOT IN 無法使用索引。

  • 多用指定查詢,只返回自己想要的列,少用select *。

  • 查詢條件中使用函數,索引將會失效,這和列的離散性有關,一旦使用到函數,函數具有不確定性。

  • 聯合索引中,如果不是按照索引最左列開始查找,無法使用索引。

  • 對聯合索引精確匹配最左前列並範圍匹配另一列,可以使用到索引。

  • 聯合索引中,如果查詢有某個列的範圍查詢,其右邊所有的列都無法使用索引。

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