MySQL性能優化學習(二)—— MySQL的索引原理分析

1.索引是什麼?

先拋出幾個問題:

表的索引越全越好,對嗎?

爲什麼不要在性別字段上建索引?

爲什麼不建議使用身份證做主鍵?

模糊匹配like abc% , like %abc% , like %888都用不到索引,對嗎?

不要使用select *,要寫明具體查詢字段,爲什麼?

 

一個簡單的加索引查找測試

先準備一張user表,在其中插入五百萬條記錄,執行查詢:

select name from `user` where name="Jesses";

執行SQL消耗時間12s。

在name字段上創建索引:

alter table `user` add index idx_name(`name`);

再次執行:

select name from `user` where name="Jesses";

執行SQL消耗時間0.081s。

從demo中可以看出索引對於查找性能的顯著提升。

 

維基百科對數據庫索引的定義:

數據庫索引,是數據庫管理系統(DBMS)中一個排序的數據結構,以協助快速查詢、 更新數據庫表中數據。

怎麼理解這個定義呢?

形象描述就如一本書的目錄,一本字典的索引。

我們說索引是一種數據結構,那麼它到底應該選擇一種什麼數據結構,才能實現數據的高效檢索呢?

 

2. 索引應該選擇一種什麼數據結構?

2.1 二分查找

雙十一過去之後,你女朋友跟你玩了一個猜數字的遊戲。
猜猜我昨天買了多少錢,給你五次機會。
10000?低了。30000?高了。接下來你會猜多少?
20000。爲什麼你不猜 11000,也不猜 29000 呢?
 

其實這個就是二分查找的一種思想,也叫折半查找。
每一次,我們都把候選數據縮小了一半。如果數據已經排過序的話,這種方式效率比較高。
所以我們可以考慮用有序數組作爲索引的數據結構。
 
有序數組的等值查詢和比較查詢效率非常高,但是更新數據的時候會出現一個問題,
可能要挪動大量的數據(改變數據的index),所以只適合存儲靜態的數據。
 
爲了支持頻繁的修改,如插入數據,我們需要採用鏈表。但如果是單鏈表,它的查找效率還是不夠高。
有沒有可以使用二分查找的鏈表呢?
爲了解決這個問題,BST(Binary Search Tree)也就是我們所說的二叉查找樹誕生了。

 

2.2 二叉查找樹(Binary Search Tree )

二叉查找樹的特點是什麼?
左子樹所有的節點都小於父節點,右子樹所有的節點都大於父節點。投影到平面以
後,就是一個有序的線性表。
 
二叉查找樹既能夠實現快速查找,又能夠實現快速插入。
但是二叉查找樹有一個問題:
就是它的查找耗時是和這棵樹的深度相關的,在最壞的情況下時間複雜度會退化成 O(n)。
 
如下圖,最壞的情況,當數據有序的時候:
 

當數據是有序的時候,它會變成鏈表(這種樹又叫做“斜樹”),
這種情況下不能達到加快檢索速度的目的,和順序查找效率是沒有區別的。

造成它傾斜的原因是什麼呢?
因爲左右子樹深度差太大,這棵樹的左子樹根本沒有節點——也就是它不夠平衡。
我們有沒有左右子樹深度相差不是那麼大,更加平衡的樹呢?
這個就是平衡二叉樹,叫做 Balanced binary search trees,或者 AVL 樹。

 

2.3 平衡二叉樹(AVL Tree)(左旋、右旋)

AVL Trees (Balanced binary search trees)

平衡二叉樹的定義:左右子樹深度差絕對值不能超過 1。
比如左子樹的深度是 2,右子樹的深度只能是 1 或者 3。
這個時候我們再按順序插入 1、2、3、4、5、6,一定是這樣,不會變成一棵“斜樹”。
 
右型-左旋
當我們插入了 1、2 、3 時,3肯定是要在 2 的右邊的,
這個時候根節點 1 的右節點深度會變成 2,但是左節點的深度是 0,
因爲它 沒有子節點,所以就會違反平衡二叉樹的定義。
那應該怎麼辦呢?因爲它是右節點下面接一個右節點,右-右型,所以這個時候我們
要把 2 提上去,這個操作叫做左旋。
 
同樣的,如果我們插入 7、6、5,這個時候會變成左左型,就會發生右旋操作,把 6
提上去
 
