MySQL索引那些事

大家有沒有遇到過慢查詢的情況,執行一條SQL需要幾秒,甚至十幾、幾十秒的時間,這時候DBA就會建議你去把查詢的 SQL 優化一下,怎麼優化?你能想到的就是加索引吧?
爲什麼加索引就查的快了?這就要從索引的本質以及他的底層原理說起。
索引是什麼
那索引到底是什麼呢?你是不是還停留在大學學『數據庫原理』時老師講的“索引就像字典的目錄”這樣的概念?老師講的沒錯,但沒有深入去講。
其實索引就是一種用於快速查找數據的數據結構,是幫助MySQL高效獲取數據的排好序的數據結構。
索引的好處
舉例說明索引的好處以及是怎麼加快查詢的。
假設我們有一個表t,它有倆字段,Col1和Col2,如下:

不加索引
不加索引的情況下,SQL: select * from t where t.col2=89 ,需要從表的第一行一行行遍歷比對col2的值是否等於89,這樣需要比對6次才能查到。這只是只有幾行記錄的表,那如果是百萬級千萬級的表呢?是不是就比較的次數就更多了,那還不得慢死。
加索引
如果col2這列加了索引,mysql內部會維護一個數據結構。假設mysql用的數據結構是紅黑樹(右子樹的元素大於根節點,左子樹的元素小於根節點)的數據結構建立索引,那就像上圖右邊那樣。這樣的話,剛纔的那條SQL是不是只需要2次磁盤IO就查到了,是不是快很多了。
這就是索引的好處。索引使用比較巧妙的數據結構,利用數據結構的特性來大大減少查找遍歷次數。
索引底層數據結構的探索
既然索引底層原理是利用一些巧妙的數據結構維護我們的數據,使得查詢效率很高,那索引底層使用的什麼數據結構呢?又是怎樣來維護我們的數據呢?下面就帶着大家一起探索一下索引的底層數據結構。
索引可選的數據結構 :

二叉樹
紅黑樹
hash
B-tree

但mysql索引的底層用的並不是二叉樹和紅黑樹。因爲二叉樹和紅黑樹在某些場景下都會暴露出一些弊端或者說缺點。
二叉樹
我們看一下二叉樹如果作爲索引的底層數據結構在什麼樣的場景下有怎麼樣的缺點和不足。
假設把剛纔的SQL改一下,用col1作爲條件來查找,SQL:select * from t where t.col1 = 6。
假如把col1作爲索引,col1這列的數據特點是從上到下依次遞增,類似於自增主鍵,那在每插入一行在維護二叉樹這樣一個數據結構的時候,我們看一下二叉樹維護成什麼樣子了。
打開這個網址(國外的),可以演示數據結構維護的過程。依次插入1、2、3、4、5…
通過這個網站的演示插入這些數據,我們可以看到這樣的一個二叉樹是不是一直在單邊增長,沒有左子樹。再仔細看一下和我們學過的鏈表是不是很像,也就是說二叉樹在某些場景下退化成了鏈表。

鏈表的查找是不是需要從頭部遍歷啊,這時候和沒加索引從表的第一行遍歷是不是沒什麼太大區別?這就是mysql索引底層沒有使用二叉樹這種數據結構的原因之一。
當二叉樹像上圖一樣退化成鏈表後,我們去查col1=6的記錄是不是從二叉樹的根節點依次遍歷,遍歷6次才能查到,和不加索引從表裏一行行的遍歷沒太大差別。這是二叉樹所謂索引底層數據結構的弊端之一。
紅黑樹
那有沒有更好的數據結構用來存儲索引,幫助我們更快的查找呢?比方說紅黑樹或hash表。
我們先看下紅黑樹。
紅黑樹是什麼?
是一種平衡二叉樹,JDK1.8的HashMap就用到了紅黑樹。
那我們把剛纔的一樣的數據用紅黑樹來看一下是什麼樣的效果,同樣打開剛纔的網址,我們選擇紅黑樹。

依次插入1、2、3、4、5、6、7看一下效果,可以看到,當有單邊增長的趨勢時紅黑樹會進行一個平衡(旋轉)。這時,我們查詢col1=6的數據時,查了3次,比二叉樹又有了改進。

