Mysql系列第二十一講 mysql索引原理詳解

qwer1030274531發表於2020-10-13

背景

使用mysql最多的就是查詢,我們迫切的希望mysql能查詢的更快一些,我們經常用到的查詢有:

  1. 按照id查詢唯一一條記錄

  2. 按照某些個欄位查詢對應的記錄

  3. 查詢某個範圍的所有記錄(between and)

  4. 對查詢出來的結果排序

mysql的索引的目的是使上面的各種查詢能夠更快。

預備知識

什麼是索引?
上一篇中有詳細的介紹,可以過去看一下:什麼是索引?

索引的本質:透過不斷地縮小想要獲取資料的範圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是說,有了這種索引機制,我們可以總是用同一種查詢方式來鎖定資料。

磁碟中資料的存取
以機械硬碟來說,先了解幾個概念。

扇區:磁碟儲存的最小單位,扇區一般大小為512Byte。

磁碟塊:檔案系統與磁碟互動的的最小單位(計算機系統讀寫磁碟的最小單位),一個磁碟塊由連續幾個(2^n)扇區組成,塊一般大小一般為4KB。

磁碟讀取資料:磁碟讀取資料靠的是機械運動,每次讀取資料花費的時間可以分為尋道時間、旋轉延遲、傳輸時間三個部分,尋道時間指的是磁臂移動到指定磁軌所需要的時間,主流磁碟一般在5ms以下;旋轉延遲就是我們經常聽說的磁碟轉速,比如一個磁碟7200轉,表示每分鐘能轉7200次,也就是說1秒鐘能轉120次,旋轉延遲就是1/120/2 = 4.17ms;傳輸時間指的是從磁碟讀出或將資料寫入磁碟的時間,一般在零點幾毫秒,相對於前兩個時間可以忽略不計。那麼訪問一次磁碟的時間,即一次磁碟IO的時間約等於5+4.17 = 9ms左右,聽起來還挺不錯的,但要知道一臺500 -MIPS的機器每秒可以執行5億條指令,因為指令依靠的是電的性質,換句話說執行一次IO的時間可以執行40萬條指令,資料庫動輒十萬百萬乃至千萬級資料,每次9毫秒的時間,顯然是個災難。

mysql中的頁

mysql中和磁碟互動的最小單位稱為頁,頁是mysql內部定義的一種資料結構,預設為16kb,相當於4個磁碟塊,也就是說mysql每次從磁碟中讀取一次資料是16KB,要麼不讀取,要讀取就是16KB,此值可以修改的。

資料檢索過程

我們對資料儲存方式不做任何最佳化,直接將資料庫中表的記錄儲存在磁碟中,假如某個表只有一個欄位,為int型別,int佔用4個byte,每個磁碟塊可以儲存1000條記錄,100萬的記錄需要1000個磁碟塊,如果我們需要從這100萬記錄中檢索所需要的記錄,需要讀取1000個磁碟塊的資料(需要1000次io),每次io需要9ms,那麼1000次需要9000ms=9s,100條資料隨便一個查詢就是9秒,這種情況我們是無法接受的,顯然是不行的。

我們迫切的需求是什麼?

我們迫切需要這樣的資料結構和演算法:

  1. 需要一種資料儲存結構:當從磁碟中檢索資料的時候能,夠減少磁碟的io次數,最好能夠降低到一個穩定的常量值

  2. 需要一種檢索演算法:當從磁碟中讀取磁碟塊的資料之後,這些塊中可能包含多條記錄,這些記錄被載入到記憶體中,那麼需要一種演算法能夠快速從記憶體多條記錄中快速檢索出目標資料

我們來找找,看是否能夠找到這樣的演算法和資料結構。

我們看一下常見的檢索演算法和資料結構。

迴圈遍歷查詢

從一組無序的資料中查詢目標資料,常見的方法是遍歷查詢,n條資料,時間複雜度為O(n),最快需要1次,最壞的情況需要n次,查詢效率不穩定。

二分法查詢

二分法查詢也稱為折半查詢,用於在一個有序陣列中快速定義某一個需要查詢的資料。

原理是:

先將一組無序的資料排序(升序或者降序)之後放在陣列中,此處用升序來舉例說明:用陣列中間位置的資料A和需要查詢的資料F對比,如果A=F,則結束查詢;如果A<F,則將查詢的範圍縮小至陣列中A資料右邊的部分;如果A>F,則將查詢範圍縮小至陣列中A資料左邊的部分,繼續按照上面的方法直到找到F為止。

