索引原理與慢查詢優化
一 我們要搞明白的問題
讓我們帶着以下問題展開對索引的探索
1、為何索引叫key
2、索引是如何加速查詢的,它的原理是啥?
索引模型/結構從二叉樹-》平衡二叉樹-》b樹最后到b+樹,每種樹到底有什么問題最終演變成到了b+樹
3、為何b+樹不僅能夠加速等值查詢,還能加速范圍查詢
4、什么是聚集索引,什么是輔助索引
5、什么情況下叫覆蓋了索引
6、什么情況下叫回表操作
7、什么是聯合索引,最左前綴匹配原則
8、索引下推,查詢優化
9、如何正確使用索引?
二 索引介紹
2.1 什么是索引
索引是存儲引擎中一種數據結構,或者說數據的組織方式,又稱之為鍵key,是存儲引擎用於快速找到記錄的一種數據結構。
為數據建立索引就好比是為書建目錄,或者說是為字典創建音序表,如果要查某個字,如果不使用音序表,則需要從幾百頁中逐頁去查。
2.2 為何要用索引?
一般的應用系統,讀寫比例在10:1左右,而且插入操作和一般的更新操作很少出現性能問題,在生產環境中,我們遇到最多的、也是最容易出問題的,還是一些復雜的查詢操作,因此對查詢語句的優化顯然是重中之重。說起加速查詢,就不得不提到索引了。
索引優化應該是對查詢性能優化最有效的手段了。索引能夠輕易將查詢性能提高好幾個數量級。
2.3 如何正確看待索引?
# 先說結論:
索引不是越多越好,並且jason建議大家最好在提前創建好索引,而不是等火燒眉毛
# 詳解如下
索引是應用程序設計和開發的一個重要方面。若索引太多,應用程序的性能可能會受到影響。而索引太少,對查詢性能又會產生影響,要找到一個平衡點,這對應用程序的性能至關重要。
一些開發人員總是在事后才想起添加索引----我一直認為,這源於一種錯誤的開發模式。如果知道數據的使用,從一開始就應該在需要處添加索引。開發人員往往對數據庫的使用停留在應用的層面,比如編寫SQL語句、存儲過程之類,他們甚至可能不知道索引的存在,或認為事后讓相關DBA加上即可。DBA往往不夠了解業務的數據流,而添加索引需要通過監控大量的SQL語句進而從中找到問題,這個步驟所需的時間肯定是遠大於初始添加索引所需的時間,並且可能會遺漏一部分的索引。當然索引也並不是越多越好,我曾經遇到過這樣一個問題:某台MySQL服務器iostat顯示磁盤使用率一直處於100%,經過分析后發現是由於開發人員添加了太多的索引,在刪除一些不必要的索引之后,磁盤使用率馬上下降為20%。可見索引的添加也是非常有技術含量的。
三 理解索引的儲備知識
要了解索引的數據結構我來先來儲備一些知識
1)儲備知識1:機械磁盤一次IO的時間
機械磁盤一次io的時間 = 尋道時間 + 旋轉延遲 + 傳輸時間
# 尋道時間
道時間指的是磁臂移動到指定磁道所需要的時間,主流磁盤一般在5ms以下
# 旋轉延遲
旋轉延遲就是我們經常聽說的磁盤轉速,比如一個磁盤7200轉,表示每分鍾能轉7200次,也就是說1秒鍾能轉120次,旋轉延遲就是1/120/2 = 4.17ms;
# 傳輸時間
傳輸時間指的是從磁盤讀出或將數據寫入磁盤的時間,一般在零點幾毫秒,相對於前兩個時間可以忽略不計
所以訪問一次磁盤的時間,即一次磁盤IO的時間約等於5+4.17 = 9ms左右
這9ms對於人來說可能非常短,但對於計算機來可是非常長的一段時間,長到什么程度呢?
一台500 -MIPS(Million Instructions Per Second)的機器每秒可以執行5億條指令,因為指令依靠的是電的性質,換句話說執行一次IO的時間可以執行約450萬條指令,數據庫動輒十萬百萬乃至千萬級數據,每次9毫秒的時間,顯然是個災難。
2)儲備知識2:磁盤的預讀
# 考慮到磁盤IO是非常高昂的操作,計算機操作系統做了一些優化:
當一次IO時,不光把當前磁盤地址的數據,而是把相鄰的數據也都讀取到內存緩沖區內,因為局部預讀性原理告訴我們,當計算機訪問一個地址的數據的時候,與其相鄰的數據也會很快被訪問到。每一次IO讀取的數據我們稱之為一頁(page)。具體一頁有多大數據跟操作系統有關,一般為4k或8k,也就是我們讀取一頁內的數據時候,實際上才發生了一次IO,這個理論對於索引的數據結構設計非常有幫助。
3)儲備知識3:索引原理精髓提煉
索引的目的在於提高查詢效率,與我們查閱圖書所用的目錄是一個道理:先定位到章,然后定位到該章下的一個小節,然后找到頁數。相似的例子還有:查字典,查火車車次,飛機航班等
!!!!!!!!!!!!!!!!!!!!!!!!!!!!
本質都是:
通過不斷地縮小想要獲取數據的范圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是說,有了這種索引機制,我們可以總是用同一種查找方式來鎖定數據。
!!!!!!!!!!!!!!!!!!!!!!!!!!!!
數據庫也是一樣,但顯然要復雜的多,因為不僅面臨着等值查詢,還有范圍查詢(>、<、between、in)、模糊查詢(like)、並集查詢(or)等等。數據庫應該選擇怎么樣的方式來應對所有的問題呢?我們回想字典的例子,能不能把數據分成段,然后分段查詢呢?最簡單的如果1000條數據,1到100分成第一段,101到200分成第二段,201到300分成第三段......這樣查第250條數據,只要找第三段就可以了,一下子去除了90%的無效數據。但如果是1千萬的記錄呢,分成幾段比較好?稍有算法基礎的同學會想到搜索樹,其平均復雜度是lgN,具有不錯的查詢性能。但這里我們忽略了一個關鍵的問題,復雜度模型是基於每次相同的操作成本來考慮的。而數據庫實現比較復雜,一方面數據是保存在磁盤上的,另外一方面為了提高性能,每次又可以把部分數據讀入內存來計算,因為我們知道訪問磁盤的成本大概是訪問內存的十萬倍左右,所以簡單的搜索樹難以滿足復雜的應用場景。
四 索引分類
索引模型分為很多種類
#===========B+樹索引(等值查詢與范圍查詢都快)
二叉樹->平衡二叉樹->B樹->B+樹
#===========HASH索引(等值查詢快,范圍查詢慢)
將數據打散再去查詢
#===========FULLTEXT:全文索引 (只可以用在MyISAM引擎)
通過關鍵字的匹配來進行查詢,類似於like的模糊匹配
like + %在文本比較少時是合適的
但是對於大量的文本數據檢索會非常的慢
全文索引在大量的數據面前能比like快得多,但是准確度很低
百度在搜索文章的時候使用的就是全文索引,但更有可能是ES
不同的存儲引擎支持的索引類型也不一樣
-
InnoDB存儲引擎
支持事務,支持行級別鎖定,支持 B-tree(默認)、Full-text 等索引,不支持 Hash 索引;
-
MyISAM存儲引擎
不支持事務,支持表級別鎖定,支持 B-tree、Full-text 等索引,不支持 Hash 索引;
-
Memory存儲引擎
不支持事務,支持表級別鎖定,支持 B-tree、Hash 等索引,不支持 Full-text 索引;
因為mysql默認的存儲引擎是innodb,而innodb存儲引擎的索引模型/結構是B+樹,所以我們着重介紹B+樹,那么大家最關注的問題來了:
B+樹索引到底是如何加速查詢的呢?
五 索引的數據結構
5.1 創建索引的兩大步驟
為某個字段創建索引,即以某個字段的值為基礎構建索引結構,那么如何構建呢?分為兩大步驟
- 1、提取每行記錄中該字段的值,以該值當作key,至於key對的value是什么?每種索引結構各不相同
- 2、然后以key值為基礎構建索引結構
以后的查詢條件中使用了該字段,則會命中索引結構
例
# 1、為user表的id字段創建索引,會以每條記錄的id字段值為基礎生成索引結構
create index 索引名 on user(id);
使用索引
select * from user where id = xxx;
# 2、為user表的name字段創建索引,會以每條記錄的name字段值為基礎生成索引結構
create index 索引名 on user(id);
使用索引
select * from user where name = xxx;
那么索引的結構到底長什么樣子,讓其能夠加速查詢呢?
innodb存儲引擎默認的索引結構為B+樹,而B+樹是由二叉樹、平衡二叉樹、B樹再到B+樹一路演變過來的
5.2 二叉查找樹
有user表,我們以id字段值為基礎創建索引
-
1、提取每一條記錄的id值作為key值,value為本行完整記錄,即
key value 10 (10,zs) 7 (7,ls) 13 (13,ls) 5 (5,ls) 8 (8,ls) 12 (12,ls) 17 (17,ls)
-
2、以key值的大小為基礎構建二叉樹,如上圖所示
二叉查找樹的特點就是任何節點的左子節點的鍵值都小於當前節點的鍵值,右子節點的鍵值都大於當前節點的鍵值。 頂端的節點我們稱為根節點,沒有子節點的節點我們稱之為葉節點。
如果我們需要查找id=12的用戶信息
select * from user where id=12;
利用我們創建的二叉查找樹索引,查找流程如下:
1、將根節點作為當前節點,把12與當前節點的鍵值10比較,12大於10,接下來我們把當前節點>的右子節點作為當前節點。
2、繼續把12和當前節點的鍵值13比較,發現12小於13,把當前節點的左子節點作為當前節點。
3、把12和當前節點的鍵值12對比,12等於12,滿足條件,我們從當前節點中取出data,即id=1>2,name=xm。
利用二叉查找樹我們只需要3次即可找到匹配的數據。如果在表中一條條的查找的話,我們需要6次才能找到。
5.3 平衡二叉樹
基於5.1所示的二叉樹,我們確實可以快速地找到數據。
但是,但是,但是讓我們回到二叉查找樹地特點上,只論二叉查找樹,它的特點只是
任何節點的左子節點的鍵值都小於當前節點的鍵值,右子節點的鍵值都大於當前節點的鍵值。
所以,依據二叉查找樹的特點,二叉樹可以是這樣構造的
這個時候可以看到我們的二叉查找樹變成了一個鏈表。如果我們需要查找id=17的用戶信息,我們需要查找7次,也就相當於全表掃描了。 導致這個現象的原因其實是二叉查找樹變得不平衡了,也就是高度太高了,從而導致查找效率的不穩定。 為了解決這個問題,我們需要保證二叉查找樹一直保持平衡,就需要用到平衡二叉樹了。
平衡二叉樹又稱AVL樹,在滿足二叉查找樹特性的基礎上,要求每個節點的左右子樹的高度不能超過1。 下面是平衡二叉樹和非平衡二叉樹的對比:
由平衡二叉樹的構造我們可以發現第一張圖中的二叉樹其實就是一棵平衡二叉樹。平衡二叉樹保證了樹的構造是平衡的,當我們插入或刪除數據導致不滿足平衡二叉樹不平衡時,平衡二叉樹會進行調整樹上的節點來保持平衡。具體的調整方式這里就不介紹了。平衡二叉樹相比於二叉查找樹來說,查找效率更穩定,總體的查找速度也更快。
那么是不是說基於平衡二叉樹構建索引的結構就可以了呢?答案是否!
5.4 B樹
那么直接用平衡二叉樹這種數據結構來構建索引有什么問題?
1、首先,因為內存的易失性。一般情況下,我們都會選擇將user表中的數據和索引存儲在磁盤這種外圍設備中。但是和內存相比,從磁盤中讀取數據的速度會慢上百倍千倍甚至萬倍,所以,我們應當盡量減少從磁盤中讀取數據的次數。
2、另外,從磁盤中讀取數據時,都是按照磁盤塊來讀取的,並不是一條一條的讀。 如果我們能把盡量多的數據放進磁盤塊中,那一次磁盤讀取操作就會讀取更多數據,那我們查找數據的時間也會大幅度降低。
3、所以,如果我們單純用平衡二叉樹這種數據結構作為索引的數據結構,即每個磁盤塊只放一個節點,每個節點中只存放一組鍵值對,此時如果數據量過大,二叉樹的節點則會非常多,樹的高度也隨即變高,我們查找數據的也會進行很多次磁盤IO,查找數據的效率也會變得極低!
綜上,如果我們能夠在平衡二叉的樹的基礎上,把更多的節點放入一個磁盤塊中,那么平衡二叉樹的弊端也就解決了。即構建一個單節點可以存儲多個鍵值對的平衡樹,這就是B樹。
B樹(Balance Tree)即為平衡樹的意思,下圖即是一顆B樹。
注意:
– 1、圖中的p節點為指向子節點的指針,二叉查找樹和平衡二叉樹其實也有,因為圖的美觀性,被省略了。– 2、圖中的每個節點里面放入了多組鍵值對,一個節點也稱為一頁,一頁即一個磁盤塊,在mysql中數據讀取的基本單位都是頁,即一次io讀取一個頁的數據,所以我們這里叫做頁更符合mysql中索引的底層數據結構。
從上圖可以看出,B樹相對於平衡二叉樹,每個節點存儲了更多的鍵值(key)和數據(data),並且每個節點擁有更多的子節點,子節點的個數一般稱為階,上述圖中的B樹為3階B樹,高度也會很低。 基於這個特性,B樹查找數據讀取磁盤的次數將會很少,數據的查找效率也會比平衡二叉樹高很多。
假如我們要查找id=28的用戶信息,那么我們在上圖B樹中查找的流程如下:
- 1、先找到根節點也就是頁1,判斷28在鍵值17和35之間,我們那么我們根據頁1中的指針p2找到頁3。
- 2、將28和頁3中的鍵值相比較,28在26和30之間,我們根據頁3中的指針p2找到頁8。
- 3、將28和頁8中的鍵值相比較,發現有匹配的鍵值28,鍵值28對應的用戶信息為(28,bv)。
注意:
- 1、B樹的構造是有一些規定的,但這不是本文的關注點,有興趣的同學可以令行了解。
- 2、B樹也是平衡的,當增加或刪除數據而導致B樹不平衡時,也是需要進行節點調整的。
那么B樹是否就是索引的最終結構了呢?答案是no,B樹只擅長做等值查詢,而對於范圍查詢(范圍查詢的本質就是n次等值查詢),或者說排序操作,B樹也幫不了我們
select * from user where id=3; -- 擅長
select * from user where id>3; -- 不擅長
上帝說要有光,於是有了B+樹
5.5 B+樹
B+樹是對B樹的進一步優化。讓我們先來看下B+樹的結構圖:
根據上圖我們來看下B+樹和B樹有什么不同。
1、B+樹非葉子節點non-leaf node上是不存儲數據的,僅存儲鍵,而B樹的非葉子節點中不僅存儲鍵,也會存儲數據。B+樹之所以這么做的意義在於:樹一個節點就是一個頁,而數據庫中頁的大小是固定的,innodb存儲引擎默認一頁為16KB,所以在頁大小固定的前提下,能往一個頁中放入更多的節點,相應的樹的階數(節點的子節點樹)就會更大,那么樹的高度必然更矮更胖,如此一來我們查找數據進行磁盤的IO次數有會再次減少,數據查詢的效率也會更快。
2、B+樹的階數是等於鍵的數量的,例如上圖,我們的B+樹中每個節點可以存儲3個鍵,3層B+樹存可以存儲3*3*3
=9個數據。所以如果我們的B+樹一個節點可以存儲1000個鍵值,那么3層B+樹可以存儲1000×1000×1000=10億個數據。而一般根節點是常駐內存的,所以一般我們查找10億數據,只需要2次磁盤IO,真是屌炸天的設計。
3、因為B+樹索引的所有數據均存儲在葉子節點leaf node,而且數據是按照順序排列的。那么B+樹使得范圍查找,排序查找,分組查找以及去重查找變得異常簡單。而B樹因為數據分散在各個節點,要實現這一點是很不容易的。
而且B+樹中各個頁之間也是通過雙向鏈表連接的,葉子節點中的數據是通過單向鏈表連接的。其實上面的B樹我們也可以對各個節點加上鏈表。其實這些不是它們之前的區別,是因為在mysql的innodb存儲引擎中,索引就是這樣存儲的。也就是說上圖中的B+樹索引就是innodb中B+樹索引真正的實現方式,准確的說應該是聚集索引(聚集索引和非聚集索引下面會講到)。
通過上圖可以看到,在innodb中,我們通過數據頁之間通過雙向鏈表連接以及葉子節點中數據之間通過單向鏈表連接的方式可以找到表中所有的數據。
MyISAM中的B+樹索引實現與innodb中的略有不同。在MyISAM中,B+樹索引的葉子節點並不存儲數據,而是存儲數據的文件地址。
六 聚集索引於非聚集索引
6.1 什么是聚集索引,什么是非聚集索引
在上節介紹B+樹索引的時候,我們提到了圖中的索引其實是聚集索引的實現方式。那什么是聚集索引呢?什么是又是非聚集索引呢?
在MySQL中,B+樹索引按照存儲方式的不同分為聚集索引和非聚集索引。這里我們主要介紹innodb存儲引擎中的聚集索引和非聚集索引。
1、聚集索引(又稱聚簇索引、主鍵索引,一張表必須有且只有一個):以innodb作為存儲引擎的表,表中的數據都會有一個主鍵,即使你不創建主鍵,系統也會幫你創建一個隱式的主鍵。這是因為innodb是把數據存放在B+樹中的,而B+樹的鍵用的就是主鍵,在B+樹的葉子節點中,存儲了表中所有的數據。這種以主鍵作為B+樹索引的鍵值而構建的B+樹索引,我們稱之為聚集索引。
2、非聚集索引(又稱非聚簇索引、輔助索引,一張表可以創建多個輔助索引):以主鍵以外的列值作為鍵值構建的B+樹索引,我們稱之為非聚集索引。非聚集索引與聚集索引的區別在於非聚集索引的葉子節點不存儲表中的數據,而是存儲該列對應的主鍵,想要查找數據我們還需要根據主鍵再去聚集索引中進行查找,這個再根據聚集索引查找數據的過程,我們稱為回表。
明白了聚集索引和非聚集索引的定義,我們應該明白這樣一句話:數據即索引,索引即數據。
6.2 利用聚集索引和非聚集索引查找數據
前面我們講解B+樹索引的時候並沒有去說怎么在B+樹中進行數據的查找,主要就是因為還沒有引出聚集索引和非聚集索引的概念。下面我們通過講解如何通過聚集索引以及非聚集索引查找數據表中數據的方式介紹一下B+樹索引查找數據方法。
6.2.1 利用聚集索引查找數據
還是這張B+樹索引圖,現在我們應該知道這就是聚集索引,表中的數據存儲在其中。現在假設我們要查找id>=18並且id<40的用戶數據。對應的sql語句為
select * from user where id>=18 and id <40,
其中id為主鍵。具體的查找過程如下:
1、一般根節點都是常駐內存的,也就是說頁1已經在內存中了,此時不需要到磁盤中讀取數據,直接從內存中讀取即可。從內存中讀取到頁1,要查找這個id>=18 and id <40或者范圍值,我們首先需要找到id=18的鍵值。從頁1中我們可以找到鍵值18,此時我們需要根據指針p2,定位到頁3。
2、要從頁3中查找數據,我們就需要拿着p2指針去磁盤中進行讀取頁3。從磁盤中讀取頁3后將頁3放入內存中,然后進行查找,我們可以找到鍵值18,然后再拿到頁3中的指針p1,定位到頁8。
3、同樣的頁8頁不在內存中,我們需要再去磁盤中將頁8讀取到內存中。將頁8讀取到內存中后。因為頁中的數據是鏈表進行連接的,而且鍵值是按照順序存放的,此時可以根據二分查找法定位到鍵值18。此時因為已經到數據頁了,此時我們已經找到一條滿足條件的數據了,就是鍵值18對應的數據。因為是范圍查找,而且此時所有的數據又都存在葉子節點,並且是有序排列的,那么我們就可以對頁8中的鍵值依次進行遍歷查找並匹配滿足條件的數據。我們可以一直找到鍵值為22的數據,然后頁8中就沒有數據了,此時我們需要拿着頁8中的p指針去讀取頁9中的數據。
4、因為頁9不在內存中,就又會加載頁9到內存中,並通過和頁8中一樣的方式進行數據的查找,直到將頁12加載到內存中,發現41大於40,此時不滿足條件。那么查找到此終止。最終我們找到滿足條件的所有數據為:(18,kl),(19,kl),(22,hj),(24,io),(25,vg),(29,jk),(31,jk),(33,rt),(34,ty),(35,yu),(37,rt),(39,rt)。總共12條記錄。
下面看下具體的查找流程圖:
[rml_read_more]:
6.2.2 利用非聚集索引查找數據
讀者看到這張圖的時候可能會蒙,這是啥東西啊?怎么都是數字。如果有這種感覺,請仔細看上圖中紅字的解釋。什么?還看不懂?那我再來解釋下吧。首先,這個非聚集索引表示的是用戶幸運數字的索引(為什么是幸運數字?一時興起想起來的),此時表結構是這樣的。
id | name | luckyNum |
---|---|---|
1 | zs | 23 |
2 | ls | 7 |
在葉子節點中,不再存儲所有的數據了,存儲的是鍵和主鍵。對於葉子節點中的x-y,比如10-23。左邊的10表示的是索引的鍵值,右邊的23表示的其對應的主鍵值。如果我們要找到幸運數字為33的用戶信息,對應的sql語句為
select * from user where luckNum=33;
查找的流程跟聚集索引一樣,這里就不詳細介紹了。我們最終會找到主鍵值47,因為我們select要的是*,即所有字段值,在輔助索引中並不存在,所以找到主鍵后我們需要再到聚集索引中查找具體對應的數據信息,此時又回到了聚集索引的查找流程,着
下面看下具體的查找流程圖:
在MyISAM中,聚集索引和非聚集索引的葉子節點都會存儲數據的文件地址。
6.2.3 覆蓋索引與回表操作
命中了輔助索引,然后要找的字段值不存在與輔助索引的葉子節點上,則需要根據拿到的主鍵值再去聚集索引中查詢一遍,然后再聚集索引的葉子節點找到你想要的內容,這就叫回表操作
例如
create index xxx on user(name);
下述語句,名中了輔助索引,但是select需要查詢出的除了輔助索引葉子節點有的name字段值外還想要age字段的值,那么需要進行回表操作
select name,age from user where name="jason";
命中了某棵索引樹,然后在其葉子節點就找到了你想要的值,即不需要回表操作,就是覆蓋了索引
例1:
create index xxx on user(name);
下述語句,覆蓋了索引
select name from user where name="jason";
例2:使用主鍵字段當作條件,百分百覆蓋了索引,效率極高,推薦使用
如果id字段是主鍵,那么下述語句也覆蓋了索引
select * from user where id=3;
慢查詢優化:https://www.cnblogs.com/Dominic-Ji/articles/15426531.html