mysql 查看索引深度_mysql 学习 - 索引深度理解
使用索引的代價
在熟悉了B+樹索引原理之后,本篇文章的主題是嘮叨如何更好的使用索引,雖然索引是個好東西,可不能亂建,在介紹如何更好的使用索引之前先要了解一下使用這玩意兒的代價,它在空間和時間上都會拖后腿: 空間上的代價:
這個是顯而易見的,每建立一個索引都要為它建立一棵B+樹,每一棵B+樹的每一個節點都是一個數據頁,一個頁默認會占用16KB的存儲空間,一棵很大的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)
);
1.表中的主鍵是id列,它存儲一個自動遞增的整數。所以InnoDB存儲引擎會自動為id列建立聚簇索引。
2.我們額外定義了一個二級索引idx_name_birthday_phone_number,它是由3個列組成的聯合索引。所以在這個索引對應的B+樹的葉子節點處存儲的用戶記錄只保留name、birthday、phone_number這三個列的值以及主鍵id的值,并不會保存country列的值。
idx_name_birthday_phone_number的二級索引示意圖(簡略版):
從圖中可以看出,這個 idx_name_birthday_phone_number 索引對應的 B+ 樹中頁面和記錄的排序方式就是這樣的:
先按照name列的值進行排序。
如果name列的值相同,則按照birthday列的值進行排序。
如果birthday列的值也相同,則按照phone_number的值進行排序。
這個排序方式是非常非常重要的, 涉及到索引能否為你的查詢語句提升效率.
全值匹配
如果我們的搜索條件中的列和索引列一致的話,這種情況就稱為全值匹配,比方說下邊這個查找語句:
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 的值去查找.
所以聯合索引的使用技巧就是盡量匹配左邊的列, 比方說聯合索引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值進行排序。
匹配列前綴
上個小節說明了使用聯合索引時的注意項, 這一小節講一下當查詢時需要進行模糊匹配的情況.
我們聯合查詢的索引首先按照 name 進行排序, 所以根據 name 列的字符集以及排序規則, 這個索引的頁目錄記錄大概是按照如下進行排序的:
所以只匹配 name 的前綴也是可以快速定位記錄的, 對于這樣的查詢語句:
SELECT * FROM person_info WHERE name LIKE 'As%';
但是需要注意的是,如果只給出后綴或者中間的某個字符串,比如這樣:
SELECT * FROM person_info WHERE name LIKE '%As%';
因為我們的聯合索引中并沒有這種 %As% 排序規則, 所以只能全表掃描了. 對于這種模糊匹配需要特殊的處理, 也就是考驗開發者的查詢優化能力.
匹配范圍值
回頭看我們 idx_name_birthday_phone_number 索引的B+樹示意圖,所有記錄都是按照索引列的值從小到大的順序排好序的,所以這極大的方便我們查找索引列的值在某個范圍內的記錄。比方說下邊這個查詢語句:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';
由于B+樹中的數據頁和記錄是先按name列排序的,所以我們上邊的查詢過程其實是這樣的:
找到name值為Asa的記錄。
找到name值為Barlow的記錄。
由于所有記錄都是由鏈表連起來的(記錄之間用單鏈表,數據頁之間用雙鏈表),所以他們之間的記錄都可以很容易的取出來
找到這些記錄的主鍵值,再到聚簇索引中回表查找完整的記錄
不過在使用聯合進行范圍查找的時候需要注意,如果對多個列同時進行范圍查找的話,只有對索引最左邊的那個列進行范圍查找的時候才能用到B+樹索引,比方說這樣:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' AND birthday > '1980-01-01';
上邊的查詢過程如下:
通過條件name > 'Asa' AND name < 'Barlow'來對name進行范圍,查找的結果可能有多條name值不同的記錄,
對這些name值不同的記錄繼續通過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個部分:
1.name = 'Ashburn',對name列進行精確查找,當然可以使用B+樹索引了。
2. birthday > '1980-01-01' AND birthday < '2000-12-31',由于name列是精確查找,所以通過name = 'Ashburn'條件查找后得到的結果的name值都是相同的,它們會再按照birthday的值進行排序。所以此時對birthday列進行范圍查找是可以用到B+樹索引的。
3. phone_number > '15100000000',通過birthday的范圍查找的記錄的birthday的值可能不同,所以這個條件無法再利用B+樹索引了,只能遍歷上一步查詢得到的記錄。
同理,下邊的查詢也是可能用到這個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;
不可以使用索引進行排序的幾種情況
ASC、DESC混用
對于使用聯合索引進行排序的場景,我們要求各個排序列的排序順序是一致的,也就是要么各個列都是ASC規則排序,要么都是DESC規則排序。
排序列包含非同一個索引的列
WHERE子句中出現非排序使用到的索引列
用于分組
有時候我們為了方便統計表中的一些信息,會把表中的記錄按照某些列進行分組。比如下邊這個分組查詢:
SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number
這個查詢語句相當于做了3次分組操作:
先把記錄按照name值進行分組,所有name值相同的記錄劃分為一組。
將每個name值相同的分組里的記錄再按照birthday的值進行分組,將birthday值相同的記錄放到一個小分組里,所以看起來就像在一個大分組里又化分了好多小分組。
再將上一步中產生的小分組按照phone_number的值分成更小的分組,所以整體上看起來就像是先把記錄分成一個大分組,然后把大分組分成若干個小分組,然后把若干個小分組再細分成更多的小小分組。
回表的代價
首先看一個查詢:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';
分析一下上面的sql 語句:
從索引idx_name_birthday_phone_number對應的B+樹中取出name值在Asa~Barlow之間的用戶記錄。 由于索引idx_name_birthday_phone_number對應的B+樹用戶記錄中只包含name、birthday、phone_number、id這4個字段,而查詢列表是*,意味著要查詢表中所有字段,也就是還要包括country字段。這時需要把從上一步中獲取到的每一條記錄的id字段都到聚簇索引對應的B+樹中找到完整的用戶記錄,也就是我們通常所說的回表,然后把完整的用戶記錄返回給查詢用戶。
由于索引idx_name_birthday_phone_number對應的B+樹中的記錄首先會按照name列的值進行排序,所以值在Asa~Barlow之間的記錄在磁盤中的存儲是相連的,集中分布在一個或幾個數據頁中,我們可以很快的把這些連著的記錄從磁盤中讀出來,這種讀取方式我們也可以稱為順序I/O
根據第1步中獲取到的記錄的id字段的值可能并不相連,而在聚簇索引中記錄是根據id(也就是主鍵)的順序排列的,所以根據這些并不連續的id值到聚簇索引中訪問完整的用戶記錄可能分布在不同的數據頁中,這樣讀取完整的用戶記錄可能要訪問更多的數據頁,這種讀取方式我們也可以稱為隨機I/O
一般情況下,順序I/O比隨機I/O的性能高很多,所以步驟1的執行可能很快,而步驟2就慢一些。需要回表的記錄越多,使用二級索引的性能就越低,甚至讓某些查詢寧愿使用全表掃描也不使用二級索引。比方說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 列的值了,這樣就省去了回表操作帶來的性能損耗。我們把這種只需要用到索引的查詢方式稱為 索引覆蓋。排序操作也優先使用覆蓋索引的方式進行查詢
如何挑選索引
只為用于搜索、排序或分組的列創建索引
也就是說,只為出現在WHERE子句中的列、連接子句中的連接列,或者出現在ORDER BY或GROUP BY子句中的列創建索引。而出現在查詢列表中的列就沒必要建立索引了
考慮列的基數
列的基數指的是某一列中不重復數據的個數,比方說某個列包含值2, 5, 8, 2, 5, 8, 2, 5, 8,雖然有9條記錄,但該列的基數卻是3。這個列的基數指標非常重要,直接影響我們是否能有效的利用索引。假設某個列的基數為1,也就是所有記錄在該列中的值都一樣,那為該列建立索引是沒有用的,因為所有值都一樣就無法排序. 而且如果某個建立了二級索引的列的重復值特別多,那么使用這個二級索引查出的記錄還可能要做回表操作,這樣性能損耗就更大了
索引列的類型盡量小
數據類型越小,在查詢時進行的比較操作越快
數據類型越小,索引占用的存儲空間就越少,在一個數據頁內就可以放下更多的記錄,從而減少磁盤I/O帶來的性能損耗,也就意味著可以把更多的數據頁緩存在內存中,從而加快讀寫效率
讓索引列在比較表達式中單獨出現
看一下下面兩條語句的區別:
WHERE my_col * 2 < 4
WHERE my_col < 4/2
第1個WHERE子句中my_col列并不是以單獨列的形式出現的,而是以my_col * 2這樣的表達式的形式出現的,存儲引擎會依次遍歷所有的記錄,計算這個表達式的值是不是小于4,所以這種情況下是使用不到為my_col列建立的B+樹索引的。而第2個WHERE子句中my_col列并是以單獨列的形式出現的,這樣的情況可以直接使用B+樹索引
主鍵插入順序
如果我們插入數據時, 數據的主鍵忽大忽小, 則可能會引起原來排列好的數據頁發生頁分列, 就意味著性能損耗. 所以我們建議主鍵一般設置成為自增序列, 讓數據庫為我們自己生成主鍵值, 這樣在插入數據的時候, 一般不會產生頁分列現象.
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)
);
不要建立重復索引
CREATE TABLE repeat_index_demo (
c1 INT PRIMARY KEY,
c2 INT,
UNIQUE uidx_c1 (c1),
INDEX idx_c1 (c1)
);
主鍵已經是聚簇索引了, 又給 c1 列添加了唯一索引以及普通索引. 這種情況要避免.
總結
以上是生活随笔為你收集整理的mysql 查看索引深度_mysql 学习 - 索引深度理解的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: mysql 5.7.21 主从_Mysq
- 下一篇: php5.3无法加载mysql数据库模块
