Mysql原理篇之索引不懂不要瞎用---04
Mysql原理篇之索引不懂不要瞎用---04
- 索引的代價(jià)
- B+樹(shù)索引適用的條件
- 全值匹配
- 匹配左邊的列
- 匹配列前綴
- 匹配范圍值
- 精確匹配某一列并范圍匹配另外一列
- 用于排序
- 使用聯(lián)合索引進(jìn)行排序注意事項(xiàng)
- 不可以使用索引進(jìn)行排序的幾種情況
- ASC、DESC混用
- 排序列包含非同一個(gè)索引的列
- 排序列使用了復(fù)雜的表達(dá)式
- 用于分組
- 回表的代價(jià)
- 覆蓋索引
- 如何挑選索引
- 只為用于搜索、排序或分組的列創(chuàng)建索引
- 考慮列的基數(shù)
- 索引列的類(lèi)型盡量小
- 索引字符串值的前綴
- 索引列前綴對(duì)排序的影響
- 讓索引列在比較表達(dá)式中單獨(dú)出現(xiàn)
- 主鍵插入順序
- 冗余和重復(fù)索引
- 總結(jié)
我們前邊詳細(xì)、詳細(xì)又詳細(xì)的嘮叨了InnoDB存儲(chǔ)引擎的B+樹(shù)索引,我們必須熟悉下邊這些結(jié)論:
- 每個(gè)索引都對(duì)應(yīng)一棵B+樹(shù),B+樹(shù)分為好多層,最下邊一層是葉子節(jié)點(diǎn),其余的是內(nèi)節(jié)點(diǎn)。所有用戶記錄都存儲(chǔ)在B+樹(shù)的葉子節(jié)點(diǎn),所有目錄項(xiàng)記錄都存儲(chǔ)在內(nèi)節(jié)點(diǎn)。
- InnoDB存儲(chǔ)引擎會(huì)自動(dòng)為主鍵(如果沒(méi)有它會(huì)自動(dòng)幫我們添加)建立聚簇索引,聚簇索引的葉子節(jié)點(diǎn)包含完整的用戶記錄。
- 我們可以為自己感興趣的列建立二級(jí)索引,二級(jí)索引的葉子節(jié)點(diǎn)包含的用戶記錄由索引列 + 主鍵組成,所以如果想通過(guò)二級(jí)索引來(lái)查找完整的用戶記錄的話,需要通過(guò)回表操作,也就是在通過(guò)二級(jí)索引找到主鍵值之后再到聚簇索引中查找完整的用戶記錄。
- B+樹(shù)中每層節(jié)點(diǎn)都是按照索引列值從小到大的順序排序而組成了雙向鏈表,而且每個(gè)頁(yè)內(nèi)的記錄(不論是用戶記錄還是目錄項(xiàng)記錄)都是按照索引列的值從小到大的順序而形成了一個(gè)單鏈表。如果是聯(lián)合索引的話,則頁(yè)面和記錄先按照聯(lián)合索引前邊的列排序,如果該列值相同,再按照聯(lián)合索引后邊的列排序。
- 通過(guò)索引查找記錄是從B+樹(shù)的根節(jié)點(diǎn)開(kāi)始,一層一層向下搜索。由于每個(gè)頁(yè)面都按照索引列的值建立了Page Directory(頁(yè)目錄),所以在這些頁(yè)面中的查找非???。
索引的代價(jià)
在熟悉了B+樹(shù)索引原理之后,本篇文章的主題是嘮叨如何更好的使用索引,雖然索引是個(gè)好東西,可不能亂建,在介紹如何更好的使用索引之前先要了解一下使用這玩意兒的代價(jià),它在空間和時(shí)間上都會(huì)拖后腿:
-
空間上的代價(jià)
這個(gè)是顯而易見(jiàn)的,每建立一個(gè)索引都要為它建立一棵B+樹(shù),每一棵B+樹(shù)的每一個(gè)節(jié)點(diǎn)都是一個(gè)數(shù)據(jù)頁(yè),一個(gè)頁(yè)默認(rèn)會(huì)占用16KB的存儲(chǔ)空間,一棵很大的B+樹(shù)由許多數(shù)據(jù)頁(yè)組成,那可是很大的一片存儲(chǔ)空間呢。
-
時(shí)間上的代價(jià)
每次對(duì)表中的數(shù)據(jù)進(jìn)行增、刪、改操作時(shí),都需要去修改各個(gè)B+樹(shù)索引。而且我們講過(guò),B+樹(shù)每層節(jié)點(diǎn)都是按照索引列的值從小到大的順序排序而組成了雙向鏈表。不論是葉子節(jié)點(diǎn)中的記錄,還是內(nèi)節(jié)點(diǎn)中的記錄(也就是不論是用戶記錄還是目錄項(xiàng)記錄)都是按照索引列的值從小到大的順序而形成了一個(gè)單向鏈表。而增、刪、改操作可能會(huì)對(duì)節(jié)點(diǎn)和記錄的排序造成破壞,所以存儲(chǔ)引擎需要額外的時(shí)間進(jìn)行一些記錄移位,頁(yè)面分裂、頁(yè)面回收啥的操作來(lái)維護(hù)好節(jié)點(diǎn)和記錄的排序。如果我們建了許多索引,每個(gè)索引對(duì)應(yīng)的B+樹(shù)都要進(jìn)行相關(guān)的維護(hù)操作,這還能不給性能拖后腿么?
所以說(shuō),一個(gè)表上索引建的越多,就會(huì)占用越多的存儲(chǔ)空間,在增刪改記錄的時(shí)候性能就越差。為了能建立又好又少的索引,我們先得學(xué)學(xué)這些索引在哪些條件下起作用的。
B+樹(shù)索引適用的條件
下邊我們將嘮叨許多種讓B+樹(shù)索引發(fā)揮最大效能的技巧和注意事項(xiàng),不過(guò)大家要清楚,所有的技巧都是源自你對(duì)B+樹(shù)索引本質(zhì)的理解,所以如果你還不能保證對(duì)B+樹(shù)索引充分的理解,那么再次建議回過(guò)頭把前邊的內(nèi)容看完了再來(lái),要不然讀文章對(duì)你來(lái)說(shuō)是一種折磨。首先,B+樹(shù)索引并不是萬(wàn)能的,并不是所有的查詢語(yǔ)句都能用到我們建立的索引。下邊介紹幾個(gè)我們可能使用B+樹(shù)索引來(lái)進(jìn)行查詢的情況。為了故事的順利發(fā)展,我們需要先創(chuàng)建一個(gè)表,這個(gè)表是用來(lái)存儲(chǔ)人的一些基本信息的:
CREATE TABLE person_info(id INT NOT NULL auto_increment,name VARCHAR(100) NOT NULL,birthday DATE NOT NULL,phone_number CHAR(11) NOT NULL,country varchar(100) NOT NULL,PRIMARY KEY (id),KEY idx_name_birthday_phone_number (name, birthday, phone_number) );對(duì)于這個(gè)person_info表我們需要注意兩點(diǎn):
- 表中的主鍵是id列,它存儲(chǔ)一個(gè)自動(dòng)遞增的整數(shù)。所以InnoDB存儲(chǔ)引擎會(huì)自動(dòng)為id列建立聚簇索引。
- 我們額外定義了一個(gè)二級(jí)索引idx_name_birthday_phone_number,它是由3個(gè)列組成的聯(lián)合索引。所以在這個(gè)索引對(duì)應(yīng)的B+樹(shù)的葉子節(jié)點(diǎn)處存儲(chǔ)的用戶記錄只保留name、birthday、phone_number這三個(gè)列的值以及主鍵id的值,并不會(huì)保存country列的值。
從這兩點(diǎn)注意中我們可以再次看到,一個(gè)表中有多少索引就會(huì)建立多少棵B+樹(shù),person_info表會(huì)為聚簇索引和idx_name_birthday_phone_number索引建立2棵B+樹(shù)。下邊我們畫(huà)一下索引idx_name_birthday_phone_number的示意圖,不過(guò)既然我們已經(jīng)掌握了InnoDB的B+樹(shù)索引原理,那我們?cè)诋?huà)圖的時(shí)候?yàn)榱俗寛D更加清晰,所以在省略一些不必要的部分,比如記錄的額外信息,各頁(yè)面的頁(yè)號(hào)等等,其中內(nèi)節(jié)點(diǎn)中目錄項(xiàng)記錄的頁(yè)號(hào)信息我們用箭頭來(lái)代替,在記錄結(jié)構(gòu)中只保留name、birthday、phone_number、id這四個(gè)列的真實(shí)數(shù)據(jù)值,所以示意圖就長(zhǎng)這樣:
為了方便大家理解,我們特意標(biāo)明了哪些是內(nèi)節(jié)點(diǎn),哪些是葉子節(jié)點(diǎn)。再次強(qiáng)調(diào)一下,內(nèi)節(jié)點(diǎn)中存儲(chǔ)的是目錄項(xiàng)記錄,葉子節(jié)點(diǎn)中存儲(chǔ)的是用戶記錄(由于不是聚簇索引,所以用戶記錄是不完整的,缺少country列的值)。從圖中可以看出,這個(gè)idx_name_birthday_phone_number索引對(duì)應(yīng)的B+樹(shù)中頁(yè)面和記錄的排序方式就是這樣的:
- 先按照name列的值進(jìn)行排序。
- 如果name列的值相同,則按照birthday列的值進(jìn)行排序。
- 如果birthday列的值也相同,則按照phone_number的值進(jìn)行排序。
這個(gè)排序方式十分、特別、非常、巨、very very very重要,因?yàn)橹灰?yè)面和記錄是排好序的,我們就可以通過(guò)二分法來(lái)快速定位查找。下邊的內(nèi)容都仰仗這個(gè)圖了,大家對(duì)照著圖理解。
全值匹配
如果我們的搜索條件中的列和索引列一致的話,這種情況就稱(chēng)為全值匹配,比方說(shuō)下邊這個(gè)查找語(yǔ)句:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';我們建立的idx_name_birthday_phone_number索引包含的3個(gè)列在這個(gè)查詢語(yǔ)句中都展現(xiàn)出來(lái)了。大家可以想象一下這個(gè)查詢過(guò)程:
- 因?yàn)锽+樹(shù)的數(shù)據(jù)頁(yè)和記錄先是按照name列的值進(jìn)行排序的,所以先可以很快定位name列的值是Ashburn的記錄位置。
- 在name列相同的記錄里又是按照birthday列的值進(jìn)行排序的,所以在name列的值是Ashburn的記錄里又可以快速定位birthday列的值是'1990-09-27'的記錄。
- 如果很不幸,name和birthday列的值都是相同的,那記錄是按照phone_number列的值排序的,所以聯(lián)合索引中的三個(gè)列都可能被用到。
有的同學(xué)也許有個(gè)疑問(wèn),WHERE子句中的幾個(gè)搜索條件的順序?qū)Σ樵兘Y(jié)果有啥影響么?也就是說(shuō)如果我們調(diào)換name、birthday、phone_number這幾個(gè)搜索列的順序?qū)Σ樵兊膱?zhí)行過(guò)程有影響么?比方說(shuō)寫(xiě)成下邊這樣:
SELECT * FROM person_info WHERE birthday = '1990-09-27' AND phone_number = '15123983239' AND name = 'Ashburn';答案是:沒(méi)影響哈。MySQL有一個(gè)叫查詢優(yōu)化器的東東,會(huì)分析這些搜索條件并且按照可以使用的索引中列的順序來(lái)決定先使用哪個(gè)搜索條件,后使用哪個(gè)搜索條件。我們后邊兒會(huì)有專(zhuān)門(mén)的章節(jié)來(lái)介紹查詢優(yōu)化器,敬請(qǐng)期待。
匹配左邊的列
其實(shí)在我們的搜索語(yǔ)句中也可以不用包含全部聯(lián)合索引中的列,只包含左邊的就行,比方說(shuō)下邊的查詢語(yǔ)句:
SELECT * FROM person_info WHERE name = 'Ashburn';或者包含多個(gè)左邊的列也行:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';那為什么搜索條件中必須出現(xiàn)左邊的列才可以使用到這個(gè)B+樹(shù)索引呢?比如下邊的語(yǔ)句就用不到這個(gè)B+樹(shù)索引么?
SELECT * FROM person_info WHERE birthday = '1990-09-27';是的,的確用不到,因?yàn)锽+樹(shù)的數(shù)據(jù)頁(yè)和記錄先是按照name列的值排序的,在name列的值相同的情況下才使用birthday列進(jìn)行排序,也就是說(shuō)name列的值不同的記錄中birthday的值可能是無(wú)序的。而現(xiàn)在你跳過(guò)name列直接根據(jù)birthday的值去查找,臣妾做不到呀~ 那如果我就想在只使用birthday的值去通過(guò)B+樹(shù)索引進(jìn)行查找咋辦呢?這好辦,你再對(duì)birthday列建一個(gè)B+樹(shù)索引就行了,創(chuàng)建索引的語(yǔ)法不用我嘮叨了吧。
但是需要特別注意的一點(diǎn)是,如果我們想使用聯(lián)合索引中盡可能多的列,搜索條件中的各個(gè)列必須是聯(lián)合索引中從最左邊連續(xù)的列。比方說(shuō)聯(lián)合索引idx_name_birthday_phone_number中列的定義順序是name、birthday、phone_number,如果我們的搜索條件中只有name和phone_number,而沒(méi)有中間的birthday,比方說(shuō)這樣:
SELECT * FROM person_info WHERE name = 'Ashburn' AND phone_number = '15123983239';這樣只能用到name列的索引,birthday和phone_number的索引就用不上了,因?yàn)閚ame值相同的記錄先按照birthday的值進(jìn)行排序,birthday值相同的記錄才按照phone_number值進(jìn)行排序。
匹配列前綴
我們前邊說(shuō)過(guò)為某個(gè)列建立索引的意思其實(shí)就是在對(duì)應(yīng)的B+樹(shù)的記錄中使用該列的值進(jìn)行排序,比方說(shuō)person_info表上建立的聯(lián)合索引idx_name_birthday_phone_number會(huì)先用name列的值進(jìn)行排序,所以這個(gè)聯(lián)合索引對(duì)應(yīng)的B+樹(shù)中的記錄的name列的排列就是這樣的:
Aaron Aaron ... Aaron Asa Ashburn ... Ashburn Baird Barlow ... Barlow字符串排序的本質(zhì)就是比較哪個(gè)字符串大一點(diǎn)兒,哪個(gè)字符串小一點(diǎn),比較字符串大小就用到了該列的字符集和比較規(guī)則,這個(gè)我們前邊兒嘮叨過(guò),就不多嘮叨了。這里需要注意的是,一般的比較規(guī)則都是逐個(gè)比較字符的大小,也就是說(shuō)我們比較兩個(gè)字符串的大小的過(guò)程其實(shí)是這樣的:
- 先比較字符串的第一個(gè)字符,第一個(gè)字符小的那個(gè)字符串就比較小。
- 如果兩個(gè)字符串的第一個(gè)字符相同,那就再比較第二個(gè)字符,第二個(gè)字符比較小的那個(gè)字符串就比較小。
- 如果兩個(gè)字符串的第二個(gè)字符也相同,那就接著比較第三個(gè)字符,依此類(lèi)推。
所以一個(gè)排好序的字符串列其實(shí)有這樣的特點(diǎn):
- 先按照字符串的第一個(gè)字符進(jìn)行排序。
- 如果第一個(gè)字符相同再按照第二個(gè)字符進(jìn)行排序。
- 如果第二個(gè)字符相同再按照第三個(gè)字符進(jìn)行排序,依此類(lèi)推。
也就是說(shuō)這些字符串的前n個(gè)字符,也就是前綴都是排好序的,所以對(duì)于字符串類(lèi)型的索引列來(lái)說(shuō),我們只匹配它的前綴也是可以快速定位記錄的,比方說(shuō)我們想查詢名字以'As'開(kāi)頭的記錄,那就可以這么寫(xiě)查詢語(yǔ)句:
SELECT * FROM person_info WHERE name LIKE 'As%';但是需要注意的是,如果只給出后綴或者中間的某個(gè)字符串,比如這樣:
SELECT * FROM person_info WHERE name LIKE '%As%';MySQL就無(wú)法快速定位記錄位置了,因?yàn)樽址虚g有'As'的字符串并沒(méi)有排好序,所以只能全表掃描了。有時(shí)候我們有一些匹配某些字符串后綴的需求,比方說(shuō)某個(gè)表有一個(gè)url列,該列中存儲(chǔ)了許多url:
+----------------+ | url | +----------------+ | www.baidu.com | | www.google.com | | www.gov.cn | | ... | | www.wto.org | +----------------+假設(shè)已經(jīng)對(duì)該url列創(chuàng)建了索引,如果我們想查詢以com為后綴的網(wǎng)址的話可以這樣寫(xiě)查詢條件:WHERE url LIKE '%com',但是這樣的話無(wú)法使用該url列的索引。為了在查詢時(shí)用到這個(gè)索引而不至于全表掃描,我們可以把后綴查詢改寫(xiě)成前綴查詢,不過(guò)我們就得把表中的數(shù)據(jù)全部逆序存儲(chǔ)一下,也就是說(shuō)我們可以這樣保存url列中的數(shù)據(jù):
+----------------+ | url | +----------------+ | moc.udiab.www | | moc.elgoog.www | | nc.vog.www | | ... | | gro.otw.www | +----------------+這樣再查找以com為后綴的網(wǎng)址時(shí)搜索條件便可以這么寫(xiě):WHERE url LIKE 'moc%',這樣就可以用到索引了。
匹配范圍值
回頭看我們idx_name_birthday_phone_number索引的B+樹(shù)示意圖,所有記錄都是按照索引列的值從小到大的順序排好序的,所以這極大的方便我們查找索引列的值在某個(gè)范圍內(nèi)的記錄。比方說(shuō)下邊這個(gè)查詢語(yǔ)句:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';由于B+樹(shù)中的數(shù)據(jù)頁(yè)和記錄是先按name列排序的,所以我們上邊的查詢過(guò)程其實(shí)是這樣的:
- 通過(guò)B+樹(shù)在葉子節(jié)點(diǎn)中找到第一條name值大于Asa的二級(jí)索引記錄,讀取該記錄的主鍵值進(jìn)行回表操作,獲得對(duì)應(yīng)的聚簇索引記錄后發(fā)送給客戶端。
- 根據(jù)上一步找到的記錄,沿著記錄所在的鏈表向后查找(同一頁(yè)面中的記錄使用單向鏈表連接起來(lái),數(shù)據(jù)頁(yè)之間用雙向鏈表連接起來(lái))下一條二級(jí)索引記錄,判斷該記錄是否符合name < 'Barlow’條件,如果符合,則進(jìn)行回表操作后發(fā)送至客戶端。
- 重復(fù)上一步驟,直到某條二級(jí)索引記錄不符合name <'Barlow’條件為止。
不過(guò)在使用聯(lián)合進(jìn)行范圍查找的時(shí)候需要注意,如果對(duì)多個(gè)列同時(shí)進(jìn)行范圍查找的話,只有對(duì)索引最左邊的那個(gè)列進(jìn)行范圍查找的時(shí)候才能用到B+樹(shù)索引,比方說(shuō)這樣:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' AND birthday > '1980-01-01';上邊這個(gè)查詢可以分成兩個(gè)部分:
這樣子對(duì)于聯(lián)合索引idx_name_birthday_phone_number來(lái)說(shuō),只能用到name列的部分,而用不到birthday列的部分,因?yàn)橹挥衝ame值相同的情況下才能用birthday列的值進(jìn)行排序,而這個(gè)查詢中通過(guò)name進(jìn)行范圍查找的記錄中可能并不是按照birthday列進(jìn)行排序的,所以在搜索條件中繼續(xù)以birthday列進(jìn)行查找時(shí)是用不到這個(gè)B+樹(shù)索引的。
精確匹配某一列并范圍匹配另外一列
對(duì)于同一個(gè)聯(lián)合索引來(lái)說(shuō),雖然對(duì)多個(gè)列都進(jìn)行范圍查找時(shí)只能用到最左邊那個(gè)索引列,但是如果左邊的列是精確查找,則右邊的列可以進(jìn)行范圍查找,比方說(shuō)這樣:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone_number > '15100000000';這個(gè)查詢的條件可以分為3個(gè)部分:
同理,下邊的查詢也是可能用到這個(gè)idx_name_birthday_phone_number聯(lián)合索引的:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1980-01-01' AND phone_number > '15100000000';用于排序
我們?cè)趯?xiě)查詢語(yǔ)句的時(shí)候經(jīng)常需要對(duì)查詢出來(lái)的記錄通過(guò)ORDER BY子句按照某種規(guī)則進(jìn)行排序。一般情況下,我們只能把記錄都加載到內(nèi)存中,再用一些排序算法,比如快速排序、歸并排序、吧啦吧啦排序等等在內(nèi)存中對(duì)這些記錄進(jìn)行排序,有的時(shí)候可能查詢的結(jié)果集太大以至于不能在內(nèi)存中進(jìn)行排序的話,還可能暫時(shí)借助磁盤(pán)的空間來(lái)存放中間結(jié)果,排序操作完成后再把排好序的結(jié)果集返回到客戶端。在MySQL中,把這種在內(nèi)存中或者磁盤(pán)上進(jìn)行排序的方式統(tǒng)稱(chēng)為文件排序(英文名:filesort),跟文件這個(gè)詞兒一沾邊兒,就顯得這些排序操作非常慢了(磁盤(pán)和內(nèi)存的速度比起來(lái),就像是飛機(jī)和蝸牛的對(duì)比)。但是如果ORDER BY子句里使用到了我們的索引列,就有可能省去在內(nèi)存或文件中排序的步驟,比如下邊這個(gè)簡(jiǎn)單的查詢語(yǔ)句:
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;這個(gè)查詢的結(jié)果集需要先按照name值排序,如果記錄的name值相同,則需要按照birthday來(lái)排序,如果birthday的值相同,則需要按照phone_number排序。大家可以回過(guò)頭去看我們建立的idx_name_birthday_phone_number索引的示意圖,因?yàn)檫@個(gè)B+樹(shù)索引本身就是按照上述規(guī)則排好序的,所以直接從索引中提取數(shù)據(jù),然后進(jìn)行回表操作取出該索引中不包含的列就好了。簡(jiǎn)單吧?是的,索引就是這么牛逼。
使用聯(lián)合索引進(jìn)行排序注意事項(xiàng)
對(duì)于聯(lián)合索引有個(gè)問(wèn)題需要注意,ORDER BY的子句后邊的列的順序也必須按照索引列的順序給出,如果給出ORDER BY phone_number, birthday, name的順序,那也是用不了B+樹(shù)索引,這種顛倒順序就不能使用索引的原因我們上邊詳細(xì)說(shuō)過(guò)了,這就不贅述了。
同理,ORDER BY name、ORDER BY name, birthday這種匹配索引左邊的列的形式可以使用部分的B+樹(shù)索引。當(dāng)聯(lián)合索引左邊列的值為常量,也可以使用后邊的列進(jìn)行排序,比如這樣:
SELECT * FROM person_info WHERE name = 'A' ORDER BY birthday, phone_number LIMIT 10;這個(gè)查詢能使用聯(lián)合索引進(jìn)行排序是因?yàn)閚ame列的值相同的記錄是按照birthday, phone_number排序的,說(shuō)了好多遍了都。
不可以使用索引進(jìn)行排序的幾種情況
ASC、DESC混用
對(duì)于使用聯(lián)合索引進(jìn)行排序的場(chǎng)景,我們要求各個(gè)排序列的排序順序是一致的,也就是要么各個(gè)列都是ASC規(guī)則排序,要么都是DESC規(guī)則排序。
小貼士:ORDER BY子句后的列如果不加ASC或者DESC默認(rèn)是按照ASC排序規(guī)則排序的,也就是升序排序的。為啥會(huì)有這種奇葩規(guī)定呢?這個(gè)還得回頭想想這個(gè)idx_name_birthday_phone_number聯(lián)合索引中記錄的結(jié)構(gòu):
- 先按照記錄的name列的值進(jìn)行升序排列。
- 如果記錄的name列的值相同,再按照birthday列的值進(jìn)行升序排列。
- 如果記錄的birthday列的值相同,再按照phone_number列的值進(jìn)行升序排列。
如果查詢中的各個(gè)排序列的排序順序是一致的,比方說(shuō)下邊這兩種情況:
-
ORDER BY name, birthday LIMIT 10
這種情況直接從索引的最左邊開(kāi)始往右讀10行記錄就可以了。
-
ORDER BY name DESC, birthday DESC LIMIT 10,
這種情況直接從索引的最右邊開(kāi)始往左讀10行記錄就可以了。
但是如果我們查詢的需求是先按照name列進(jìn)行升序排列,再按照birthday列進(jìn)行降序排列的話,比如說(shuō)這樣的查詢語(yǔ)句:
SELECT * FROM person_info ORDER BY name, birthday DESC LIMIT 10;這樣如果使用索引排序的話過(guò)程就是這樣的:
- 先從索引的最左邊確定name列最小的值,然后找到name列等于該值的所有記錄,然后從name列等于該值的最右邊的那條記錄開(kāi)始往左找10條記錄。
- 如果name列等于最小的值的記錄不足10條,再繼續(xù)往右找name值第二小的記錄,重復(fù)上邊那個(gè)過(guò)程,直到找到10條記錄為止。
累不累?累!重點(diǎn)是這樣不能高效使用索引,而要采取更復(fù)雜的算法去從索引中取數(shù)據(jù),設(shè)計(jì)MySQL的大叔覺(jué)得這樣還不如直接文件排序來(lái)的快,所以就規(guī)定使用聯(lián)合索引的各個(gè)排序列的排序順序必須是一致的。
排序列包含非同一個(gè)索引的列
有時(shí)候用來(lái)排序的多個(gè)列不是一個(gè)索引里的,這種情況也不能使用索引進(jìn)行排序,比方說(shuō):
SELECT * FROM person_info ORDER BY name, country LIMIT 10;name和country并不屬于一個(gè)聯(lián)合索引中的列,所以無(wú)法使用索引進(jìn)行排序,至于為啥我就不想再?lài)Z叨了,自己用前邊的理論自己捋一捋吧~
排序列使用了復(fù)雜的表達(dá)式
要想使用索引進(jìn)行排序操作,必須保證索引列是以單獨(dú)列的形式出現(xiàn),而不是修飾過(guò)的形式,比方說(shuō)這樣:
SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10;使用了UPPER函數(shù)修飾過(guò)的列就不是單獨(dú)的列啦,這樣就無(wú)法使用索引進(jìn)行排序啦。
用于分組
有時(shí)候我們?yōu)榱朔奖憬y(tǒng)計(jì)表中的一些信息,會(huì)把表中的記錄按照某些列進(jìn)行分組。比如下邊這個(gè)分組查詢:
SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number這個(gè)查詢語(yǔ)句相當(dāng)于做了3次分組操作:
然后針對(duì)那些小小分組進(jìn)行統(tǒng)計(jì),比如在我們這個(gè)查詢語(yǔ)句中就是統(tǒng)計(jì)每個(gè)小小分組包含的記錄條數(shù)。如果沒(méi)有索引的話,這個(gè)分組過(guò)程全部需要在內(nèi)存里實(shí)現(xiàn),而如果有了索引的話,恰巧這個(gè)分組順序又和我們的B+樹(shù)中的索引列的順序是一致的,而我們的B+樹(shù)索引又是按照索引列排好序的,這不正好么,所以可以直接使用B+樹(shù)索引進(jìn)行分組。
和使用B+樹(shù)索引進(jìn)行排序是一個(gè)道理,分組列的順序也需要和索引列的順序一致,也可以只使用索引列中左邊的列進(jìn)行分組,吧啦吧啦的~
回表的代價(jià)
上邊的討論對(duì)回表這個(gè)詞兒多是一帶而過(guò),可能大家沒(méi)啥深刻的體會(huì),下邊我們?cè)敿?xì)嘮叨下。還是用idx_name_birthday_phone_number索引為例,看下邊這個(gè)查詢:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';在使用idx_name_birthday_phone_number索引進(jìn)行查詢時(shí)大致可以分為這兩個(gè)步驟:
由于索引idx_name_birthday_phone_number對(duì)應(yīng)的B+樹(shù)中的記錄首先會(huì)按照name列的值進(jìn)行排序,所以值在Asa~Barlow之間的記錄在磁盤(pán)中的存儲(chǔ)是相連的,集中分布在一個(gè)或幾個(gè)數(shù)據(jù)頁(yè)中,我們可以很快的把這些連著的記錄從磁盤(pán)中讀出來(lái),這種讀取方式我們也可以稱(chēng)為順序I/O。根據(jù)第1步中獲取到的記錄的id字段的值可能并不相連,而在聚簇索引中記錄是根據(jù)id(也就是主鍵)的順序排列的,所以根據(jù)這些并不連續(xù)的id值到聚簇索引中訪問(wèn)完整的用戶記錄可能分布在不同的數(shù)據(jù)頁(yè)中,這樣讀取完整的用戶記錄可能要訪問(wèn)更多的數(shù)據(jù)頁(yè),這種讀取方式我們也可以稱(chēng)為隨機(jī)I/O。一般情況下,順序I/O比隨機(jī)I/O的性能高很多,所以步驟1的執(zhí)行可能很快,而步驟2就慢一些。所以這個(gè)使用索引idx_name_birthday_phone_number的查詢有這么兩個(gè)特點(diǎn):
- 會(huì)使用到兩個(gè)B+樹(shù)索引,一個(gè)二級(jí)索引,一個(gè)聚簇索引。
- 訪問(wèn)二級(jí)索引使用順序I/O,訪問(wèn)聚簇索引使用隨機(jī)I/O。
需要回表的記錄越多,使用二級(jí)索引的性能就越低,甚至讓某些查詢寧愿使用全表掃描也不使用二級(jí)索引。比方說(shuō)name值在Asa~Barlow之間的用戶記錄數(shù)量占全部記錄數(shù)量90%以上,那么如果使用idx_name_birthday_phone_number索引的話,有90%多的id值需要回表,這不是吃力不討好么,還不如直接去掃描聚簇索引(也就是全表掃描)。
那什么時(shí)候采用全表掃描的方式,什么時(shí)候使用采用二級(jí)索引 + 回表的方式去執(zhí)行查詢呢?這個(gè)就是傳說(shuō)中的查詢優(yōu)化器做的工作,查詢優(yōu)化器會(huì)事先對(duì)表中的記錄計(jì)算一些統(tǒng)計(jì)數(shù)據(jù),然后再利用這些統(tǒng)計(jì)數(shù)據(jù)根據(jù)查詢的條件來(lái)計(jì)算一下需要回表的記錄數(shù),需要回表的記錄數(shù)越多,就越傾向于使用全表掃描,反之傾向于使用二級(jí)索引 + 回表的方式。當(dāng)然優(yōu)化器做的分析工作不僅僅是這么簡(jiǎn)單,但是大致上是個(gè)這個(gè)過(guò)程。一般情況下,限制查詢獲取較少的記錄數(shù)會(huì)讓優(yōu)化器更傾向于選擇使用二級(jí)索引 + 回表的方式進(jìn)行查詢,因?yàn)榛乇淼挠涗浽缴?#xff0c;性能提升就越高,比方說(shuō)上邊的查詢可以改寫(xiě)成這樣:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' LIMIT 10;添加了LIMIT 10的查詢更容易讓優(yōu)化器采用二級(jí)索引 + 回表的方式進(jìn)行查詢。
對(duì)于有排序需求的查詢,上邊討論的采用全表掃描還是二級(jí)索引 + 回表的方式進(jìn)行查詢的條件也是成立的,比方說(shuō)下邊這個(gè)查詢:
SELECT * FROM person_info ORDER BY name, birthday, phone_number;由于查詢列表是*,所以如果使用二級(jí)索引進(jìn)行排序的話,需要把排序完的二級(jí)索引記錄全部進(jìn)行回表操作,這樣操作的成本還不如直接遍歷聚簇索引然后再進(jìn)行文件排序(filesort)低,所以優(yōu)化器會(huì)傾向于使用全表掃描的方式執(zhí)行查詢。如果我們加了LIMIT子句,比如這樣:
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;這樣需要回表的記錄特別少,優(yōu)化器就會(huì)傾向于使用二級(jí)索引 + 回表的方式執(zhí)行查詢。
覆蓋索引
為了徹底告別回表操作帶來(lái)的性能損耗,我們建議:最好在查詢列表里只包含索引列,比如這樣:
SELECT name, birthday, phone_number FROM person_info WHERE name > 'Asa' AND name < 'Barlow'因?yàn)槲覀冎徊樵僴ame, birthday, phone_number這三個(gè)索引列的值,所以在通過(guò)idx_name_birthday_phone_number索引得到結(jié)果后就不必到聚簇索引中再查找記錄的剩余列,也就是country列的值了,這樣就省去了回表操作帶來(lái)的性能損耗。我們把這種只需要用到索引的查詢方式稱(chēng)為索引覆蓋。排序操作也優(yōu)先使用覆蓋索引的方式進(jìn)行查詢,比方說(shuō)這個(gè)查詢:
SELECT name, birthday, phone_number FROM person_info ORDER BY name, birthday, phone_number;雖然這個(gè)查詢中沒(méi)有LIMIT子句,但是采用了覆蓋索引,所以查詢優(yōu)化器就會(huì)直接使用idx_name_birthday_phone_number索引進(jìn)行排序而不需要回表操作了。
當(dāng)然,如果業(yè)務(wù)需要查詢出索引以外的列,那還是以保證業(yè)務(wù)需求為重。但是我們很不鼓勵(lì)用*號(hào)作為查詢列表,最好把我們需要查詢的列依次標(biāo)明。
如何挑選索引
上邊我們以idx_name_birthday_phone_number索引為例對(duì)索引的適用條件進(jìn)行了詳細(xì)的嘮叨,下邊看一下我們?cè)诮⑺饕龝r(shí)或者編寫(xiě)查詢語(yǔ)句時(shí)就應(yīng)該注意的一些事項(xiàng)。
只為用于搜索、排序或分組的列創(chuàng)建索引
也就是說(shuō),只為出現(xiàn)在WHERE子句中的列、連接子句中的連接列,或者出現(xiàn)在ORDER BY或GROUP BY子句中的列創(chuàng)建索引。而出現(xiàn)在查詢列表中的列就沒(méi)必要建立索引了:
SELECT birthday, country FROM person_name WHERE name = 'Ashburn';像查詢列表中的birthday、country這兩個(gè)列就不需要建立索引,我們只需要為出現(xiàn)在WHERE子句中的name列創(chuàng)建索引就可以了。
考慮列的基數(shù)
列的基數(shù)指的是某一列中不重復(fù)數(shù)據(jù)的個(gè)數(shù),比方說(shuō)某個(gè)列包含值2, 5, 8, 2, 5, 8, 2, 5, 8,雖然有9條記錄,但該列的基數(shù)卻是3。也就是說(shuō),在記錄行數(shù)一定的情況下,列的基數(shù)越大,該列中的值越分散,列的基數(shù)越小,該列中的值越集中。這個(gè)列的基數(shù)指標(biāo)非常重要,直接影響我們是否能有效的利用索引。假設(shè)某個(gè)列的基數(shù)為1,也就是所有記錄在該列中的值都一樣,那為該列建立索引是沒(méi)有用的,因?yàn)樗兄刀家粯泳蜔o(wú)法排序,無(wú)法進(jìn)行快速查找了~ 而且如果某個(gè)建立了二級(jí)索引的列的重復(fù)值特別多,那么使用這個(gè)二級(jí)索引查出的記錄還可能要做回表操作,這樣性能損耗就更大了。所以結(jié)論就是:最好為那些列的基數(shù)大的列建立索引,為基數(shù)太小列的建立索引效果可能不好。
索引列的類(lèi)型盡量小
我們?cè)诙x表結(jié)構(gòu)的時(shí)候要顯式的指定列的類(lèi)型,以整數(shù)類(lèi)型為例,有TINYINT、MEDIUMINT、INT、BIGINT這么幾種,它們占用的存儲(chǔ)空間依次遞增,我們這里所說(shuō)的類(lèi)型大小指的就是該類(lèi)型表示的數(shù)據(jù)范圍的大小。能表示的整數(shù)范圍當(dāng)然也是依次遞增,如果我們想要對(duì)某個(gè)整數(shù)列建立索引的話,在表示的整數(shù)范圍允許的情況下,盡量讓索引列使用較小的類(lèi)型,比如我們能使用INT就不要使用BIGINT,能使用MEDIUMINT就不要使用INT~ 這是因?yàn)?#xff1a;
- 數(shù)據(jù)類(lèi)型越小,在查詢時(shí)進(jìn)行的比較操作越快(這是CPU層次的東東)
- 數(shù)據(jù)類(lèi)型越小,索引占用的存儲(chǔ)空間就越少,在一個(gè)數(shù)據(jù)頁(yè)內(nèi)就可以放下更多的記錄,從而減少磁盤(pán)I/O帶來(lái)的性能損耗,也就意味著可以把更多的數(shù)據(jù)頁(yè)緩存在內(nèi)存中,從而加快讀寫(xiě)效率。
這個(gè)建議對(duì)于表的主鍵來(lái)說(shuō)更加適用,因?yàn)椴粌H是聚簇索引中會(huì)存儲(chǔ)主鍵值,其他所有的二級(jí)索引的節(jié)點(diǎn)處都會(huì)存儲(chǔ)一份記錄的主鍵值,如果主鍵適用更小的數(shù)據(jù)類(lèi)型,也就意味著節(jié)省更多的存儲(chǔ)空間和更高效的I/O。
索引字符串值的前綴
我們知道一個(gè)字符串其實(shí)是由若干個(gè)字符組成,如果我們?cè)贛ySQL中使用utf8字符集去存儲(chǔ)字符串的話,編碼一個(gè)字符需要占用1~3個(gè)字節(jié)。假設(shè)我們的字符串很長(zhǎng),那存儲(chǔ)一個(gè)字符串就需要占用很大的存儲(chǔ)空間。在我們需要為這個(gè)字符串列建立索引時(shí),那就意味著在對(duì)應(yīng)的B+樹(shù)中有這么兩個(gè)問(wèn)題:
- B+樹(shù)索引中的記錄需要把該列的完整字符串存儲(chǔ)起來(lái),而且字符串越長(zhǎng),在索引中占用的存儲(chǔ)空間越大。
- 如果B+樹(shù)索引中索引列存儲(chǔ)的字符串很長(zhǎng),那在做字符串比較時(shí)會(huì)占用更多的時(shí)間。
我們前邊兒說(shuō)過(guò)索引列的字符串前綴其實(shí)也是排好序的,所以索引的設(shè)計(jì)者提出了個(gè)方案 — 只對(duì)字符串的前幾個(gè)字符進(jìn)行索引也就是說(shuō)在二級(jí)索引的記錄中只保留字符串前幾個(gè)字符。這樣在查找記錄時(shí)雖然不能精確的定位到記錄的位置,但是能定位到相應(yīng)前綴所在的位置,然后根據(jù)前綴相同的記錄的主鍵值回表查詢完整的字符串值,再對(duì)比就好了。這樣只在B+樹(shù)中存儲(chǔ)字符串的前幾個(gè)字符的編碼,既節(jié)約空間,又減少了字符串的比較時(shí)間,還大概能解決排序的問(wèn)題,何樂(lè)而不為,比方說(shuō)我們?cè)诮ū碚Z(yǔ)句中只對(duì)name列的前10個(gè)字符進(jìn)行索引可以這么寫(xiě):
CREATE TABLE person_info(name VARCHAR(100) NOT NULL,birthday DATE NOT NULL,phone_number CHAR(11) NOT NULL,country varchar(100) NOT NULL,KEY idx_name_birthday_phone_number (name(10), birthday, phone_number) );name(10)就表示在建立的B+樹(shù)索引中只保留記錄的前10個(gè)字符的編碼,這種只索引字符串值的前綴的策略是我們非常鼓勵(lì)的,尤其是在字符串類(lèi)型能存儲(chǔ)的字符比較多的時(shí)候。
索引列前綴對(duì)排序的影響
如果使用了索引列前綴,比方說(shuō)前邊只把name列的前10個(gè)字符放到了二級(jí)索引中,下邊這個(gè)查詢可能就有點(diǎn)兒尷尬了:
SELECT * FROM person_info ORDER BY name LIMIT 10;因?yàn)槎?jí)索引中不包含完整的name列信息,所以無(wú)法對(duì)前十個(gè)字符相同,后邊的字符不同的記錄進(jìn)行排序,也就是使用索引列前綴的方式無(wú)法支持使用索引排序,只好乖乖的用文件排序嘍。
讓索引列在比較表達(dá)式中單獨(dú)出現(xiàn)
假設(shè)表中有一個(gè)整數(shù)列my_col,我們?yōu)檫@個(gè)列建立了索引。下邊的兩個(gè)WHERE子句雖然語(yǔ)義是一致的,但是在效率上卻有差別:
第1個(gè)WHERE子句中my_col列并不是以單獨(dú)列的形式出現(xiàn)的,而是以my_col * 2這樣的表達(dá)式的形式出現(xiàn)的,存儲(chǔ)引擎會(huì)依次遍歷所有的記錄,計(jì)算這個(gè)表達(dá)式的值是不是小于4,所以這種情況下是使用不到為my_col列建立的B+樹(shù)索引的。而第2個(gè)WHERE子句中my_col列并是以單獨(dú)列的形式出現(xiàn)的,這樣的情況可以直接使用B+樹(shù)索引。
所以結(jié)論就是:如果索引列在比較表達(dá)式中不是以單獨(dú)列的形式出現(xiàn),而是以某個(gè)表達(dá)式,或者函數(shù)調(diào)用形式出現(xiàn)的話,是用不到索引的。
主鍵插入順序
我們知道,對(duì)于一個(gè)使用InnoDB存儲(chǔ)引擎的表來(lái)說(shuō),在我們沒(méi)有顯式的創(chuàng)建索引時(shí),表中的數(shù)據(jù)實(shí)際上都是存儲(chǔ)在聚簇索引的葉子節(jié)點(diǎn)的。而記錄又是存儲(chǔ)在數(shù)據(jù)頁(yè)中的,數(shù)據(jù)頁(yè)和記錄又是按照記錄主鍵值從小到大的順序進(jìn)行排序,所以如果我們插入的記錄的主鍵值是依次增大的話,那我們每插滿一個(gè)數(shù)據(jù)頁(yè)就換到下一個(gè)數(shù)據(jù)頁(yè)繼續(xù)插,而如果我們插入的主鍵值忽大忽小的話,這就比較麻煩了,假設(shè)某個(gè)數(shù)據(jù)頁(yè)存儲(chǔ)的記錄已經(jīng)滿了,它存儲(chǔ)的主鍵值在1~100之間:
如果此時(shí)再插入一條主鍵值為9的記錄,那它插入的位置就如下圖:
可這個(gè)數(shù)據(jù)頁(yè)已經(jīng)滿了啊,再插進(jìn)來(lái)咋辦呢?我們需要把當(dāng)前頁(yè)面分裂成兩個(gè)頁(yè)面,把本頁(yè)中的一些記錄移動(dòng)到新創(chuàng)建的這個(gè)頁(yè)中。頁(yè)面分裂和記錄移位意味著什么?意味著:性能損耗!所以如果我們想盡量避免這樣無(wú)謂的性能損耗,最好讓插入的記錄的主鍵值依次遞增,這樣就不會(huì)發(fā)生這樣的性能損耗了。所以我們建議:讓主鍵具有AUTO_INCREMENT,讓存儲(chǔ)引擎自己為表生成主鍵,而不是我們手動(dòng)插入 ,比方說(shuō)我們可以這樣定義person_info表:
CREATE TABLE person_info(id INT UNSIGNED NOT NULL AUTO_INCREMENT,name VARCHAR(100) NOT NULL,birthday DATE NOT NULL,phone_number CHAR(11) NOT NULL,country varchar(100) NOT NULL,PRIMARY KEY (id),KEY idx_name_birthday_phone_number (name(10), birthday, phone_number) );我們自定義的主鍵列id擁有AUTO_INCREMENT屬性,在插入記錄時(shí)存儲(chǔ)引擎會(huì)自動(dòng)為我們填入自增的主鍵值。
冗余和重復(fù)索引
有時(shí)候有的同學(xué)有意或者無(wú)意的就對(duì)同一個(gè)列創(chuàng)建了多個(gè)索引,比方說(shuō)這樣寫(xiě)建表語(yǔ)句:
CREATE TABLE person_info(id INT UNSIGNED NOT NULL AUTO_INCREMENT,name VARCHAR(100) NOT NULL,birthday DATE NOT NULL,phone_number CHAR(11) NOT NULL,country varchar(100) NOT NULL,PRIMARY KEY (id),KEY idx_name_birthday_phone_number (name(10), birthday, phone_number),KEY idx_name (name(10)) );我們知道,通過(guò)idx_name_birthday_phone_number索引就可以對(duì)name列進(jìn)行快速搜索,再創(chuàng)建一個(gè)專(zhuān)門(mén)針對(duì)name列的索引就算是一個(gè)冗余索引,維護(hù)這個(gè)索引只會(huì)增加維護(hù)的成本,并不會(huì)對(duì)搜索有什么好處。
另一種情況,我們可能會(huì)對(duì)某個(gè)列重復(fù)建立索引,比方說(shuō)這樣:
CREATE TABLE repeat_index_demo (c1 INT PRIMARY KEY,c2 INT,UNIQUE uidx_c1 (c1),INDEX idx_c1 (c1) );我們看到,c1既是主鍵、又給它定義為一個(gè)唯一索引,還給它定義了一個(gè)普通索引,可是主鍵本身就會(huì)生成聚簇索引,所以定義的唯一索引和普通索引是重復(fù)的,這種情況要避免。
總結(jié)
上邊只是我們?cè)趧?chuàng)建和使用B+樹(shù)索引的過(guò)程中需要注意的一些點(diǎn),后邊我們還會(huì)陸續(xù)介紹更多的優(yōu)化方法和注意事項(xiàng),敬請(qǐng)期待。本集內(nèi)容總結(jié)如下:
- 全值匹配
- 匹配左邊的列
- 匹配范圍值
- 精確匹配某一列并范圍匹配另外一列
- 用于排序
- 用于分組
- 只為用于搜索、排序或分組的列創(chuàng)建索引
- 為列的基數(shù)大的列創(chuàng)建索引
- 索引列的類(lèi)型盡量小
- 可以只對(duì)字符串值的前綴建立索引
- 只有索引列在比較表達(dá)式中單獨(dú)出現(xiàn)才可以適用索引
- 為了盡可能少的讓聚簇索引發(fā)生頁(yè)面分裂和記錄移位的情況,建議讓主鍵擁有AUTO_INCREMENT屬性。
- 定位并刪除表中的重復(fù)和冗余索引
- 盡量使用覆蓋索引進(jìn)行查詢,避免回表帶來(lái)的性能損耗。
總結(jié)
以上是生活随笔為你收集整理的Mysql原理篇之索引不懂不要瞎用---04的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 列表到字典的函数,针对好玩游戏物品清单
- 下一篇: 【Java学习笔记】 MYSQL03 学