示例:

從下列有序數字中查詢數字9,過程如下

[1,2,3,4,5,6,7,8,9]

第1次查詢:[1,2,3,4,5,6,7,8,9]中間位置值為5,9>5,將查詢範圍縮小至5右邊的部分:[6、7、8、9]

第2次查詢:[6、7、8、9]中間值為8,9>8 ,將範圍縮小至8右邊部分:[9]

第3次查詢:在[9]中查詢9,找到了。

可以看到查詢速度是相當快的,每次查詢都會使範圍減半,如果我們採用順序查詢,上面資料最快需要1次,最多需要9次,而二分法查詢最多隻需要3次,耗時時間也比較穩定。

二分法查詢時間複雜度是:O(logN)(N為資料量),100萬資料查詢最多隻需要20次(2^20=1048576‬)

二分法查詢資料的優點:定位資料非常快,前提是:目標陣列是有序的。

有序陣列

如果我們將mysql中表的資料以有序陣列的方式儲存在磁碟中,那麼我們定位資料步驟是:

  1. 取出目標表的所有資料,存放在一個有序陣列中

  2. 如果目標表的資料量非常大,從磁碟中載入到記憶體中需要的記憶體也非常大

步驟取出所有資料耗費的io次數太多,步驟2耗費的記憶體空間太大,還有新增資料的時候,為了保證陣列有序,插入資料會涉及到陣列內部資料的移動,也是比較耗時的,顯然用這種方式儲存資料是不可取的。

連結串列

連結串列相當於在每個節點上增加一些指標,可以和前面或者後面的節點連線起來,就像一列火車一樣,每節車廂相當於一個節點,車廂內部可以儲存資料,每個車廂和下一節車廂相連。

連結串列分為單連結串列和雙向連結串列。

單連結串列

每個節點中有持有指向下一個節點的指標,只能按照一個方向遍歷連結串列,結構如下:

//單項鍊表class Node1{
    private Object data;//儲存資料
    private Node1 nextNode;//指向下一個節點}12345

雙向連結串列

每個節點中兩個指標,分別指向當前節點的上一個節點和下一個節點,結構如下:

//雙向連結串列class Node2{
    private Object data;//儲存資料
    private Node1 prevNode;//指向上一個節點
    private Node1 nextNode;//指向下一個節點}123456

連結串列的優點:
可以快速定位到上一個或者下一個節點

可以快速刪除資料,只需改變指標的指向即可,這點比陣列好

連結串列的缺點:
無法向陣列那樣,透過下標隨機訪問資料

查詢資料需從第一個節點開始遍歷,不利於資料的查詢,查詢時間和無需資料類似,需要全遍歷,最差時間是O(N)

二叉查詢樹

二叉樹是每個結點最多有兩個子樹的樹結構,通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用於實現二叉查詢樹和二叉堆。二叉樹有如下特性:

1、每個結點都包含一個元素以及n個子樹,這裡0≤n≤2。
2、左子樹和右子樹是有順序的,次序不能任意顛倒,左子樹的值要小於父結點,右子樹的值要大於父結點。

陣列[20,10,5,15,30,25,35]使用二叉查詢樹儲存如下:

在這裡插入圖片描述

每個節點上面有兩個指標(left,rigth),可以透過這2個指標快速訪問左右子節點,檢索任何一個資料最多隻需要訪問3個節點,相當於訪問了3次資料,時間為O(logN),和二分法查詢效率一樣,查詢資料還是比較快的。

但是如果我們插入資料是有序的,如[5,10,15,20,30,25,35],那麼結構就變成下面這樣:
在這裡插入圖片描述

二叉樹退化為了一個連結串列結構,查詢資料最差就變為了O(N)。

二叉樹的優缺點:

查詢資料的效率不穩定,若樹左右比較平衡的時,最差情況為O(logN),如果插入資料是有序的,退化為了連結串列,查詢時間變成了O(N)

資料量大的情況下,會導致樹的高度變高,如果每個節點對應磁碟的一個塊來儲存一條資料,需io次數大幅增加,顯然用此結構來儲存資料是不可取的

平衡二叉樹(AVL樹)

平衡二叉樹是一種特殊的二叉樹,所以他也滿足前面說到的二叉查詢樹的兩個特性,同時還有一個特性:

它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。

平衡二叉樹相對於二叉樹來說,樹的左右比較平衡,不會出現二叉樹那樣退化成連結串列的情況,不管怎麼插入資料,最終透過一些調整,都能夠保證樹左右高度相差不大於1。

