定義
一句話總結,索引是一個排好序的用於快速查找的數據結構。這句話說明了索引的三個特點,第一個是有序的,已經將索引列數據排好序了;第二個是快速查找,這就意味着使用索引可以快速定位到符合條件的數據;第三個是一個數據結構。我們平時使用 SQL 語句查詢數據時,比如執行 select * from student where name = "張三" ;那么它會去存儲數據的磁盤上一行一行進行 name 字段的判斷,如果符合就返回,不符合就跳過,這樣在數據量較大時查詢效率就很低,所以索引的使用就可以在數據量很大時快速定位到數據。因為索引也是需要占用磁盤空間的,所以如果要查的數據量並不是很大時就沒有必要使用索引,否則效率會沒有提升甚至降低,而且還浪費了磁盤空間。
結構
索引共有三種數據結構,Hash、B樹,B+ 樹。其中 B+ 樹是默認的,也是使用最廣泛的。
Hash
hash 結構就是以哈希表的方式進行存儲。
優點:數據量小時,發生 hash 沖突的次數很少,查找效率是非常高,因為 hash 沖突次數少大部分數據都存儲在哈希表的數組下標上,並且如果是鏈地址法解決哈希沖突的話產生的鏈表長度也不會太長,而數組的數據因為內存連續所以查詢效率自然就高。
缺點:1、數據量大時,hash 沖突的次數就變多,形成鏈表的長度也會變長,而鏈表遍歷是低效的,所以查找數據就會很慢;2、因為 hash 表是沒有順序的,所以不能進行一些范圍查詢例如 >,< 等,也不能進行 like 模糊查詢。
絕大多數情況使用的索引都是 B+ 樹或 B 樹結構的,所以開頭說索引的定義是排好序的快速查詢結構就排除了 Hash 結構的索引。
B樹
B 樹是多叉樹的一種,相比於二叉樹它的特點是每個父節點可能包含多個子節點,而它的數據也是存儲在各個節點上。每個節點的左子結點小於當前節點所攜帶數據的值,右子節點大於當前節點的值。每次查詢從根節點出發進行判斷。
優點:有序,查詢高效,尤其是靠近根節點的數據比如上圖中的 20 ,40會很快查到。
缺點:遍歷效率低,遍歷時每次都需要從根節點出發查找每個值所在的位置。
B+樹(默認)
B+樹相比於 B 樹改善了遍歷效率,它是將數據統一存儲在葉子節點上,並且對於相鄰的葉子節點以鏈表的形式連接,這樣在遍歷時就不需要每次都從根節點開始。
優點:遍歷速度快,查詢速度快。
缺點:單個數據查詢時效率不一定有 B 樹高。
不同存儲引擎下的實現
上面說得是索引的基本結構,因為一般都是使用 B+ 樹實現的,所以這里也用 B+ 來講解在 MyISAM 與 InnoDB 中的不同實現。
MyISAM
在 MyISAM 中,主鍵索引和非主鍵索引的結構是一樣,葉子節點存儲的都是最終行數據所在的磁盤地址,當找到符合的條件地址后需要去磁盤地址上找到對應的行數據。
索引和數據存儲:索引和數據是用兩種文件格式存儲的,數據使用的是 .MYD結尾的文件存儲的,而索引是使用 .MYI結尾的文件存儲的(.frm 是表結構文件)。 數據文件和配圖轉自【MySQL】索引原理(二):B+Tree索引的實現,MyISAM和InnoDB 。
不足:5.6之前的 MyISAM 相比於 InnoDB 多了全文索引,同時內部維護了一個計數器,對於 conut 函數可以直接讀取(不含where等條件篩選時)。模糊查詢效率會比 InnoDB 高。但是其不支持事務,沒有行級鎖,同時 5.6之后 InnoDB 也引入了全文索引,所以 MyISAM 慢慢被遺棄了。
InnoDB(默認)
在 InnoDB 中將索引分為聚簇索引與非聚簇索引。
聚簇索引默認是該表主鍵對應的索引;如果該表沒有主鍵,那么會從該表中選擇一個唯一非空的列作為聚簇索引;而如果表中也沒有唯一非空的列,那么就會 InnoDB 就會自己維護一個聚簇索引,但是這樣遍歷的效率就會低一些。
非聚簇索引指得就是除聚簇索引外的其他索引。
之所以這樣分是因為聚簇索引的結構與非聚簇索引的結構不同,非聚簇索引結構的 B+樹葉子結點存儲的是主鍵值和當前的索引列的值。而聚簇索引結構的 B+樹葉子結點存儲的就是對應的行數據。如果使用聚簇索引進行查詢只需要在一個索引結構中進行查詢就可以得到這一行的所有數據,所以在 InnoDB 中使用主鍵查找數據效率非常高;而如果使用非聚簇索引進行查詢那么在查出主鍵值后還需要去主鍵所在的索引結構中進行回表查詢(沒有發生索引覆蓋時),也就是比直接通過聚簇索引查詢多了一次查詢。
索引和數據存儲:InnoDB 索引的數據因為直接存儲在聚簇索引所在的 B+ 樹中,所以只有一種格式的文件,文件以 .ibd 結尾。
Innodb 結構的主鍵一般推薦設置為自增的主鍵,因為如果設置為 UUID 的,那么每次插入都可能會在原有的數據之間,因為 InnoDB 管理數據的最小磁盤單位是數據頁,如果要插入的數據頁沒有合適位置插入,就會將此條記錄以及后面的記錄全部另開一個數據頁存儲,造成 "頁分裂",從而影響了效率;而如果是自增,那么只會在原有數據后面新增,不會造成這樣的問題。
但如果業務字段有唯一字段且字段長度也不長那么也可以將其設置成主鍵,因為如果拋開所有的業務列而單獨創建一列作為主鍵列,那么每次使用業務字段查詢時都必須進行一次回表查詢,換句話說就是會使 "覆蓋索引" 失效,而使用熱點字段作為主鍵可以減少一次回表查詢。
另外需要注意的是數據頁是磁盤記錄的最小操作單位,所以聚簇索引所在的 B+樹葉子結點存儲的並不是對應的行記錄,而是行記錄所在的頁,上面那張圖只是用來理解聚簇索引與非聚簇索引的關系的,聚簇索引的結構應該如下
在找到葉子結點后還需要去數據頁中進一步進行查詢,直到找到最終的行記錄。如果對數據頁不清楚可以查看 博客。
種類
1、主鍵索引(Primary key)。一張表的主鍵默認就是一個主鍵索引。
2、唯一索引(Unique Index)。某一列的值是唯一的可以使用唯一索引。與普通索引的區別是在查詢數據時,唯一索引查到數據后就可以直接返回,而普通索引需要繼續向后查直到查到的值不等於目標值。添加數據時唯一索引需要判斷是否已存在相同值的記錄,而普通索引不需要。
3、普通索引(Index(列名))。
4、全文索引(Fulltext Index)。5.6 之前是 MyISAM 專屬的,5.6 開始 InnoDB 也引入了,適用於在數據量大的場景進行模糊查詢。
5、聯合索引(Index(列名,列名))。為多列設置一個索引,這些列在篩選時都可以用到這個索引,提升效率,同時減小了索引的占用空間。聯合索引的使用必須遵守最左匹配原則,否則相應的列索引就會失效。
最左匹配原則:在查詢時會先從聯合索引的最左列開始匹配,如果某列沒有出現或者出現了范圍查詢,那么后面的列都不會用到索引。
注意:1、這里的范圍查詢是指 >、<(這種不含有 = 情況的),如果是 like,那么只要它的開頭不是 "%"、"_" 通配符就和 in、<=、>= 一樣符合最左匹配原則,而如果是通配符開頭的,那么它就和 not in 一樣失效,后面的列也會失效。比如聯合索引(name ,age),where name like '張%' and age =20;這個name 與age 都會用到索引,而如果是 where name like '%三' and age =20;那么 name 與 age 都不會用到索引。
2、and 條件的列可以改變位置,比如對於聯合索引(name,age),那么where age>20 and name like '張%'; 在mysql 解析時會將 name 優化到 age 條件的前面保持最左匹配原則,age 與 name 都會用到索引。
6、前綴索引(Index(列名(截取長度)))。其實就是前面幾種索引的一種特殊變種。對於某個字段長度過長,同時前幾位的數據段就可以代表當前值的重復水平。那么就可以創建前綴索引來減小該索引的占用空間。需要注意的是非主鍵列的前綴索引會使覆蓋索引失效。因為前綴索引存儲的值只是最終列數據的部分值,所以在匹配到對應的記錄后還需要去比較完整值,而如果是非主鍵列就需要回表查詢才能驗證。也正因為要驗證,所以前綴索引截取的長度應該使每個數據的唯一率和原數據一致來避免過多的回表驗證。這個長度的獲取可以通過這種方式:首先查詢 select count(*)/count(distinct 列名),然后不斷調整參數 select count(*)/count(distinct 列名(截取長度)),當結果無限接近於前面的值就取該長度作為前綴索引的截取字段。
延伸問題:對於前面幾位重復率高的列創建索引如何優化?如身份證號。
1、將該列的值全部倒序存儲,因為后面幾位的數據是隨機的,所以唯一性比較好。而在查詢時將結果倒序展示。優點:占用空間小、CPU 消耗低(reverse 函數復雜度比 crc32 函數低)
select field_list from t where id_card = reverse('input_id_card_string');
2、增加字符串哈希值列並創建索引,也就是將身份證號通過 crc32 函數轉成一個哈希值,然后存在新增的創建索引的列中,在查詢時優先判斷哈希列,如果相等再匹配原列。優點:執行效率更穩定
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
使用
操作
創建:1、create 索引類型 索引名 on 表名(列名),如 create index idx_name on student(name) 2、Alter table 表名 add 索引類型 索引名(列名),如 alter table student add index idx_name(name)
刪除:1、drop index 索引名 on 表名,如 drop index idx_name on student 2、Alter table 表名 drop index 索引名,如 alter table student drop index idx_name
對於主鍵只能先移除 "主鍵身份" 再刪除。 alter table 表名 modify 主鍵列名 int; 然后執行刪除操作: Alter table 表名 drop index primary key;。
使用場景
因為索引會占用空間,所以需要滿足特定條件才推薦使用索引。
1、數據量大。數據量小不會提升效率甚至會降低效率。
2、讀取操作多,修改少。每次對索引列數據的修改都需要去修改索引對應的結構,如果一張表的修改次數大於讀取次數,那么使用索引反倒會使程序總體的效率降低。
3、重復數據少,列數據種類多。如果對 sex 性別列創建索引,那么索引的作用也不大甚至會降低效率。
失效場景
1、對索引列篩選值是該列所包含的大部分值時,就會優化成全表查詢,跳過索引來保證更好的效率。比如說 age 字段都是大於0 的,而在 where 篩選條件為 age>0,那么這個篩選就會被優化成全表查詢。這一條很容易被忽視,也經常出現,比如使用 in、>、>=、<、<= 等等都可能出現有時用到索引,換一個檢索條件值就用不到。這是因為mysql 底層進行了諸多優化,很多情況都會判斷為使用全表掃描效率更高,所以會優化成全表掃描。
2、對索引列進行 is null、is not null 判斷可能會失效。當該列本身就設置為非空的,那么由於1的作用is not null直接就被優化成全表查詢,索引失效,而is null 則會生效;如果列可以為空,那么is null 和 is not null 還是會根據該列的null 值多少來判斷,如果null 值幾乎沒有,那么和前面的情況一樣,如果幾乎都是 null 值,那么is null 就會優化成全表掃描,而 is not null 則會用到索引,而兩者數據差不多則兩者都會用到索引。
3、對索引列使用函數或表達式。這個失效並不是一定就會沒有用上索引,如果可以不用回表那么優化器可能還會選擇使用索引。
4、模糊查詢 like 時不以具體值為開頭,而是以 "%"、"_" 通配符開頭。
5、對索引列使用 !=、not in 。索引中存儲的是最終的數據或地址,如果是查詢不等於某個值必然是需要進行全表掃描的。
6、索引列進行了自動類型轉換。比如 phone 字段類型是 varchar,而在查詢的時候沒有使用單引號, 那么執行后也能得到最終結果,但是卻沒有走索引。這是因為在數據比較時會調用函數將兩者類型變成一致才能比較。
7、使用 or 如果左右條件有一方沒有用到索引時那么用到索引的那一方索引也會失效。
8、對於聯合索引沒有遵守最左匹配原則。
9、參與查詢的兩張表的字符集不同。原因和第6條一樣,也是因為需要調用函數使數據字符集一致才能比較
優化:1、將兩表的字符集轉成一致
2、如果表處於上線狀態且數據量特別大。可以修改SQL,主動對驅動表的數據類型進行轉換,避免默認將被驅動表進行類型轉換。
如:
select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2;
可以優化成
select d.* from tradelog l , trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2;
tradelog 是驅動表,通過它計算出的 tradeid 相當於一個常量,對常量使用表達式不會影響其他索引列,所以會生效
所謂的全表掃描指的是按主鍵順序一條一條掃描。