B+树 -- MySQL数据库索引
文章目錄
- 1. 定義清楚問題
- 2. 嘗試學過的數據結構解決問題
- 3. 改造二叉查找樹來解決問題
- 4. 總結
為了加速數據庫中數據的查找速度,我們常對表中數據創建索引。數據庫索引是如何實現的呢?底層使用的是什么數據結構和算法呢?
1. 定義清楚問題
如何定義清楚問題呢?除了問題調研,還可以對一些模糊的需求進行假設,來限定要解決的問題的范圍。
假設要解決的問題,只包含兩個常用的需求:
- 根據某個值查找,比如 select * from user where id = 1234
- 根據區間來查找,比如 select * from user where id > 1234 and id < 2345
非功能性需求,比如安全、性能、用戶體驗等等。性能方面,主要考察時間和空間兩方面,就是執行效率和存儲空間。
- 在執行效率方面,希望通過索引,查詢數據的效率盡可能的高;
- 在存儲空間方面,希望索引不要消耗太多的內存空間。
2. 嘗試學過的數據結構解決問題
支持快速查詢、插入等操作的動態數據結構,已經學過散列表、平衡二叉查找樹、跳表。
- 散列表。散列表的查詢性能很好,時間復雜度是O(1)。但是,散列表不支持按照區間快速查找數據。
- 平衡二叉查找樹。盡管平衡二叉查找樹查詢的性能也很高,時間復雜度是O(log n)。對樹進行中序遍歷,還可以得到一個從小到大有序的序列,但不足以支持按照區間快速查找數據。
- 跳表。跳表是在鏈表之上加上多層索引構成的。它支持快速地插入、查找、刪除數據,對應的時間復雜度是O(log n)。并且,跳表也支持按照區間快速地查找數據。只需要定位到區間起點值對應在鏈表中的結點,然后從這個結點開始,順序遍歷鏈表,直到區間終點對應的結點為止。
這樣看來,跳表是可以解決這個問題。實際上,數據庫索引所用到的數據結構跟跳表非常相似,叫作B+樹。不過,它是通過二叉查找樹演化來的。
3. 改造二叉查找樹來解決問題
為了讓二叉查找樹支持按照區間查找數據,對它進行改造:樹中的節點并不存儲數據本身,而是只是作為索引。除此之外,我們把每個葉子節點串在一條鏈表上,鏈表中的數據是從小到大有序的。
改造后,要查某個區間的數據。只需拿區間的起始值,在樹中進行查找,當查找到某個葉子節點之后,再順著鏈表往后遍歷,直到鏈表中的結點數據值大于區間的終止值為止。所有遍歷到的數據,就是符合區間值的數據。
但是,要為幾千萬、上億的數據構建索引,將索引存儲在內存中,盡管內存訪問速度非常快,查詢效率非常高,但是,占用的內存非常多。
比如,給一億個數據構建二叉查找樹索引,索引中大約1億個節點,每個節點假設占用16個字節,要大約1GB的內存。給一張表建立索引,需要1GB的內存。如果要給10張表建立索引,那對內存的需求是無法滿足的。如何解決這個索引占用太多內存的問題呢?
可以借助時間換空間的思路,把索引存儲在硬盤中,而非內存中。硬盤非常慢。通常內存訪問納秒級,而磁盤訪問毫秒級。將索引存儲在硬盤中,盡管減少了內存消耗,但是數據查找,需讀取磁盤中的索引,因此查詢效率降低很多。
二叉查找樹,經過改造之后,支持區間查找的功能就實現了。節省內存,把樹存儲在硬盤中,每個節點的讀取(或者訪問),對應一次磁盤IO操作。樹的高度就等于每次查詢數據時磁盤IO操作的次數。
所以優化的重點就是盡量減少磁盤IO操作,也就是, 盡量降低樹的高度 。如何降低樹的高度呢?
如果把索引構建成m叉樹,高度是不是比二叉樹要小呢?
如圖所示,給16個數據構建二叉樹索引,樹的高度是4,查找一個數據,就需要4個磁盤IO操作(如果根節點存儲在內存中,其他結點存儲磁盤),
如果對16個數據構建五叉樹索引,那高度只有2,查找一個數據,對應只需要2次磁盤操作。
如果m叉樹中的m是100,那對一億個數據構建索引,樹的高度也只是3,最多3次磁盤IO就能取到數據。磁盤lO少了,查找效率也就高了。
m叉樹中的m越大,那樹的高度就越小,那m叉樹中的m是不是越大越好呢?多大才最合適?
不管是內存數據,還是磁盤數據,操作系統都是按頁(一頁大小通常是4KB,這個值可以通過 getconfig PAGE_SIZE命令查看)來讀取的,一次會讀一頁的數據。如果要讀取的數據量超過一頁的大小,就會觸發多次IO操作。所以,我們在選擇m大小的時候,要盡量讓每個節點的大小等于一個頁的大小。讀取一個節點,只需一次磁盤IO操作。
盡管索引提高數據庫查詢效率,索引有利也有弊,它也會讓寫入數據的效率下降。這是為什么呢?
數據寫入過程,會涉及索引的更新,這是主要原因。
對于B+樹來說,m值是根據頁的大小事先算好的,也就是說,每個節點最多只能有m個子節點。往數據庫寫入過程中,有可能使某些節點的子節點個數超過m,這個節點的大小超了一個頁的大小,讀取這個節點,就會導致多次磁盤IO操作。該如何解決?
處理思路并不復雜。只需將這個節點分裂成兩個節點。但是,節點分裂之后,上層父節點的子節點個數可能超過m個。用同樣的方法,將父節點也分裂成兩個節點。這種級聯反應會從下往上,一直影響到根節點。
這個分裂過程,可以結合下圖,會更容易理解(圖中的B+樹是一個三叉樹。限定葉子節點中,數據個數超過2個就分裂節點;非葉子節點中,子節點的個數超過3個就分裂節點)。
因為要時刻保證B+樹索引是一個m叉樹,索引的存在會導致數據庫寫入速度降低。刪除數據也會變慢。為什么呢?
刪除數據時,也要更新索引節點。頻繁刪除,導致某些結點中,子節點個數變得非常少,長此以往,如果每個節點的子節點都比較少,勢必會影響索引的效率。
可以設置一個閾值。在B+樹中,這個閾值等于m/2。某個節點的子節點個數小于m/2,將它跟相鄰的兄弟節點合并。不過,合并之后結點的子節點個數有可能會超過m。再分裂節點。
文字不直觀,舉個刪除例子,(圖中的B+樹是一個五叉樹。限定葉子節點中,數據少于2個就合并節點;非葉子節點中,子節點少于3個就合并)
B+樹的結構和操作,跟跳表非常類似。理論上,對跳表稍加改造,也可以替代B+樹。
4. 總結
- 數據庫索引實現,依賴的底層數據結構,B+樹。
- 通過存儲在磁盤的多叉樹結構,做到了時間、空間的平衡,既保證了執行效率,又節省了內存。
總結一下B+樹的特點:
- 每個節點中子節點的個數不超過m,也不小于m/2;
- 根節點的子節點個數可以不超過m/2,這是一個例外;
- m叉樹只存儲索引,并不存儲數據,這個有點兒類似跳表;
- 通過鏈表將葉子節點串在一起,可以方便按區間查找;
- 一般情況,根節點會被存儲在內存中,其他節點存儲在磁盤中。
簡單提一下。
B- 樹就是B樹,英文翻譯都是B-Tree,這里的 “-” 不是相對B+樹中的“+”,只是一個連接符。這個很容易誤解。
B樹實際上是低級版的B+樹,或者說B+樹是B樹的改進版。B樹跟B+樹的不同點主要集中在這幾個地方:
- B+樹中的節點不存儲數據,只是索引,而B樹中的節點存儲數據;
- B樹中的葉子節點并不需要鏈表來串聯。
也就是說,B樹只是一個每個節點的子節點個數不能小于m/2的m叉樹。
總結
以上是生活随笔為你收集整理的B+树 -- MySQL数据库索引的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LeetCode 217. 存在重复元素
- 下一篇: mysql format row_MyS