1. 程式人生 > 資料庫 >MySQL索引前世今生

MySQL索引前世今生

“騰訊雲資料庫負責人林曉斌說過:“我們面試 MySQL 同事時只考察兩點,索引和鎖”。

言簡意賅,MySQL 索引的重要性不言而喻。MySQL 索引歷經了多個版本的迭代,從語法到底層資料結構都有很多改變。

 

MySQL 索引,我們真的瞭解麼?好了,今天我們一起來看看 MySQL 索引的前世今生,一起聊聊索引的那些事兒。

什麼是索引?

在關係資料庫中,索引是一種單獨的、物理的對資料庫表中一列或多列的值進行排序的一種儲存結構,它是某個表中一列或若干列值的集合和相應的指向表中物理標識這些值的資料頁的邏輯指標清單。

索引的作用相當於圖書的目錄,可以根據目錄中的頁碼快速找到所需的內容。

當表中有大量記錄時,若要對錶進行查詢:

  • 第一種搜尋資訊方式是全表搜尋,是將所有記錄一一取出,和查詢條件進行一一對比,然後返回滿足條件的記錄,這樣做會消耗大量資料庫系統時間,並造成大量磁碟 I/O 操作。

  • 第二種就是在表中建立索引,然後在索引中找到符合查詢條件的索引值,最後通過儲存在索引中的 ROWID(相當於頁碼)快速找到表中對應的記錄。

 

MySQL 5.5 以後 InnoDB 儲引擎使用的索引資料結構主要用:B+Tree;本篇文章帶大家以 B+Tree 前世今生為主線來聊一聊。

Mark:B+Tree 可以對 <,<=,=,>,>=,BETWEEN,IN,以及不以萬用字元開始的 LIKE 使用索引。(MySQL 5.5 後)

這些事實或許會顛覆你的一些認知,比如在你讀過的其他文章或書中。以上這些都屬於“範圍查詢”,都是不走索引的!

 

沒錯,早在 5.5 以前,優化器是不會選擇通過索引搜尋的,優化器認為這樣取出的行多與全表掃描的行,因為還要回表查一次嘛,可能會涉及 I/O 的行數更多,被優化器放棄。

經過演算法(B+Tree)優化後,支援對部分範圍型別的掃描(得利於 B+Tree 資料結構的有序性)。

該做法同時也違反了最左字首原則,導致範圍查詢後的條件無法用到聯合索引,我們在後面詳細說明。

 

索引的優缺點

 

索引的優點如下:

  • 索引大大減小了伺服器需要掃描的資料量。

  • 索引可以幫助伺服器避免排序和臨時表。

  • 索引可以將隨機 I/O 變成順序 I/O。

索引的缺點如下:

  • 雖然索引大大提高了查詢速度,同時卻會降低更新表的速度,如對錶進行 INSERT、UPDATE 和 DELETE。因為更新表時,MySQL 不僅要儲存資料,還要儲存索引檔案。

  • 建立索引會佔用磁碟空間的索引檔案。一般情況這個問題不算嚴重,但如果你在一個大表上建立了多種組合索引,且伴隨大量資料量插入,索引檔案大小也會快速膨脹。

  • 如果某個資料列包含許多重複的內容,為它建立索引就沒有太大的實際效果。

  • 對於非常小的表,大部分情況下簡單的全表掃描更高效。

 

因此應該只為最經常查詢和最經常排序的資料列建立索引。(MySQL 同一個資料表裡的索引總數限制為 16 個)

 

資料庫存在的意義之一就是是解決資料儲存和快速查詢的。那麼資料庫的資料存在哪?沒錯,是磁碟,磁碟的優點是啥?便宜!缺點呢?相比記憶體訪問速度慢。

 

那麼你知道 MySQL 索引主要使用的資料結構麼?B+樹!你脫口而出。

 

那 B+樹是什麼樣的資料結構?MySQL 索引又是為什麼選擇了 B+樹呢?

 

其實最終選用 B+樹是經歷了漫長的演化:

