詳解Mysql索引原理及其優化
目錄
前言
網上都說學會mysql需要學會兩個部分,索引和事務,其實在最近的Mysql學習過程中,我覺得應該是有三個部分的,索引,查詢,事務.其中的查詢主要是指查詢優化即編寫高效率的SQL語句.
本文記錄一下學習MySQL的索引過程中的一些知識.主要為閱讀《高效能MySQL》的一些理解和擴充套件.
什麼是索引
索引是儲存引擎用於快速找到記錄的一種資料結構.
這是MySQL官方對於索引的定義,可以看到索引是一種資料結構,那麼我們應該怎樣理解索引呢?一個常見的例子就是書的目錄.我們都已經養成了看目錄的習慣,拿到一本書時,我們首先會先去檢視他的目錄,並且當我們要查詢某個內容時,我們會在目錄中查詢,然後找到該片段對應的頁碼,再根據相應的頁碼去書中查詢.如果沒有索引(目錄)的話,我們就只能一頁一頁的去查找了.
在MySQL中,假設我們有一張如下記錄的表:
id | name | age |
---|---|---|
1 | huyan | 10 |
2 | huiui | 18 |
3 | lumingfei | 20 |
4 | chuzihang | 15 |
5 | nono | 21 |
如果我們希望查詢到年齡為15的人的名字,在沒有索引的情況下我們只能遍歷所有的資料去做逐一的對比,那麼時間複雜度是O(n).
而如果我們在插入資料的過程中,額外維護一個陣列,將age欄位有序的儲存.得到如下陣列.
[10,15,18,20,21]
| | | | |
[x1,x4,x2,x3,x5]
複製程式碼
下面的x是模擬資料再磁碟上的儲存位置.這個時候如果我們需要查詢15歲的人的名字.我們可以對蓋陣列進行二分查詢.眾所周知,二分查詢的時間複雜度為O(logn).查詢到之後再根據具體的位置去獲取真正的資料.
PS:MySQL中的索引不是使用的陣列,而是使用的B+樹(後面講),這裡用陣列舉例只是因為比較好理解.
索引能為我們帶來什麼?
如上面所說,索引能幫助我們快速的查詢到資料.其次因為索引中的值是順序儲存,那麼可以幫助我們進行orderby操作.而且索引中也是儲存了真正的值的,因此有一些的查詢直接可以在索引中完成(也就是覆蓋索引的概念,後面會提到).
總結一下索引的優點就是(《高效能》書中總結的):
- 減少查詢需要掃描的資料量(加快了查詢速度)
- 減少伺服器的排序操作和建立臨時表的操作(加快了groupby和orderby等操作)
- 將伺服器的隨機IO變為順序IO(加快查詢速度).
索引有哪些缺點呢?
首先索引也是資料,也需要儲存,因此會帶來額外的儲存空間佔用.其次,在插入,更新和刪除操作的同時,需要維護索引,因此會帶來額外的時間開銷.
總結一下:
- 索引佔用磁碟或者記憶體空間
- 減慢了插入更新操作的速度
實際上,在一定資料範圍內(索引沒有超級多的情況下),建立索引帶來的開銷是遠遠小於它帶來的好處的,但是我們仍然要防止索引的濫用.
都有哪些型別的索引?
對於MySQL來說,在伺服器層並不實現索引,而是交給儲存引擎來實現的,因此不同的儲存引擎實現的索引型別不太一樣.InnoDB作為當前使用最為廣泛的儲存引擎,使用的是B+樹索引,因此我們大部分時間提到的索引也都是指的它.
MySQL主要有以下幾種索引:
- B-樹索引/B+樹索引
- 雜湊索引
- 空間資料索引
- 全文索引
本文只學習B-樹索引和B+樹索引.
B-樹索引和B+樹索引
這裡不會特別詳細的解釋B-樹和B+樹的資料結構原理,有興趣的小夥伴可以移步參考文章中的文章.或者通過google自行了解.
B-樹
B-樹是一棵多路平衡查詢樹,對於一棵M階的B-樹有以下的性質:
- 根節點至少有兩個子女.
- 每個節點包含k-1個元素和k個孩子,其中m/2 <= k <= m.
- 每一個葉子節點都包含k-1個元素,其中m/2 <= k <= m.
- 所有的葉子節點位於同一層.
- 每個節點中的元素從小到大排列,那麼k-1個元素正好是k個孩子包含的值域的劃分.
這麼說可能會有一些難理解,可以將B-樹理解為一棵更加矮胖的二叉搜尋樹.
B+樹
B+樹是B-樹的進階版本,在B-樹的基礎上又做了如下的限制:
- 每個中間節點不儲存資料,只用來索引,也就意味著所有非葉子節點的值都被儲存了一份在葉子節點中.
- 葉子節點之間根據自身的順序進行了連結.
這樣可以帶來什麼好處呢?
- 中間節點不儲存資料,那麼就可以儲存更多的索引,減少資料庫磁碟IO的次數.
- 因為中間節點不儲存資料,所以每一次的查詢都會命中到葉子節點,而葉子節點是處在同一層的,因此查詢的效能更加的穩定.
- 所有的葉子節點按順序連結成了連結串列,因此可以方便的話進行範圍查詢.
怎樣建立高效能的索引?
由於優化索引和優化查詢一般是分不開的,因此這一塊可能會包含部分的查詢優化內容.
字首索引和索引選擇性
如果希望給一個很長的字串上新增索引,那麼可以考慮使用字首索引.在正式介紹字首索引之前,我們先大概考慮一下索引的工作步驟,資料庫使用索引進行查詢的時候,一般是如下幾步:
- 在索引的B+樹上找到對應的值,比如找到學校名稱為
卡塞爾學院
的一條記錄,並且拿到這條資料在磁碟上的地址. - 根據地址去磁碟上查詢,拿到該條資料所有的值.
那麼假如在所有的學校名稱的值中,卡塞爾
就可以唯一的標識這條資料,那麼用卡塞爾
來做索引是否可以達到和卡塞爾學院
做索引相同的效果?
答案是肯定的,而使用卡塞爾
的話,是可以減少索引的大小到原來的60%的.這就是字首索引的作用.
字首索引: 在對一個比較長的字串進行索引時,可以僅索引開始的一部分字元,這樣可以大大的節約索引空間,從而提高索引效率.但是這樣也會降低索引的選擇性.
索引的選擇性: 不重複的值/所有的值. 可以看出索引的選擇性為0-1
,最高的就是該列唯一,沒有重複值.所以唯一索引的效率是比較好的.
但是在一般情況下,較長的字串的一些字首的選擇性也是比較好的,這個我們可以算出來.使用下面的語句:
select
count(distinct left(school_name,3))/count(*) as sch3,count(distinct left(school_name,4))/count(*) as sch4,5))/count(*) as sch5,count(distinct school_name)/count(*) as original
from
user;
複製程式碼
其中查詢到的original
就是原本的選擇性,sch3,sch4,sch5
分別是取該列的前3,4,5個字元作為索引的時候的選擇性.逐步增加這個數值,當選擇性與原來相差不大的時候,就是一個比較合適的字首索引的長度.(一般情況下是這樣,但是也有例外,當資料極其不均勻時,這樣的字首索引會在某個特殊的case上表現很差勁).
找到合適的長度之後,就可以建立一個字首索引了:alter table user add index sch_pre3(`school(3)`)
注意:字首索引和覆蓋索引是很難一起使用的,我今天早上剛試過,對索引的優化進行到這一步之後無功而返,具體的原因在下面介紹完覆蓋索引之後解釋.
聯合索引
一般我們都是有對多個列進行索引
的需求的,因為查詢的需求多種多樣.這個時候我們可以選擇建立多個獨立的索引或者建立一個聯合索引.大多數時候都是聯合索引更加合適一些.
假設我們要執行這個語句:select * from user where school_name = '卡塞爾' and age > 20
,我們在school
和age
上分別建立兩個獨立的索引,那麼我們預期這條查詢語句會命中兩個索引,但是使用explain命令檢視會發現不一定.這是一個玄學的過程.個人沒有研究清楚.
從理論上來講,MySQL在5.0之後的版本里面對支援合併索引,也就是同時使用兩個索引,但是MySQL的優化器不一定這樣認為,他可能會認為,查詢兩次B+樹的代價高於查詢一次索引之後去資料表進行過濾
,因此會選擇只用一個索引.(我在自己的5張表上做了類似此case的測試,結果都是隻使用了一個索引.)
建立聯合索引的語法:alter table user add index school_age(`school`,`age`)
.
使用聯合索引的時候,有一個非常重要的因素就是所有的索引列只可以進行最左字首匹配,例如上面的school_age
聯合索引,當僅使用age作為查詢條件的時候是不能使用的,也就是說select * from user where age =20
是不能命中上面的聯合索引的.
在不考慮任何查詢的情況下,我們應該講選擇性高的列放在聯合索引的前面,但是實際上我們更多的是通過查詢來反推索引,以使某個固定的查詢可以儘可能的命中索引以提高查詢速度
.畢竟我們建立索引的目的也是為了加快查詢的速度.
因此聯合索引的優化更多的是根據某個或者某些語句來優化的,不具備一個通用的法則.
最左字首索引的原理
當資料列有序的時候,mysql可以使用索引,那麼假設我們建立了school_age
索引,示例資料如下:
school | age |
---|---|
a | 12 |
b | 12 |
b | 14 |
b | 15 |
c | 1 |
在這份資料中,school欄位是完全有序的,索引school可以使用索引.
而從全表來看,age欄位不是有序的,因此無法直接使用索引,那麼觀察一下資料表,在什麼時候age有序呢?在school進行定值匹配的時候,例如當school=b
的時候,對於這三條資料而言,age是有序的,因此可以使用age索引.這就是最左字首的原理.
此外,最左字首索引只能使用一個範圍查詢,例如select * from user where school > a
,select * from user where school = a and age > 12
,都是可以命中索引的,但是select * from user where school > a and age > 12
中,僅school可以命中索引,這也可以從上面得出結論.因為當school是範圍匹配的時候,mysql無法確認age欄位是否嚴格有序,比如 school的範圍匹配命中了b,c的四條資料,那麼age就不是有序的.無法使用後續的索引.
聚簇索引
聚簇索引不是一種索引型別,而是一種儲存資料的方式.Innodb的聚簇索引是在同一個資料結構中儲存了索引和資料.
因為資料真正的資料只能有一種排序方式,所以一個表上只能有一個聚簇索引.Innodb使用主鍵來進行聚簇索引,沒有主鍵的話就會選擇一個唯一的非空索引,如果還還沒有,innodb會選擇生成一個隱式的主鍵來進行聚簇索引.為什麼innodb這麼執著的需要搞一個聚簇索引呢,因為一個資料表中的資料總得有且只有一種排序方式來儲存在磁碟上,因此這是必須的.
這也是innodb推薦我們使用自增主鍵的原因,因為自增主鍵自增且連續,在插入的時候只需要不斷的在資料後面追加即可.設想一下使用UUID來作為主鍵,那麼每一次的插入操作,都需要找到當前主鍵在已排序的主鍵中的位置,然後插入,並且要移動該主鍵後的資料,以使得資料和主鍵保持相同的順序,這無疑是代價非常高的.
也是因為這個原因,在其他索引的葉子節點中,儲存的"資料"其實不是該資料的真實實體地址,而是該資料的主鍵,查詢到主鍵之後,再根據主鍵進行一次索引,拿到資料.
聚簇索引和非聚簇索引的區別可以用一個簡單的例子來說明:
當我們拿到一本書的時候,目錄就是主鍵,是一個聚簇索引,因為在目錄中連續的內容,在正文中也是連續的,當我們想要檢視迎著陽光盛大逃亡
章節,只需要在目錄中找到它對應的頁面,比如459,然後去對應的頁碼檢視正文即可.
而非聚簇索引呢,則類似於書後面的附錄專有名詞索引一樣(二級普通索引),當你查詢邦達列夫
的時候,附錄會告訴你,這個名詞出現在了迎著陽光盛大逃亡
一節,然後你需要去目錄(主鍵索引)中再次查詢到對應的頁碼.
覆蓋索引
當一個索引包含(或者說是覆蓋)需要查詢的所有欄位的值時,我們稱之為覆蓋索引.
設想有如下的查詢語句:
select
school_name,age
from
user
where
school_name = '金色鶯尾花學院'
複製程式碼
這個語句根據學校名稱來查詢資料行的學校名稱和年齡,從上面的資料查詢的步驟我們可以知道,當在索引中找到要求的值的時候,還需要根據主鍵去進行一次索引,以拿到全部的資料,然後從其中挑選出需要的列,返回.但是現在索引中已經包含了所有的需要返回的列,那麼就不用進行回資料表查詢的操作了,此外索引的大小一般是遠遠小於真正的資料大小的,覆蓋索引可以極大的減少從磁碟載入資料的數量.
為什麼字首索引和覆蓋索引無法一起使用?
因為字首索引的目的是用字首來代表真正的值,他們在選擇性上幾乎沒有區別,但是MySQL仍然無法判斷真正的資料是什麼,比如阿里巴巴
和阿里媽媽
在字首為2的時候是一樣的,但是為了確保你查詢阿里巴巴的時候不會出現阿里媽媽的內容,是需要回到資料表拿到資料再次進行一個精準匹配來進行過濾的.
因此,覆蓋索引無法和列字首索引一起使用,這是我用一個早晨的時間測試得出的結論.
刪除掉冗餘和重複的索引
有一些索引是從未在查詢中使用過,卻白白增加資料插入時開銷的,對於這種索引我們應該及時的進行刪除.
比如在主鍵上再建立一個普通索引,無疑是毫無作用的.
還比如在有聯合索引school_age
的情況下,再建立一個school
的獨立索引,因為索引的最左字首匹配原則,school_age
是完全可以命中對school
的單獨查詢的,因此後者可以刪掉.
如何檢視索引的一些相關資訊?
索引資訊
在mysql中可以使用show index from table_name
來檢視某個表上的索引,它將會有如下的輸出:
或者使用show create table table_name
來檢視建表語句,其中包含建立索引的語句.
索引大小
在5.0以後的版本中,我們可以通過檢視information_schema.TABLES
表中的資料來獲取更加詳細的資料.
該表各欄位的含義如下表:
欄位 | 含義 |
---|---|
Table_catalog | 資料表登記目錄 |
Table_schema | 資料表所屬的資料庫名 |
Table_name | 表名稱 |
Table_type | 表型別[system view |
Engine | 使用的資料庫引擎[MyISAM |
Version | 版本,預設值10 |
Row_format | 行格式[Compact |
Table_rows | 表裡所存多少行資料 |
Avg_row_length | 平均行長度 |
Data_length | 資料長度 |
Max_data_length | 最大資料長度 |
Index_length | 索引長度 |
Data_free | 空間碎片 |
Auto_increment | 做自增主鍵的自動增量當前值 |
Create_time | 表的建立時間 |
Update_time | 表的更新時間 |
Check_time | 表的檢查時間 |
Table_collation | 表的字元校驗編碼集 |
Checksum | 校驗和 |
Create_options | 建立選項 |
Table_comment | 表的註釋、備註 |
我們可以通過一些查詢語句來獲取詳細的資訊,比如:
// 檢視當前MySQL伺服器所有索引的大小(以MB為單位,預設是位元組)
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024),2),' MB') AS 'Total Index Size' FROM TABLES
// 檢視某一個庫的所有大小
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024),' MB') AS 'Total Index Size' FROM TABLES WHERE table_schema = 'XXX';
// 檢視某一個表的索引大小
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024),' MB') AS 'Total Index Size' FROM TABLES WHERE table_schema = 'yyyy' and table_name = "xxxxx";
// 彙總檢視一個庫中的資料大小及索引大小
SELECT CONCAT(table_schema,'.',table_name) AS 'Table Name',CONCAT(ROUND(table_rows/1000000,4),'M') AS 'Number of Rows',CONCAT(ROUND(data_length/(1024*1024*1024),'G') AS 'Data Size',CONCAT(ROUND(index_length/(1024*1024*1024),'G') AS 'Index Size',CONCAT(ROUND((data_length+index_length)/(1024*1024*1024),'G') AS'Total'FROM information_schema.TABLES WHERE table_schema LIKE 'xxxxx';
複製程式碼
對tables表的資料的所有檢視方式都是可以的,其中還包含了一些表格本身的資料資訊,但是因為和本文的主題不符合,這裡就不舉例子了.
注意:上面的表格是有快取的,當更新資料庫索引之後,最好執行analyze table xxxx
,然後再進行檢視.MySQL會在表格資料發生較大的變化時才更新此表(大小變化超過1/16或者插入20億行).
索引碎片
在索引的建立刪除過程中,不可避免的會產品索引碎片,當然還有資料碎片,我們可以通過執行optimize table xxx
來重新整理索引及資料,對於不支援此命令的儲存引擎來說,可以通過一條無意義的alter語句來觸發整理,比如:將表的儲存引擎更換為當前的引擎,alter table xxxx engine=innodb
.
參考文章
完。
ChangeLog
2019-06-01 完成以上皆為個人所思所得,如有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連結。
聯絡郵箱:[email protected]
更多學習筆記見個人部落格------>呼延十