平衡的問題就解決了。
 
在平衡二叉樹中,一個節點,它的大小是一個固定的單位,作爲索引應該存儲什麼 內容?
它應該存儲三塊的內容:
1)索引的鍵值。比如在 id 上面創建了一個索引,在用 where id =1 的條件查詢時,
就會找到索引裏面的 id =1的這個鍵值。
2)數據的磁盤地址,因爲索引的作用就是去查找數據的存放的地址。
3)因爲是二叉樹,它必須還要有左子節點和右子節點的引用,
這樣才能找到下一個節點。比如大於 26 的時候,走右邊,到下一個樹的節點,繼續判斷。
 
如圖,如果使用AVL-Tree這樣存儲數據,又會有什麼問題呢?
在說明問題前,先來了解InnoDB的邏輯存儲結構。
 

2.4 InnoDB 邏輯存儲結構

https://dev.mysql.com/doc/refman/5.7/en/innodb-disk-management.html

https://dev.mysql.com/doc/refman/5.7/en/innodb-file-space.html
 
MySQL 的存儲結構分爲 5 級:表空間、段、簇、頁、行
 
表空間 Table Space
表空間可以看做是 InnoDB 存儲引擎邏輯結構的最高層,所有的數據都存放在表空間中。
分爲:系統表空間、獨佔表空間、通用表空間、 臨時表空間、Undo 表空間。
 
段 Segment
表空間是由各個段組成的,常見的段有數據段、索引段、回滾段等,段是一個邏輯的概念。
一個 ibd 文件(獨立表空間文件)裏面會由很多個段組成。
創建一個索引會創建兩個段,一個是索引段:leaf node segment,一個是數據段: non-leaf node segment。
索引段管理非葉子節點的數據。數據段管理葉子節點的數據。
也就是說,一個表的段數,就是索引的個數乘以 2。
 
簇 Extent
一個段(Segment)又由很多的簇(也可以叫區)組成,每個區的大小是 1MB(64 個連續的頁)。
每一個段至少會有一個簇,一個段所管理的空間大小是無限的,可以一直擴展下去,
但是擴展的最小單位就是簇。
 
頁 Page
爲了高效管理物理空間,對簇進一步細分,就得到了頁。
簇是由連續的頁(Page)組成的空間,一個簇中有 64 個連續的頁(1MB/16KB=64)。
這些頁面在物理上和邏輯上都是連續的。
跟大多數數據庫一樣,InnoDB 也有頁的概念(也可以稱爲塊),每個頁默認 16KB。
頁是 InnoDB 存儲引擎磁盤管理的最小單位,通過 innodb_page_size 設置。
一個表空間最多擁有 2^32 個頁,默認情況下一個頁的大小爲 16KB,也就是說一個
表空間最多存儲 64TB 的數據。
 

2.5 使用AVL存儲索引數據存在的問題

1)浪費空間
當我們用樹的結構來存儲索引的時候,訪問一個節點就要跟磁盤之間發生一次 IO。
InnoDB 操作磁盤的最小的單位是一頁(或者叫一個磁盤塊),大小是 16K(16384 字節)。
那麼,一個樹的節點就是 16K 的大小
如果我們一個節點只存一個鍵值+數據+引用,例如整形的字段,可能只用了十幾個
或者幾十個字節,它遠遠達不到 16K 的容量,所以訪問一個樹節點進行一次 IO 的時候,
浪費了大量的空間。
2)IO次數多
如果每個節點存儲的數據太少,從索引中找到我們需要的數據,就要訪問更多 的節點,
這就意味着跟磁盤交互次數就會過多。
如果是機械硬盤時代,每次從磁盤讀取數據需要 10ms 左右的尋址時間,
交互次數越多,消耗的時間就越多。
 
如上面這張圖,我們一張表裏面有 6 條數據,當我們查詢 id=37 的時候,
要查詢兩個子節點,就需要跟磁盤交互 3 次,如果有幾百萬的數據這個時間更加難以估計。
 

試想解決方案,可以讓每個節點存放更多的數據。
我們的指針數也越多,也就是意味着可以有更多的分叉(我們把它叫做“路數”)
因爲分叉數越多,樹的深度就會減少(根節點是 0)
這樣最終形成的樹就不再是瘦高的型狀,而是矮胖的型狀。
這樣樹也就不再是二叉樹,而是多叉樹(多路);

 

