MySQL學習之——索引(普通索引、唯一索引、全文索引、索引匹配原則、索引命中等)

在上一篇博客中,我們主要探討了關於MySQL鎖的一些問題。這一次,我們主要來聊聊,MySQL中的索引。

MySQL是目前絕大多數互聯網公司使用的關係型數據庫,它性能出色、資源豐富、成本低廉,是快速搭建互聯網應用的首選關係型數據庫。但是,俗話說,“好馬配好鞍”,僅僅會使用MySQL是不夠的,對MySQL在不同場景下使用性能的最小化使用代價,是一個重要的課題。一般,在互聯網公司的大部分業務中,讀寫的比例大約是10:1,也就是說,查詢的場景往往比更新或寫入的場景多得多,那麼問題來了,如何優化查詢呢?


慢查詢

<span style="font-size:14px;">SELECT 
COUNT(*) AS count 
FROM trade_base AS a
WHERE 
a.trade_status = 7 
AND a.create_time 
BETWEEN '2015-09-01' AND '2016-01-14' 
AND a.booking_source = '2'</span>

在公司的慢查詢日誌中,開發人員找到了這樣一條SQL語句。對於一般的開發人員而言,優化這條SQL的方式是,在SQL中查詢條件裏的字段上,添加索引。但是如何添加索引?索引的順序如何?索引是如何匹配命中的?一般的開發人員可能只知道大概,並沒有很深入的瞭解。

首先解釋一下,慢查詢,指的是SQL查詢的時間超過了預設的“慢查詢定義時間”。在MySQL中,使用

<span style="font-size:14px;">SHOW variables LIKE "long%"</span>

來查詢慢查詢的時間定義。


MySQL索引原理

索引的目的與原理

在日常生活中,經常有這樣的場景:有一個沒見到過的英文單詞,我們查字典找到這個單詞的意思;我們要出去旅行,查詢到具體地點的航班號;諸如此類。在這樣的場景中,我們都是通過不斷的縮小範圍來篩選出最終預期的結果,同時把隨機的事件變成順序事件:查詢字典,比如查單詞mysql,我們是按照一個字母一個字母的順序來查詢的;查詢航班號,我們也是通過地點機場航空公司一個一個來篩選縮小範圍的。我們總是通過同一種查找方式來鎖定數據。

數據庫也是一樣,但顯然要比現實生活中的場景要複雜得多,因爲不僅會有等值查詢(=),還有範圍查詢(>,<,BETWEEN,IN)、模糊查詢(LIKE)、交集查詢(AND)、並集查詢(OR)等等。數據庫應該選擇什麼樣的方式來應對所有的問題呢?我們使用查字典的例子,能不能把數據分段呢?比如,如果有1000條數據,1到100分成第一段,101到200分成第二段...這樣,如果要查詢第328條數據,只要找第三段就可以了,這樣就省去了90%的無效數據。但是,如果數據量達到100億,要分成多少段呢?學過數據結構的童鞋知道,在數據結構中,有一種數據結構是樹(Tree),樹裏面有一種樹叫二叉搜索樹(Binary Search Tree),平均複雜度是O(logN),具有不錯的查詢性能。但是在這裏,我們忽略了一個關鍵的問題,複雜度模型是基於每次相同的操作成本來考慮的,數據庫的實現比較複雜,數據保存在磁盤上,而爲了提高性能,每次又可以把部分數據讀入內存來計算,因爲我們知道——磁盤訪問的成本大概是內存訪問成本的十萬倍左右,所以簡單的搜索樹,難以滿足複雜的應用場景。

磁盤I/O與預讀

剛剛提到了磁盤訪問(別問題剛剛是誰...),那麼這裏先簡單介紹一下磁盤的I/O與預讀。磁盤讀取數據,考的是機械運動,每次讀取數據花費的時間可以分成:尋道時間、旋轉延遲、傳輸時間三個部分。尋道時間指的是磁臂移動到指定磁盤所需要的時間,主流的磁盤一般在5ms以下;旋轉延遲指的是我們經常說的磁盤轉速,比如一個磁盤7200轉,表示的就是每分鐘磁盤能轉7200次,轉換成秒也就是120次每秒,旋轉延遲就是1/120/2=4.17ms;傳輸時間指的是從磁盤讀取出數據或將數據寫入磁盤的時間,一般都在零點幾毫秒,相對於前兩個,可以忽略不計。那麼訪問一次磁盤的時間,即一次磁盤I/O的時間約等於5+4.17=9.17ms,9ms左右,聽起來還是不錯的哈,但要知道一臺500-MIPS的機器每秒可以執行5億條指令,因爲指令依靠的是電的性質,換句話說,執行一次I/O的時間可以執行40萬條指令,數據庫動輒百萬級甚至千萬級的數據,每次9ms的時間,顯然是一個災難。


上圖是計算機硬件延遲時間的對比圖。