二叉排序樹 → 二叉平衡樹 → B-Tree(B樹) → B+Tree(B+樹)

有小夥伴問我“B 樹跟 B-樹有什麼區別”?這裡普及一下,MySQL 資料結構只有B-Tree(B 樹)和 B+Tree(B+樹),多隻是讀法不同罷了,“B-Tree” 一般統稱為 B 樹,你叫他 B-樹也行!

還有小夥伴提到的紅黑樹,是程式語言中的儲存結構,不是 MySQL 的;如 Java 的 HashMap 就是用的連結串列加紅黑樹。

好了,今天就帶著大家一起看一下演化成 B+樹的過程吧。

 

B+Tree 索引的前世今生

 

①二叉排序樹

 

理解 B+樹之前,簡單說一下二叉排序樹,對於一個節點,它的左子樹的孩子節點值都要小於它本身,它的右子樹的孩子節點值都要大於它本身。

 

如果所有節點都滿足這個條件,那麼它就是二叉排序樹。(此處可以串一下二分查詢的知識點)

上圖是一顆二叉排序樹,你可以嘗試利用它的特點,體驗查詢 9 的過程:

  • 9 比 10 小,去它的左子樹(節點 3)查詢。

  • 9 比 3 大,去節點 3 的右子樹(節點 4)查詢。

  • 9 比 4 大,去節點 4 的右子樹(節點 9)查詢。

  • 節點 9 與 9 相等,查詢成功。

 

一共比較了 4 次,那你有沒有想過上述結構的優化方式?

 

②AVL 樹(自平衡二叉查詢樹)

 

上圖是 AVL 樹,節點個數和值均和二叉排序樹一摸一樣。

 

再來看一下查詢 9 的過程:

  • 9 比 4 大,去它的右子樹查詢。

  • 9 比 10 小,去它的左子樹查詢。

  • 節點 9 與 9 相等,查詢成功。

 

一共比較了 3 次,同樣的資料量比二叉排序樹少了一次,為什麼呢?因為 AVL 樹高度要比二叉排序樹小,高度越高意味著比較的次數越多;不要小看優化的這一次,假如是 200w 條資料,比較次數會明顯地不同。

 

你可以想象一下一棵 100 萬節點的平衡二叉樹,樹高 20。一次查詢可能需要訪問 20 個數據塊。在機械硬碟時代,從磁碟隨機讀一個數據塊需要 10 ms 左右的定址時間。

 

也就是說,對於一個 100 萬行的表,如果使用二叉樹來儲存,單獨訪問一個行可能需要 20 個 10 ms 的時間,這個查詢可真夠慢的!

 

③B 樹(Balanced Tree)多路平衡查詢樹,多叉的

 

B 樹是一種多路自平衡搜尋樹,它類似普通的二叉樹,但是 B 樹允許每個節點有更多的子節點。

 

B 樹示意圖如下:

B 樹的特點如下:

  • 所有鍵值分佈在整個樹中。

  • 任何關鍵字出現且只出現在一個節點中。

  • 搜尋有可能在非葉子節點結束。

  • 在關鍵字全集內做一次查詢,效能逼近二分查詢演算法。

 

為了提升效率,要儘量減少磁碟 I/O 的次數。實際過程中,磁碟並不是每次嚴格按需讀取,而是每次都會預讀。

 

磁碟讀取完需要的資料後,會按順序再多讀一部分資料到記憶體中,這樣做的理論依據是電腦科學中註明的區域性性原理:

  • 由於磁碟順序讀取的效率很高(不需要定址時間,只需很少的旋轉時間),因此對於具有區域性性的程式來說,預讀可以提高 I/O 效率。預讀的長度一般為頁(page)的整倍數。

  • MySQL(預設使用 InnoDB 引擎),將記錄按照頁的方式進行管理,每頁大小預設為 16K(可以修改)。

 

B-Tree 藉助計算機磁碟預讀機制:每次新建節點的時候,都是申請一個頁的空間,所以每查詢一個節點只需要一次 I/O;因為實際應用當中,節點深度會很少,所以查詢效率很高。

 

