MySQL的查詢需要遍歷幾次B+樹,理論上需要幾次磁盤I/O?
一、前言 這個問題是博主去年面試的時候被大佬問過的問題,當時也不大清楚里面的原理,硬着頭皮回答的,當然,最終面試也沒過,哈哈。最近剛好研究了這塊的一些東西,就有種恍然大悟的感覺,這里分享給大家,歡迎拍磚~
二、遍歷B+樹的次數 首先,既然問題是一次查詢,那我們肯定是要知道mysql使用的存儲引擎是哪個,要根據存儲引擎的不同判斷索引的結構,然后通過索引的B+樹來回答這些問題。 MySQL中MyISAM和InnoDB的索引方式以及區別與選擇
1、mysql的innodb引擎的聚集索引和非聚集索引 網上看到很多資料,有的叫innodb的索引為聚集索引,有的叫做聚簇索引,其實都是一樣的,只是在翻譯過來了時候命名產生了分歧,聚簇(集)索引的葉子節點就是數據節點,而非聚簇(集)索引的葉子節點仍然是索引節點,只不過有指向對應數據塊的指針。非聚簇(集)索引在innodb引擎中,又叫做二級索引,輔助索引等。
2、分別遍歷了幾次B+樹 主鍵索引從上至下遍歷一次B+樹,直到找到具體的主鍵,拿到葉子結點存儲的數據。 二級索引需要遍歷兩次B+樹,第一次遍歷是找到對應的主鍵,第二次遍歷是根據主鍵找到具體的數據。
比如查詢二級索引的sql,先通過遍歷二級索引的B+樹來找到對應的主鍵,然后回表即通過主鍵遍歷聚集索引B+樹,拿到具體的數據。(PS:mysql里面每次新建索引都會生成新的B+樹,這也是索引文件會隨着索引字段不斷增加的原因)
這部分是要參照索引的圖來的,如圖:
(1)主鍵索引(聚集索引)
(2)輔助索引(二級索引)
3、回表的概念 回表就是通過輔助索引拿到主鍵id之后,要再去遍歷聚集索引的B+樹,這個過程就叫做回表。回表的操作更多的是隨機io,隨機io在性能上還是比較低下的,例如:
- 比如user表中有三個字段,a,b,c,給a和b建立聯合索引idex_a_b(a,b) sql:select * from user where a=1 and b=2; (1)首先是用二級索引index_a_b來查詢,速度會很快。(順序IO) (2)拿到主鍵id之后,這個主鍵id並不是順序排列的,還要用主鍵去查詢聚簇索引(隨機io) (3)當隨機io很多,也就是拿到的主鍵id很多的時候,回表的代價是巨大的。所以最好是選用覆蓋索引或者讓where 之后的條件篩選更多的數據
三、聚集索引和非聚集索引執行一次sql的io次數 1、聚集索引
大致步驟如下:
(1) 數據量小的話,直接把索引放到內存中,內存的O(logn)消耗是遠遠低於磁盤io的,所以可以忽略不計 (2) 數據量大的話,采用索引結構,我們這部分先從二叉樹說起,對於普通二叉樹,第一個步驟是二分,每次判斷都是一次半數的數量級檢索。假如有100W的數據,大概的時間復雜度是:log2N=1000000即N=20的節點獲取,也就是磁盤I/O復雜度最大為O(20),二分的時間復雜度是O(log2N)。 (3) 但是對於數據庫來說,存儲場景會更加復雜,二叉樹的性能雖然好,但我們還是想要樹的高度更低一些,存儲的數據更多一些。因此mysql引入了B+樹的概念。除了根節點之外,第二層級的數量得到了充分的擴展,相對於普通的二叉樹,B+樹的結構更加龐大又不失美感,假設葉節點不同元素占用情況為:左右指針各占4Byte,id值8Byte,目標記錄指針4Byte,那么一個4Kb的磁盤塊將大致可以容納250個下級指針,100萬行目標記錄只需log250N=1000000即N=3的I/O次數,充分提升了每次節點I/O帶來的檢索效用,時間復雜度是O(lognN),這里的n是非葉子結點的個數。(PS:實際上innodb的數據頁大小是16kb,這個n會更大,那么對應的,io次數也會更少) (4) 在實際的查詢中,IO次數可能會更小,因為有可能會把部分用到的索引讀取到內存中,相對於磁盤IO來說,內存的io消耗可以忽略不計。一般來說B+Tree的高度一般都在2-4層,MySQL的InnoDB存儲引擎在設計時是將根節點常駐內存的,也就是說查找某一鍵值的行記錄時最多只需要1~3次磁盤I/O操作(根節點的那次不算磁盤I/O)。
關於二分,我們假設有50W數據,下面看一下效果
1 50W 2 25W 3 12.5W 4 6W 5 3W 6 1.5W 7 7000 8 3500 9 1800 //第9次的時候,數據范圍就已經很小了,當然,它的效率還不夠高,但是比遍歷所有數據要快多了 根據以上的解釋,我們可以知道聚集索引的磁盤IO次數大概是1-3次,這一切都是因為高效的B+樹。如果大家也碰到類似的問題,就照着上面一頓胡扯,絕對很穩了。
2、輔助索引
(1) 參考上面對於B+樹的解釋,輔助索引獲取主鍵的時間復雜度是 lognN(假設第二層級是n個節點) (2) 再通過lognN獲取主鍵對應的數據列 參考 io解釋 四、引申問題 在進行相關測試的時候,可能會因為一部分索引放到了內存,從而造成一定的誤差。因此咱們這邊就來探討探討,這個放進內存的索引有多大。
1、多大的索引數據可以放到內存中?
(1) 要參照自己的mysql設置,一般是innodb_buffer_pool_size的值,這個值默認是128M,具體的要根據機器的性能設置。
關於Innodb_buffer_pool_size:《深入淺出MySQL》一文中這樣描述Innodb_buffer_pool_size:
該參數定義了 InnoDB 存儲引擎的表數據和索引數據的最大內存緩沖區大小。和 MyISAM 存儲引擎不同,
MyISAM 的 key_buffer_size只緩存索引鍵, 而 innodb_buffer_pool_size 卻是同時為數據塊和索引塊做緩存,
這個特性和 Oracle 是一樣的。這個值設得越高,訪問 表中數據需要的磁盤 I/O 就越少。在一個專用的數據庫 服務器上,可以設置這個參數達機器 物理內存大小的 80%。盡管如此,還是建議用戶不要把它設置得太大, 因為對物理內存的競 爭可能在操作系統上導致內存調度。
(2) 內存緩沖區主要包含 上面第一條提到的內存緩沖區主要包括:數據緩存(innodb的行數據),索引數據,緩沖數據(在內存中修改尚未刷新(寫入)到磁盤的數據),內部結構(自適應哈希索引,行鎖等。)
(3) 所以說,放到內存中的索引大小,和這些配置息息相關,當索引在內存中的時候,自然是用不到磁盤io的
具體參考: 如何在MySQL中分配innodb_buffer_pool_size
2、mysql一次普通查詢經過的步驟 從查詢過程上看,大致步驟是:
查看緩存中是否存在id,
如果有 則從內存中訪問,否則要訪問磁盤,
並將索引數據存入內存,利用索引來訪問數據,
對於數據也會檢查數據是否存在於內存,
如果沒有則訪問磁盤獲取數據,讀入內存。
返回結果給用戶。
據實際的情況分析。
五、總結 寫完博客之后,越發感覺到自己的才疏學淺。對於mysql來說,這些本來就都是基礎知識,但我到現在才算明白了一點點。而且這一點點也許還不夠准確,實在是讓人絕望呢。翻看mysql的手冊,上面有很多介紹,也有很多配置項都沒有用過,各位且學且珍惜吧