先告訴你mysql索引用的數據結構也不是紅黑樹,而是B+Tree(B-Tree的變種)。那爲什麼MySQL也沒用紅黑樹做索引的數據結構呢?說白了紅黑樹還是有缺陷的。
紅黑樹做索引底層數據結構的缺陷
我們可以想一下,對於一些大公司特別是互聯網公司,表數據動輒數百萬數千萬,那這樣的表我們可以想象一下,現在我們只有7條記錄,樹的高度就達到了4層,那數百萬數千萬甚至上億記錄的表創建的索引它的樹高得有多高?
假如說我查找的數據在底層的葉子節點上,一般來說都是從根節點開始查找,假如樹的高度是50,那我要進行50次查找,50次磁盤IO那得多慢啊這開銷已經很大了。這就是紅黑樹作爲索引數據結構的弊端:樹的高度過高導致查詢效率變慢。
那能不能做一點改造呢?我們看,紅黑樹的樹越高遍歷次數會越多,會因爲樹的高度影響查詢效率。所以我們要解決的問題就是減少樹的高度,儘量控制它的高度在一個閾值範圍內。假設說不大於5,即使數據達到1千萬2千萬最多也就5次磁盤IO就找到了,5次磁盤IO也是可以接受的畢竟表數據這麼大嘛。
怎麼改造能達到這個效果呢????想一下,既然樹的高度不讓增加,又想存很多數據。也就是說限制了縱向發展,那就橫向發展唄。(身高已經增長不了了,長胖還是可以的)
對於上圖的紅黑樹來說每個節點的子節點最多就2個,那基於橫向增長的思想就讓他變成3叉、4叉、5叉…讓子節點增加,讓每一個高度可以存儲更多的索引元素,每個節點又分叉,分出來的叉又有很多個節點。那麼存儲同等數量級別的數據,橫向存儲的越多,樹高就越小了。這樣的一個改造結果就是B-Tree。
Hash
待會兒有別的問題會引入hash。
B-Tree

葉節點具有相同的深度,葉節點的指針爲空
所有索引元素不重複
節點中的數據索引從左到右遞增排序

就這樣的一個結構。也就是說在一個節點上可以存儲更多的元素,k-v,key就是索引字段,data就是索引字段所在的那一行的數據或是那一行數據坐在的的磁盤文件地址、指針,再去查找元素的時候一次性不是Load一個小元素,而是把一個大的節點的數據一次性全部load到內存,然後再在內存裏再去比對,在內存裏操作是比較快的。
如果我們要查找49這個元素,實際上是從根節點開始查找的,它一次性將根節點這個大節點一次性load到內存裏,然後用要查找的元素在這裏去比對,49大於15小於56,在15和56之間有一個節點存儲的是下一個節點的磁盤地址指向下一個節點(這個節點的索引都是大於15小於56的),然後再將這個節點一次性load到內存去找這個元素,然後比對就找到了。
注意,一次load節點是一次磁盤IO,是非常慢的,但是我們把它load到內存中之後在你內存裏隨機的找某一個元素是非常快的,跟一次磁盤IO這個時間消耗去比對的話幾乎可以忽略不計。
那按這種說法樹的高度越小越好,那按這種思路可不可以把一個表的數據都放到一個大的節點上?然後把這個節點一次性load到內存裏,我再在內存裏一個個去比對不行嗎?不是說內存裏去比較查找元素是非常的快嘛,跟一次磁盤IO去比對快的多。不可以這樣嗎?
答案是否定的。
凡事都有個度。你想想,假如我們有幾千萬數據,在磁盤上面全部放到一個節點上去是不可能的,你的數據表是一行行插入的,存在磁盤上面幾百兆甚至幾個G,一次性load到內存中合適嗎?內存本來就有限,一次性load這麼大的數據,而且如果你學過計算機組成原理你也知道,磁盤IO跟內存打交道的單位是4K,一次可能讀取4K的數據,可能有時候有一些局部讀取的原理可能會取幾十K(4的整數倍),取個16K,24K也是可以的 。但是一次交互取這麼大是搞不定的,這是計算機組成原理定的,一次磁盤IO取那麼多數據,對內存也是非常的浪費,而且這一次磁盤IO也是非常慢的。所以這個節點的大小設置要合適,不能太大也不能太小,mysql對這個節點大小設置的是16K,用下面這個SQL就是可以查到 show clobal status like ‘Innodb_page_size’。

爲啥設置16K?爲什麼不是更大的如16M呢,16K已經足夠用了。等會兒會具體講。
MySQL索引選擇的不是原生的B-Tree,而是對他進行了改造,得到的是一種叫做B+Tree的數據結構
B+Tree(B-Tree變種)

非葉子節點不存儲data,只存儲索引(冗餘),可以放更多的索引
葉子節點包含所有索引字段
葉子節點用指針連接,提高區間訪問的性能
(實際上葉子節點間的指針是雙向的,圖有問題)