考慮到磁盤I/O是非常高昂代價的操作,計算機系統做了一些優化,當一次I/O時,不光會把當前磁盤地址的數據讀取到內存中,而且會把相鄰的數據也讀取到內存緩衝區中,因爲局部預讀性原理告訴我們,當計算機訪問一個地址的數據的時候,與其相鄰的數據也會很快訪問到。每一次I/O讀取的數據我們稱之爲一頁(Page)。具體一頁的數據有多大,這個跟操作系統有關,一般爲4K或8K,也就是我們讀取一頁數據的時候,實際上才發生了一次I/O,這個理論對於索引的數據結構設計很有幫助。

索引的數據結構

上面講了索引的基本原理,數據庫的複雜性,以及操作系統的一些內容,目的就是讓大家瞭解到,任何一種數據結構都不是憑空產生的,一定有它的背景和使用場景。那麼,我們需要這些數據結構能夠做什麼呢?其實很簡單,就是:每次查找數據的時候,把磁盤I/O次數限制在一個很小的數量級,最好是一個常量數量級。那麼我們就想到,如果一個高度可控的多路搜索樹,是否能夠滿足需求呢?在這樣的背景下,B+樹應運而生。


詳解B+樹

如上圖,是一棵B+樹。B+樹的定義,童鞋可以自行百度,我們只說一些重點。圖中淺藍色的塊,我們稱之爲一個磁盤,可以看到,每個磁盤塊包含幾個數據項(深藍色)和指針(黃色)。如:磁盤塊1包含數據17和數據35,包含指針P1,P2,P3,P1指向數據小於17的磁盤塊,P2指向數據在17到35之間的數據所在磁盤塊,P3指向數據大於35的數據所在的磁盤塊。真實數據存在於葉子節點,即3,5,9,10,13,15,28,29,36,60,75,79,90,99 。 非葉子節點不存儲真實數據,只存儲指引搜索方向的數據項,如17、35並不真實存在於數據表中。


B+樹的查找過程

還是使用上面的B+樹。假設,我們要查找數據項29,那麼我們首先會把磁盤塊1由磁盤加載到內存中,此時進行了一次I/O,在內存中用二分查找確定29在17和35之間,鎖定磁盤塊1的P2指針,內存計算時間由於非常短(對比於I/O)可以忽略不計,通過磁盤塊1的P2指針的磁盤地址指向磁盤塊3,由磁盤加載到內存,此時進行了第二次I/O,29在26和30之間,鎖定磁盤塊3的P2指針,通過指針加載磁盤塊8到內存,此時進行了第三次I/O,同時內存中計算二分查找找到29,查詢結束。這一過程,一共進行了3次I/O。在真實使用場景中,三層的B+樹可以表示上百萬的數據,如果上百萬的數據查詢只需要三次I/O,性能提高將會是巨大的。B+樹就是一種索引數據結構,如果沒有這樣的索引,每個數據項發生一次I/O,那麼成本將會大大提升。


B+樹的性質

在上面的查找例子中,我們可以分析出一些B+樹的性質:

1,I/O的次數取決於B+樹的高度H,假設當前數據表的數據爲N,每個磁盤塊的數據項的數量是M,則有:H=log(M+1)N,當數據量N一定的情況下,M越大,H越小;而M=磁盤塊大小/數據項大小,磁盤塊大小也就是一個數據頁的大小,是固定的,如果數據項佔的空間越小,數據項的數量越多,樹的高度也就越低。這也就是爲什麼每個數據項,即索引字段要儘量的小,比如int佔4個字節,要比bigint的8個字節小一半。這也是爲什麼B+樹要求把真實數據放在葉子節點內而不是內層節點內,一旦放到內層節點內,磁盤塊的數據項會大幅度的下降,導致樹層級的增高。當數據項爲1時,B+樹會退化成線性表。

2,B+樹的數據項是複合性數據結構,比如(name,age,gender)的時候,B+樹是按照從左到右的順序來建立搜索樹的,比如當(小張,22,女)這樣的數據來檢索的時候,B+樹會優先比較name來確定下一步的搜索方向,如果name相同再依次比較age和gender,最後得到檢索的數據。但是,當(22,女)這樣沒有name的數據來的時候,B+樹就不知道下一步該查哪個節點,因爲建立搜索樹的時候,name就是第一個比較因子,必須根據name來搜索才知道下一步去哪裏查詢。比如,當(小張,男)這樣的數據來檢索時,B+樹就可以根據name來指定搜索方向,但下一字段age缺失,所以只能把名字是“小張”的所有數據都找到,然後再匹配性別是“男”的數據了。這個是非常重要的一條性質,即索引的最左匹配特性。


索引的類型

在MySQL中,索引分爲兩大類:聚簇索引和非聚簇索引。聚簇索引是按照數據存放的物理位置爲順序的,而非聚簇索引則不同;聚簇索引能夠提高多行檢索的速度,而非聚簇索引則對單行的檢索速度很快。

在這兩大類的索引類型下,還可以將索引分成四個小類:

1,普通索引:最基本的索引,沒有任何限制,是我們大多數情況下使用到的索引。

2,唯一索引:與普通索引類型,不同的是唯一索引的列值必須唯一,但允許爲空值。

3,全文索引:全文索引(FULLTEXT)僅可以適用於MyISAM引擎的數據表;作用於CHAR、VARCHAR、TEXT數據類型的列。

