MySQL 入門(2):索引

摘要

在這篇文章中,我會先介紹一下什麼是索引,索引有什麼做用。數據庫

以後會介紹一下索引的數據結構是什麼樣的,有什麼優勢,又會帶來什麼樣的問題。緩存

在分析完數據結構後,咱們能夠根據這個數據結構,研究索引的用法,以及如何設計更高效的緩存。數據結構

最後,我會對上一篇的內容進行補充,介紹change buffer的做用以及分析change buffer對性能的影響。性能

1 目的

在咱們學習索引以前,咱們要先了解它是什麼,以及有什麼做用。學習

官方對於索引的定義是這樣的:優化

Indexes are used to find rows with specific column values quickly. Without an index, MySQL must begin with the first row and then read through the entire table to find the relevant rows.ui

也就是說,索引是用來快速查找具備特定值的一行數據(的一種數據結構)。若是沒有索引,MySQL必須得從第一行開始逐行掃描數據。設計

尤爲是當咱們的數據量愈來愈大的時候,恰當的索引是能夠幫助咱們擁有更優秀的性能的。3d

這句話的另一層含義在於:若是索引設計的很差,可能會使得咱們的數據庫性能變得更加的糟糕。指針

那麼,索引究竟是什麼呢?咱們接着往下看。

2 模型

在講索引具體的數據結構以前,咱們來想象一下咱們在英文詞典裏面找一個單詞。

若是咱們須要找一個單詞:"awesome"!

咱們會在目錄裏面找到以字母 A 開頭的一系列單詞,而後從以字母 A 開頭的一系列單詞中找到 W ,而後是 E ...

就這樣不斷的往下查找,不斷縮小咱們的查找範圍。若是咱們不適用目錄,直接在正文裏面找這個單詞,可能須要花費更多的時間。

何況,這個詞典裏面的單詞是排好序的,若是咱們找 Z 開頭的字母,可能得找好幾百頁,才能最終找到。

這個例子不能說特別的準確,可是反映了索引的核心:減小查找的次數

咱們都知道,MySQL的數據保存在了磁盤中。而磁盤的IO是最慢的。因此,減小磁盤的讀寫是提升性能必不可少的作法。雖然如今大多數計算機已經使用了SSD,再也不須要尋道等,可是索引的原則仍是成立的。

這裏咱們來看看InnoDB的B+樹是怎麼實現的(圖來自於《高性能MySQL》):

能夠看出,這是一顆N叉樹,樹中的每個結點,都是MySQL中的一個數據頁。

其實說白了這裏的N叉樹,和二叉查找樹查找邏輯是同樣的。只不過不一樣的地方在於這裏的每個結點,包含了比二叉查找樹更多的數據與指針。這樣作的目的是使得在數據量相同的狀況下,B+樹可使得樹的高度更低。

而又由於全部的數據頁都是持久化保存在磁盤中的,因此更低的高度意味着查找一個數據須要進行磁盤IO的次數越少,效率變得更高。

注意,由於N叉樹的N越大,對應的樹的高度就會越低。而每個結點(每個數據頁)的大小是固定的(默認是16K,可使用innodb_page_size參數修改),因此當設置爲索引的key越小的時候,N就會越大。

3 分類

在通過上面的介紹以後,我想你應該能理解索引的查找方法了。下面咱們再來講說索引的分類:

主鍵索引非主鍵索引

主鍵索引,就是非葉子結點中存儲的值都是主鍵的值,在查找的時候經過主鍵查找。直到查找到最後的葉子節點。在最後的葉子節點中保存了這個主鍵對應的整行數據

非主鍵索引,就是非葉子結點中存儲的值都是索引的值,查找的時候經過這一個數值進行查找。查找到最後的葉子節點,保存了對應的主鍵ID。而後,MySQL會根據查到的主鍵,再查找主鍵索引對應的B+樹,直到找到這一行的全部數據。而這個經過查找到主鍵,而後再利用主鍵來再次查找,或者這一行數據的過程,稱爲回表

注意,咱們在新建一張表的時候,必定會有一顆以主鍵爲索引的B+樹。哪怕你沒有設置主鍵,MySQL都會選一個不包含NULL的第一個惟一索引列做爲主鍵列,並把它用做一個主鍵索引。若是沒有這樣的索引就會使用行號生成一個彙集索引,把它當作主鍵。

此外,每增長一個索引,MySQL就會多維護一顆B+樹。維護B+樹的過程也是很複雜的,涉及到了頁的分裂等,我想在之後的文章進行介紹。

另外以前也提到了,影響MySQL性能的一個很重要的因素就是磁盤IO。而回表這個操做,無異於增長了不少的IO次數。

那麼有什麼辦法能夠減小這一部分的開銷嗎,咱們接着往下看。

4 聯合索引

