MySQL學習筆記之索引

我們的數據存儲於計算機的磁盤上,以512個字節爲一個扇區,InnoDB的數據頁大小是16KB每頁,同一個應用程序的數據可能是連續存儲的,也可能是分開存儲的,如果沒有索引,MySQL要查找指定的數據,只能挨個讀取各個扇區的數據並進行比較,如果有一種方法能夠讓我們直接找到數據所在的扇區,那麼無疑會大大提高我們查找數據的效率,而這個方法就是使用索引。通過查找索引,能夠讓我們快速找到數據所在物理地址。

1.索引常用的數據結構

索引常用的數據結構有哈希表、有序數組、搜索樹三種。

哈希表是K-V形式的數據結構,通過哈希函數計算key值得到一個數組下標,通過這個下標找到相應的鏈表,再遍歷鏈表比較值。適用於等值查詢,不適用於範圍查詢。範圍查詢需要每個值都進行一遍哈希查找。

有序數組將數據按照遞增的方式存儲,適用於等值查詢和範圍查詢,等值查詢可通過二分法查找;範圍查詢可通過二分法先查到首數據,再向右遍歷找到最後一個值,哈希表需要hash計算、取出鏈表、遍歷鏈表比較大小几個步驟,而有序數組找到第一個數據之後只需要順序遍歷比較大小就可以取到需要的值,效率更高。但有序數組也有個問題,更新效率太差,只適用於靜態表。

搜索樹,查找和更新效率都屬於中間的數據結構。

2.InnoDB的索引數據結構

InnoDB採用B+樹數據模型,樹高爲4的時候就能表示17億數據,而每個節點都設計成佔用一個扇區,所以即使的最壞情況,一次查找也最多訪問3次磁盤就能查找到相關的數據頁。而B+樹每個節點的順序是遞增排列,兄弟節點之間也是遞增排列,通過上一個兄弟節點能找到下一個兄弟節點的存儲位置。

但B+樹也有一個缺點,新增數據的時候如果從中間插入,可能會引起頁分裂,而主鍵索引又存儲了所有的數據信息,一旦引起頁分裂就會產生很多額外操作,而使用自增字段做主鍵就可以避免這樣的問題。

3.索引相關概念

回表:InnoDB中主鍵索引包含了當前表的所有字段信息,比如name字段上有普通索引,在name的索引上只包含name和ID兩個字段的信息,如果select age from student where name = '小明',那麼執行過程是先去name索引查找小明,再拿到相應的數據去查找主鍵索引。這個過程就稱爲回表。

覆蓋索引:比如我們select age from student where name = '小明' ,我們爲了省略這個回表的過程,就建立(name,age)的聯合索引,而這個覆蓋了我們所有查詢信息的索引,就稱爲覆蓋索引。

最左查詢原則:索引的B+樹存儲結構當中,按照第一個字段的遞增順序排列,第一個字段相同,則按照第二個字段的遞增順序排列,以此類推。比如(name,age)聯合索引,我們查找name,或者name and age都可以使用到聯合索引(name,age),但單獨查找age就不能用到該索引。

索引下推:select * from student where name like '張%' and age = 10,對於這樣的查詢語句,(1)、先在(name,age)中查找到姓張的所有數據,(2)、對(1)中查找到的數據進行age = 10的篩選,(3)、進行回表查詢。如果沒有索引下推,判斷age的步驟就在主鍵索引中完成。

4.普通索引和唯一索引的比較

查詢過程:唯一索引在B+樹上查到數據就立刻返回,普通索引在B+樹上查到數據之後一直向後遍歷,直到找到第一個不符合條件的數據,這個過程的性能差別微乎其微。或者剛好查詢的數據是在一頁的最後一條數據上,要找下一條數據就必須訪問磁盤獲取下一頁,這個操作性能消耗多一點,但一個數據頁能存放上千個Key,平均下來這也是可以忽略不計的。

更新過程:普通索引的數據更新,(1)如果內存中沒有相應數據頁,會將更新內容記錄在change buffer中,直到有線程訪問相應數據頁,才觸發merge過程,即將數據寫入磁盤中。MySQL後臺有線程定時merge,MySQL正常關閉也會merge。而唯一索引的每一次更新,都必須將響應數據頁更新到磁盤當中進行唯一性校驗,不能夠使用change buffer記錄。 也就是說,在寫多讀少的業務場景中,使用普通索引+change buffer可以將隨機寫磁盤變爲順序寫磁盤,提高寫磁盤的性能。(2)如果內存中有相應數據頁,那麼直接在內存中對數據頁進行修改,下一個讀請求訪問的時候,也直接從內存中獲取數據進行返回。這一步普通索引和唯一索引是沒有區別的。

