MySQL索引的數據結構-B+樹介紹


一、樹

樹狀圖是一種數據結構 ,它是由n(n>=1)個有限結點組成一個具有層次關系的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。
它具有以下的特點:每個結點有零個或多個子結點;沒有父結點的結點稱為根結點;每一個非根結點有且只有一個父結點;除了根結點外,每個子結點可以分為多個不相交的子樹

205-MySQL索引的數據結構-B樹介紹-01.png?x-oss-process=style/watermark

根結點:A

父節點:A是B,C的父節點

葉子節點:D,E是葉子節點

樹的深度/樹的高度:高度為3

二、B+樹

前面講了索引的基本原理,數據庫的復雜性,又講了操作系統的相關知識,目的就是讓大家了解,任何一種數據結構都不是憑空產生的,一定會有它的背景和使用場景,我們現在總結一下,我們需要這種數據結構能夠做些什么,其實很簡單,那就是:每次查找數據時把磁盤IO次數控制在一個很小的數量級,最好是常數數量級。那么我們就想到如果一個高度可控的多路搜索樹是否能滿足需求呢?就這樣,b+樹應運而生(B+樹是通過二叉查找樹,再由平衡二叉樹,B樹演化而來)。

205-MySQL索引的數據結構-B樹介紹-02.png?x-oss-process=style/watermark

2.1 B+樹性質

  1. 索引字段要盡量的小:通過上面的分析,我們知道IO次數取決於b+數的高度h,假設當前數據表的數據為N,每個磁盤塊的數據項的數量是m,則有h=㏒(m+1)N,當數據量N一定的情況下,m越大,h越小;而m = 磁盤塊的大小 / 數據項的大小,磁盤塊的大小也就是一個數據頁的大小,是固定的,如果數據項占的空間越小,數據項的數量越多,樹的高度越低。這就是為什么每個數據項,即索引字段要盡量的小,比如int占4字節,要比bigint8字節少一半。這也是為什么b+樹要求把真實的數據放到葉子節點而不是內層節點,一旦放到內層節點,磁盤塊的數據項會大幅度下降,導致樹增高。當數據項等於1時將會退化成線性表。
  2. 索引的最左匹配特性:當b+樹的數據項是復合的數據結構,比如(name,age,sex)的時候,b+數是按照從左到右的順序來建立搜索樹的,比如當(張三,20,F)這樣的數據來檢索的時候,b+樹會優先比較name來確定下一步的所搜方向,如果name相同再依次比較age和sex,最后得到檢索的數據;但當(20,F)這樣的沒有name的數據來的時候,b+樹就不知道下一步該查哪個節點,因為建立搜索樹的時候name就是第一個比較因子,必須要先根據name來搜索才能知道下一步去哪里查詢。比如當(張三,F)這樣的數據來檢索時,b+樹可以用name來指定搜索方向,但下一個字段age的缺失,所以只能把名字等於張三的數據都找到,然后再匹配性別是F的數據了, 這個是非常重要的性質,即索引的最左匹配特性。

三、聚集索引和輔助索引

在數據庫中,B+樹的高度一般都在2~4層,這也就是說查找某一個鍵值的行記錄時最多只需要2到4次IO,這倒不錯。因為當前一般的機械硬盤每秒至少可以做100次IO,2~4次的IO意味着查詢時間只需要0.02~0.04秒。

數據庫中的B+樹索引可以分為聚集索引(clustered index)和輔助索引(secondary index),

聚集索引與輔助索引相同的是:不管是聚集索引還是輔助索引,其內部都是B+樹的形式,即高度是平衡的,葉子結點存放着所有的數據。

聚集索引與輔助索引不同的是:葉子結點存放的是否是一整行的信息

3.1 聚集索引

InnoDB存儲引擎表是索引組織表,即表中數據按照主鍵順序存放。
而聚集索引(clustered index)就是按照每張表的主鍵構造一棵B+樹,同時葉子結點存放的即為整張表的行記錄數據,也將聚集索引的葉子結點稱為數據頁。
聚集索引的這個特性決定了索引組織表中數據也是索引的一部分。同B+樹數據結構一樣,每個數據頁都通過一個雙向鏈表來進行鏈接。

如果未定義主鍵,MySQL取第一個唯一索引(unique)而且只含非空列(NOT NULL)作為主鍵,InnoDB使用它作為聚簇索引。

如果沒有這樣的列,InnoDB就自己產生一個這樣的ID值,它有六個字節,而且是隱藏的,使其作為聚簇索引。

由於實際的數據頁只能按照一棵B+樹進行排序,因此每張表只能擁有一個聚集索引。
在多數情況下,查詢優化器傾向於采用聚集索引。因為聚集索引能夠在B+樹索引的葉子節點上直接找到數據。
此外由於定義了數據的邏輯順序,聚集索引能夠特別快地訪問針對范圍值得查詢。

205-MySQL索引的數據結構-B樹介紹-13.png?x-oss-process=style/watermark