那麼最終版的 B+樹是如何做的呢?

 

④B+Tree (B+樹是 B 樹的變體,也是一種多路搜尋樹)

 

從圖中也可以看到,B+樹與 B 樹的不同在於:

  • 所有關鍵字儲存在葉子節點,非葉子節點不儲存真正的 data,從而可以快速定位到葉子節點。

  • 為所有葉子節點增加了一個鏈指標,意味著所有的值都是按順序儲存的,並且每一個葉子頁到根的距離相同,很適合查詢範圍資料。

 

因此,B+Tree 可以對 <,<=,=,>,>=,BETWEEN,IN,以及不以萬用字元開始的 LIKE 使用索引。

 

B+ 樹的優點,比較的次數均衡,減少了 I/O 次數,提高了查詢速度,查詢也更穩定:

  • B+樹的磁碟讀寫代價更低。

  • B+樹的查詢效率更加穩定。

 

要知道的是,你每次建立表,系統會為你自動建立一個基於 ID 的聚集索引(上述 B+樹),儲存全部資料。

 

你每次增加索引,資料庫就會為你建立一個附加索引(上述 B+樹),索引選取的欄位個數就是每個節點儲存資料索引的個數,注意該索引並不儲存全部資料。

 

為什麼 MySQL 索引選擇了 B+樹而不是 B 樹?

 

原因有如下兩點:

  • B+樹更適合外部儲存(一般指磁碟儲存),由於內節點(非葉子節點)不儲存 data,所以一個節點可以儲存更多的內節點,每個節點能索引的範圍更大更精確。也就是說使用 B+樹單次磁碟 I/O 的資訊量相比較 B 樹更大,I/O 效率更高。

  • MySQL 是關係型資料庫,經常會按照區間來訪問某個索引列,B+樹的葉子節點間按順序建立了鏈指標,加強了區間訪問性,所以 B+樹對索引列上的區間範圍查詢很友好。而 B 樹每個節點的 key 和 data 在一起,無法進行區間查詢。

 

程式設計師,你應該知道的索引知識點

 

①回表查詢

 

比如你建立了 name, age 索引 name_age_index,查詢資料時使用了:

select * from table where name ='陳哈哈' and age = 26;

由於附加索引中只有 name 和 age,因此命中索引後,資料庫還必須回去聚集索引中查詢其他資料,這就是回表,這也是你背的那條:少用 select * 的原因。

 

②索引覆蓋

 

結合回表會更好理解,比如上述 name_age_index 索引,有查詢:

select name, age from table where name ='陳哈哈' and age = 26;

此時 select 的欄位 name,age 在索引 name_age_index 中都能獲取到,所以不需要回表,滿足索引覆蓋,直接返回索引中的資料,效率高。是 DBA 同學優化時的首選優化方式。

 

③最左字首原則

 

B+樹的節點儲存索引順序是從左向右儲存,在匹配的時候自然也要滿足從左向右匹配。

 

通常我們在建立聯合索引的時候,也就是對多個欄位建立索引,相信建立過索引的同學們會發現,無論是 Oracle 還是 MySQL 都會讓我們選擇索引的順序。

 

比如我們想在 a,b,c 三個欄位上建立一個聯合索引,我們可以選擇自己想要的優先順序,a、b、c,或者是 b、a、c 或者是 c、a、b 等順序。

 

為什麼資料庫會讓我們選擇欄位的順序呢?不都是三個欄位的聯合索引麼?這裡就引出了資料庫索引的最左字首原理。

 

在我們開發中經常會遇到明明這個欄位建了聯合索引,但是 SQL 查詢該欄位時卻不會使用索引的問題。

 

比如索引 abc_index:(a,b,c)是 a,b,c 三個欄位的聯合索引,下列 sql 執行時都無法命中索引 abc_index 的。

select * from table where c = '1';
select * from table where b ='1' and c ='2';

 

以下三種情況卻會走索引:

