最近,在 mysql 測試最左前綴原則,發現了匪夷所思的事情。根據最左前綴原則,原本應該索引失效,走全表掃描的,可是,卻發現能夠正常走索引。mysql
表結構以下( Mysql 版本 5.7.22):算法
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL, `age` int(11) DEFAULT NULL, `address` varchar(128) COLLATE utf8mb4_bin DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user` (`name`,`age`,`address`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin INSERT INTO user(`id`, `name`, `age`, `address`) VALUES (1, 'zs', 12, 'beijing');
表中總共有四個字段。 id 爲主鍵,還有一個由 name,age,address 組成的聯合索引。 存儲引擎爲 InnoDB,並插入一條測試數據。sql
根據最左前綴原則,如下 sql ,確定會使索引失效的。(若不懂最左前綴原則,稍後會講~)測試
EXPLAIN select * from user where address='beijing';
然而結果,倒是讓人大失所望。以下,經過查看執行計劃,發現它走索引了。優化
這就讓我很是疑惑了,難不成最左前綴原則是錯的?又或者,是 Mysql 隨着版本升級,已經智能到不須要 care 最左前綴原則了嗎?spa
帶着這個疑問,咱們一探究竟。在這以前須要瞭解一些前置知識。本篇文章目錄以下:3d
因爲,如今基本上都是用的 InnoDB引擎,因此下面都以 InnoDB爲例,MyISAM 順帶提一下。指針
咱們知道 Mysql 底層是用 B+ 樹來存儲索引的,且數據都存在葉子節點。對於 InnoDB 來講,它的主鍵索引和行記錄是存儲在一塊兒的,所以叫作彙集索引(clustered index)。code
PS:MyISAM 的行記錄是單獨存儲的,不和索引在一塊兒,所以 MyISAM也就沒有彙集索引。blog
除了彙集索引,其它索引都叫作非彙集索引(secondary index)。包括普通索引,惟一索引等。
另外須要注意,在 InnoDB 中有且只有一個彙集索引。它有三種狀況:
爲了方便理解,下邊以 InnoDB 的主鍵索引和普通索引爲例,看下它們的存儲結構。
建立一張表,結構以下,並添加幾條記錄(張三,李四,王五,孫七):
CREATE TABLE `student` ( `id` int(11) NOT NULL, `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_stu` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin insert into student(id,name,age) values(1,'zs',12); insert into student(id,name,age) values(5,'ls',14); insert into student(id,name,age) values(9,'ww',12); insert into student(id,name,age) values(11,'sq',13);
在 InnoDB 中,主鍵索引的葉子節點存儲的是主鍵和行記錄,而普通索引的葉子節點存儲的是主鍵(對於 MyISAM來講主鍵索引的葉子節點存儲的是主鍵和對應行記錄的指針,普通索引的葉子節點存儲的是當前索引列和對應行記錄的指針)。
所以,id 爲彙集索引,name 爲非彙集索引。它們對應的 B+ 樹結構以下圖所示,
從上邊的索引存儲結構,咱們能夠看到,在主鍵索引樹上,經過主鍵就能夠一次性查出來咱們所須要的數據,速度很是的快。
由於主鍵和行記錄就存儲在一塊兒,定位到了主鍵,也就定位到了所要找的記錄,當前行的全部字段都在這(這也是爲何咱們說,在建立表的時候,最好是建立一個主鍵,查詢時也儘可能用主鍵來查詢)。
對於普通索引,如例子中的 name,則須要根據 name 的索引樹(非彙集索引)找到葉子節點對應的主鍵,而後再經過主鍵去主鍵索引樹查詢一遍,才能夠獲得要找的記錄。這就叫 回表查詢。
以以下 sql 爲例。
select * from student where name='zs';
它須要查詢兩遍索引樹。
它的查詢過程圖以下,
對於上邊的回表查詢來講,無疑會下降查詢效率。那麼,有的童鞋就會問了,有沒有什麼辦法,讓它不回表呢?
答案固然是有了,就是索引覆蓋。
何爲索引覆蓋,就是在用這個索引查詢時,使它的索引樹,查詢到的葉子節點上的數據能夠覆蓋到你查詢的全部字段,這樣就能夠避免回表。
仍是以上邊的表爲例,如今 zs 對應的索引樹上邊,只有它自己和主鍵的數據,並不能覆蓋到 age 字段。那麼,咱們就能夠建立聯合索引,如 KEY(name,age)。而且,查詢的時候,顯式的寫出聯合索引對應的字段(name和age)。
建立聯合索引以下,
KEY `idx_stu` (`name`,`age`)
查詢語句修改以下,
-- 覆蓋聯合索引中的字段 select id,name,age from student where name='zs' and age=12;
這樣,當查詢索引樹的時候,就不用回表,能夠一次性查出全部的字段。對應的索引樹結構以下:
PS:圖中,聯合索引中的字段(name,age)都應該出如今索引樹上的,這裏爲了畫圖方便,且因數據量過小,沒有畫出來。只表現出了:葉子節點存儲了全部的聯合索引字段。
最左前綴原則,顧名思義,就是最左邊的優先。指的是聯合索引中,優先走最左邊列的索引。如上表中,name和age的聯合索引,至關於建立了 name 單列索引和 (name,age)聯合索引。在查詢時,where 條件中如有 name 字段,則會走這個聯合索引。
對於多個字段的聯合索引,也同理。如 index(a,b,c) 聯合索引,則至關於建立了 a 單列索引,(a,b)聯合索引,和(a,b,c)聯合索引。
爲了驗證最左前綴原則,咱們須要對原來的表結構進行改造。再添加兩個字段(address,sex),而後建立三列的聯合索引(name,age,address)。
drop table student; CREATE TABLE `student` ( `id` int(11) NOT NULL, `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `age` int(11) DEFAULT NULL, `address` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `sex` int(1) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_stu` (`name`,`age`,`address`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; insert into student(id,name,age,address,sex) values(1,'zs',12,'beijing',1); insert into student(id,name,age,address,sex) values(5,'ls',14,'tianjin',0); insert into student(id,name,age,address,sex) values(9,'ww',12,'shanghai',1); insert into student(id,name,age,address,sex) values(11,'sq',13,'hebei',1);
查看錶數據以下,
分別用三種方式,使之符合最左前綴原則。
explain select * from student where name='zs'; explain select * from student where name='zs' and age=12; explain select * from student where name='zs' and age=12 and address='beijing';
而後查看它們的執行計劃以下,
能夠看到,最終都走了索引。如今,修改 sql 以下,如何?
explain select * from student where address='beijing';
如咱們所料,這不符合最左前綴原則,所以索引失效,走了全表掃描。
PS:拓展思考,若 sql 改成以下,會致使全表掃描嗎?(本身動手嘗試哦)
explain select * from student where name='zs' and address='beijing';
到如今爲止,咱們發現最左前綴原則一切正常。而後回到最開始拋出的問題,爲何這個原則就不生效了呢?(建立的聯合索引,還有 sql 語句都是同樣的啊!)
彆着急,還記得前面咱們說的索引覆蓋嗎? 此次,咱們利用索引覆蓋原理,只查詢特定的字段(只有主鍵和聯合索引字段)。
explain select id,name,age,address from student where address='beijing';
再查看執行計劃,
問題來了,此時違反了最左前綴原則,可是符合覆蓋索引,爲何就走索引了呢?
咱們對比一下,若用最左列,和不用最左列,它們的執行計劃有何不一樣。
會發現,若不符合最左前綴原則,則 type爲 index,若符合,則 type 爲 ref。
index 表明的是會對整個索引樹進行掃描,如例子中的,最右列 address,就會致使掃描整個索引樹。
ref 表明 mysql 會根據特定的算法查找索引,這樣的效率比 index 全掃描要高一些。可是,它對索引結構有必定的要求,索引字段必須是有序的。而聯合索引就符合這樣的要求!
聯合索引內部就是有序的,咱們能夠把它理解爲相似於 order by name,age,address 這樣的排序規則。會先根據 name 排序,若name 相同,再根據 age 排序,依次類推。
因此,這也解釋了,爲何咱們要遵照最左前綴原則。當最左列有序時,才能夠保證右邊的索引列有序。
退而求其次,若不符合最左前綴原則,可是符合覆蓋索引,就能夠掃描整個索引樹,從而找到覆蓋索引對應的列(避免了回表)。
若不符合最左前綴原則,且也不符合覆蓋索引(形同 select *),則須要掃描整個索引樹。完成以後,還須要再回表,查詢對應的行記錄。
此時,查詢優化器,就會認爲,這樣的兩次查詢索引樹,還不如全表掃描來的快(由於聯合索引此時不符合最左前綴原則,要比普通單列索引查詢慢的多)。所以,此時就會走全表掃描。
有童鞋就要問了,你在這廢話了一大堆,仍是沒有解答最初的疑惑啊 !!!
否則,其實上邊的分析就已經解答了。咱們仔細觀察最開始的 user 表,和此時的 student 表有什麼不一樣。
user 表中,和 student 表相比,少了 sex 字段。可是,它們所創建的聯合索引倒是同樣的 KEY(name,age,address)。
因此,在 user 中,咱們最初的 sql 語句就等同於 ,
-- 最初的sql EXPLAIN select * from user where address='beijing'; -- 等同於 EXPLAIN select id,name,age,address from user where address='beijing';
這個結構就是咱們上邊討論的狀況:不符合最左前綴原則,可是符合索引覆蓋。這種狀況,是會走索引的。
那麼,結論也就出來了。並非最左前綴原則失效了,也不是 Mysql 變的更智能了,而是此時建立的表結構,以及查詢的 sql 語句剛好符合了索引覆蓋而已。真的是虛驚一場 !!
若本文對你有用,歡迎關注我,給我點贊吧 ~