每當咱們執行某個 SQL 發現很慢時,都會下意識地反應是否加了索引,那麼你們是否有想過加了索引爲啥會使數據查找更快呢,索引的底層通常又是用什麼結構存儲的呢,相信你們看了標題已經有答案了,沒錯!B+樹!那麼它相對於通常的鏈表,哈希等有何不一樣,爲什麼多數存儲引擎都使用它呢,今天我就來揭開 B+ 樹的面紗,相信看了此文,B+ 樹再也不神祕,對你理解如下高頻面試題會大有幫助!html
本文將會從如下幾個方面來說解 B+ 樹mysql
要知道索引底層爲啥使用 B+ 樹,得看它解決了什麼問題,咱們能夠想一想,平常咱們用到的比較多的 SQL 有哪些呢。web
假設咱們有一張如下的用戶表:面試
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL COMMENT '姓名',
`idcard` varchar(20) DEFAULT NULL COMMENT '身份證號碼',
`age` tinyint(10) DEFAULT NULL COMMENT '年齡',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶信息';
通常咱們會有以下需求:sql
一、根據用戶 id 查用戶信息緩存
select * from user where id = 123;
二、根據區間值來查找用戶信息數據結構
select * from user where id > 123 and id < 234;
三、按 id 逆序排列,分頁取出用戶信息app
select * from user where id < 1234 order by id desc limit 10;
從以上的幾個經常使用 SQL 咱們能夠看到索引所用的數據結構必須知足如下三個條件 函數
接下來咱們以主鍵索引(id 索引)爲例來看看如何用相應的數據結構來構造它性能
接下來咱們想一想有哪些數據結構知足以上的條件
一、散列表
散列表(也稱哈希表)是根據關鍵碼值(Key value)而直接進行訪問的數據結構,它讓碼值通過哈希函數的轉換映射到散列表對應的位置上,查找效率很是高。哈希索引就是基於散列表實現的,假設咱們對名字創建了哈希索引,則查找過程以下圖所示:
對於每一行數據,存儲引擎都會對全部的索引列(上圖中的 name 列)計算一個哈希碼(上圖散列表的位置),散列表裏的每一個元素指向數據行的指針,因爲索引自身只存儲對應的哈希值,因此索引的結構十分緊湊,這讓哈希索引查找速度很是快!可是哈希索引也有它的劣勢,以下:
綜上所述,哈希索引只適用於特定場合, 若是用得對,確實能再帶來很大的性能提高,如在 InnoDB 引擎中,有一種特殊的功能叫「自適應哈希索引」,若是 InnoDB 注意到某些索引列值被頻繁使用時,它會在內存基於 B+ 樹索引之上再建立一個哈希索引,這樣就能讓 B+樹也具備哈希索引的優勢,好比快速的哈希查找。
二、鏈表
雙向鏈表支持順序查找和逆序查找,如圖下
但顯然不支持咱們說的按某個值或區間的快速查找,另外咱們知道表中的數據是要不斷增長的,索引也是要及時插入更新的,鏈表顯然也不支持數據的快速插入,因此可否在鏈表的基礎上改造一下,讓它支持快速查找,更新,刪除。有一種結構恰好能知足咱們的需求,這裏引入跳錶的概念。
什麼是跳錶?簡單地說,跳錶是在鏈表之上加上多層索引構成的。以下圖所示
假設咱們如今要查找區間 7- 13 的記錄,不再用從頭開始查找了,只要在上圖中的二級索引開始找便可,遍歷三次便可找到鏈表的區間位置,時間複雜度是 O(logn),很是快,這樣看來,跳錶是能知足咱們的需求的,實際上它的結構已經和 B+ 樹很是接近了,只不過 B+ 樹是從平衡二叉查找樹演化而來的而已,接下來咱們一步步來看下如何將平衡二叉查找樹改形成 B+ 樹。
先來看看什麼是平衡二叉查找樹,平衡二叉查找樹具備以下性質:
下圖就是一顆平衡二叉查找樹
從其特性就能夠看到平衡二叉查找樹查找節點的時間複雜度是 O(log2n)
如今咱們將其改形成 B+ 樹
能夠看到主要區別就是全部的節點值都在最後葉節點上用雙向鏈表鏈接在了一塊兒,仔細和跳錶對比一下 ,是否是很像,如今若是咱們要找15 ~ 27 這個區間的數只要先找到 15 這個節點(時間複雜度 logn = 3 次)再從前日後遍歷直到 27 這個節點便可,便可找到這區間的節點,這樣它完美地支持了咱們提的三個需求:快速查找值,區間,順序逆序查找。
假設有 1 億個節點,每一個節點要查詢多少次呢,顯然最多爲 log21億 = 27 次,若是這 1 億個節點都在內存裏,那 27 次顯然不是問題,能夠說是很是快了,但一個新的問題出現了,這 1 億個節點在內存大小是多少呢,咱們簡單算一下,假設每一個節點 16 byte,則 1 億個節點大概要佔用 1.5G 內存!對於內存這麼寶貴的資源來講是很是可怕的空間消耗,這還只是一個索引,通常咱們都會在表中定義多個索引,或者庫中定義多張表,這樣的話內存很快就爆滿了!因此在內存中徹底裝載一個 B+ 樹索引顯然是有問題的,如何解決呢。
內存放不下, 咱們能夠把它放到磁盤嘛,磁盤空間比內存大多了,但新的問題又來了,咱們知道內存與磁盤的讀取速度相差太大了,一般內存是納秒級的,而磁盤是毫秒級的,讀取一樣大小的數據,二者可能相差上萬倍,因而上一步咱們計算的 27 次查詢若是放在磁盤中來看就很是要命了(查找一個節點能夠認爲是一次磁盤 IO,也就是說有 27 次磁盤 IO!),27 次查詢是否能夠優化?
能夠很明顯地觀察到查詢次數和樹高有關,那樹高和什麼有關,很明顯和每一個節點的子節點個數有關,即 N 叉樹中的 N,假設如今有 16 個數,咱們分別用二叉樹和五叉樹來構建,看下樹高分別是多少
能夠看到若是用二叉樹 ,要遍歷 5 個節點,若是用五叉樹 ,只要遍歷 3 次,一下少了兩次磁盤 IO,回過頭來看 上文的一億個節點,若是咱們用 100 叉樹來構建,須要幾回 IO 呢
能夠看到,最多遍歷五次(實際上根節點通常存在內存裏的,因此能夠認爲是 4 次)!磁盤 IO 一下從 27 減小到了 5!性能能夠說是大大提高了,有人說 5 次仍是太多,是否是能夠把 100 叉樹改爲 1000 或 10000 叉樹呢,這樣 IO 次數不就就能進一步減小了。
這裏咱們就須要瞭解頁(page)的概念,在計算機裏,不管是內存仍是磁盤,操做系統都是按頁的大小進行讀取的(頁大小一般爲 4 kb),磁盤每次讀取都會預讀,會提早將連續的數據讀入內存中,這樣就避免了屢次 IO,這就是計算機中有名的局部性原理,即我用到一塊數據,很大可能這塊數據附近的數據也會被用到,乾脆一塊兒加載,免得屢次 IO 拖慢速度, 這個連續數據有多大呢,必須是是操做系統頁大小的整數倍,這個連續數據就是 MySQL 的頁,默認值爲 16 KB,也就是說對於 B+ 樹的節點,最好設置成頁的大小(16 KB),這樣一個 B+ 樹上的節點就只會有一次 IO 讀。
那有人就會問了,這個頁大小是否是越大越好呢,設置大一點,節點可容納的數據就越多,樹高越小,IO 不就越小了嗎,這裏要注意,頁大小並非越大越好,InnoDB 是經過內存中的緩存池(pool buffer)來管理從磁盤中讀取的頁數據的。頁太大的話,很快就把這個緩存池撐滿了,可能會形成頁在內存與磁盤間頻繁換入換出,影響性能。
經過以上分析,相信咱們不難猜想出 N 叉樹中的 N 該怎麼設置了,只要選的時候儘可能保證每一個節點的大小等於一個頁(16kb)的大小便可。
如今咱們來看看開頭的問題, 爲啥推薦自增 id 做爲主鍵,自建主鍵不行嗎,有人可能會說用戶的身份證是惟一的,能夠用它來作主鍵,假設以身份證做主鍵,會有什麼問題呢。
B+ 樹爲了維護索引的有序性,每插入或更新一條記錄的時候,會對索引進行更新。假設原來基於身份證做索引的 B+ 樹以下(假設爲二叉樹 ,圖中只列出了身份證的前四位)
如今有一個開頭是 3604 的身份證對應的記錄插入 db ,此時要更新索引,按排序來更新的話,顯然這個 3604 的身份證號應該插到左邊節點 3504 後面(以下圖示,假設爲二叉樹)
若是把 3604 這個身份證號插入到 3504 後面的話,這個節點的元素個數就有 3 個了,顯然不符合二叉樹的條件,此時就會形成頁分裂,就須要調整這個節點以讓它符合二叉樹的條件
如圖示:調整事後符合二叉樹條件
這種因爲頁分裂形成的調整必然致使性能的降低,尤爲是以身份證做爲主鍵的話,因爲身份證的隨機性,必然形成大量的隨機結點中的插入,進而形成大量的頁分裂,進而形成性能的急劇降低,那若是是以自增 id 做爲主鍵呢,因爲新插入的表中生成的 id 比索引中全部的值都大,因此它要麼合到已存在的節點(元素個數未滿)中,要麼放入新建的節點中(以下圖示)因此若是是以自增 id 做爲主鍵,就不存在頁分裂的問題了,推薦!
有頁分裂就必然有頁合併,何時會發生頁合併呢,當刪除表記錄的時候,索引也要刪除,此時就有可能發生頁合併,如圖示
當咱們刪除 id 爲 7,9 對應行的時候,上圖中的索引就要更新,把 7,9 刪掉,此時 8,10 就應該合到一個節點,否則 8,10 分散在兩個節點上,可能形成兩次 IO 讀,勢必會影響查找效率! 那何時會發生頁合併呢,咱們能夠定個閾值,好比對於 N 叉樹來講,當節點的個數小於 N/2 的時候就應該和附近的節點合併,不過須要注意的是合併後節點裏的元素大小可能會超過 N,形成頁分裂,須要再對父節點等進行調整以讓它知足 N 叉樹的條件。
相信你們看完以上的 B+ 樹索引的介紹應該還有個疑惑,怎麼根據對應的索引值查找行記錄呢,其實相應的行記錄就放在最後的葉子節點中,找到了索引值,也就找到了行記錄。如圖示
能夠看到,非葉子節點只存了索引值,只在最後一行才存放了行記錄,這樣極大地減少了索引了大小,並且只要找到索引值就找到了行記錄,也提高了效率,
這種在葉節點存放一整行記錄的索引被稱爲聚簇索引,其餘的就稱爲非聚簇索引。
綜上所述,B+樹有如下特色:
本文由平常中經常使用的 SQL 由淺入深地總結了 B+ 樹的特色,相信你們應該對 B+ 樹索引有了比較清晰地認識,因此說爲啥咱們要掌握底層原來,學完了 B+ 樹,再看開頭提的幾個問題,其實也不過如此,深挖底層,有時候確實能讓你以不變應萬變。
最後,歡迎你們關注個人公號「碼海」,共同進步!
巨人的肩膀
http://www.rainybowe.com/blog/2016/05/10/mysql%E7%B4%A2%E5%BC%95/index.htmlhttps://time.geekbang.org/column/article/69236