和B-Tree有啥區別?
非葉子節點沒有數據,數據都挪到葉子節點,葉子節點之間還有指針,非葉子節點之間跟原來一樣沒有指針。
爲啥data元素挪到葉子節點?
非葉子節點只存儲索引元素,葉子節點存儲了一份完整表的所有行的索引字段,data元素是每個索引元素對應要查找的行記錄的位置或行數據,這樣非葉子節點的每個節點就可以存儲更多的索引元素(等會會有一個大致的估算)。實際上非葉子節點存儲的是一些冗餘索引,看一下上圖,15/20/49,選擇的是整張表的哪些數據作爲索引?選擇的是處於中間位置的,因爲它要用到B+Tree一些比大小去查找,B+Tree本質可以叫做多叉平衡樹,單看B+Tree的某一小塊他還是一個二叉樹。

還有一個特點,某一個節點的元素處於一個遞增的順序,會提取葉子節點的一些處於中間位置的數據作爲冗餘索引,查找的時候從根節點開始查找,先把根節點加載到內存裏去,然後在內存裏去比對。

比如要查找索引爲30的數據,先在根節點跟15去比較,大於15,然後小於56,然後從他倆中間的指針查找下一個節點把它load到內存,再在內存裏去比對,大於15,大於20,然後小於49,就根據20和49之間的指針找到下一個節點,然後load到內存,去比對,不等於20下一個30,相等,OK了。
爲什麼把中間的元素提取出來做冗餘元素,爲的是查找效率更高。
回到剛剛的問題,爲啥要搞這些冗餘索引,而且把這些冗餘索引的data元素搞到葉子節點?也就是說B+Tree相對於B-Tree來說我的非葉子節點是不存儲data元素的,葉子節點才存儲data元素?
你想一下,一個節點不能太大也不能太小,就是16K,把data元素挪走以後,是不是這個節點就能存更多的冗餘索引了,意味着分叉就更多了,意味着葉子節點就能存儲更多的數據了。
mysql爲什麼把節點大小設置爲16K,而不是更大?
假設索引字段類型是Bigint,8bit,每兩個元素之間存的是下一個節點的地址,mysql分配的是6bit,也就是說一個索引後面配對一個節點地址,成對出現,可以算一下16K的節點可以存多少對也就是多少個索引,8b+6b=14b,16K /14b=1170個索引,葉子節點有索引有data元素,假設佔1K,那一個節點就放16K/1K=16個元素,假設樹高是3,所有節點都放滿,能放多少數據?可以算一下,1170117016=21902400,2千多萬,mysql設置16K的大小,數據就可以存2千多萬就已經足夠了吧,既能保證一次磁盤IO不要Load太多的數據 又能保證一次load的性能,即便表的數據在幾千萬的數量也能保證樹的高度在一個可控的範圍。
可以看一下幾千萬的數據表是不是加了索引幾十毫秒幾百毫秒就出結果了,所以就解釋了幾千萬的表精確的使用索引後他的性能依舊比較高。
樹的高度只有3的情況下就能存儲2千多萬的數據,即便某一個索引在葉子節點,那也就2、3次磁盤IO就能查找到,當然很快了。而且mysql底層的索引他的根節點,是常駐內存的,直接就放到內存的,查找葉子節點,一個2千萬的數據放到B+Tree上面,要查找葉子節點,就只需要2次磁盤IO就搞定了,在內存裏比對的時間基本可以忽略。
MySQL是如何存儲索引和數據的
剛纔講的原理性的比較多,現在結合具體的mysql的表不同的索引來看一下它底層到底是如何運用B+Tree來維護索引的。
索引和數據存放位置是哪?
首先問下mysql的表、數據、索引是放到那裏的?
磁盤=》默認是安裝目錄的data文件裏(不同版本可能有所不同),每個數據庫對應data文件夾裏的一個文件夾

我們打開一個walking_mybatis數據庫看一下有一個user表,再打開對應的文件夾看一下,裏面的文件名和表名有關係,然後有不同的後綴,這裏面的不同的放法和mysql的存儲引擎有關,和你選擇的哪種存儲引擎有關。

存儲引擎是修飾什麼的?
大家都知道,mysql常見的存儲引擎有InnoDB存儲引擎,MYISAM存儲引擎,那存儲引擎是形容mysql數據庫的還是某一張表的?
是表,儘管數據庫級別也有存儲引擎選項,但最終還是以表的存儲引擎爲主的。
如果你用Navicat工具去建表,也許你最多就用了“字段”這一欄去增加字段,你可以點一下“選項”看一下,可以選擇存儲引擎。

我這邊又新建一個order表,然後選擇爲MYISAM存儲引擎

在表上右鍵選擇『對象信息』->『DDL』查看

看一下user表的

索引和數據文件
再來看一下這個數據庫文件夾下這倆表的數據文件。