注:change buffer 用的是 buffer pool 裏的內存,因此不能無限增大。change buffer 的大小,可以通過參數innodb_change_buffer_max_size 來動態設置。這個參數設置爲 50 的時候,表示 change buffer 的大小最多隻能佔用 buffer pool 的 50%。

5.遇到MySQL選錯索引怎麼辦?

有時候由於MySQL優化器的一些bug,無法選擇到執行速度快的索引。如果是由於索引統計信息不準確導致的,可以使用analyze table解決。如果還是不行,簡單粗暴的方法是force index吧,直接人爲指定,但如果數據庫索引名稱改動或者遷移數據庫,很可能需要動手修改相應的sql語句。

6.給字符串加索引

前綴索引:比如郵箱這樣的字段,我們可以給郵箱加上前綴索引,每個字符串都只取前面6個或者7個字符,好處是可以節省索引存儲空間。至於選擇多少個字符作爲前綴,需要根據業務來進行判斷,如果6個能夠區分開每個字符串,而5個不行,那麼就選擇6個。前綴索引的缺點是不能使用到覆蓋索引,因爲查找到的字符串前綴必須要通過回表進行再次驗證,才能夠保證所查的數據就是需要的數據。

倒序存儲:如果有的字符串前綴前面區分度不高,後面區分度高,可以存儲時將使用reverse函數將數據翻轉,然後建立前綴索引。

hash存儲:給表新增一個字段,將字符串經過hash計算後的值存入,然後給該字段創建索引,可以使用mysql提供的crc32()函數。查詢時爲了避免計算後的hash值相同,需要把獲取到的結果再精確匹配一下。如下代碼

select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'

7.order by排序過程

全字段排序:比如select name,age from student where city = '四川' order by name limit 1000 這樣的查詢,InnoDB初始化一個sort_buffer,先查city索引上所有符合city='四川'的數據,逐行取出ID到主鍵索引獲取name,age字段,存入每個線程都有的sort_buffer,給sort_buffer放入所有city = '四川'的數據之後按照name排序,取前1000行排序結果返回給客戶端。

如果sort_buffer_size大小超過排序所需數據的大小,那麼可以在內存中完成排序;如果需要排序的數據過大,那麼會使用到磁盤臨時表進行排序,那麼就會有大量磁盤IO操作。

rowid排序:爲了提升性能,使用盡量少的磁盤臨時表,可以設置max_length_for_sort_data參數,用於控制sort_buffer中的字段總長度,比如上面的排序需要name,id兩個字段,而這兩個字段加起來長度爲18,那麼可以設置max_length_for_sort_data參數爲18,這樣InnoDB就可以在sort_buffer中對這兩個字段的值進行排序,然後拿到前1000行的id值去主鍵索引當中獲取相應數據,返回給客戶端。

最後一種方式是使用覆蓋索引的排序,因爲索引存儲本身是按照遞增的順序排列的,如果有(city,name,age)這樣的聯合索引,那麼我們查找該聯合索引獲取相同城市的數據,就是按照name升序排列的,獲取limit 1000的數據也就可以不需要經歷排序過程。

優先隊列排序:filesort_priority_queue_optimization,比如我們對數據進行limit 3選取,那麼該算法不會對所有匹配的結果進行排序,而是會維護一個大小爲3的堆,如果我們要取的是最小的三個數,那麼會把每一次的新數據拿來和堆中值最大的數進行比較,如果有新的更小的數,就把這個堆中最大的值給替換掉,這樣遍歷一次所有數據之後,就排出了limit 3的數據。不需要使用臨時表,通過sort_buffer就可以搞定了。上面的例子沒有使用優先隊列排序,是因爲limit 1000需要維護一個大小爲1000的堆,而1000的堆已經超過了我們sort_buffer_size所設置的大小,所以必須要使用到磁盤臨時表。

8.使用索引的幾個坑

(1).給條件字段使用函數:比如select * from user where month(last_date) > 7這樣的語句,InnoDB存儲的是2017-05-23這樣類似的數據,如果要找月份大於7的數據,就必須把索引last_date的所有數據都通過函數month進行一次計算,那麼勢必會導致全索引掃描,可以使用last_date>'2016-7-1' and last_date<'2017-1-1'等等類似的寫法來代替。

(2).隱式類型轉換:select * from user where str > 10333,str字段存儲的都是字符串,如果遇到了這樣的寫法,MySQL默認是把字符串轉數字進行比較,那麼相當於給str字段使用了一個轉換函數,就和(1)的情況相同了。

(3).隱式字符編碼轉換:連表查詢的時候,如果兩個字段使用的字符串都有索引,但字符集不相同,那麼就有可能出現類似(1)的情況,通常把字符集變成相同的就可以了。

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