1. 程式人生 > >MySQL 入門(2):索引

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》): ![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200505163046571-1511445757.png) 可以看出,這是一顆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 最左字首 ![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200505163058261-1073782284.png) 以上面這張圖為例: 如果我們需要找一個15歲的法外狂徒(誤)張三: ``` select * from user where name = "張三" and age = 15; ``` MySQL會先從第一個條件開始查詢,找到名為“張三”的資料,此時會繼續判斷第二個條件,年齡為15歲,大於第一個指標中的10歲,且小於第二個資料中的20歲,所以會查詢大於10歲且小於20歲的"張三"。 假設我們沒有設定多個索引,只用名字來作為索引,那麼此時的查詢過程就是從8歲的張三開始,不斷的向後遍歷,直到找到這個15歲的張三。 也就是說,設定了多個索引,可以幫助MySQL更好的進行**剪枝**,更快速的定位到需要的資料。 只要滿足**最左字首**,就可以利用索引來加速檢索。這個最左字首可以是聯合索引的最左N個欄位,也可以是字串索引的最左M個字元。 所以,索引的**複用能力**是我們在建立聯合索引時候的一個評估標準。因為可以支援最左字首,所以當已經有了(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**。在更新完畢之後,才把查詢結果返回。 這個過程對於普通索引來說是提升的非常大的。 因為`merge`的時候是真正進行資料更新的時刻,而`change buffer`的主要目的就是將記錄的變更動作**快取**下來,所以在一個數據頁做`merge`之前,`change buffer`記錄的變更越多(也就是這個頁面上要更新的次數越多),收益就越大。 但是對於唯一索引來說,因為唯一索引的約束是“資料唯一”。所以還是需要找到這個資料頁,判斷有無衝突,才會進行插入。這樣的話,`change buffer`不起作用。 然後我們來把`change buffer`與之前提到的`redo log`聯絡在一起。 比如我們需要插入兩條資料,其中一條資料所在的資料頁在記憶體中,另外一條資料所在的資料頁在磁碟中(還未讀入記憶體),且這兩條資料所用到的索引是普通索引(不需要驗證是否重複)。 此時,對於在資料頁在記憶體中的插入操作,直接修改記憶體,對於資料頁不在記憶體中的插入操作,將這個插入操作記錄在`change buffer`中。隨後,將這兩次的操作,記錄在了`redo log`中,然後增加`binlog`。當這兩個日誌檔案都寫好後,返回,操作結束。 ![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200505163116164-1670534587.png) 而對於何時將記憶體中的髒頁刷回磁碟,是另外的一個操作。 此外,這裡的`change buffer`也同樣可以被持久化,也遵循`checkpoint`機制,即`change buffer`會標記哪些記錄是已經`merge`到資料頁中,哪些還沒有。 在`MySQL5.5`以後,除了插入操作,更新操作和刪除操作,也支援使用`change buffer`。 ## 寫到最後 首先,謝謝你能看到這裡! 關於MySQL索引相關的內容,大概就是這些了。同樣的,也在這篇文章中挖了很多坑沒有填上。限於篇幅以及文章的連貫性,沒有詳細介紹。但是會在後面的文章中提到的。 如果在這篇文章中,有什麼是我沒有解釋清楚的,又或者是我的理解出現了錯誤,還請留言指正,謝謝啦! PS:如果有其他的問題,也可以在公眾號找到作者。並且,所有文章第一時間會在公眾號更新,歡迎來找作者玩~ ![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200505163132423-13018781