咱們在上面提到的索引,都是單個的數據進行查找。

這樣的話,咱們每次對其中一個列創建一個索引,就得多維護一顆B+樹,一樣對性能和空間形成了浪費。

那麼咱們有沒有可能同時對多個數據進行排序,而後再進行查找呢?答案是能夠的,咱們能夠採用聯合索引。

4.1 最左前綴

以上面這張圖爲例:

咱們創建了一個(姓名,年齡)的聯合索引

若是咱們須要找一個15歲的法外狂徒(誤)張三:

select * from user where name = "張三" and age = 15;

由於此時咱們的查找條件徹底匹配了咱們定義的索引,因此MySQL會先從查找的第一個條件開始,找到名爲「張三」的數據,而後此時會繼續判斷第二個年齡爲15歲的條件,由於此時大於第一個數據項中的10歲,且小於第二個數據項中的20歲,因此會從第二個指針往下尋找,查找大於10歲且小於20歲的「張三」。

這種條件和索引徹底匹配的查找過程,稱爲全值匹配查詢

可是,假設咱們沒有設置多個查找條件,只搜索名字爲「張三」的人。

select * from user where name = "張三";

那麼此時的查找過程不會去匹配年齡這一列,只會比較姓名這一列。因此,會從這顆B+樹最左邊的結點,8歲的張三開始,不斷的向後遍歷,直到這個數據的姓名不叫「張三」爲止。

這樣的查找過程,稱爲最左前綴查找。簡單的來解釋,就是查找的條件只要是符合這個聯合索引,或者符合這個聯合索引的最左邊幾項,索引就會生效,也就實現了「剪枝」的目的,加速了查找的速度。只有剩下的那些不符合最左前綴的條件,纔會依次遍從來進行匹配。

也就是說,只要知足最左前綴,就能夠利用索引來加速檢索。這個最左前綴能夠是聯合索引的最左N個字段,也能夠是字符串索引的最左M個字符。

那麼,何時最左前綴不會生效呢?

假設有這麼一個聯合索引(a, b, c, d, e, f)。那麼查找條件是(a)、(a, b)、(a, b, c)等都是稱爲符合最左前綴的。也就是說,必定要從索引的最左邊開始,任意N個字段或者M個字符。

可是若是咱們使用了(a, c, d)這樣的查找條件,那麼只會對(a)起做用,(c, d)是不會生效的。由於最左前綴被中斷了。

而若是是(e, f)這樣的查找條件,也一樣不會生效,由於也不符合最左邊的N個字段的規則,不屬於最左前綴。

因此,索引的複用能力是咱們在創建聯合索引時候的一個評估標準。由於能夠支持最左前綴,因此當已經有了(a,b)這個聯合索引後,通常就不須要單獨在a上創建索引了。所以,第一原則是,若是經過調整順序,能夠少維護一個索引,那麼這個順序每每就是須要優先考慮採用的。

可是,聯合索引也不是越長越好。咱們在前面提到過,要儘量的讓N叉樹的N比較大,這樣樹的高度會比較低,以此來減小磁盤的IO次數。若是聯合索引包含的字段比較多,在頁面大小固定的狀況下,會形成N值的減小,反而會減慢效率。

4.2 索引下推

繼續上面的法外狂徒的例子。

假設咱們的語句是這樣的:

select * from user where name like "張%" and age = 15;

很好理解,咱們會以爲MySQL會從名字以「張」開頭的數據開始遍歷,而後判斷年齡是否爲15。

可是最左前綴有一個很是重要的原則:MySQL會一直向右匹配直到遇到範圍查詢(>、<、between、like)就中止匹配。

也就是說,此時咱們的查詢,age這個索引是用不上的。

因此,在MySQL5.6以前,只要找到了符合以「張」開頭的名字這個條件,就會經過這個數據的主鍵ID,進行回表的操做,而後查找這個數據的年齡是否爲15。

而MySQL 5.6 引入的索引下推優化(index condition pushdown), 能夠在索引遍歷過程當中,對索引中包含的字段先作判斷,直接過濾掉不知足條件的記錄,減小回表次數。也就是說,直到找到了以「張」開頭的名字而且年齡爲15,纔會進行回表。

此外,在回表以前,若是使用了Multi-Range Read (MRR)這個策略,在取出主鍵後,回表以前,會在對全部獲取到的主鍵排序。

4.3 覆蓋索引

還記得咱們前面說到的嗎,若是咱們採用的是非主鍵索引,那麼咱們查到了這個數據以後,還須要根據葉子節點中的主鍵,再回表一次。

覆蓋索引能夠解決這個問題。好比咱們前面查找「張三」的時候,咱們也能夠同時找到他的年齡。好比(a,b)這樣的聯合索引,在咱們使用

select b form table_name where a = xxx;