這樣可以讓查詢速度比較穩定,查詢中遍歷節點控制在O(logN)範圍內

如果資料都儲存在記憶體中,採用AVL樹來儲存,還是可以的,查詢效率非常高。不過我們的資料是存在磁碟中,用過採用這種結構,每個節點對應一個磁碟塊,資料量大的時候,也會和二叉樹一樣,會導致樹的高度變高,增加了io次數,顯然用這種結構儲存資料也是不可取的。

B-樹

B槓樹,千萬不要讀作B減樹了,B-樹在是平衡二叉樹上進化來的,前面介紹的幾種樹,每個節點上面只有一個元素,而B-樹節點中可以放多個元素,主要是為了降低樹的高度。

一棵m階的B-Tree有如下特性【特徵描述的有點繞,看不懂的可以跳過,看後面的圖】:

  1. 每個節點最多有m個孩子,m稱為b樹的階

  2. 除了根節點和葉子節點外,其它每個節點至少有Ceil(m/2)個孩子

  3. 若根節點不是葉子節點,則至少有2個孩子

  4. 所有葉子節點都在同一層,且不包含其它關鍵字資訊

  5. 每個非終端節點包含n個關鍵字(健值)資訊

  6. 關鍵字的個數n滿足:ceil(m/2)-1 <= n <= m-1

  7. ki(i=1,…n)為關鍵字,且關鍵字升序排序

  8. Pi(i=1,…n)為指向子樹根節點的指標。P(i-1)指向的子樹的所有節點關鍵字均小於ki,但都大於k(i-1)

B-Tree結構的資料可以讓系統高效的找到資料所在的磁碟塊。為了描述B-Tree,首先定義一條記錄為一個二元組[key, data] ,key為記錄的鍵值,對應表中的主鍵值,data為一行記錄中除主鍵外的資料。對於不同的記錄,key值互不相同。

B-Tree中的每個節點根據實際情況可以包含大量的關鍵字資訊和分支,如下圖所示為一個3階的B-Tree:

在這裡插入圖片描述

每個節點佔用一個盤塊的磁碟空間,一個節點上有兩個升序排序的關鍵字和三個指向子樹根節點的指標,指標儲存的是子節點所在磁碟塊的地址。兩個鍵將資料劃分成的三個範圍域,對應三個指標指向的子樹的資料的範圍域。以根節點為例,關鍵字為17和35,P1指標指向的子樹的資料範圍為小於17,P2指標指向的子樹的資料範圍為17~35,P3指標指向的子樹的資料範圍為大於35。

模擬查詢關鍵字29的過程:

  1. 根據根節點找到磁碟塊1,讀入記憶體。【磁碟I/O操作第1次】

  2. 比較關鍵字29在區間(17,35),找到磁碟塊1的指標P2

  3. 根據P2指標找到磁碟塊3,讀入記憶體。【磁碟I/O操作第2次】

  4. 比較關鍵字29在區間(26,30),找到磁碟塊3的指標P2

  5. 根據P2指標找到磁碟塊8,讀入記憶體。【磁碟I/O操作第3次】

  6. 在磁碟塊8中的關鍵字列表中找到關鍵字29

分析上面過程,發現需要3次磁碟I/O操作,和3次記憶體查詢操作,由於記憶體中的關鍵字是一個有序表結構,可以利用二分法快速定位到目標資料,而3次磁碟I/O操作是影響整個B-Tree查詢效率的決定因素。

B-樹相對於avl樹,透過在節點中增加節點內部資料的個數來減少磁碟的io操作。

上面我們說過mysql是採用頁方式來讀寫資料,每頁是16KB,我們用B-樹來儲存mysql的記錄,每個節點對應mysql中的一頁(16KB),假如每行記錄加上樹節點中的1個指標佔160Byte,那麼每個節點可以儲存1000(16KB/160byte)條資料,樹的高度為3的節點大概可以儲存(第一層1000+第二層1000 2+第三層10003)10億條記錄,是不是非常驚訝,一個高度為3個B-樹大概可以儲存10億條記錄,我們從10億記錄中查詢資料只需要3次io操作可以定位到目標資料所在的頁,而頁內部的資料又是有序的,然後將其載入到記憶體中用二分法查詢,是非常快的。

可以看出使用B-樹定位某個值還是很快的(10億資料中3次io操作+記憶體中二分法),但是也是有缺點的:B-不利於範圍查詢,比如上圖中我們需要查詢[15,36]區間的資料,需要訪問7個磁碟塊(1/2/7/3/8/4/9),io次數又上去了,範圍查詢也是我們經常用到的,所以b-樹也不太適合在磁碟中儲存需要檢索的資料。

