1. 為什么會有這篇文章?
選擇DB作為Training的第一個系列,而不是其他,是因為這貨太重要,而且也是大多數開發人員忽略的最多的地方。為什么一開始就選擇索引,是因為查詢是我們寫的最多的SQL語句,索引也是能最快最方便提升一個查詢速度的捷徑,這個99%的開發人員都知道這貨是干這個事情的,但是僅僅不到10%的開發人員能真正了解和用好它。所以有了這個系列已經這個文章的想法。
2. 索引類型
對索引最簡單的理解就是一本書的頁碼,可以通過書的目錄(這個就相當于是數據庫中的索引)對應的頁碼可以找到這頁的所有內容。在這節中我們從MySql中索引類型來說一下數據庫中的索引工作原理,從索引的數據結構算法上拆分,在MySql的Innodb存儲引擎里面有兩個類型,BTree和Hash類型索引,從索引順序和整行數據物理順序一致或不一致來區分的話,分為聚族(聚集)索引和非聚族(非聚集)索引。下面我們首先來說一下BTree和Hash類型。
2.1 BTree索引
BTree索引在我們MySql的Innodb中使用的是最多的索引類型,我們一般說的索引就是BTree類型的索引。BTree的全稱是Binary Tree,就是我們熟悉的二叉樹數據結構;下面我們就用一張圖來說明一下BTree在MySql的工作原理;
2.2 Hash索引
Hash索引我們用的不是特別多,但是在某些場景下我們選擇Hash索引可能更適用一點兒,我們還是先用一張圖來看一下Hash索引在Mysql里面的工作原理,他工作原理和Java中的HashMap有些類似:
2.3 BTree和Hash索引各自特點
1.從順序性上來說,BTree是有序的,所以在Order By的時候BTree索引會被使用上,而Hash不會被使用;Hash索引的無序性也讓它在執行范圍查找的時候也不能被使用,而BTree可以;但是這種順序性在插入新數據的時候Hash索引是優于BTree索引的,因為插入的時候BTree需要遍歷樹,最壞的時候遍歷整個樹來創建對應的索引來保證順序,而Hash本身的無序性,不需要這樣做;
2.從查詢條件符使用限制來說,BTree所以可以用在=,<>, >, <, IN, Between, Like(這里說的Like指Like 'ryan%',是后匹配,前匹配不支持的),而Hash索引還是因為它的無序性,它不支持范圍查詢條件符,它僅僅支持等值查詢條件=, IN不支持范圍條件>, <, <=, >=, Between, Like之類的;
3.索引效率上來說,假如都用到了索引Hash索引比BTree索引更加快的,為什么?這主要從兩點來說,第一是Hash索引的存儲很緊湊,它可以把較長的字符串轉換成一串較短數字,另外它只存儲了hash值和對Row的指針,這也是為什么它無序了,而BTree存儲了整個索引列的值,所以從這塊講他的效率也是相當快的;第二是從結構算法來講,不知道你們看到右邊的復雜度沒有,一個是O(log2n),一個O(1+(bucket capacity-1)),BTree隨著表的變大他的樹的深度也會隨著增加,最壞是遍歷整個樹的深度;而Hash索引如果沒有Hash值的碰撞他的復雜度只有O(1),大部分是O(1),如果有碰撞,也只是(bucket capacity-1)的增加,所以從算法的復雜度來說,Hash索引執行效率也是優于BTree的;
4.在多列組合上面來說,BTree索引優于Hash索引,如果(A, B, C)3列作為一個組合索引,如果使用A作為搜索條件,BTree索引可以利用上,而Hash索引不會用上,這個是因為Hash索引是將ABC這3列給Hash value了,所以A的部分索引的話是不能使用的;
2.4 聚族索引
聚族索引他的索引順序和數據的存儲順序是一致的,可以理解成他們是在一起的,就像一本書的頁碼和該頁碼中的內容;通常我們一個表的主鍵就是聚族索引,如果一個表沒有定義主鍵,那么MySql的Innodb會自動生成一個主鍵列,以及主鍵索引;另外在innodb中聚族索引是按順序進行物理存儲層面存數據的,所以建立聚族索引最好選擇自增長的數字作為聚族索引,這樣正好在每次最后一位索引值增加新的索引值,如果換成GUID列作為聚族索引,因為GUID是隨即生成,并且無序的所以每次插入一行紀錄的時候,創建聚族索引為了保證聚族索引的順序,會去查找指定的順序位置,產生額外的開銷,另外GUID占用36位unicode字符串,不管是比較字符串,還是存儲需要的長度都是開銷挺大的,所以聚族索引優先選擇整數列作為聚族索引;
2.5 非聚族索引
在一個表只有一個聚族索引,除開聚族索引,該表剩下的索引都是非聚族索引;非聚族索引是基于聚族索引的,這話怎么講呢?看下圖,我們在name上建立了一個BTree的非聚族索引,最終匹配到對應Name以后,該非聚族索引還存儲了主鍵,我們使用主鍵再在聚族索引BTree找到真正對應的該Row的數據,所以非聚集索引還有另外一個稱號叫“二級索引”;
3. MySql的執行計劃
由于下一節我們會用到執行計劃來判斷我們查詢語句是否用了合理的索引,所以這節我們先來了解執行計劃,首先我們來看一個很簡單的執行計劃:
我們看計劃重點要關注type和rows(更多查看這里explain-output.html#explain-join-types),rows自然就是該語句查詢的行數是多少,重點說說type,type類型一共有這幾種,按執行效率排序是這樣system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > all;
我這里重點說這幾個,也是我們平時用的比較多的:const > eq_ref > ref > index_merge > range > index > all;
1.const一般是聚族索引或者Unique索引,直接Where條件或者多表聯合查詢是只只返回一條數據的情況下使用,如下:
select * from ORDER_ITEM where ID=12422;
2.eq_ref是用于聚族索引或者Unique索引在作為多表聯合查詢的關聯列的時候,如下:
select * from ORDER A LEFT JOIN ORDER_ITEM B ON A.ID=B.ORDER_ID WHERE B.ID (123, 4242);
這里會用到兩個索引,其中第二個索引A表就是用eq_ref索引;
3.ref是用在用在重復的非聚族索引上面,作為Where條件,或者多表聯合查詢的條件時會使用到,如下:
select * from ORDER_ITEM where ORDER_ID=1;
select * from ORDER A LEFT JOIN ORDER_ITEM B ON B.ORDER_ID=A.ID where A.ID IN (1,2);
這里會用到兩個索引,其中第二個索引B表就是用ref索引;
4.index_merge這個類型一般是優于建立了兩個單列索引,innodb為了執行效率,幫助你優化講兩個單列索引合并成1個多列索引,如下:
select * from ORDER_ITEM WHERE ORDER_ID=1 OR ITEM_ID=1301;
5.range這個類型估計我們看的最多最多了,使用了索引列的作為Where后的范圍查詢條件都會是這種類型,Between, IN, >, <, >=, <=,如下:
select * from ORDER_ITEM WHERE ID < 22234;
6.index這個就是掃描整個索引值;
select * ORDER_ID from ORDER_ITEM ORDER BY ORDER_ID;
這個就是ORDER BY和返回列使用了整個索引值,由于沒有加任何條件,就走了這種類型;
7.all這個是最慢最慢的,在沒有建立索引的列作為條件語句使用,以及ON條件中使用,以及沒有任何條件返回列中沒有索引(那怕是一列沒有索引),都會走這個all類型;
select * from ORDER_ITEM;
select * from ORDER_ITEM where ITEM_NAME='apple';
4. 索引使用技巧
4.1 索引的選擇性
我們使用SHOW INDEX執行語句來顯示INDEX的一個情況,如下:
我們主要關注Cardinality這個屬性,對于主鍵列聚族索引來說它的值等于表的Row Size的,那Cardinality這個屬性是什么意思呢?看看官方的解釋:
An estimate of the number of unique values in the index. This is updated by running ANALYZE TABLE
or myisamchk -a. Cardinality
is counted based on statistics stored as integers, so the value is not necessarily exact even for small tables. The higher the cardinality, the greater the chance that MySQL uses the index when doing joins.
從第一句已經說明它的意思,它是一個索引唯一的個數值,所以我說Primary Key的這個值就是Row Szie,那么對于Primary Key的選擇性就是100%,那對于非聚族索引而且是非唯一的索引,那么它的選擇性就是小于100%,選擇性越高,那么如果該索引作為查詢條件那么它的查詢速度就越快。
4.2 多列組合索引
1.多列組合索引一般適用于查詢條件有多列,而且同時出現在很多查詢場景中;或者說有一部分列在條件中,另外一部分用于了Order By;還有一種場景就是返回字段很少列,也可以把這些少部分的列作為組合索引中的部分列,這樣索引就可以直接返回值,而無序回表中獲取;
2.多列組合索引是從左至右來應用索引的,是有順序的,如果直接使用列中或者列尾作為索引條件,是不會走組合索引列的;所以在這里我們應該把使用度和選擇性都比較高的列作為組合列的第一個列;
首先我們建立該索引(name, email, phone),id為主鍵
下面SQL語句都會很好的使用索引:
select id,name from user where name='ryan' order by id;
select id,name from user where name='ryan' and email='ryan@..';
則下面的SQL語句不會使用該索引,而會走scan table的操作:
select id,name from user where email ='ryan@..';
select id,name from user where phone='152322....';
4.3 覆蓋索引
覆蓋索引其實就是指查詢條件中用到了索引,并且查詢的結果直接用了索引中的列值,而沒有回表去查找數據;覆蓋索引一般是用BTree實現,而且一般都是一個組合索引,看下面的示例:
如果我們建立一個name列的索引,然后返回的是PK ID,如下:
select id from user where name='ryan';
那么這個查詢的執行效率是相當高的,根據圖4的展示,我們可以知道,它不需要回表查詢ID,而是直接從name列上的索引返回了,而且大部分MySql索引都存儲在數據庫服務器內存里面的;
再看一個示例:
如果我們建立一個(name, phone, email)的索引,該索引在如下查詢語句里面也可以看作覆蓋索引:
select id,name,phone,email from where name='ryan' order by name;
4.4 三星索引
三星索引是值,一個好索引應該滿足,一是查詢條件中使用了索引;二是返回的列中都是索引中的值,沒有回表里面查找;三是我們很好的利用組合索引的順序,來放在Order By中也使排序使用了索引;但是我們很難造出三星索引,因為我們索引不單單是用在一條SQL語句,甚至我們有時候連覆蓋索引很難造出來。上面覆蓋索引這一節,最后那個示例就一個三星索引的使用,另外我們在這個查詢語句使用的完美,但是該索引對其它查詢語句不一定就工作的很好,所以有時候我們要全局考慮,用盡可能少的索引去滿足整個系統查詢需要。而不是糾結一個查詢,除非這個查詢使用頻繁度非常高,當然這個查詢語句就另開小灶為他單獨定制了;
4.5 延遲關聯
我們在使用MySql分頁的時候都會寫這樣的語句:
select * from ORDER limit 10,10
這種使用limit的語句在翻前面幾頁的時候十分的塊,但是如果執行到下面這種語句的時候就會非常的慢:
select * from ORDER limit 10000,10
這是因為scan了表10000條出來,結果拋棄了9990條沒用,只顯示了10行,這就會讓人感覺只有10行為什么都這么慢,就不知道它scan了9990條所耗費的時間。為了解決這種問題,我們需要使用延遲關聯來解決,用聚族索引來加速查詢,再看下面的寫法:
select * from ORDER AS T1 INNER JOIN (select id from ORDER limit 10000,10) AS T2 ON T1.ID=T2.ID
這樣里面那個查詢,和外面查詢都走了索引,而且里面把返回列盡量的減少,這樣查詢速度是相當快的,里面那個select走的是index,外面走的是eq_ref;
4.6 索引與鎖
在數據庫執行事務處理的時候,如果你用到了
select ... for update或者select ... lock in share mode
一個是X(排他所),一個是S(讀共享鎖),這兩個鎖都會lock住表或者查詢結果幾行數據,這個取決于你where后面查詢條件建立了索引沒有,如果走的是聚族索引,那么鎖定的肯定是一行數據,如果走的是非聚族索引可能是查詢的幾行也可能是一個范圍;如果沒有走任何索引那將是很糟糕的結果鎖定整個表;還有就算走了索引,不適查詢了顯示結果幾行就鎖定了幾行數據,而且要看執行計劃中的rows,用該索引走了幾行數據,這個才是鎖定的真正行數,如下面SQL:
select * from ORDER where ID < 5 and ID <> 1
顯示的結果肯定沒有ID等于1的那行數據,但是ID等于1的那行數據也被鎖定了,ID走的聚族索引,但是實際走的range 4行的查詢計劃;