B+树索引的使用
B+樹索引的使用
- 索引的代價(jià)
- B+樹索引適用的條件
- 全值匹配
- 最左匹配原則
- 匹配列前綴
- 匹配范圍值
- ORDER BY
- GROUP BY
- 回表的代價(jià)
- 覆蓋索引
索引的代價(jià)
索引雖好,但是不能夠亂建。
- 空間上的代價(jià)
每個建立一個索引都需要建立一顆B+樹,每棵B+樹的每一個節(jié)點(diǎn)都是一個數(shù)據(jù)頁,一個頁默認(rèn)會占用16KB的存儲空間,而一顆索引樹由許多頁組成… - 時(shí)間上的代價(jià)
索引雖然讓我們查詢變得更為快速,我們都知道B+樹每層節(jié)點(diǎn)都是按照索引列的值從小到大的順序排序而組成雙向鏈表。無論是葉子節(jié)點(diǎn)的記錄,還是內(nèi)節(jié)點(diǎn)的記錄都是按照索引列的值從小到大的順序而形成的一個單向鏈表。我們每次對表中的數(shù)據(jù)進(jìn)行CUD操作時(shí),可能會對節(jié)點(diǎn)和記錄排序造成破壞,所以存儲引擎需要額外的時(shí)間進(jìn)行移位、頁分裂、頁面回收等操作來維護(hù)節(jié)點(diǎn)和記錄的順序。對性能影響較大。
B+樹索引適用的條件
以下面的表為例
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) );二級索引如圖(記錄結(jié)構(gòu)中保留了name,birthday,phone_number,id):
全值匹配
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';查詢流程如下:
- 在二級索引中,先按照name列進(jìn)行排序索引,很快能夠找到name列的值是Ashburn的位置。
- 在name列相同的記錄里,在按照birthday列進(jìn)行排序索引,很快就能夠找到name列為Ashburn,birthday列是1990-09-27的記錄。
- 在name列和birthday值相同的數(shù)據(jù)中查找,根據(jù)phone_number排序索引,最終找到唯一確定的那條記錄。
注意:MySQL中有優(yōu)化器能夠在查詢之前將我們的SQL語句優(yōu)化為可按照索引的查詢的形式(當(dāng)然如果無能夠使用的索引則沒辦法)
最左匹配原則
對于以下三條SQL,為什么第三條索引無法走索引呢?
SELECT * FROM person_info WHERE name = 'Ashburn';SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';SELECT * FROM person_info WHERE birthday = '1990-09-27';前面我們闡述了查找過程,因?yàn)锽+樹的數(shù)據(jù)頁和記錄先是按照name列的值排序的,在name列的值相同的情況下才使用birthday列進(jìn)行排序,即name列不相同的記錄中birthday的值不確保有序。
如果我們跳過索引的順序直接查找可能的沒辦法走索引的。如果我們想使用聯(lián)合索引中盡可能多的列,搜索條件中的各個列必須是聯(lián)合索引中從最左邊連續(xù)的列
比如說:
這樣的只能用到name列的索引,birthday和phone_number的索引均用不到,理由前面闡述過了。
匹配列前綴
比如我們想查詢url以www.開頭的記錄:
> SELECT * FROM url WHERE name LIKE 'www.%'; +----------------+ | url | +----------------+ | www.baidu.com | | www.google.com | | www.gov.cn | | ... | | www.wto.org | +----------------+假設(shè)我們已經(jīng)給url列創(chuàng)建了索引,如果我們想查詢以com結(jié)尾的網(wǎng)址的話索引條件:WHERE url LIKE '%com',這樣的話我們是無法使用url列索引的。
為什么呢?
牽扯到MySQL中的字符串排序規(guī)則,大概是這樣的:
-
先比較字符串的第一個字符,第一個字符小的那個字符串就比較小。
-
如果兩個字符串的第一個字符相同,那就再比較第二個字符,第二個字符比較小的那個字符串就比較小。
-
如果兩個字符串的第二個字符也相同,那就接著比較第三個字符,依此類推。
所以一個排好序的字符串列其實(shí)有這樣的特點(diǎn):
-
先按照字符串的第一個字符進(jìn)行排序。
-
如果第一個字符相同再按照第二個字符進(jìn)行排序。
-
如果第二個字符相同再按照第三個字符進(jìn)行排序,依此類推。
這樣就會有一個問題——我們字符串的前綴的有序排列的,是能夠索引的,所以LIKE 'www.%'能夠走索引,而%.com這樣的因?yàn)橹挥星熬Y有序,尾綴是com字符串并沒有排好序,所以MySQL無法快速定位記錄的位置,只能夠全表查詢。
對于這樣的數(shù)據(jù),我們能夠?qū)?shù)據(jù)倒置存儲,這樣就能夠索引moc,如:
+----------------+ | url | +----------------+ | moc.udiab.www | | moc.elgoog.www | | nc.vog.www | | ... | | gro.otw.www | +----------------+匹配范圍值
回頭看第一個例子,B+樹中所有的記錄都是按照索引列的值從小到大排序,所以這也讓我們的范圍查詢更加的方便了,比如
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';由于B+樹中的數(shù)據(jù)頁和記錄是先按name列排序的,所以我們上邊的查詢過程其實(shí)是這樣的:
- 找到name值為Asa的記錄。
- 找到name值為Barlow的記錄。
由于所有記錄都是由鏈表連起來的(記錄之間用單鏈表,數(shù)據(jù)頁之間用雙鏈表),所以他們之間的記錄都可以很容易的取出
找到這些記錄的主鍵值,再到聚簇索引中回表查找完整的記錄。
同樣的也需要遵從最左匹配原則,否則會全表查詢
ORDER BY
需要對查詢出來的記錄通過ORDER BY子句按照某種規(guī)則進(jìn)行排序時(shí),一般情況下,我們只能把記錄都加載到內(nèi)存中,再用一些排序算法,比如快速排序、歸并排序等等在內(nèi)存中對這些記錄進(jìn)行排序,有的時(shí)候可能查詢的結(jié)果集太大以至于不能在內(nèi)存中進(jìn)行排序的話,還可能暫時(shí)借助磁盤的空間來存放中間結(jié)果,排序操作完成后再把排好序的結(jié)果集返回到客戶端。在MySQL中,把這種在內(nèi)存中或者磁盤上進(jìn)行排序的方式統(tǒng)稱為文件排序,跟文件這個詞兒一沾邊兒,就顯得這些排序操作非常慢了。
但是如果ORDER BY子句里使用到了索引列,就有可能省去在內(nèi)存或文件中排序的步驟,比如下邊這個簡單的查詢語句:
ORDER BY默認(rèn)升序排序
這個查詢的結(jié)果集需要先按照name值排序,如果記錄的name值相同,則需要按照birthday來排序,如果birthday的值相同,則需要按照phone_number排序。數(shù)據(jù)在查詢的過程中已經(jīng)完成了排序。
對于聯(lián)合索引有個問題需要注意:ORDER BY的子句后邊的列的順序也必須按照索引列的順序給出,如果給出ORDER BY phone_number, birthday, name的順序,那用不了B+樹索引。
不可以使用索引的情況:
- ASC和DESC混用
對于使用聯(lián)合索引進(jìn)行排序的場景,我們要求各個排序列的排序順序是一致的,也就是要么各個列都是ASC規(guī)則排序,要么都是DESC規(guī)則排序。
原因:
在查詢中的排序順序是一致的情況下,例如:
ORDER BY name, birthday LIMIT 10 這樣直接從索引的最左邊開始往右讀取10行記錄就可以。
ORDER BY name DESC, birthday DESC LIMIT 10這樣直接從索引的最右邊開始往左讀10行記錄就可以。
如果我們先按照name列進(jìn)行升序排列,再按照birthday列進(jìn)行降序排列的話,比如說這樣的查詢語句:
索引過程:
-
先從索引的最左邊確定name列最小的值,然后找到name列等于該值的所有記錄,然后從name列等于該值的最右邊的那條記錄開始往左找10條記錄。
-
如果name列等于最小的值的記錄不足10條,再繼續(xù)往右找name值第二小的記錄,重復(fù)上邊那個過程,直到找到10條記錄為止。
-
排序列使用了復(fù)雜的表達(dá)式
要想使用索引進(jìn)行排序操作,必須保證索引列是以單獨(dú)列的形式出現(xiàn),而不是修飾過的形式,比方說這樣:
使用了UPPER函數(shù)修飾過的列就不是單獨(dú)的列了,這樣就無法使用索引進(jìn)行排序。
GROUP BY
有時(shí)候我們?yōu)榱朔奖憬y(tǒng)計(jì)表中的一些信息,會把表中的記錄按照某些列進(jìn)行分組。比如下邊這個分組查詢:
SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number這個查詢語句相當(dāng)于做了3次分組操作:
-
先把記錄按照name值進(jìn)行分組,所有name值相同的記錄劃分為一組。
-
將每個name值相同的分組里的記錄再按照birthday的值進(jìn)行分組,將birthday值相同的記錄放到一個小分組里,所以看起來就像在一個大分組里又化分了好多小分組。
-
再將上一步中產(chǎn)生的小分組按照phone_number的值分成更小的分組,所以整體上看起來就像是先把記錄分成一個大分組,然后把大分組分成若干個小分組,然后把若干個小分組再細(xì)分成更多的小小分組。
然后針對那些小小分組進(jìn)行統(tǒng)計(jì),比如在我們這個查詢語句中就是統(tǒng)計(jì)每個小小分組包含的記錄條數(shù)。如果沒有索引的話,這個分組過程全部需要在內(nèi)存里實(shí)現(xiàn),而如果有了索引的話,恰巧這個分組順序又和我們的B+樹中的索引列的順序是一致的,而我們的B+樹索引又是按照索引列排好序的,這不正好么,所以可以直接使用B+樹索引進(jìn)行分組。
和使用B+樹索引進(jìn)行排序是一個道理,分組列的順序也需要和索引列的順序一致,也可以只使用索引列中左邊的列進(jìn)行分組
回表的代價(jià)
例如這個查詢:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';在使用二級索引進(jìn)行查詢時(shí)大致可以分為兩個步驟:
從索引對應(yīng)的B+樹中取出name值在Asa~Barlow之間的用戶記錄。
由于索引對應(yīng)的B+樹用戶記錄中只包含name、birthday、phone_number、id這4個字段,而查詢列表是*,意味著要查詢表中所有字段,也就是還要包括country字段。這時(shí)需要把從上一步中獲取到的每一條記錄的id字段都到聚簇索引對應(yīng)的B+樹中找到完整的用戶記錄,也就是我們通常所說的回表,然后把完整的用戶記錄返回給查詢用戶。
由于二級索引對應(yīng)的B+樹會按照name列的值進(jìn)行排序,所以值在Asa~Barlow之間的記錄在磁盤中的存儲是相連的,集中分布在一個或幾個數(shù)據(jù)頁中,我們可以很快的把這些記錄從磁盤讀取出來,這種讀取方式稱為順序I/O,而我們根據(jù)二級索引獲取的主鍵是不連續(xù)的,把這些不連續(xù)的主鍵值到聚簇索引中訪問完整的數(shù)據(jù)記錄,但是完整的用戶記錄很可能分布在不同的數(shù)據(jù)頁中,這樣讀取完整的用戶記錄可能要訪問更多的數(shù)據(jù)頁,這種讀取方式我們也可以稱為隨機(jī)I/O,一般情況下,順序I/O比隨機(jī)I/O性能高很多.
所以這個使用索引的查詢有這么兩個特點(diǎn):
需要回表的記錄越多,使用二級索引的性能就越低 甚至讓某些查詢寧愿使用全表掃描也不使用二級索引。
那什么時(shí)候采用全表掃描的方式,什么時(shí)候使用采用二級索引 + 回表的方式去執(zhí)行查詢呢?這個就是傳說中的查詢優(yōu)化器做的工作,查詢優(yōu)化器會事先對表中的記錄計(jì)算一些統(tǒng)計(jì)數(shù)據(jù),然后再利用這些統(tǒng)計(jì)數(shù)據(jù)根據(jù)查詢的條件來計(jì)算一下需要回表的記錄數(shù),需要回表的記錄數(shù)越多,就越傾向于使用全表掃描,反之傾向于使用二級索引 + 回表的方式。
覆蓋索引
為了解決回表帶來的性能損耗,最好在查詢列表中只包含索引列
比如:
因?yàn)槲覀儾樵兊氖撬饕?#xff0c;所以索引得到的結(jié)果就不必到聚簇索引中再查找記錄的剩余列。
總結(jié)
- 上一篇: bed格式转vcf格式
- 下一篇: 数码视讯Q5、Q7_晶晨S905L/M/