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.

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

尤其是當我們的數據量越來越大的時候,恰當的索引是可以幫助我們擁有更優秀的性能的。

這句話的另外一層含義在於:如果索引設計的不好,可能會使得我們的數據庫性能變得更加的糟糕。

那么,索引到底是什么呢?我們接着往下看。

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:如果有其他的問題,也可以在公眾號找到作者。並且,所有文章第一時間會在公眾號更新,歡迎來找作者玩~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM