mysql 主要是由 server 層和存儲層兩部分構成的。server 層主要包括連接器、查詢緩存,分析器、優化器、執行器。存儲層主要是用來存儲和查詢數據的,常用的存儲引擎有 InnoDB、MyISAM,MySQL 5.5.5版本后使用 InnoDB 作為默認存儲引擎。
mysql 中的 InnoDB 在底層是采用 B+ 樹這種數據結構來存儲數據的。
一、幾種常見的數據結構
mysql 的索引主要是為了提高查詢效率的,那一定得找一個合適的數據結構來存儲數據,哈希表、數組、二叉搜索樹這三種常見的數據結構都可以提高查詢效率。
-
哈希表
哈希表就是一種以鍵值對來存儲數據的結構,你可以通過一個 key 就可以很快的查詢出對用的 value 值。哈希表主要是利用了數組的隨機訪問特性,實現思想主要是通過一個哈希函數把 key 轉換成一個哈希值,這個哈希值就對應數組中的某個下標。
但是由於哈希表是無序的,區間查詢效率會非常的慢,所以哈希表通常只用於查詢單個值。
-
有序數組
數組就好說了,數組具有連續性和隨機訪問特性,因此數組都能很高效的進行單個等值查詢和區間查詢,但是 mysql 不僅僅是查詢數據,還會有插入和刪除數據的操作。在有序數組中插入或刪除一個數據會需要批量移動數組中其他數據,這是一個不小的消耗,影響性能。因此有序數組適合處理靜態數據,比如一些過往的不會再修改的數據。
在這里你可能會問,既然哈希表其實也是利用了數組的特性,那有了數組為啥還需要哈希表呢。是因為數組下標 key 只能是數字,而哈希表可以支持字符串 key,哈希函數可以把這個 key 轉換成一個數組下標。
同時,不同的 key 如果通過哈希函數轉換成了相同的數組下標,這就會造成沖突,在哈希表中一般會通過再拉出一個鏈表來保存這個沖突的值。
-
二叉搜索樹
注意,二叉搜索樹和二叉樹不一樣,二叉樹是指每個節點的左兒子小於父節點,父節點又小於右兒子,即二叉搜索樹的中序遍歷就是一個有序序列。
由於索引不僅僅是存在內存中,還會存儲在硬盤中,因此就會涉及到 IO 性能了,就要求樹的高度不能太高。實際上 B+ 樹就是通過二叉搜索樹推演改進的。
- B+ 樹
B+ 樹就是一種多叉樹,是由二叉搜索樹不斷演變過來的,為了滿足區間快速查詢,B+ 樹的葉子節點通過雙向鏈表串聯起來。
這里使用雙向鏈表是為了支持順序和倒序查詢,雖然雙向鏈表相對於單向鏈表雖然會浪費一倍的指針空間,但是在硬盤中這點空間幾乎微乎其微,用這點空間換時間是一件很值得的事情。
B+ 樹的子節點數不超過 m 個,同時也不能少於 m/2 個,一旦超過就需要分裂,一旦少於就需要合並。
小結
哈希表適合等值查詢,由於是無序的,區間查詢會很慢。有序數組適合等值和區間查詢,但是數組具有連續性,插入和刪除操作都可能需要移動其他元素。二叉搜索樹由於樹的高度,區間查詢需要中序遍歷,都會導致查詢效率很慢。
注意,在一些文章中經常會把 B+ 樹說成 B 樹或者 B-tree,這其實是錯誤的,B 樹和 B+ 是兩種不同的樹,B+ 樹是 B 樹的一個優化。
而且 B- 樹其實也就是 B 樹,這個符號並不是加減中的減號,並不是所謂的 "B 減樹",只是一個連接符號而已。
二、索引為什么要保存在硬盤中
服務器存儲一般分內存和硬盤,內存的大小相對於硬盤來說是很小的。內存的訪問速度是納秒級別的,非常快,而硬盤的訪問速度相對內存來說就比較慢了。
不管是訪問內存還是硬盤數據,操作系統都是按數據頁來讀取數據的,即每訪問一次硬盤或內存,只讀取一頁大小的數據,一頁的大小約等於 4 kb,向硬盤讀取數據的操作叫做磁盤 IO。
看到這里你或許會知道了 mysql 索引為啥不保存在內存中了吧,一方面是雖然內存訪問速度快但容量一般都比較小,存不了多少數據,再一個 mysql 需要讓數據持久化,如果服務器斷電或異常重啟會導致數據丟失。
三、怎么讓二叉搜索樹支持區間查詢
為了讓二叉搜索樹也支持區間查詢,我們把二叉樹的葉子節點通過一個雙向鏈表來連接,並且這個鏈表是有序的,注意葉子節點和普通節點是不一樣的,注意看下面的圖。
因此只需要先找到區間的起始值在鏈表中的位置,然后再往后遍歷,直到遍歷到區間的終止值,即可完成區間查詢。如下圖查找 7-30 這個區間的數據。
四、如何提升查詢速度
因為二叉搜索樹保存在硬盤中,我們每訪問一個節點,就對應着一次硬盤 IO 操作,上面有說過向硬盤讀取數據速度比較慢。因此樹的高度就代表硬盤 IO 操作的次數,所以我們要想辦法讓樹的高度變矮,來減少硬盤 IO。
要想樹變矮一些,那就把樹多分一些叉來吧,變成一顆多叉樹。下面分別用二叉樹和五叉樹來存儲 16 條數據,看下樹的高度又怎樣的變化。
根節點一般存儲在內存中,普通節點和葉子結點保存在硬盤中,因此顯然二叉樹的高度為 5,需要 5 次硬盤 IO,而五叉樹的高度為 2,查詢一個數據只需要 2 次硬盤 IO。
那么問題又來了,對於相同的數據量,是不是構建的多叉樹的叉越多越好呢,因為叉越多樹的高度就會越矮?
上面有說過操作系是按數據頁大小來訪問硬盤的,每次 IO 只讀取一個數據頁大小的數據,如果要讀取的數據大於一個數據頁,則會導致多次 IO。因此我們要盡量讓每個節點的數據大小剛好等於一個數據頁大小,即每訪問一個節點只需一次 IO。
五、插入和刪除數據怎么辦
這里我們把多叉樹稱作 m 叉樹,這個 m 值是通過數據頁大小和節點數計算出來的,盡量保證每訪問一個節點就是一個數據頁的大小,而且每個節點最多只有 m 個子節點。
現在我們要往數據庫中插入新的數據,即要往 m 叉樹中插入新的節點,這可能就會導致某些節點的子節點個數大於 m,也就會導致該節點大小大於一個數據頁,訪問該節點就需要多次 IO。
為了解決這個問題,m 叉樹會把該節點分裂成兩個節點,然后改分裂操作又會導致其父節點的子節點數可能超過 m,我們再用同樣的方法分裂節點,一直影響到根節點。
刪除操作也是類似的思想,如果有頻繁的刪除節點,就會導致某些節點的子節點過少,就會浪費存儲空間並降低查詢效率。所以就要想辦法讓這些節點合並起來,合並的話就有可能會導致其子節點數超過 m,超過的話就再用上面的分裂方法分裂子節點。