select * from table where a = '1';
select * from table where a = '1' and b = '2';
select * from table where a = '1' and b = '2'  and c='3';

 

從上面兩個例子大家是否可以看出點眉目?

 

是的,索引 abc_index:(a,b,c),只會在(a)、(a,b)、(a,b,c)三種類型的查詢中使用。

 

其實這裡說的有一點歧義,其實(a,c)也會走,但是隻走 a 欄位索引,不會走 c 欄位。

 

另外還有一個特殊情況說明下,下面這種型別的也只會有 a 與 b 走索引,c 不會走。

select * from table where a = '1' and b > '2'  and c='3';

 

像上面這種型別的 sql 語句,在 a、b 走完索引後,c 已經是無序了,所以 c 就沒法走索引,優化器會認為還不如全表掃描 c 欄位來的快。

 

最左字首:顧名思義,就是最左優先,上例中我們建立了 a_b_c 多列索引,相當於建立了(a)單列索引,(a,b)組合索引以及(a,b,c)組合索引。

 

因此,在建立多列索引時,要根據業務需求,where 子句中使用最頻繁的一列放在最左邊。

 

④索引下推優化

 

還是索引 name_age_index,有如下 sql:

select * from table where name like '陳%' and age > 26;

 

該語句有兩種執行可能:

  • 命中 name_age_index 聯合索引,查詢所有滿足 name 以"陳"開頭的資料, 然後回表查詢所有滿足的行。

  • 命中 name_age_index 聯合索引,查詢所有滿足 name 以"陳"開頭的資料,然後順便篩出 age>20 的索引,再回表查詢全行資料。

 

顯然第 2 種方式回表查詢的行數較少,I/O 次數也會減少,這就是索引下推。所以不是所有 like 都不會命中索引。

 

使用索引時的注意事項

①索引不會包含有 null 值的列

  只要列中包含有 null 值都將不會被包含在索引中,複合索引中只要有一列含有 null 值,那麼這一列對於此複合索引就是無效的。所以我們在資料庫設計時建議不要讓欄位的預設值為 null。

②使用短索引

 對串列進行索引,如果可能應該指定一個字首長度。

 例如,如果有一個 char(255)的列,如果在前 10 個或 20 個字元內,多數值是惟一的,那麼就不要對整個列進行索引。短索引不僅可以提高查詢速度而且可以節省磁碟空間和 I/O 操作。

③索引列排序

 查詢只使用一個索引,因此如果 where 子句中已經使用了索引的話,那麼 order by 中的列是不會使用索引的。

 因此資料庫預設排序可以符合要求的情況下不要使用排序操作;儘量不要包含多個列的排序,如果需要最好給這些列建立複合索引。

④like 語句操作

 一般情況下不推薦使用 like 操作,如果非使用不可,如何使用也是一個問題。like “%陳%” 不會使用索引而 like “陳%”可以使用索引。

⑤不要在列上進行運算

 這將導致索引失效而進行全表掃描,例如:

SELECT * FROM table_name WHERE YEAR(column_name)<2017;

⑥不使用 not in 和 <> 操作

 這不屬於支援的範圍查詢條件,不會使用索引。

 

我的體會

曾經,我一度以為我很懂 MySQL。

剛入職那年,我還是個孩子,記得第一個需求是做個統計介面,查詢近兩小時每隔 5 分鐘為一時間段的網站訪問量,JSONArray 中一共返回 24 個值,當時菜啊,寫了個介面迴圈二十四遍,傳送 24 條 SQL 去查(捂臉)。

由於那個介面,被技術經理嘲諷表示他寫的 SQL 比我吃的米都多。雖然我們山東人基本不吃米飯,但我還是羞愧不已。

然後經理通過呼叫一個 dateTime 函式分組查詢處理一下,就 OK 了,效率是我的幾十倍吧。

從那時起,我就定下目標,深入 MySQL 學習,萬一日後有機會嘲諷回去?

筒子們,MySQL 路漫漫,其修遠兮。永遠不要眼高手低,一起加油,希望本文能對你有所幫助。