聚集索引的好處之一:它對主鍵的排序查找和范圍查找速度非常快,葉子節點的數據就是用戶所要查詢的數據。如用戶需要查找一張表,查詢最后的10位用戶信息,由於B+樹索引是雙向鏈表,所以用戶可以快速找到最后一個數據頁,並取出10條記錄

# 參照第六小結測試索引的准備階段來創建出表s1
mysql> desc s1; #最開始沒有主鍵
+--------+-------------+------+-----+---------+-------+
| Field  | Type        | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| id     | int(11)     | NO   |     | NULL    |       |
| name   | varchar(20) | YES  |     | NULL    |       |
| gender | char(6)     | YES  |     | NULL    |       |
| email  | varchar(50) | YES  |     | NULL    |       |
+--------+-------------+------+-----+---------+-------+
rows in set (0.00 sec)

mysql> explain select * from s1 order by id desc limit 10; #Using filesort,需要二次排序
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra          |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------+
|  1 | SIMPLE      | s1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 2633472 |   100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------+
row in set, 1 warning (0.11 sec)

mysql> alter table s1 add primary key(id); #添加主鍵
Query OK, 0 rows affected (13.37 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select * from s1 order by id desc limit 10; #基於主鍵的聚集索引在創建完畢后就已經完成了排序,無需二次排序
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------+
|  1 | SIMPLE      | s1    | NULL       | index | NULL          | PRIMARY | 4       | NULL |   10 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------+
row in set, 1 warning (0.04 sec)

聚集索引的好處之二:范圍查詢(range query),即如果要查找主鍵某一范圍內的數據,通過葉子節點的上層中間節點就可以得到頁的范圍,之后直接讀取數據頁即可

mysql> alter table s1 drop primary key;
Query OK, 2699998 rows affected (24.23 sec)
Records: 2699998  Duplicates: 0  Warnings: 0

mysql> desc s1;
+--------+-------------+------+-----+---------+-------+
| Field  | Type        | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| id     | int(11)     | NO   |     | NULL    |       |
| name   | varchar(20) | YES  |     | NULL    |       |
| gender | char(6)     | YES  |     | NULL    |       |
| email  | varchar(50) | YES  |     | NULL    |       |
+--------+-------------+------+-----+---------+-------+
rows in set (0.12 sec)

mysql> explain select * from s1 where id > 1 and id < 1000000; # 沒有聚集索引,預估需要檢索的rows數如下
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | s1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 2690100 |    11.11 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
row in set, 1 warning (0.00 sec)

mysql> alter table s1 add primary key(id);
Query OK, 0 rows affected (16.25 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select * from s1 where id > 1 and id < 1000000; # 有聚集索引,預估需要檢索的rows數如下
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | s1    | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL | 1343355 |   100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
row in set, 1 warning (0.09 sec)

3.2 輔助索引

表中除了聚集索引外其他索引都是輔助索引(Secondary Index,也稱為非聚集索引),與聚集索引的區別是:輔助索引的葉子節點不包含行記錄的全部數據。

葉子節點除了包含鍵值以外,每個葉子節點中的索引行中還包含一個書簽(bookmark)。該書簽用來告訴InnoDB存儲引擎去哪里可以找到與索引相對應的行數據。

由於InnoDB存儲引擎是索引組織表,因此InnoDB存儲引擎的輔助索引的書簽就是相應行數據的聚集索引鍵。如下圖

205-MySQL索引的數據結構-B樹介紹-14.png?x-oss-process=style/watermark

輔助索引的存在並不影響數據在聚集索引中的組織,因此每張表上可以有多個輔助索引,但只能有一個聚集索引。當通過輔助索引來尋找數據時,InnoDB存儲引擎會遍歷輔助索引並通過葉子級別的指針獲得只想主鍵索引的主鍵,然后再通過主鍵索引來找到一個完整的行記錄。

舉例來說,如果在一棵高度為3的輔助索引樹種查找數據,那需要對這個輔助索引樹遍歷3次找到指定主鍵,如果聚集索引樹的高度同樣為3,那么還需要對聚集索引樹進行3次查找,最終找到一個完整的行數據所在的頁,因此一共需要6次邏輯IO訪問才能得到最終的一個數據頁。

205-MySQL索引的數據結構-B樹介紹-15.png?x-oss-process=style/watermark

3.3 聚集索引和非聚集索引的區別

聚集索引

  1. 紀錄的索引順序與無力順序相同
    因此更適合between and和order by操作
  2. 葉子結點直接對應數據
    從中間級的索引頁的索引行直接對應數據頁
  3. 每張表只能創建一個聚集索引

非聚集索引

  1. 索引順序和物理順序無關
  2. 葉子結點不直接指向數據頁
  3. 每張表可以有多個非聚集索引,需要更多磁盤和內容
  4. 多個索引會影響insert和update的速度

四、再看B+樹

B+樹和二叉樹、平衡二叉樹一樣,都是經典的數據結構。B+樹由B樹和索引順序訪問方法(ISAM,是不是很熟悉?對,這也是MyISAM引擎最初參考的數據結構)演化而來,但是在實際使用過程中幾乎已經沒有使用B樹的情況了。

B+樹的定義十分復雜,因此只簡要地介紹B+樹:B+樹是為磁盤或其他直接存取輔助設備而設計的一種平衡查找樹,在B+樹中,所有記錄節點都是按鍵值的大小順序存放在同一層的葉節點中,各葉節點指針進行連接。

我們先來看一個B+樹,其高度為2,每頁可存放4條記錄,扇出(fan out)為5。

205-MySQL索引的數據結構-B樹介紹-03.png?x-oss-process=style/watermark

可以看出,所有記錄都在葉節點中,並且是順序存放的,如果我們從最左邊的葉節點開始順序遍歷,可以得到所有鍵值的順序排序:5、10、15、20、25、30、50、55、60、65、75、80、85、90。

4.1 B+樹的插入操作

B+樹的插入必須保證插入后葉節點中的記錄依然排序,同時需要考慮插入B+樹的三種情況,每種情況都可能會導致不同的插入算法,如表5-1所示。

205-MySQL索引的數據結構-B樹介紹-04.png?x-oss-process=style/watermark

我們用實例來分析B+樹的插入,我們插入28這個鍵值,發現當前Leaf Page和Index Page都沒有滿,我們直接插入就可以了。

205-MySQL索引的數據結構-B樹介紹-05.png?x-oss-process=style/watermark

這次我們再插入一條70這個鍵值,這時原先的Leaf Page已經滿了,但是Index Page還沒有滿,符合表5-1的第二種情況,這時插入Leaf Page后的情況為50、55、60、65、70。我們根據中間的值60拆分葉節點。

205-MySQL索引的數據結構-B樹介紹-06.png?x-oss-process=style/watermark

因為圖片顯示的關系,這次我沒有能在各葉節點加上雙向鏈表指針。最后我們來插入記錄95,這時符合表5-1討論的第三種情況,即Leaf Page和Index Page都滿了,這時需要做兩次拆分。

205-MySQL索引的數據結構-B樹介紹-07.png?x-oss-process=style/watermark

可以看到,不管怎么變化,B+樹總是會保持平衡。但是為了保持平衡,對於新插入的鍵值可能需要做大量的拆分頁(split)操作,而B+樹主要用於磁盤,因此頁的拆分意味着磁盤的操作,應該在可能的情況下盡量減少頁的拆分。因此,B+樹提供了旋轉(rotation)的功能。

旋轉發生在Leaf Page已經滿了、但是其左右兄弟節點沒有滿的情況下。(旋轉是為了減少拆分頁,如果葉子節點的左右兄弟節點還有空位置,那就旋轉一下就能把當前數據插入了;如果自己和左右兄弟位置都滿了那再怎么旋轉也出不來位置了,只能拆分頁了)這時B+樹並不會急於去做拆分頁的操作,而是將記錄移到所在頁的兄弟節點上。通常情況下,左兄弟被首先檢查用來做旋轉操作,這時我們插入鍵值70,其實B+樹並不會急於去拆分葉節點,而是做旋轉,50,55,55旋轉。

205-MySQL索引的數據結構-B樹介紹-08.png?x-oss-process=style/watermark

可以看到,采用旋轉操作使B+樹減少了一次頁的拆分操作,而這時B+樹的高度依然還是2。

4.2 B+樹的刪除操作

B+樹使用填充因子(fill factor)來控制樹的刪除變化,50%是填充因子可設的最小值。B+樹的刪除操作同樣必須保證刪除后葉節點中的記錄依然排序,同插入一樣,B+樹的刪除操作同樣需要考慮如表5-2所示的三種情況,與插入不同的是,刪除根據填充因子的變化來衡量。

205-MySQL索引的數據結構-B樹介紹-09.png?x-oss-process=style/watermark

首先,刪除鍵值為70的這條記錄,該記錄符合表5-2討論的第一種情況,刪除后。

205-MySQL索引的數據結構-B樹介紹-10.png?x-oss-process=style/watermark

接着我們刪除鍵值為25的記錄,這也是表5-2討論的第一種情況,但是該值還是Index Page中的值,因此在刪除Leaf Page中25的值后,還應將25的右兄弟節點的28更新到Page Index中,最后可得到圖。

205-MySQL索引的數據結構-B樹介紹-11.png?x-oss-process=style/watermark

最后我們來看刪除鍵值為60的情況,刪除Leaf Page中鍵值為60的記錄后,填充因子小於50%,這時需要做合並操作,同樣,在刪除Index Page中相關記錄后需要做Index Page的合並操作,最后得到圖。

205-MySQL索引的數據結構-B樹介紹-12.png?x-oss-process=style/watermark


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM