Mysql B+树索引的使用
原文地址:https://amos-x.com/index.php/amos/archives/mysql-btree-use/
正文
上一篇,說了B+樹索引的原理和其為什么能提高查詢效率,那么有個索引時不是就可以為所欲為呢,答案顯然并不是,下面就詳細介紹一下,索引的使用和限制,這雖然不常在面試中出現,但能幫助我們更好的設計數據庫Model結構和使用數據庫查詢語句,以提升系統性能。
我覺得是必須知道的重要知識,很有其必要。
正文
我們前邊詳細、詳細又詳細的嘮叨了InnoDB存儲引擎的B+樹索引,我們必須熟悉下邊這些結論:
- 每個索引都對應一棵B+樹,B+樹分為好多層,最下邊一層是葉子節點,其余的是內節點。所有用戶記錄都存儲在B+樹的葉子節點,所有目錄項記錄都存儲在內節點。
- InnoDB存儲引擎會自動為主鍵(如果沒有它會自動幫我們添加)建立聚簇索引,聚簇索引的葉子節點包含完整的用戶記錄。
- 我們可以為自己感興趣的列建立二級索引,二級索引的葉子節點包含的用戶記錄由索引列 + 主鍵組成,所以如果想通過二級索引來查找完整的用戶記錄的話,需要通過回表操作,也就是在通過二級索引找到主鍵值之后再到聚簇索引中查找完整的用戶記錄。
- B+樹中每層節點都是按照索引列值從小到大的順序排序而組成了雙向鏈表,而且每個頁內的記錄(不論是用戶記錄還是目錄項記錄)都是按照索引列的值從小到大的順序而形成了一個單鏈表。如果是聯合索引的話,則頁面和記錄先按照聯合索引前邊的列排序,如果該列值相同,再按照聯合索引后邊的列排序。
- 通過索引查找記錄是從B+樹的根節點開始,一層一層向下搜索。由于每個頁面都按照索引列的值建立了Page Directory(頁目錄),所以在這些頁面中的查找非常快。
如果你讀上邊的幾點結論有些任何一點點疑惑的話,那下邊的內容不適合你,回過頭先去看前邊的內容去。
索引的代價
在熟悉了B+樹索引原理之后,本篇文章的主題是嘮叨如何更好的使用索引,雖然索引是個好東西,可不能亂建,在介紹如何更好的使用索引之前先要了解一下使用這玩意兒的代價,它在空間和時間上都會拖后腿:
- 空間上的代價這個是顯而易見的,每建立一個索引都要為它建立一棵B+樹,每一棵B+樹的每一個節點都是一個數據頁,一個頁默認會占用16KB的存儲空間,一棵很大的B+樹由許多數據頁組成,那可是很大的一片存儲空間呢。
- 時間上的代價每次對表中的數據進行增、刪、改操作時,都需要去修改各個B+樹索引。而且我們講過,B+樹每層節點都是按照索引列的值從小到大的順序排序而組成了雙向鏈表。不論是葉子節點中的記錄,還是內節點中的記錄(也就是不論是用戶記錄還是目錄項記錄)都是按照索引列的值從小到大的順序而形成了一個單向鏈表。而增、刪、改操作可能會對節點和記錄的排序造成破壞,所以存儲引擎需要額外的時間進行一些記錄移位,頁面分裂、頁面回收啥的操作來維護好節點和記錄的排序。如果我們建了許多索引,每個索引對應的B+樹都要進行相關的維護操作,這還能不給性能拖后腿么?
所以說,一個表上索引建的越多,就會占用越多的存儲空間,在增刪改記錄的時候性能就越差。為了能建立又好又少的索引,我們先得學學這些索引在哪些條件下起作用的。
B+樹索引適用的條件
下邊我們將嘮叨許多種讓B+樹索引發揮最大效能的技巧和注意事項,不過大家要清楚,所有的技巧都是源自你對B+樹索引本質的理解,所以如果你還不能保證對B+樹索引充分的理解,那么再次建議回過頭把前邊的內容看完了再來,要不然讀文章對你來說是一種折磨。首先,B+樹索引并不是萬能的,并不是所有的查詢語句都能用到我們建立的索引。下邊介紹幾個我們可能使用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) );對于這個person_info表我們需要注意兩點:
- 表中的主鍵是id列,它存儲一個自動遞增的整數。所以InnoDB存儲引擎會自動為id列建立聚簇索引。
- 我們額外定義了一個二級索引idx_name_birthday_phone_number,它是由3個列組成的聯合索引。所以在這個索引對應的B+樹的葉子節點處存儲的用戶記錄只保留name、birthday、phone_number這三個列的值以及主鍵id的值,并不會保存country列的值。
從這兩點注意中我們可以再次看到,一個表中有多少索引就會建立多少棵B+樹,person_info表會為聚簇索引和idx_name_birthday_phone_number索引建立2棵B+樹。下邊我們畫一下索引idx_name_birthday_phone_number的示意圖,不過既然我們已經掌握了InnoDB的B+樹索引原理,那我們在畫圖的時候為了讓圖更加清晰,所以在省略一些不必要的部分,比如記錄的額外信息,各頁面的頁號等等,其中內節點中目錄項記錄的頁號信息我們用箭頭來代替,在記錄結構中只保留name、birthday、phone_number、id這四個列的真實數據值,所以示意圖就長這樣:
為了方便大家理解,我們特意標明了哪些是內節點,哪些是葉子節點。再次強調一下,內節點中存儲的是目錄項記錄,葉子節點中存儲的是用戶記錄(由于不是聚簇索引,所以用戶記錄是不完整的,缺少country列的值)。從圖中可以看出,這個idx_name_birthday_phone_number索引對應的B+樹中頁面和記錄的排序方式就是這樣的:
- 先按照name列的值進行排序。
- 如果name列的值相同,則按照birthday列的值進行排序。
- 如果birthday列的值也相同,則按照phone_number的值進行排序。
這個排序方式十分、特別、非常、巨、very very very重要,因為只要頁面和記錄是排好序的,我們就可以通過二分法來快速定位查找。下邊的內容都仰仗這個圖了,大家對照著圖理解。
全值匹配
如果我們的搜索條件中的列和索引列一致的話,這種情況就稱為全值匹配,比方說下邊這個查找語句:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';我們建立的idx_name_birthday_phone_number索引包含的3個列在這個查詢語句中都展現出來了。大家可以想象一下這個查詢過程:
- 因為B+樹的數據頁和記錄先是按照name列的值進行排序的,所以先可以很快定位name列的值是Ashburn的記錄位置。
- 在name列相同的記錄里又是按照birthday列的值進行排序的,所以在name列的值是Ashburn的記錄里又可以快速定位birthday列的值是'1990-09-27'的記錄。
- 如果很不幸,name和birthday列的值都是相同的,那記錄是按照phone_number列的值排序的,所以聯合索引中的三個列都可能被用到。
有的同學也許有個疑問,WHERE子句中的幾個搜索條件的順序對查詢結果有啥影響么?也就是說如果我們調換name、birthday、phone_number這幾個搜索列的順序對查詢的執行過程有影響么?比方說寫成下邊這樣:
SELECT * FROM person_info WHERE birthday = '1990-09-27' AND phone_number = '15123983239' AND name = 'Ashburn';答案是:沒影響哈。MySQL有一個叫查詢優化器的東東,會分析這些搜索條件并且按照可以使用的索引中列的順序來決定先使用哪個搜索條件,后使用哪個搜索條件。我們后邊兒會有專門的章節來介紹查詢優化器,敬請期待。
匹配左邊的列
其實在我們的搜索語句中也可以不用包含全部聯合索引中的列,只包含左邊的就行,比方說下邊的查詢語句:
SELECT * FROM person_info WHERE name = 'Ashburn';或者包含多個左邊的列也行:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';那為什么搜索條件中必須出現左邊的列才可以使用到這個B+樹索引呢?比如下邊的語句就用不到這個B+樹索引么?
SELECT * FROM person_info WHERE birthday = '1990-09-27';是的,的確用不到,因為B+樹的數據頁和記錄先是按照name列的值排序的,在name列的值相同的情況下才使用birthday列進行排序,也就是說name列的值不同的記錄中birthday的值可能是無序的。而現在你跳過name列直接根據birthday的值去查找,臣妾做不到呀~ 那如果我就想在只使用birthday的值去通過B+樹索引進行查找咋辦呢?這好辦,你再對birthday列建一個B+樹索引就行了,創建索引的語法不用我嘮叨了吧。
但是需要特別注意的一點是,如果我們想使用聯合索引中盡可能多的列,搜索條件中的各個列必須是聯合索引中從最左邊連續的列。比方說聯合索引idx_name_birthday_phone_number中列的定義順序是name、birthday、phone_number,如果我們的搜索條件中只有name和phone_number,而沒有中間的birthday,比方說這樣:
SELECT * FROM person_info WHERE name = 'Ashburn' AND phone_number = '15123983239';這樣只能用到name列的索引,birthday和phone_number的索引就用不上了,因為name值相同的記錄先按照birthday的值進行排序,birthday值相同的記錄才按照phone_number值進行排序。
匹配列前綴
我們前邊說過為某個列建立索引的意思其實就是在對應的B+樹的記錄中使用該列的值進行排序,比方說person_info表上建立的聯合索引idx_name_birthday_phone_number會先用name列的值進行排序,所以這個聯合索引對應的B+樹中的記錄的name列的排列就是這樣的:
Aaron Aaron ... Aaron Asa Ashburn ... Ashburn Baird Barlow ... Barlow字符串排序的本質就是比較哪個字符串大一點兒,哪個字符串小一點,比較字符串大小就用到了該列的字符集和比較規則,這個我們前邊兒嘮叨過,就不多嘮叨了。這里需要注意的是,一般的比較規則都是逐個比較字符的大小,也就是說我們比較兩個字符串的大小的過程其實是這樣的:
- 先比較字符串的第一個字符,第一個字符小的那個字符串就比較小。
- 如果兩個字符串的第一個字符相同,那就再比較第二個字符,第二個字符比較小的那個字符串就比較小。
- 如果兩個字符串的第二個字符也相同,那就接著比較第三個字符,依此類推。
所以一個排好序的字符串列其實有這樣的特點:
- 先按照字符串的第一個字符進行排序。
- 如果第一個字符相同再按照第二個字符進行排序。
- 如果第二個字符相同再按照第三個字符進行排序,依此類推。
也就是說這些字符串的前n個字符,也就是前綴都是排好序的,所以對于字符串類型的索引列來說,我們只匹配它的前綴也是可以快速定位記錄的,比方說我們想查詢名字以'As'開頭的記錄,那就可以這么寫查詢語句:
SELECT * FROM person_info WHERE name LIKE 'As%';但是需要注意的是,如果只給出后綴或者中間的某個字符串,比如這樣:
SELECT * FROM person_info WHERE name LIKE '%As%';MySQL就無法快速定位記錄位置了,因為字符串中間有'As'的字符串并沒有排好序,所以只能全表掃描了。有時候我們有一些匹配某些字符串后綴的需求,比方說某個表有一個url列,該列中存儲了許多url:
+----------------+ | url | +----------------+ | www.baidu.com | | www.google.com | | www.gov.cn | | ... | | www.wto.org | +----------------+假設已經對該url列創建了索引,如果我們想查詢以com為后綴的網址的話可以這樣寫查詢條件:WHERE url LIKE '%com',但是這樣的話無法使用該url列的索引。為了在查詢時用到這個索引而不至于全表掃描,我們可以把后綴查詢改寫成前綴查詢,不過我們就得把表中的數據全部逆序存儲一下,也就是說我們可以這樣保存url列中的數據:
+----------------+ | url | +----------------+ | moc.udiab.www | | moc.elgoog.www | | nc.vog.www | | ... | | gro.otw.www | +----------------+這樣再查找以com為后綴的網址時搜索條件便可以這么寫:WHERE url LIKE 'moc%',這樣就可以用到索引了。
匹配范圍值
回頭看我們idx_name_birthday_phone_number索引的B+樹示意圖,所有記錄都是按照索引列的值從小到大的順序排好序的,所以這極大的方便我們查找索引列的值在某個范圍內的記錄。比方說下邊這個查詢語句:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';由于B+樹中的數據頁和記錄是先按name列排序的,所以我們上邊的查詢過程其實是這樣的:
- 通過B+樹在葉子節點中找到第一條name值大于Asa的二級索引記錄,讀取該記錄的主鍵值進行回表操作,獲得對應的聚簇索引記錄后發送給客戶端。
- 根據上一步找到的記錄,沿著記錄所在的鏈表向后查找(同一頁面中的記錄使用單向鏈表連接起來,數據頁之間用雙向鏈表連接起來)下一條二級索引記錄,判斷該記錄是否符合name < ‘Barlow’條件,如果符合,則進行回表操作后發送至客戶端。
- 重復上一步驟,直到某條二級索引記錄不符合name <‘Barlow’條件為止。
不過在使用聯合進行范圍查找的時候需要注意,如果對多個列同時進行范圍查找的話,只有對索引最左邊的那個列進行范圍查找的時候才能用到B+樹索引,比方說這樣:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' AND birthday > '1980-01-01';上邊這個查詢可以分成兩個部分:
這樣子對于聯合索引idx_name_birthday_phone_number來說,只能用到name列的部分,而用不到birthday列的部分,因為只有name值相同的情況下才能用birthday列的值進行排序,而這個查詢中通過name進行范圍查找的記錄中可能并不是按照birthday列進行排序的,所以在搜索條件中繼續以birthday列進行查找時是用不到這個B+樹索引的。
精確匹配某一列并范圍匹配另外一列
對于同一個聯合索引來說,雖然對多個列都進行范圍查找時只能用到最左邊那個索引列,但是如果左邊的列是精確查找,則右邊的列可以進行范圍查找,比方說這樣:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone_number > '15100000000';這個查詢的條件可以分為3個部分:
同理,下邊的查詢也是可能用到這個idx_name_birthday_phone_number聯合索引的:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1980-01-01' AND phone_number > '15100000000';用于排序
我們在寫查詢語句的時候經常需要對查詢出來的記錄通過ORDER BY子句按照某種規則進行排序。一般情況下,我們只能把記錄都加載到內存中,再用一些排序算法,比如快速排序、歸并排序、吧啦吧啦排序等等在內存中對這些記錄進行排序,有的時候可能查詢的結果集太大以至于不能在內存中進行排序的話,還可能暫時借助磁盤的空間來存放中間結果,排序操作完成后再把排好序的結果集返回到客戶端。在MySQL中,把這種在內存中或者磁盤上進行排序的方式統稱為文件排序(英文名:filesort),跟文件這個詞兒一沾邊兒,就顯得這些排序操作非常慢了(磁盤和內存的速度比起來,就像是飛機和蝸牛的對比)。但是如果ORDER BY子句里使用到了我們的索引列,就有可能省去在內存或文件中排序的步驟,比如下邊這個簡單的查詢語句:
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;這個查詢的結果集需要先按照name值排序,如果記錄的name值相同,則需要按照birthday來排序,如果birthday的值相同,則需要按照phone_number排序。大家可以回過頭去看我們建立的idx_name_birthday_phone_number索引的示意圖,因為這個B+樹索引本身就是按照上述規則排好序的,所以直接從索引中提取數據,然后進行回表操作取出該索引中不包含的列就好了。簡單吧?是的,索引就是這么牛逼。
使用聯合索引進行排序注意事項
對于聯合索引有個問題需要注意,ORDER BY的子句后邊的列的順序也必須按照索引列的順序給出,如果給出ORDER BY phone_number, birthday, name的順序,那也是用不了B+樹索引,這種顛倒順序就不能使用索引的原因我們上邊詳細說過了,這就不贅述了。
同理,ORDER BY name、ORDER BY name, birthday這種匹配索引左邊的列的形式可以使用部分的B+樹索引。當聯合索引左邊列的值為常量,也可以使用后邊的列進行排序,比如這樣:
SELECT * FROM person_info WHERE name = 'A' ORDER BY birthday, phone_number LIMIT 10;這個查詢能使用聯合索引進行排序是因為name列的值相同的記錄是按照birthday,?phone_number排序的,說了好多遍了都。
不可以使用索引進行排序的幾種情況
ASC、DESC混用
對于使用聯合索引進行排序的場景,我們要求各個排序列的排序順序是一致的,也就是要么各個列都是ASC規則排序,要么都是DESC規則排序。
小貼士: ORDER BY子句后的列如果不加ASC或者DESC默認是按照ASC排序規則排序的,也就是升序排序的。
為啥會有這種奇葩規定呢?這個還得回頭想想這個idx_name_birthday_phone_number聯合索引中記錄的結構:
- 先按照記錄的name列的值進行升序排列。
- 如果記錄的name列的值相同,再按照birthday列的值進行升序排列。
- 如果記錄的birthday列的值相同,再按照phone_number列的值進行升序排列。
如果查詢中的各個排序列的排序順序是一致的,比方說下邊這兩種情況:
- ORDER BY name, birthday LIMIT 10這種情況直接從索引的最左邊開始往右讀10行記錄就可以了。
- ORDER BY name DESC, birthday DESC LIMIT 10,這種情況直接從索引的最右邊開始往左讀10行記錄就可以了。
但是如果我們查詢的需求是先按照name列進行升序排列,再按照birthday列進行降序排列的話,比如說這樣的查詢語句:
SELECT * FROM person_info ORDER BY name, birthday DESC LIMIT 10;這樣如果使用索引排序的話過程就是這樣的:
- 先從索引的最左邊確定name列最小的值,然后找到name列等于該值的所有記錄,然后從name列等于該值的最右邊的那條記錄開始往左找10條記錄。
- 如果name列等于最小的值的記錄不足10條,再繼續往右找name值第二小的記錄,重復上邊那個過程,直到找到10條記錄為止。
累不累?累!重點是這樣不能高效使用索引,而要采取更復雜的算法去從索引中取數據,設計MySQL的大叔覺得這樣還不如直接文件排序來的快,所以就規定使用聯合索引的各個排序列的排序順序必須是一致的。
排序列包含非同一個索引的列
有時候用來排序的多個列不是一個索引里的,這種情況也不能使用索引進行排序,比方說:
SELECT * FROM person_info ORDER BY name, country LIMIT 10;name和country并不屬于一個聯合索引中的列,所以無法使用索引進行排序,至于為啥我就不想再嘮叨了,自己用前邊的理論自己捋一捋吧~
排序列使用了復雜的表達式
要想使用索引進行排序操作,必須保證索引列是以單獨列的形式出現,而不是修飾過的形式,比方說這樣:
SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10;使用了UPPER函數修飾過的列就不是單獨的列啦,這樣就無法使用索引進行排序啦。
用于分組
有時候我們為了方便統計表中的一些信息,會把表中的記錄按照某些列進行分組。比如下邊這個分組查詢:
SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number這個查詢語句相當于做了3次分組操作:
然后針對那些小小分組進行統計,比如在我們這個查詢語句中就是統計每個小小分組包含的記錄條數。如果沒有索引的話,這個分組過程全部需要在內存里實現,而如果有了索引的話,恰巧這個分組順序又和我們的B+樹中的索引列的順序是一致的,而我們的B+樹索引又是按照索引列排好序的,這不正好么,所以可以直接使用B+樹索引進行分組。
和使用B+樹索引進行排序是一個道理,分組列的順序也需要和索引列的順序一致,也可以只使用索引列中左邊的列進行分組,吧啦吧啦的~
回表的代價
上邊的討論對回表這個詞兒多是一帶而過,可能大家沒啥深刻的體會,下邊我們詳細嘮叨下。還是用idx_name_birthday_phone_number索引為例,看下邊這個查詢:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';在使用idx_name_birthday_phone_number索引進行查詢時大致可以分為這兩個步驟:
由于索引idx_name_birthday_phone_number對應的B+樹中的記錄首先會按照name列的值進行排序,所以值在Asa~Barlow之間的記錄在磁盤中的存儲是相連的,集中分布在一個或幾個數據頁中,我們可以很快的把這些連著的記錄從磁盤中讀出來,這種讀取方式我們也可以稱為順序I/O。根據第1步中獲取到的記錄的id字段的值可能并不相連,而在聚簇索引中記錄是根據id(也就是主鍵)的順序排列的,所以根據這些并不連續的id值到聚簇索引中訪問完整的用戶記錄可能分布在不同的數據頁中,這樣讀取完整的用戶記錄可能要訪問更多的數據頁,這種讀取方式我們也可以稱為隨機I/O。一般情況下,順序I/O比隨機I/O的性能高很多,所以步驟1的執行可能很快,而步驟2就慢一些。所以這個使用索引idx_name_birthday_phone_number的查詢有這么兩個特點:
- 會使用到兩個B+樹索引,一個二級索引,一個聚簇索引。
- 訪問二級索引使用順序I/O,訪問聚簇索引使用隨機I/O。
需要回表的記錄越多,使用二級索引的性能就越低,甚至讓某些查詢寧愿使用全表掃描也不使用二級索引。比方說name值在Asa~Barlow之間的用戶記錄數量占全部記錄數量90%以上,那么如果使用idx_name_birthday_phone_number索引的話,有90%多的id值需要回表,這不是吃力不討好么,還不如直接去掃描聚簇索引(也就是全表掃描)。
那什么時候采用全表掃描的方式,什么時候使用采用二級索引 + 回表的方式去執行查詢呢?這個就是傳說中的查詢優化器做的工作,查詢優化器會事先對表中的記錄計算一些統計數據,然后再利用這些統計數據根據查詢的條件來計算一下需要回表的記錄數,需要回表的記錄數越多,就越傾向于使用全表掃描,反之傾向于使用二級索引 + 回表的方式。當然優化器做的分析工作不僅僅是這么簡單,但是大致上是個這個過程。一般情況下,限制查詢獲取較少的記錄數會讓優化器更傾向于選擇使用二級索引 + 回表的方式進行查詢,因為回表的記錄越少,性能提升就越高,比方說上邊的查詢可以改寫成這樣:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' LIMIT 10;添加了LIMIT 10的查詢更容易讓優化器采用二級索引 + 回表的方式進行查詢。
對于有排序需求的查詢,上邊討論的采用全表掃描還是二級索引 + 回表的方式進行查詢的條件也是成立的,比方說下邊這個查詢:
SELECT * FROM person_info ORDER BY name, birthday, phone_number;由于查詢列表是*,所以如果使用二級索引進行排序的話,需要把排序完的二級索引記錄全部進行回表操作,這樣操作的成本還不如直接遍歷聚簇索引然后再進行文件排序(filesort)低,所以優化器會傾向于使用全表掃描的方式執行查詢。如果我們加了LIMIT子句,比如這樣:
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;這樣需要回表的記錄特別少,優化器就會傾向于使用二級索引 + 回表的方式執行查詢。
覆蓋索引
為了徹底告別回表操作帶來的性能損耗,我們建議:最好在查詢列表里只包含索引列,比如這樣:
SELECT name, birthday, phone_number FROM person_info WHERE name > 'Asa' AND name < 'Barlow'因為我們只查詢name,?birthday,?phone_number這三個索引列的值,所以在通過idx_name_birthday_phone_number索引得到結果后就不必到聚簇索引中再查找記錄的剩余列,也就是country列的值了,這樣就省去了回表操作帶來的性能損耗。我們把這種只需要用到索引的查詢方式稱為索引覆蓋。排序操作也優先使用覆蓋索引的方式進行查詢,比方說這個查詢:
SELECT name, birthday, phone_number FROM person_info ORDER BY name, birthday, phone_number;雖然這個查詢中沒有LIMIT子句,但是采用了覆蓋索引,所以查詢優化器就會直接使用idx_name_birthday_phone_number索引進行排序而不需要回表操作了。
當然,如果業務需要查詢出索引以外的列,那還是以保證業務需求為重。但是我們很不鼓勵用*號作為查詢列表,最好把我們需要查詢的列依次標明。
如何挑選索引
上邊我們以idx_name_birthday_phone_number索引為例對索引的適用條件進行了詳細的嘮叨,下邊看一下我們在建立索引時或者編寫查詢語句時就應該注意的一些事項。
只為用于搜索、排序或分組的列創建索引
也就是說,只為出現在WHERE子句中的列、連接子句中的連接列,或者出現在ORDER BY或GROUP BY子句中的列創建索引。而出現在查詢列表中的列就沒必要建立索引了:
SELECT birthday, country FROM person_name WHERE name = 'Ashburn';像查詢列表中的birthday、country這兩個列就不需要建立索引,我們只需要為出現在WHERE子句中的name列創建索引就可以了。
考慮列的基數
列的基數指的是某一列中不重復數據的個數,比方說某個列包含值2, 5, 8, 2, 5, 8, 2, 5, 8,雖然有9條記錄,但該列的基數卻是3。也就是說,在記錄行數一定的情況下,列的基數越大,該列中的值越分散,列的基數越小,該列中的值越集中。這個列的基數指標非常重要,直接影響我們是否能有效的利用索引。假設某個列的基數為1,也就是所有記錄在該列中的值都一樣,那為該列建立索引是沒有用的,因為所有值都一樣就無法排序,無法進行快速查找了~ 而且如果某個建立了二級索引的列的重復值特別多,那么使用這個二級索引查出的記錄還可能要做回表操作,這樣性能損耗就更大了。所以結論就是:最好為那些列的基數大的列建立索引,為基數太小列的建立索引效果可能不好。
索引列的類型盡量小
我們在定義表結構的時候要顯式的指定列的類型,以整數類型為例,有TINYINT、MEDIUMINT、INT、BIGINT這么幾種,它們占用的存儲空間依次遞增,我們這里所說的類型大小指的就是該類型表示的數據范圍的大小。能表示的整數范圍當然也是依次遞增,如果我們想要對某個整數列建立索引的話,在表示的整數范圍允許的情況下,盡量讓索引列使用較小的類型,比如我們能使用INT就不要使用BIGINT,能使用MEDIUMINT就不要使用INT~ 這是因為:
- 數據類型越小,在查詢時進行的比較操作越快(這是CPU層次的東東)
- 數據類型越小,索引占用的存儲空間就越少,在一個數據頁內就可以放下更多的記錄,從而減少磁盤I/O帶來的性能損耗,也就意味著可以把更多的數據頁緩存在內存中,從而加快讀寫效率。
這個建議對于表的主鍵來說更加適用,因為不僅是聚簇索引中會存儲主鍵值,其他所有的二級索引的節點處都會存儲一份記錄的主鍵值,如果主鍵適用更小的數據類型,也就意味著節省更多的存儲空間和更高效的I/O。
索引字符串值的前綴
我們知道一個字符串其實是由若干個字符組成,如果我們在MySQL中使用utf8字符集去存儲字符串的話,編碼一個字符需要占用1~3個字節。假設我們的字符串很長,那存儲一個字符串就需要占用很大的存儲空間。在我們需要為這個字符串列建立索引時,那就意味著在對應的B+樹中有這么兩個問題:
- B+樹索引中的記錄需要把該列的完整字符串存儲起來,而且字符串越長,在索引中占用的存儲空間越大。
- 如果B+樹索引中索引列存儲的字符串很長,那在做字符串比較時會占用更多的時間。
我們前邊兒說過索引列的字符串前綴其實也是排好序的,所以索引的設計者提出了個方案 —?只對字符串的前幾個字符進行索引也就是說在二級索引的記錄中只保留字符串前幾個字符。這樣在查找記錄時雖然不能精確的定位到記錄的位置,但是能定位到相應前綴所在的位置,然后根據前綴相同的記錄的主鍵值回表查詢完整的字符串值,再對比就好了。這樣只在B+樹中存儲字符串的前幾個字符的編碼,既節約空間,又減少了字符串的比較時間,還大概能解決排序的問題,何樂而不為,比方說我們在建表語句中只對name列的前10個字符進行索引可以這么寫:
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+樹索引中只保留記錄的前10個字符的編碼,這種只索引字符串值的前綴的策略是我們非常鼓勵的,尤其是在字符串類型能存儲的字符比較多的時候。
- 索引列前綴對排序的影響
如果使用了索引列前綴,比方說前邊只把name列的前10個字符放到了二級索引中,下邊這個查詢可能就有點兒尷尬了:
SELECT * FROM person_info ORDER BY name LIMIT 10;因為二級索引中不包含完整的name列信息,所以無法對前十個字符相同,后邊的字符不同的記錄進行排序,也就是使用索引列前綴的方式無法支持使用索引排序,只好乖乖的用文件排序嘍。
讓索引列在比較表達式中單獨出現
假設表中有一個整數列my_col,我們為這個列建立了索引。下邊的兩個WHERE子句雖然語義是一致的,但是在效率上卻有差別:
第1個WHERE子句中my_col列并不是以單獨列的形式出現的,而是以my_col * 2這樣的表達式的形式出現的,存儲引擎會依次遍歷所有的記錄,計算這個表達式的值是不是小于4,所以這種情況下是使用不到為my_col列建立的B+樹索引的。而第2個WHERE子句中my_col列并是以單獨列的形式出現的,這樣的情況可以直接使用B+樹索引。
所以結論就是:如果索引列在比較表達式中不是以單獨列的形式出現,而是以某個表達式,或者函數調用形式出現的話,是用不到索引的。
主鍵插入順序
我們知道,對于一個使用InnoDB存儲引擎的表來說,在我們沒有顯式的創建索引時,表中的數據實際上都是存儲在聚簇索引的葉子節點的。而記錄又是存儲在數據頁中的,數據頁和記錄又是按照記錄主鍵值從小到大的順序進行排序,所以如果我們插入的記錄的主鍵值是依次增大的話,那我們每插滿一個數據頁就換到下一個數據頁繼續插,而如果我們插入的主鍵值忽大忽小的話,這就比較麻煩了,假設某個數據頁存儲的記錄已經滿了,它存儲的主鍵值在1~100之間:
如果此時再插入一條主鍵值為9的記錄,那它插入的位置就如下圖:
可這個數據頁已經滿了啊,再插進來咋辦呢?我們需要把當前頁面分裂成兩個頁面,把本頁中的一些記錄移動到新創建的這個頁中。頁面分裂和記錄移位意味著什么?意味著:性能損耗!所以如果我們想盡量避免這樣無謂的性能損耗,最好讓插入的記錄的主鍵值依次遞增,這樣就不會發生這樣的性能損耗了。所以我們建議:讓主鍵具有AUTO_INCREMENT,讓存儲引擎自己為表生成主鍵,而不是我們手動插入?,比方說我們可以這樣定義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屬性,在插入記錄時存儲引擎會自動為我們填入自增的主鍵值。
冗余和重復索引
有時候有的同學有意或者無意的就對同一個列創建了多個索引,比方說這樣寫建表語句:
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)) );我們知道,通過idx_name_birthday_phone_number索引就可以對name列進行快速搜索,再創建一個專門針對name列的索引就算是一個冗余索引,維護這個索引只會增加維護的成本,并不會對搜索有什么好處。
另一種情況,我們可能會對某個列重復建立索引,比方說這樣:
CREATE TABLE repeat_index_demo (c1 INT PRIMARY KEY,c2 INT,UNIQUE uidx_c1 (c1),INDEX idx_c1 (c1) );我們看到,c1既是主鍵、又給它定義為一個唯一索引,還給它定義了一個普通索引,可是主鍵本身就會生成聚簇索引,所以定義的唯一索引和普通索引是重復的,這種情況要避免。
總結
上邊只是我們在創建和使用B+樹索引的過程中需要注意的一些點,后邊我們還會陸續介紹更多的優化方法和注意事項,敬請期待。本集內容總結如下:
- 全值匹配
- 匹配左邊的列
- 匹配范圍值
- 精確匹配某一列并范圍匹配另外一列
- 用于排序
- 用于分組
- 只為用于搜索、排序或分組的列創建索引
- 為列的基數大的列創建索引
- 索引列的類型盡量小
- 可以只對字符串值的前綴建立索引
- 只有索引列在比較表達式中單獨出現才可以適用索引
- 為了盡可能少的讓聚簇索引發生頁面分裂和記錄移位的情況,建議讓主鍵擁有AUTO_INCREMENT屬性。
- 定位并刪除表中的重復和冗余索引
- 盡量使用覆蓋索引進行查詢,避免回表帶來的性能損耗。
結束
以上就是Mysql事務隔離級別的詳細說明與實現原理,這是別的博主的一篇文章,我覺得寫得非常簡單易懂且詳細,就不再造輪子,分享給大家,還有更多興趣的,可以通過下面的鏈接,訪問原文,系統的了解mysql的本質。
版權聲明:本文為給予掘金博主「小孩子」的原創文章,進行的簡化說明,詳細的可以參考掘金小冊。
總結
以上是生活随笔為你收集整理的Mysql B+树索引的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 4G 物联网连接主力--- LTE Ca
- 下一篇: Crazy2048