b+樹

先看個b+樹結構圖:
在這裡插入圖片描述

b+樹的特徵

  1. 每個結點至多有m個子女

  2. 除根結點外,每個結點至少有[m/2]個子女,根結點至少有兩個子女

  3. 有k個子女的結點必有k個關鍵字

  4. 父節點中持有訪問子節點的指標

  5. 父節點的關鍵字在子節點中都存在(如上面的1/20/35在每層都存在),要麼是最小值,要麼是最大值,如果節點中關鍵字是升序的方式,父節點的關鍵字是子節點的最小值

  6. 最底層的節點是葉子節點

  7. 除葉子節點之外,其他節點不儲存資料,只儲存關鍵字和指標

  8. 葉子節點包含了所有資料的關鍵字以及data,葉子節點之間用連結串列連線起來,可以非常方便的支援範圍查詢

b+樹與b-樹的幾點不同

  1. b+樹中一個節點如果有k個關鍵字,最多可以包含k個子節點(k個關鍵字對應k個指標);而b-樹對應k+1個子節點(多了一個指向子節點的指標)

  2. b+樹除葉子節點之外其他節點值儲存關鍵字和指向子節點的指標,而b-樹還儲存了資料,這樣同樣大小情況下,b+樹可以儲存更多的關鍵字

  3. b+樹葉子節點中儲存了所有關鍵字及data,並且多個節點用連結串列連線,從上圖中看子節點中資料從左向右是有序的,這樣快速可以支撐範圍查詢(先定位範圍的最大值和最小值,然後子節點中依靠連結串列遍歷範圍資料)

B-Tree和B+Tree該如何選擇?

  1. B-Tree因為非葉子結點也儲存具體資料,所以在查詢某個關鍵字的時候找到即可返回。而B+Tree所有的資料都在葉子結點,每次查詢都得到葉子結點。所以在同樣高度的B-Tree和B+Tree中,B-Tree查詢某個關鍵字的效率更高。

  2. 由於B+Tree所有的資料都在葉子結點,並且結點之間有指標連線,在找大於某個關鍵字或者小於某個關鍵字的資料的時候,B+Tree只需要找到該關鍵字然後沿著連結串列遍歷就可以了,而B-Tree還需要遍歷該關鍵字結點的根結點去搜尋。

  3. 由於B-Tree的每個結點(這裡的結點可以理解為一個資料頁)都儲存主鍵+實際資料,而B+Tree非葉子結點只儲存關鍵字資訊,而每個頁的大小有限是有限的,所以同一頁能儲存的B-Tree的資料會比B+Tree儲存的更少。這樣同樣總量的資料,B-Tree的深度會更大,增大查詢時的磁碟I/O次數,進而影響查詢效率。

Mysql的儲存引擎和索引

mysql內部索引是由不同的引擎實現的,主要說一下InnoDB和MyISAM這兩種引擎中的索引,這兩種引擎中的索引都是使用b+樹的結構來儲存的。

InnoDB中的索引
Innodb中有2種索引:主鍵索引(聚集索引)、輔助索引(非聚集索引)。

主鍵索引:每個表只有一個主鍵索引,葉子節點同時儲存了主鍵的值也資料記錄。

輔助索引:葉子節點儲存了索引欄位的值以及主鍵的值。

MyISAM引擎中的索引 /lanzhou/
不管是主鍵索引還是輔助索引結構都是一樣的,葉子節點儲存了索引欄位的值以及資料記錄的地址。

如下圖:

有一張表,Id作為主索引,Name作為輔助索引。
在這裡插入圖片描述

InnoDB資料檢索過程

如果需要查詢id=14的資料,只需要在左邊的主鍵索引中檢索就可以了。

如果需要搜尋name='Ellison’的資料,需要2步:

先在輔助索引中檢索到name='Ellison’的資料,獲取id為14

再到主鍵索引中檢索id為14的記錄

輔助索引這個查詢過程在mysql中叫做回表。

MyISAM資料檢索過程

在索引中找到對應的關鍵字,獲取關鍵字對應的記錄的地址

透過記錄的地址查詢到對應的資料記錄

我們用的最多的是innodb儲存引擎,所以此處主要說一下innodb索引的情況,innodb中最好是採用主鍵查詢,這樣只需要一次索引,如果使用輔助索引檢索,涉及到回表操作,比主鍵查詢要耗時一些。