這麼一條語句的時候,找到了符合條件的a,不須要經過主鍵來進行回表,找到b的值,而是會直接返回記錄在這顆B+樹中的值。也就是說,在這個查詢裏面,索引(a,b)已經「覆蓋了」咱們的查詢需求,咱們稱爲覆蓋索引。

5 惟一索引與普通索引

  • 普通索引:加快對數據的訪問速度
  • 惟一索引:不容許重複的普通索引

5.1 查詢

咱們先來分析查詢方面的性能。

對於查詢來講,若是這個是普通索引,那麼在找到了符合條件的數據以後,會日後繼續遍歷,直到碰到不知足的數據爲止。

若是是惟一索引,因爲他的惟一性,只要找到了,那就直接返回就行,不須要繼續日後遍歷。

其實二者的性能差距微乎其微。

爲何呢?你可能會想:普通索引還須要繼續遍歷,有可能會更慢。可是,咱們以前提到過,查詢操做是須要把數據讀到內存的,而且是以數據頁的形式讀到內存。而在內存中的遍歷操做,速度方面的差距是特別小的。

就算普通索引的最後一項仍是相同的,須要經過磁盤IO來讀取下一頁,這個時候多是比較耗費時間的。不過由於一個數據頁包含了特別多的數據,這種可能性是特別低的。

5.2 插入

在咱們說到插入以前,我先要跟你介紹一下change buffer這個東西。

我在上一篇文章中提到:在咱們須要更新數據的時候,先把數據從磁盤讀到內存中,修改這個數據,而後修改redolog,增長binlog,等內存滿了以後或者redolog寫滿了以後,再將髒頁刷回磁盤。

那麼插入數據呢?

在咱們新增了一條數據以後,MySQL並不會將這個插入直接寫入磁盤中,而是會將這個修改寫入change buffer中。

在以後有關於這個數據頁的查詢請求的時候,纔會讀取這一個數據頁,而後根據change buffer中關於這一頁的記錄,依次更新到讀取到了內存中的數據頁中,這個過程稱爲merge。在更新完畢以後,才把查詢結果返回。

可是這樣有什麼用呢?

假設咱們插入的普通索引不在內存中,此時有兩個做用:

第一,由於咱們在插入一條數據的時候,不須要經過磁盤的IO把須要寫入的數據頁調入內存中進行修改,而是會將這個插入行爲記錄下來,在以後才統一對髒頁進行刷回磁盤的操做。也就是說,change buffer避免了每次都須要調入一個數據頁進內存中進行修改,形成髒頁過多的問題。

第二,也是最重要的,change buffer的設計避免了在每一次插入過程當中爲了尋找數據頁而進行的隨機IO。而且,在以後對髒頁進行刷新的時候,MySQL會盡量的讓髒頁能夠是以順序IO的方式刷新回磁盤中。

這個過程對於普通索引來講是提高的很是大的。

簡單的來講,change buffer的主要目的就是將記錄的變動動做緩存下來。因此在一個數據頁作merge以前,change buffer記錄的變動越多(也就是這個頁面上要更新的次數越多),收益就越大。

可是對於惟一索引來講,由於惟一索引的約束是「數據惟一」。因此仍是須要找到這個數據頁,判斷有無衝突,纔會進行插入。這樣的話,change buffer不起做用。

而後咱們來把change buffer與以前提到的redo log聯繫在一塊兒。

好比咱們須要插入兩條數據,其中一條數據所在的數據頁在內存中,另一條數據所在的數據頁在磁盤中(還未讀入內存),且這兩條數據所用到的索引是普通索引(不須要驗證是否重複)。

此時,對於在數據頁在內存中的插入操做,直接修改內存,對於數據頁不在內存中的插入操做,將這個插入操做記錄在change buffer中。隨後,將這兩次的操做,記錄在了redo log中,而後增長binlog。當這兩個日誌文件都寫好後,返回,操做結束。

而對於什麼時候將內存中的髒頁刷回磁盤,是另外的一個操做。

此外,這裏的change buffer也一樣能夠被持久化,也遵循checkpoint機制,即change buffer會標記哪些記錄是已經merge到數據頁中,哪些尚未。

MySQL5.5之後,除了插入操做,更新操做和刪除操做,也支持使用change buffer。也就是說,對於更新操做和刪除操做,也會被change buffer記錄下來,在以後才進行merge

寫到最後

首先,謝謝你能看到這裏!

關於MySQL索引相關的內容,大概就是這些了。一樣的,也在這篇文章中挖了不少坑沒有填上。限於篇幅以及文章的連貫性,沒有詳細介紹。可是會在後面的文章中提到的。

若是在這篇文章中,有什麼是我沒有解釋清楚的,又或者是個人理解出現了錯誤,還請留言指正,謝謝啦!

PS:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~

相關文章
相關標籤/搜索