前言
有人謂,再見是何意?
我曰:再見,既是相見時難別亦難,東風無力百花殘的難舍難分;
亦是,下次再見時,你我若初見般無悲畫扇......
好似知識,你理解它之后的告別,也因再次遇見它,別有一番風味......
正文(tips:終于有圖了)
備注一:參考資料部分來自于掘金小冊《從根上兒理解mysql》、極客時間《MYSQL45講》、《高性能MYSQL》
備注二:此次的圖來源于在我之前在團隊做分享畫的圖(純手畫,絕無任何摻水)
老師:小機啊,想問你個問題,你知道什么叫做二分法么?
小機:當然知道呀,這是算法的一個入門知識點呀
就是一組有序的數字里面選一個目標值(target),然后你會定義一個start指針,end指針和mid指針,然后每次比較mid和target的大小
如果target大的話,說明我要找的目標其實是在這組有序數組里面的后半部分,那么我就可以直接拋棄掉前一半數據了,每次一半一半的扔好爽啊~直到最后確認target的位置。
老師:不錯,那你知道,二分法的時間復雜度么?
小機:(小手舉高高):我知道,我知道,是log(n)。
老師:哦?你能說說為什么是log(n)么?
小機:因為如果有一組有序的數組里面有N個數字,那么按照我上面找目標值的思路,第一次篩掉一半,第二次再篩掉一半,第三次.....
我們假設篩了k次,最后終于找到了那個target,所以我們可以得到公式
也就是N*(1/2)^k = 1,那么我們的k是多少咧?也就是我們篩選的次數
(當當當當~)k=log(n)
老師:誒喲不錯,竟然上過高中,那我問你,那么為什么log(n)時間復雜度會這么快?
小機:老師你有所不知,經過我曾經嚴密的計算發現,如果你要在100億個有序的數字里面找到你想要的那個target,通過二分法只需要33次足以~
Log2(10000000000)?~?33.219281(次)
老師:那么,哪些地方用到了這些二分法的思想呢?
小機:我們大學學的《數據結構》中,二叉查找樹和2-3樹就用到了。
老師:哦,你簡單的說一下看看是怎么用的?
小機:老師,我們這一次不是聊索引么?為什么又岔開到二分法了呢,現在又撇到數據結構上去了?
老師:你懂不懂啥子叫做循循善誘?授人以漁?問你啥你就回答啥!
小機:哦~好吧。二叉查找樹是一種很有特點的數據結構,他有一個根節點,然后同時有左節點和右節點
最關鍵的一點就是,他的左節點數值比根節點值小,而右節點比根節點數值大
類似于下圖
老師:那你剛才不是還說了一種2-3樹么?這又是個啥?最好給我舉一個例子看看?
小機:老師,為什么又扯到了2-3樹了?
老師:正所謂,學貫三宗,道佛魔三法皆通;理解了2-3樹,對理解B+樹和紅黑樹都是有好處的,你會發現,道法合一,萬法歸一;
小機:能不能說人話??
老師:意思就是先在這里做個鋪墊,對你后面理解B+樹的頁的分裂,頁的融合埋下一個伏筆
小機:哦,原來如此,果然是山中有高人~那好,我就來講一下2-3樹吧。
如果你理解了上面的二叉查找樹的特性,想必會很快的了解到2-3樹的特性
(1)符合二叉查找樹的特性(左邊小,右邊大如下圖)
(2)每個內部節點有兩個或三個子節點(如下圖)
(3)所有的葉子節點到根節點的路徑相同-->這個概念就很明確的說明他是一棵完全二叉樹(如下圖)
相信很多人說看完三個要點之后很懵逼,也記不??;
當然,記不住就對了,你再去看看B+樹和紅黑樹的特性會更懵逼,有六七個要點呢~有些東西不需要去記住,我們需要用的時候回頭看一下就好了,好,下面就帶著大家構造出一棵2-3樹出來:
假設我們現在有一個數組,數據如下:[42,37,12,18,6,11,5]
tips:牢記兩個點,2-3樹的構造過程中是決不允許向下分裂的,只能向上融合(聽不懂沒關系,跟著例子來)
第一個42來了,輕輕的他來了~,相安無事各自安好~,就那么一個人靜靜地呆著~
第二個37突然光臨,發現竟有人捷足先登,那我只好按照規則1,比你小在你左邊,但是別忘了一點,我上面說過2-3樹是不允許向下分裂的,所以你以為會這樣子?
大錯特錯!?。?!他應該是這樣子的!?。。。。?/p>
看,多好,你若不離不棄,我便生死相依......歲月靜好,直至天荒地老......
不一會兒第三者插足了,12他來了,他來了,按照“法律法規”,他不可以向下分裂,且必須按照小在左,大在右的原則,所以他應該是這樣子么???
又錯了?。e忘了,2-3樹,2-3樹,只能有2節點和3節點(2節點代表只能有兩個兒子,3節點只能有3個兒子,而上圖卻會有四個兒子,也就是會分出四個叉出去),那我們應該怎么辦?
時刻提醒你:2-3樹的構造過程中不能向下分裂,只能向上融合~,所以應該是這樣子(融合的同時要滿足左小右大法則和完全平衡法則還有2-3兒子法則),完美的解決方法,就是把中間的37抽上去,就變成如下
好的,這時候18來了,他第一眼看見37,覺得我比你小,那么我就像二叉查找樹那樣子,像你的左孩子跑去,發現左孩子是12,18比12大,于是,18僅僅挨著12靠在一起,像這樣子~
看上圖,完全符合我們2-3樹的三大法則呀~perfect~
這時候竄出一個6,(玩歸玩,鬧歸鬧,別拿蹦迪開玩笑!)這個6又要來破壞我完美的2-3結構了,6進來會發生什么?
先看37,6比37小,往左探索,發現一個3節點(12,18),6比12還小,6想第三者插足,硬生生插進去,6夢中場景是這樣子的......
但,擁有者制裁權利的道德之光說:你不符合我的三大法則~因為你出現了一個四節點(6,12,18)
那么該怎么辦呢?牢記向上分裂融合之法,6后來者居上,攆走了12,但18因為也要遵守三大法則之后,成為了新生成的3節點(12,37)的中間兒子節點了,而12飽含熱淚離開,就變成了這樣子~
誒,可悲可嘆,為了追尋那規則,我們“驚卻鴛鴦,拆散鸞凰”
不一會兒11也來了,11看到這完美的2-3樹結構,不忍進入,但有些東西注定自我把控不了,它的生命又該如何演繹輝煌?
它先一看,比12都小了,趕緊去找它的左兒子,看到了6發現比6大,那么立刻在6的右邊坐下,完美無缺,又是一個完美的2-3結構體,如下圖:
這時候,最后一個數字5來了,它撇視了一眼這個完美的結構,在它心中,完美是不能存在的,我必須破壞這完美,不完美才是完美,那么它進來會怎么辦呢?
5一看比12小,那么立刻去找它的左兒子(一個3節點[6,11]),發現比6還小,于是就跑到6的左邊去,變成如下圖:
諾諾諾~又出現了一個四節點(5,6,11),制裁者又要出來管管了,怎么辦?向上融合大法,將中間者融合上去,于是6上去了
但是,我們能忍么?你上去就上去,又出現了一個4節點了(6,12,37)
于是再一次向上融合,將12向上融合
好了,至此之后,地動山搖,固若金湯,這樣子最后我們完美的構造出了一棵2-3樹
tips:大家如果想后續學習B類樹和紅黑樹的話,可以好好把這個例子復習復習~
老師:看你大學的《數據結構》沒有白學啊~,好,那么我們開始進入我們的正題,首先問你一個問題,你覺得什么是索引?索引的原理又是什么?
小機:這個,我覺得吧,索引即數據!而索引的原理就是拿空間換時間,你可以這么想,一個索引就是一棵B+樹,一棵B+樹就是一堆數據,而我們利用建造額外的B+樹(索引),來獲得更高效的查找速度,那么就是用空間換時間法則~
老師:不錯不錯,理解很深刻,那你覺得在innodb引擎下,我們是怎么去讀取數據呢?一條條讀還是一批一批讀?
小機:這個問題的答案很明顯,因為設計者不會這么傻的,一條條讀,每讀一條數據就進行一次IO操作,他家里怕不是有礦哦~
所以我們每次都是以“頁”為單位進行撈數據,一頁大小是16KB,啊~舒服,原來Batch的思想無處不在!
老師:那么這個“頁”長什么樣子呢?
小機:醬紫啦~看下圖
老師:你咔咔給我畫一張圖,然后全是英文字母,誰記得的?
小機:老師呀老師,當然不需要你全記得,我都記不得,后面會層層撥開,循循善誘的~
你現在只需要記得一點,我們經常在數據庫操作的那些數據(insert,update,delete,select)這些行記錄數據都是存在這個user records里面的,一開始,頁其實沒有這塊內容,而是你每次需要使用空間的時候,會從下面的Free Space來劃分出一條記錄空間大小給它,直到Free Space被劃分完,說明這個頁就已經被用光光啦~這時候就要去申請新的頁了。
老師:哦吼?既然你說數據都在user Records這個地方,那么頁里面的一條條數據是怎么關聯起來的?
小機:額,那我們先來看看上面我們所說的一條記錄長什么樣子吧?假設一條記錄是這樣子的
id? ? name
3? ? ?張三
那么它真實的樣子是這樣子
大家會發現,一條行記錄分為(記錄真實的數據+記錄額外的信息)
我們繼續拆開“記錄額外的信息”里面的記錄頭信息(如下圖)
相信大家一定會被各種屬性,各種概念弄的一臉懵逼,我教大家一個張三豐式訣竅,不記等于記過之后又忘了,所以一開始不記這些亂七八糟的概念即可(哈哈哈~)
大家這時候只要關注上圖里面的nextRecord屬性即可,那么我門就可以來回答這個問題,頁里面的行數據是怎么關聯起來的?
他們通過記錄頭信息里面的nextRecord串成一條鏈表,如下圖
大家看上圖,總共三條記錄(id:1 name:再見 id:2 name:伐木? id:3 name:機),這三條記錄通過頁里面的行記錄的頭信息的nextRecord字段串在一起了,成為一個單鏈表結構。
老師:是的,那么我如何在一個頁 里面查找數據呢?因為你目前在一個頁里面的記錄是用單鏈表串在一起的,我搜尋數據難道要從頭開始一條條往下搜么?
小機:老師這個問題問得好,如果是這么搜索的話,那我們之前鋪墊的二分法不就是白說了?
其實是這個樣子,如下圖
第一步:我們會把一頁里面的很多條行記錄,拆分成若干組,然后將每一組里面最后一條記錄(也就是這一組內最大的那條記錄)的在該頁內的偏移量抽出來作為一個槽(slot),按順序存儲到當前頁里面靠后的位置,作為一個頁目錄(page directory)。
此時此刻,我們就應該知道怎么在一個頁里面搜索數據了
第一步:查找一條記錄之后,首先通過二分法確定該記錄所在的“槽”,然后找到該“槽”對應的一組數據中里面主鍵值最小的那條記錄
第二步:然后就是通過next record屬性遍歷這個槽對應的組里面的數據了,從最小的那條記錄往后面找呀,找呀,直到找到目標記錄!
老師:剛才我們一直在討論在一個“頁”里面的搜索,那如果數據很多,導致有很多“頁”的時候應該怎么辦?這些“頁”是怎么關聯起來的呢?
小機:這時候我們翻到這篇文章一開頭講解頁的屬性那個圖,大家應該記不住了
幫大家回憶下,有一個屬性叫做File header:里面有三個很重要的屬性,FIL_PAGE_PREV(上一頁的頁號),FIL_PAGE_NEXT(下一頁的頁號),FIL_PAGE_OFFSET(頁號),有了這三個重要的屬性,是不是就可以把頁和頁之間穿起來了呢?
如下圖:
像上面,頁與頁之間通過雙向鏈表串在了一起......
老師:所以我們到現在討論了,頁與頁之間的數據怎么關聯起來的,頁里面但的數據是怎么串聯起來的?你能給我看一下一個完整的圖么?
小機:沒問題,今天已經完全化身為靈魂畫手了~如下圖
第一點:頁與頁之間是通過雙鏈表串起來了,頁里面的數據是單鏈表串起來了,而且在一個頁里面為了快速搜索,使用了“槽”這種東西,讓我們可以使用“二分法”快速搜索,簡直無情~
老師:嗯,非常不錯,但是如果當你數據特別多,導致有非常多的“頁”怎么辦呢?你現在是通過雙鏈表串起來的,我在頁之間搜索難道還要一個頁一個頁的往下搜索么,又變成了O(n)時間復雜度了~
小機:老師,聽我慢慢道來~
上面我們討論的2-3樹還記得了么?
我們可不可以類推~當頁太多的時候,我們用“向上融合大法”
就像下圖:
最下面的那兩個葉子節點存放了實際的數據,而為了在頁與頁之間快速搜索,我們會向上融合 抽出一個目錄出來,也就是上圖的“頁10”,里面只記錄了“主鍵+頁號”(tips:聚簇索引情況)
這樣子,我們就像新華字典那樣子,以后在搜索記錄,可以通過這個抽出來的“目錄頁”來快速定位了,同樣,把二分法用的淋漓精致......
老師:那如果頁的數據太多,多到你要建的“目錄頁”也太多了怎么辦?
小機:如果“向上融合”大法一個不行,那我我就來多個(一頓燒烤不夠,那就兩頓
),類似下圖:
把“目錄頁”和“目錄頁”再網上抽出一個目錄頁~
老師:嗯,確實不錯,原來這就是B+樹的來由了,一步一步,抽絲剝繭的描述出來,從上次JVM的討論,我就覺得小機你很不錯,那么照你所說,上面那個圖中的聚簇索引也就是我們看見的用戶存放在數據庫中的一條條記錄了,那么它的數據都是存放在葉子節點里面的,并且按照主鍵從小到大排列,而其他的節點也就只有一個主鍵值和頁號用來做目錄用途的,讓我們更好更快的進行搜索~
小機:是的,老師,這樣子我們就做到了,從宏觀來說(頁與頁之間)是一個二分法Log(n),頁之內也是一個二分法(Log(n)),所以總體來說就是一個二分法的速度來搜索,而且很關鍵的是他們在葉子節點之間有雙鏈表串起來,而很多搜索的條件都是一個“范圍查詢”,完美符合這個結構,我直接順序IO,磁盤順序往后一劃拉~就把數據都查出來了
老師:對了,你這里談到了聚簇索引,也就是主鍵索引,只是當我們用主鍵去搜索的時候會很快速的利用這個聚簇索引找到數據,那如果我的搜索條件不再是主鍵呢,如果是其他的列?
小機:這時候就要提到“二級索引”+“聯合索引”的概念了,其實二級索引和聯合索引很簡單,和上面的聚簇索引類似,只是不同之處在于
葉子節點就不在是所有的數據了,而是主鍵+你構造的二級索引列的值
目錄節點也不是主鍵+頁號了,而是頁號+二級索引列+主鍵(這三點才能唯一)
這樣子,搜索的時候用到了二級索引了,也是很快的
老師:但是我發現一個問題,因為你的二級索引里面只有二級索引列+主鍵,假設我這條sql語句不僅僅要搜索二級索引包含的那列該怎么辦?
小機:這時就會這樣子,首先通過二級索引定位到你要找到的記錄的主鍵id,然后再通過主鍵id去聚簇索引中將你需要的數據搜出來,這個操作的名字叫做(回表)
老師:查兩次感覺很浪費呀?有什么不查兩次的辦法?
小機:是的,是很浪費,你需要先順序IO在二級索引中找到對應的主鍵id,然后去聚簇索引中根據你取出來的(零散的主鍵id)去聚簇索引中進行隨機IO找數據。
所以我們建立二級索引之初,盡可能將你需要查詢的列一次性放入二級索引里面,以后查找只要一次順序io即可,但是如果你要查詢的列特別多,其實不建議二級索引的列太多,這樣子太浪費空間了,所以還是看情況斟酌~
老師:那說到底,有了這個索引,我們一直再說他的好處,那他帶來的代價是什么?
小機:
既然一個索引就是一棵B+樹,那么
第一點:我們用空間換時間,需要更大的存儲空間來帶來搜索性能的提高
第二點:就像我們最開始的2-3樹的例子,如果不停的增刪,其實會帶來頁面的頻繁分裂,和頁面回收
第三點:我們盡量讓主鍵自增,這樣子就會使得頁面分裂和移位盡可能的減少......
其實這篇文章到這里,大家對索引應該有一個宏觀的認識了,但是細枝末節還是很多的,需要大家更進一步的探索
我希望這篇文章能夠讓大家進入看山是山,看山不是山,看山還是山的境界??吹揭粭lsql而不只是一條sql,它背后設計的索引結構,它應該怎么優化才能走索引,哪些情況會導致你不走索引......
其實這里隨便列舉一個簡單的例子,本來不想寫的,因為過于簡單,一個很簡單的例子,網上經常會說對索引列增加函數,那么這個sql就不會走這個索引了,大家看完這篇文章應該很清楚了
其實就是你對索引那列加了個函數,導致索引的值已經變了,它的B+樹里面存放的是那列實際的值,你現在把人家值都變了,你去索引里面找個鬼阿~
這個例子,大家全當一樂......