大部分情況下,尤其是記錄數量較少的情況下Mysql總是能正常運轉的很好,但不可避免的,隨着數據庫記錄數的增長以及SQL語句越來越復雜,總會有一些實際效果與數據庫或SQL設計人員理解相違背的情況,這就需要開發者對Mysql的原理和存在的問題有一個基本的認識。本文主要探討了Mysql索引的使用和相關知識,這些知識並不復雜,不需要專業的數據庫學習經驗就能搞明白,理解了這些可以幫助開發人員更好的進行數據庫索引設計和SQL查詢語句的編寫。
1. Mysql 是如何使用索引的
索引可以幫助我們快速的找到包含指定列值的行。假如沒有索引的話,Mysql必須從第一行開始查找整個表,才能找到我們想要的那些行。如果沒有索引,表越大,花費的時間也就越大。如果我們在查詢條件中指定了某幾個列的值,並且這個表恰好有一個建立在這些列上的索引,那么Mysql就可以從數據文件中快速的定位到數據所在的位置,而不用查找整個數據文件。這比不斷的一行行讀取數據快多了[1]。大部分Mysql索引(Primary Key
、Unique index
和FullText
)都通過B樹
來存儲和實現。也有一些例外:空間數據類型使用的索引是基於R-樹
的;內存表
還支持哈希索引
;InnoDB為Fulltext
索引使用了逆轉鏈表
[1]。 本文不打算去贅述B樹的原理和創建過程,有興趣的可以點擊B樹了解。假設現在索引已經創建完畢了,那么Mysql是如何查找到我們需要的數據的呢?下面我們就MyISAM和Innodb兩種不同的存儲引擎做討論。關於MyISAM和Innodb我們需要知道的有:
- MyISAM不支持事務,而Innodb支持。
- MyISAM索引和數據的存儲是分開的(不同的文件),索引中最終檢索到的是數據的物理地址偏移量。而InnoDB中,索引段和數據段在同一個文件中的不同段,查到索引后可以直接取出數據。
- MyISAM是非聚集索引,而Innodb則是聚集索引。
所謂聚集索引是指索引和數據的邏輯排列順序與實際物理存儲順序一致,新華字典就是典型的聚集索引,字(葉子索引)和釋意(數據)靠在一起,且按一定順序排列的。而“非聚集索引”則相反,索引單獨放在一塊區域,並且葉子節點存放的是數據的地址偏移量。
下面4張圖分別為MyISAM和Innodb的主索引和輔助索引邏輯圖:
1.1 MyISAM存儲引擎
在MyISAM中,索引(含葉子節點)存放在單獨的.myi文件中,葉子節點存放的是數據的物理地址偏移量(通過偏移量訪問就是隨機訪問,速度很快)。
主索引是指主鍵索引,鍵值不可能重復;輔助索引則是普通索引,鍵值可能重復。
假設有以下語句
select * from table_name where id = 3
其中id為主鍵,那么首先檢索的是索引,索引中經過2層查找,找到了索引為3的節點,值為0xABAB,代表了從.myd文件中偏移量為0xABAB的地方開始讀取一行的數據。輔助索引對應普通索引,存在相同的鍵值。
1.2 Innodb存儲引擎
在Innodb中,索引分葉子節點和非葉子節點,非葉子節點就像新華字典的目錄,單獨存放在索引段中,葉子節點則是順序排列的,在數據段中。Innodb的數據文件可以按照表來切分(只需要開啟innodb_file_per_table),切分后存放在xxx.ibd中,默認不切分,存放在xxx.ibdata中。 假設有以下語句
select * from table_name where id = 3
其中id為主鍵,那么首先檢索的是索引段,索引段中查找下個索引范圍是通過地址偏移隨機訪問來實現的(這個步驟還是高效的),查找到對應葉子索引節點后,需要順序的遍歷和檢索該葉子節點找到對應的索引值對應的節點(這個步驟是遍歷,比較耗時),就可以立馬讀出數據了。
Innodb的表存儲結構由段、簇(區)、頁組成,一個段由若干簇組成,一個簇默認有64頁,每頁16KB。
1.3 Mysql索引選取規則
嚴格意義上講,如果我們在某一張表上建立了多個索引,那么MySql最終使用哪個索引是沒有一個顯而易見的規律的,但大致可以分為以下幾步:
- 尋找可能的(可選)索引:根據用戶的WHERE條件,查看每個字段是否匹配某個索引,如果匹配,就把這個索引加入待選列表中。所謂字段匹配索引有兩種情況:1)某個查詢字段上建立了
單列索引
;2)某個查詢字段按照最左匹配原則
(下文有詳細描述)匹配了某個組合索引
,即為該組合索引的第一列。3)某幾個查詢字段按照最左匹配原則
匹配了某個組合索引
。可選索引列表可使用EXPLAIN
查看(possible keys)[3]。待選列表如果為空,GOTO 3。 - 索引擇優算法:Mysql的索引擇優算法很復雜,一般來說有這幾個影響因素:1)索引對應的掃描行數,在沒有Order by的情況下,一般掃描行數少的索引會被選擇;2)查詢語句中有Order By或者group by時,如果不使用Order By后的字段做索引的話,filesort(對應索引排序,索引排序很快,而filesort則需要對結果集進行實時排序,所以很慢)會被使用,這時候Mysql給filesort的負權重很高,很容易導致Mysql放棄最優索引(哪怕該索引估計掃描行數比實際使用的索引對應的掃描行數小很多),轉而使用Order By之后的索引字段。我們在生產中遇到過這樣的情況,部分文獻也有記載[6];3)Limit 限定也會影響索引的使用,甚至Limit后的值也會影響索引的使用(有時候確實會令人費解);4)話不能說滿,官網文檔寫的實在含糊,所以這里不敢打包票沒有其他影響因素了。如果優選的掃描索引不為空,GOTO 4。
- 如果沒找到可用索引的話,再考察查詢字段,也就是
Select
之后Where
之前的那些字段。尋找查詢字段
所匹配的索引,並得到一個可用索引結果集。根據最小估算掃描行數優先原則
,可以得到最優的索引。如果可用索引結果集仍然為空,那么就會使用全表掃描(Full table scan)
。 - 如果根據最終選擇的索引估算出的掃描行數占據了表的很大一部分比例,那么Mysql優化器可能會放棄使用該索引,而退化為使用全表掃描。這是因為使用索引在有些情況下並不高效,比如索引出來的數據量很大,需要頻繁的改變
文件讀取指針
去獲取數據塊,可能效果還不如從頭到位把整個表都掃描一邊,也省去了去查找索引和頻繁重定向讀取指針(尤其在磁盤存儲器[4]上)帶來的開銷。 - 如果到這步Mysql搜索優化器仍然決定用某個索引,那么就會在實際查詢時使用該索引了。這個索引也是
EXPLAIN
分析語句結果集中key的值。
關於
使用索引隨機讀取大量記錄
和順序讀取大量記錄
之間的取舍問題,本文並沒有去研究Mysql在優化的時候是否考慮到了存儲器的類型,比如是磁盤還是SSD,對於SSD這種高效隨機存儲器來說,頻繁重定向讀取指針幾乎不耗時。如果沒有考慮到,而只是給這種頻繁讀取操作
預設了一個成本常量(消耗的時間)
參與估算的話,可能優化結果並不恰當。
1.4 何時會全表掃描
Mysql搜索優化器最終決定使用全表掃描一般有以下幾個場景[5]:
- 1.目標數據表太小了,再去查找索引(
key lookup
)太麻煩了(有點殺雞焉用牛刀的即視感)。這通常發生在10行都不到的數據表,並且每行很短的情況。(注: 10這個數字不可靠,這里只是感性的說了個數字,可能小幾十行的數據仍然會觸發全表掃描。) - 2.查詢條件中的字段(WHERE后)沒有匹配到索引的情況。(也不是說匹配不到就一定會全表掃描,見下文
默認索引選擇算法
) - 3.查詢條件中的字段與某個常量比較時(就比如
where age > 8
),並且使用這個常量值與對應索引篩選出的記錄數占了總數的大部分。優化器認為掃這么大的數據還不如掃全表了,所以選擇了掃描全表。大部分
怎么定義恐怕只有Mysql開發者才知道,官網也並沒有給出具體數值。 - 4.查詢語句匹配到的索引對應的基數太小(對應
SHOW INDEX FROM table_name
結果中的Cardinality
),並且此時又有其他列上的查詢條件(比如:select * from user where user_status > 0 and username =’tommy’)。所謂基數就是表中某列所有值的取值種數,比如一張表有5行,某一列對應的值分別為:1,2,3,3,2。那么該列基數
就是3,因為一共有三種取值:1,2,3。 基數小,意味着該索引中每個索引值對應的目標記錄數很大,在這個索引值對應的記錄數中再去一個個的檢查其他列上的條件是否滿足,整個過程總體的查找速度還未必有全表掃描來得快。
經測試,第3點當且僅當比較符號為
非等於
時才生效。如果使用了等於號
,那么只要該列有匹配的索引,一定會命中,哪怕基數為1。所以當查詢條件中有基數小的列時,某個索引值的條件只是從等於號
改成小於號
,就可能從使用索引退化到掃描全表。查看某個SQL語句是否使用某個索引靠譜的做法只有一個——使用Explain語句分析SQL。
第4個場景和第3個有點類似,Mysql優化器都是覺得用索引帶來的掃描次數和全表掃描沒啥區別,用索引還要承擔額外頻繁切換文件讀取指針
帶來的開銷,所以還不如使用全表掃描。第4個場景可能有點難懂,所以舉一個例子來說明。比如一張表Animal有以下數據:
id | category | name | other fields
1 1 asaf ...
2 2 ikashef ...
3 3 lksd ...
... ... ... ...
9999 3 asdf ...
10000 2 adsfa ...
我們在category上創建了一個索引,然后我們使用以下SQl語句進行查找:
Select * from Animal where category > 1 and name = "asaf"
這時候category對應的索引數據會如下所示:
1 --> 1,...k
2 --> 2,...p...10000
3 --> 3,...q...9999
假如使用了Category索引,那么記錄檢索器第一步會匹配出滿足的索引值,即2,3(>1),而2,3對應的所有數據集都會被比較。待掃描的記錄行數可能占了整個表的大部分,Mysql搜索優化器最終選擇放棄該索引,退化為全表掃描。
2. 多列索引(組合索引)
Mysql支持創建組合索引
(其實就是同時在多個列上創建索引)。一個索引最大可以包含16個列。對於某些數據類型的列來說,你還可以對其前綴進行索引([前綴索引(https://dev.mysql.com/doc/refman/5.7/en/column-indexes.html#column-indexes-prefix)])[2]。當查詢條件匹配了索引中的所有列、第一列、前二列、前三列等時,Mysql就會使用這個多列索引,如果我們在定義索引的時候就安排好列的順序,一個單獨的組合索引總是可以加快好幾種查詢。換句話說,定義好一個組合索引,只要某個查詢用到了里面的某些字段,很可能會命中這個索引[2]。
2.1 組合索引的數據構成
單列索引的數據構成很簡單,以B樹實現為例,每一個索引值對應了一個樹的節點,通過樹的節點可以快速找到最終對應了哪些記錄。那么組合索引
的索引值構成又是啥樣的呢,我們可以把多列索引(組合索引
、混合索引
)的索引數據當作一個排序好的數組, 索引數據的每一行就是由這些索引列對應的值組合起來的字符串[2]。比如對於index (a, b, c)
來說, 數據庫中有數據:
1..."1","2","3"...
2..."6","4","5"...
3..."3","2","9"...
4..."1","2","3"
那么索引數據就會像一樣:
[1] [2] [3] --> 1,4
[3] [2] [9] --> 3
[6] [4] [5] -->2
當然了,這里是從上至下的排序,真正Mysql使用的索引數據結構一般是是樹狀的,但本質也是排序好的。所以我們可以想象的到,如果第一個的組合索引字段差異化很小(基數很小)的話,查詢效率不會很高,因為很多索引節點的第一個值都是一樣的,使用索引篩選出的集合還是很大,Mysql搜索優化器就會估算出了一個很大的待掃描集合,然后退化為全表掃描。
注意: 這是官方文檔上打的一個比方。實際查詢條件可能是”>、<、<=”這樣的范圍比較符號,需要對每一列作比較,所以不會真正的連接成一個字符串,這里只是一個形象的比喻,告知大家
組合索引
和普通索引在數據構成上其實沒啥區別。
2.2 組合索引的一種替代方案
官方事實上,除了使用多列索引外,我們還有一個選擇,那就是在表中增加一列哈希
列,哈希列的值計算自其他的某幾列。如果哈系列很短,唯一性好,並且加了索引,那么它將比直接使用組合索引快得多。在Mysql中,使用這個哈希
列很簡單,比如[2]:
SELECT * FROM tbl_name
WHERE hash_col=MD5(CONCAT(val1,val2))
AND col1=val1 AND col2=val2;
很顯然,這種方法只適用於精確匹配的情況,如果用到了范圍比較符號(>,<,>=等),那就無法使用了。
2.3 組合索引的命中規則——最左匹配原則
下面我們再來看看哪些查詢會使用到我們定義的組合索引。假設一張表的定義如下:
CREATE TABLE test (
id INT NOT NULL,
last_name CHAR(30) NOT NULL,
first_name CHAR(30) NOT NULL,
PRIMARY KEY (id),
INDEX name (last_name,first_name)
);
name
索引是一個建立在last_name
和first_name
上的索引。 這個索引會被那些為last_name
和first_name
字段指定了已知范圍的查詢所使用,當然了,他也會被只指定了last_name
的查詢所使用,因為只指定last_name
的情況恰好符合了**最左索引匹配原則**
。我們舉些例子來說明下,name
索引會在以下查詢語句被使用到:
SELECT * FROM test WHERE last_name='Widenius';
SELECT * FROM test
WHERE last_name='Widenius' AND first_name='Michael';
SELECT * FROM test
WHERE last_name='Widenius'
AND (first_name='Michael' OR first_name='Monty');
SELECT * FROM test
WHERE last_name='Widenius'
AND first_name >='M' AND first_name < 'N';
如果一個表有一個組合索引,那么任何符合**最左匹配原則**
的查詢條件都會觸發優化器使用該索引進行數據查找。比如,有一個三列的組合索引(col1, col2, col3)
,那么當查詢:where col1=xxx
、where col1=xxx and col2=xxx
、where col1=xxx and col2=xxx and col3=xxx
時,都會觸發該索引。
最左匹配原則
——在對查詢條件中的字段進行組合索引
的匹配時,只考慮匹配其前N個字段,比如前一個(第一個)、前2個、前3個字段等。其他情況視為未匹配。
而如果某個查詢不滿足最左原則,那么Mysql將不會使用該索引。下面我們來舉幾個例子,如下所示的4個查詢語句:
1. SELECT * FROM tbl_name WHERE col1=val1;
2. SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2;
3. SELECT * FROM tbl_name WHERE col2=val2;
4. SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3;
假設我們在 (col1, col2, col3)
上創建了索引,那么只有1和2語句命中了該索引。后面兩條語句則不會命中,因為他們不滿足**最左匹配原則**
。 然而打臉的是實際情況中我們在Mysql中做實驗的時候,會發現3,4也命中了該索引。 這是因為觸發了默認索引選擇算法
選取索引。當Mysql沒找到適合的索引,准備退化到全表掃描前,會使用一個默認索引選擇算法
。Mysql認為只要能找到這樣一個索引,總會比全表掃描好一點。
**
默認索引選擇算法
**——當查詢語句的搜索條件沒有命中任何索引時,Mysql索引優化器會考量查詢語句中的目標字段(select后面,where前面的部分),目標字段除去主鍵外,如果恰好是某個索引(包括組合索引)對應列的子集,那么該索引也會被使用。如果滿足的索引有多個,將會使用索引記錄數最少的索引。這個算法在[3]中得到了旁證。
比較巧的是,*代表了所有字段,除去了主鍵外,其他3個字段剛好在上述索引中,所以上述索引被命中。這時候只需要表中再多一個沒有被索引的字段col4
,索引就不會被命中。
還是剛剛的例子,以下查詢語句將不會命中name
索引:
SELECT * FROM test WHERE first_name='Michael';
SELECT * FROM test
WHERE last_name='Widenius' OR first_name='Michael';
又一次打臉的是,如果你真的建了這么一個表來做實驗,使用Explain
命令進行執行分析時,上述語句的分析結果可能提示使用了該索引。還是因為觸發了默認索引選擇算法
。
3. 索引設計建議
針對以上討論的Mysql索引的選取規則和命中規則,可以總結出以下開發過程中需要注意的地方:
- 索引不是定義的越多越好,對於查詢條件比較多的情況,避免為每個字段創建索引,只需要創建一個聯合索引即可。創建聯合索引時,把可能存在單列查詢的那一列放前面。比如業務需求要求以下幾種查詢:
... WHERE col1 = xxx, col2 > yyy, col3 < zzz; ... WHERE col2 > yyy, col3 < zzz ... WHERE col2 > yyy
這時候,組合索引應該建成為:
index(col2, col3, col1)
這時候上述三種查詢都會使用到該索引。
-
創建聯合索引時,值差異化大的列放在前面,而不是那些取值種類很少的列。比如
用戶名
和用戶狀態
兩列,創建索引的順序應當是index(用戶名、用戶狀態),而不是index(用戶狀態、用戶名)。如果把基數很小的用戶狀態放在第一個,那么如果恰好查詢語句條件是“用戶名=某個值 AND 用戶狀態>某個值”時,Mysql索引優化器很可能根據用戶狀態得出要掃描的行數太多,退化為全表掃描,而后面的精確匹配用戶名的條件就沒用上了。 -
由於Mysql索引優化器的存在,有時候會出現很多意向不到不使用索引的情況。所以每次寫無法確定使用哪個索引的Sql語句時(尤其是WHERE條件后是>,<等范圍選擇時),一定要多用EXPLAIN語句進行分析。
-
可以認為Mysql在執行SQL語句時,一個表只可能使用一個索引(開啟了Index Merge的情況除外)。新人在建表的時候總是會忽略這個事實,從而為很多列單獨建立了索引,認為這樣會更快。
- Mysql索引優化規則是一個官網都沒說清的問題,在復雜SQL的情況下不可避免的會產生一些事與願違的情況(已知的影響因素1.1中有提到),導致Mysql很蠢的使用了不該使用的索引(這種情況的確會存在[6],這也佐證了Mysql的執行計划是估算出來的,並不總是靠譜)。實際使用中發現索引使用錯誤的情況,可以使用Force Index/Use Index等引導Mysql搜索優化器使用某個索引