innodb中輔助索引為什麼不像myisam那樣儲存記錄的地址?

表中的資料發生變更的時候,會影響其他記錄地址的變化,如果輔助索引中記錄資料的地址,此時會受影響,而主鍵的值一般是很少更新的,當頁中的記錄發生地址變更的時候,對輔助索引是沒有影響的。

我們來看一下mysql中頁的結構,頁是真正儲存記錄的地方,對應B+樹中的一個節點,也是mysql中讀寫資料的最小單位,頁的結構設計也是相當有水平的,能夠加快資料的查詢。

頁結構 /petitmal/

mysql中頁是innodb中儲存資料的基本單位,也是mysql中管理資料的最小單位,和磁碟互動的時候都是以頁來進行的,預設是16kb,mysql中採用b+樹儲存資料,頁相當於b+樹中的一個節點。

頁的結構如下圖:
在這裡插入圖片描述
每個Page都有通用的頭和尾,但是中部的內容根據Page的型別不同而發生變化。Page的頭部裡有我們關心的一些資料,下圖把Page的頭部詳細資訊顯示出來: 在這裡插入圖片描述
我們重點關注和資料組織結構相關的欄位:Page的頭部儲存了兩個指標,分別指向前一個Page和後一個Page,根據這兩個指標我們很容易想象出Page連結起來就是一個雙向連結串列的結構,如下圖:
在這裡插入圖片描述
再看看Page的主體內容,我們主要關注行資料和索引的儲存,他們都位於Page的User Records部分,User Records佔據Page的大部分空間,User Records由一條一條的Record組成。在一個Page內部,單連結串列的頭尾由固定內容的兩條記錄來表示,字串形式的"Infimum"代表開頭,"Supremum"代表結尾,這兩個用來代表開頭結尾的Record儲存在System Records的,Infinum、Supremum和User Records組成了一個單向連結串列結構。最初資料是按照插入的先後順序排列的,但是隨著新資料的插入和舊資料的刪除,資料物理順序會變得混亂,但他們依然透過連結串列的方式保持著邏輯上的先後順序,如下圖: 在這裡插入圖片描述
把User Record的組織形式和若干Page組合起來,就看到了稍微完整的形式。
在這裡插入圖片描述
innodb為了快速查詢記錄,在頁中定義了一個稱之為page directory的目錄槽(slots),每個槽位佔用兩個位元組(用於儲存指向記錄的地址),page directory中的多個slot組成了一個有序陣列(可用於二分法快速定位記錄,向下看),行記錄被Page Directory邏輯的分成了多個塊,塊與塊之間是有序的,能夠加速記錄的查詢,如下圖: question/

在這裡插入圖片描述
看上圖,每個行記錄的都有一個n_owned的區域(圖中粉色區域),n_owned標識所屬的slot這個這個塊有多少條資料,偽記錄Infimum的n_owned值總是1,記錄Supremum的n_owned的取值範圍為[1,8],其他使用者記錄n_owned的取值範圍[4,8],並且只有每個塊中最大的那條記錄的n_owned才會有值,其他的使用者記錄的n_owned為0。 /jilin/

資料檢索過程
在page中查詢資料的時候,先透過b+樹中查詢方法定位到資料所在的頁,然後將頁內整體載入到記憶體中,透過二分法在page directory中檢索資料,縮小範圍,比如需要檢索7,透過二分法查詢到7位於slot2和slot3所指向的記錄中間,然後從slot3指向的記錄5開始向後向後一個個找,可以找到記錄7,如果裡面沒有7,走到slot2向的記錄8結束。

n_owned範圍控制在[4,8]內,能保證每個slot管轄的範圍內資料量控制在[4,8]個,能夠加速目標資料的查詢,當有資料插入的時候,page directory為了控制每個slot對應塊中記錄的個數([4,8]),此時page directory中會對slot的數量進行調整。

對page的結構總結一下 hunan/

  1. b+樹中葉子頁之間用雙向連結串列連線的,能夠實現範圍查詢

  2. 頁內部的記錄之間是採用單向連結串列連線的,方便訪問下一條記錄

  3. 為了加快頁內部記錄的查詢,對頁內記錄上加了個有序的稀疏索引,叫頁目錄(page directory)

整體上來說mysql中的索引用到了b+樹,連結串列,二分法查詢,做到了快速定位目標資料,快速範圍查詢。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/30239065/viewspace-2726497/,如需轉載,請註明出處,否則將追究法律責任。

相關文章