我們會發現,user表(InnoDB存儲引擎)對應兩個文件,order表(MYISAM存儲引擎)對應3個文件。其中.frm文件是存儲的是表結構,兩個存儲引擎都一樣,而InnoDB的.ibd文件是索引+數據,MYISAM的.MYI(I:index)和.MYD(D:data)文件分別是索引字段的索引結構和數據文件,也就是說MYISAM存儲引擎的索引和數據是分開的,而InnoDB存儲引擎的數據和索引是在一個文件裏的。
InnoDB和MYISAM的一些不同
MYISAM存儲引擎
MYISAM索引實現(非聚集)

索引文件和數據文件是分離的(非聚集)

數據、行記錄是存儲在MYD文件,假如col1是索引字段那麼這一列是存儲在MYI文件裏以B+Tree的結構來組織的,然後他的葉子節點的data部分存儲的是索引所在行記錄的磁盤文件地址,根據磁盤文件地址指針就可以從MYD文件裏快速的找到我們的這一行記錄。
查找過程
所以MYISAM這個存儲引擎他的查找的一個大致過程就是,先看條件字段有沒有用到索引,是索引字段就先去到索引文件去查找這個索引所在的那一行的磁盤文件地址,就藉助B+Tree的特點從根節點順藤摸瓜找到磁盤文件地址指針,然後從MYD文件一次性定位到所找的數據,也就是說MYISAM會垮兩個文件。

InnoDB存儲引擎
InnoDB索引實現(聚集)

表數據文件本身就是按B+Tree組織的一個索引結構文件
聚集索引-葉子節點包含了完整的數據記錄
爲什麼InnoDB表必須有主鍵,並且推薦使用整型的自增主鍵?
爲什麼非主鍵索引結構葉子節點存儲的是主鍵值?(一致性和節省存儲空間)

用的最多的InnoDB存儲引擎是什麼樣子的呢?我們可以看到,它只有兩個文件。.frm文件和MYISAM一樣,都是表結構文件,.ibd文件就是MYISAM的MYI和MYD文件的合併,索引文件和數據文件都存儲到一個文件。
InnoDB存儲引擎索引存儲結構大概是下圖這樣的,它也是一個B+Tree,但是它的葉子節點和MYISAM有點區別,它存儲的是索引所在行的所有字段。

這個好處是是什麼?不用回表(如果把垮文件查找理解爲回表)了,性能應該比MYISAM高,你看MYISAM查找到索引所在行記錄的磁盤地址後還要回MYD文件讀取一次。
聚集索引/非聚集索引
聚集索引/聚簇索引,葉子節點包含了完整的數據記錄,InnoDB的主鍵索引就是一個聚集索引,他的索引和數據是分開的在兩個文件,MYISAM的是非聚集索引,索引和數據是分開存儲的。InnoDB的主鍵索引我們叫做聚集索引。
爲什麼InnoDB表必須有主鍵,並且推薦使用整型的自增主鍵?
我們看一下這個問題爲什麼InnoDB表必須有主鍵,並且推薦使用整型的自增主鍵?
爲甚InnoDB表建議要有自增的主鍵,儘量建主鍵,建整形自增的?其實很簡單,設計如此,mysql設計的就是innoDB把你的數據和主鍵索引用B+Tree來組織的,沒有主鍵他的數據就沒有一個結構來存儲。
建innoDB表的時候沒有建主鍵,表也能建成功,爲什麼?
不建主鍵不代表沒有主鍵,沒有建主鍵innodb會幫你選一個字段,一個可以標識唯一的字段,選爲默認字段,如果這個字段唯一的話,不重複,可以建唯一索引的話,就會作爲類似於唯一索引,用這個字段來作爲唯一索引來維護整個表的數據。如果沒有,mysql會生成一個唯一的列,類似於rowid,只不過你看不到,他會用生成的這個唯一列,維護B+Tree的結構,查數據的時候還是用B+Tree的結構去查找。
爲什麼推薦整形呢?
我們想象一下查找過程,是把節點load到內存然後在內存裏去比較大小,也就是在查找的過程中要不斷的去進行數據的比對。假設UUID,既不自增也不是整形。問一下,是整形的1<2比較的效率高還是字符串的“abc”和“abe”比較的效率高呢?顯然是前者,因爲字符串的比較是轉換成ASCII碼一位一位的比,如果最後一位不一樣,比到最後才比較出大小,就比整形比較慢多了,存儲空間來說,整形更小。索引越節約資源越好。
爲什麼是自增的呢?
我們可以看一下B-Tree的葉子節點之間是沒有指針的,B+Tree優化後增加了葉子節點之間的指針,如果我們遍歷數據,從當前節點遍歷完之後,就可以根據節點間的指針快速找到下一個節點去遍歷。講到這,穿插一下B+Tree爲什麼要比B-Tree多一個節點間指針呢?那就講一下索引的另一種數據結構就是hash。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章