2.6 多路平衡查找樹(Balanced Tree)(B Tree)(分裂、合併)

和 AVL 樹一樣,B 樹在枝節點和葉子節點存儲鍵值、數據地址、節點引用。
它有一個特點:分叉數(路數)永遠比關鍵字數多 1。
如下圖,每個節點存儲兩個關鍵字,那麼就會有三個指針指向三個子節點。
B Tree 的查找規則是什麼樣的呢?
比如要在這張表裏面查找 15。
因爲 15 小於 17,走左邊。
因爲 15 大於 12,走右邊。
在磁盤塊 7 裏面就找到了 15,只用了 3 次 IO。
so,B Tree 比 AVL 樹效率更高。
 
B Tree 又是怎麼實現一個節點存儲多個關鍵字,還保持平衡的呢?
分裂操作
比如 Max Degree(度數)是 3 的時候,我們插入數據 1、2、3,在插入 3 的時候。
本來應該在第一個磁盤塊放3,但是如果一個節點有三個關鍵字的時候,意味着有 4 個指針,
子節點會變成 4 路,所以這個時候必須進行分裂。把中間的數據 2 提上去,把 1 和 3 變
成 2 的子節點。
如果刪除節點,則會有相反的合併的操作。
注意這裏是分裂和合並,跟 AVL 樹的左旋和右旋是不一樣的。
可以看出,
在更新索引的時候會有大量的索引的結構的調整,
所以也解釋了爲什麼不要在頻繁更新的列上建索引,或者爲什麼不要更新主鍵。
節點的分裂和合並,其實就是 InnoDB 頁的分裂和合並。

 

2.7 B+樹(加強版多路平衡查找樹)

B Tree 的效率已經很高了,但 MySQL 還又對 B Tree 進行改良,最終使用了B+Tree 。
 
B+樹的存儲結構:
 
MySQL 中的 B+Tree 有幾個特點
1、它的關鍵字的數量是跟路數相等的;
2、B+Tree 的根節點和枝節點中都不會存儲數據,只有葉子節點才存儲數據。
搜索到關鍵字不會直接返回,會到最後一層的葉子節點。
比如我們搜 id=28,雖然在第一層直接命中了。
但全部的數據都在葉子節點上面,所以我還要繼續往下搜索,一直到葉子節點。

舉個例子:假設一條記錄是1K,一個葉子節點(一頁)可以存儲 16 條記錄。非葉子節點可以存儲多少個指針? 
假設索引字段是 bigint 類型,長度爲 8 字節。
指針大小在 InnoDB 源碼中設置爲 6 字節,這樣一共 14 字節。
非葉子節點(一頁)可以存儲 16384/14=1170 個這樣的 單元(鍵值+指針),代表有 1170 個指針。
樹 深 度 爲 2 的 時 候 ,有 1170^2 個 葉 子 節 點 ,可 以 存 儲 的 數 據 爲 1170*1170*16=21902400。
在查找數據時一次頁的查找代表一次 IO,也就是說,一張 2000 萬左右的表,查詢
數據最多需要訪問 3 次磁盤。
所以在 InnoDB 中 B+ 樹深度一般爲 1-3 層,它就能滿足千萬級的數據存儲。
 
總結一下,InnoDB 中的 B+Tree 的特點
1)它是 B Tree 的變種,B Tree 能解決的問題,它都能解決。
B Tree 解決的兩大問題是什麼?(每個節點存儲更多關鍵字;路數更多)
2)掃庫、掃表能力更強
(如果我們要對錶進行全表掃描,只需要遍歷葉子節點就可以了,不需要遍歷整棵 B+Tree 拿到所有的數據)
3) B+Tree 的磁盤讀寫能力相對於 B Tree 來說更強
(根節點和枝節點不保存數據區,所以一個節點可以保存更多的關鍵字,一次磁盤加載的關鍵字更多)
4)排序能力更強(因爲葉子節點上有下一個數據區的指針,數據形成了鏈表)
5)效率更加穩定(B+Tree 永遠是在葉子節點拿到數據,所以 IO 次數是穩定的)
發佈了14 篇原創文章 · 獲贊 3 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章