4,組合索引:將幾個列作爲一條索引進行檢索,使用最左匹配原則。


建立索引的原則

我們回頭來看一開始提到的慢查詢,當我們瞭解完索引原理之後,對慢查詢的優化應該有一些想法,這裏我們先總結一下建立索引的一些原則:

1,最左前綴匹配原則。這是非常重要、非常重要、非常重要(重要的事情說三遍)的原則,MySQL會一直向右匹配直到遇到範圍查詢(>,<,BETWEEN,LIKE)就停止匹配,比如: a = 1 AND b = 2 AND c > 3 AND d = 4,如果建立 (a,b,c,d)順序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引,則都可以用到,a,b,d的順序可以任意調整。

2,等於(=)和in 可以亂序。比如,a = 1 AND b = 2 AND c = 3 建立(a,b,c)索引可以任意順序,MySQL的查詢優化器會幫你優化成索引可以識別的模式。

3,儘量選擇區分度高的列作爲索引,區分度的公式是 COUNT(DISTINCT col) / COUNT(*)。表示字段不重複的比率,比率越大我們掃描的記錄數就越少,唯一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前區分度是0。可能有人會問,這個比率有什麼經驗麼?使用場景不同,這個值也很難確定,一般需要JOIN的字段我們要求在0.1以上,即平均1條掃描10條記錄。

4,索引列不能參與計算,儘量保持列“乾淨”。比如,FROM_UNIXTIME(create_time) = '2016-06-06' 就不能使用索引,原因很簡單,B+樹中存儲的都是數據表中的字段值,但是進行檢索時,需要把所有元素都應用函數才能比較,顯然這樣的代價太大。所以語句要寫成 : create_time = UNIX_TIMESTAMP('2016-06-06')。

5,儘可能的擴展索引,不要新建立索引。比如表中已經有了a的索引,現在要加(a,b)的索引,那麼只需要修改原來的索引即可。

6,單個多列組合索引和多個單列索引的檢索查詢效果不同,因爲在執行SQL時,MySQL只能使用一個索引,會從多個單列索引中選擇一個限制最爲嚴格的索引。

根據上面這些原則,我們來修改開篇的慢查詢:

SELECT 
count(*) AS count 
FROM trade_bASe AS a
WHERE 
a.trade_status = 7 
AND a.create_time BETWEEN '2015-09-01' AND '2016-01-14' 
AND a.booking_source = '2'
根據這條SQL,應該建立的索引是:trade_status, booking_source,create_time的聯合索引;其中,trade_status、booking_source的順序可以顛倒,而且 create_time 的區間查詢放到後面。這就是利用了索引的最左匹配原則。

慢查詢的優化步驟

1,查看運行效果,是否真的很慢,主要設置SQL_NO_CACHE。

2,WHERE條件單表查詢,鎖定最小返回記錄表。這句話的意思是,把查詢語句的WHERE都應用到表中返回的記錄數最小的表開始查起,單表每個字段分別查詢,看哪個字段的區分度最高

3,EXPLAIN查看執行計劃,是否與1預期一致(從鎖定記錄較少的表開始查詢)

4,ORDER BY LIMIT 形式的SQL語句,讓排序的表優先查

5,多去了解業務的使用場景

6,加索引時,要參照建立索引的幾大原則

7,觀察結果,不符合預期,則重新從1開始分析。


索引的優化方法

1,何時使用聚簇索引或非聚簇索引:

使用動作描述 使用聚簇索引 使用非聚簇索引
列經常被分組排序
返回某範圍內的數據
×
一個或極少不同的值 ×
×
小數目不同的值
×
大數目不同的值 ×

頻繁更新的列 ×

外鍵列

主鍵列

頻繁修改索引列 ×

2,索引不會包含有NULL值的列:只要列中包含有NULL值,都將不會被包含在索引中,組合索引中只要有一列有NULL值,那麼這一列對於此條組合索引就是無效的。所以我們在數據庫設計時,不要讓索引字段的默認值爲NULL。

3,使用短索引:假設,如果有一個數據類型爲CHAR(255)的列,在前10個或20個字符內,絕大部分數據的值是唯一的,那麼就不要對整個列進行索引。短索引不僅可以提高查詢速度而且可以節省I/O操作。

4,索引列排序:MySQL查詢只使用一個索引,因此如果WHERE子句中已經使用了索引的話,那麼ORDER BY中的列是不會使用索引的。因此數據庫默認排序可以符合要求的情況下,不要使用排序操作;儘量不要包含多個列的排序,如果需要,最好給這些列也創建組合索引。

5,LIKE語句操作:一般情況下,不建議使用LIKE操作;如果非使用不可,如何使用也是一個研究的課題。LIKE "%aaaaa%"不會使用索引,但是LIKE "aaa%"卻可以使用索引。

6,不要在索引列上進行運算:在建立索引的原則中,提到了索引列不能進行運算,這裏就不再贅述了。


最後,總結一下,其實任何數據庫層面的優化,都抵不上應用系統的優化,同樣是MySQL,Facebook/Google等等都可以支撐,所以且行且珍惜吧!

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