Mysql Innodb 索引原理

本文來自網易雲社區mysql

Innodb是Mysql最經常使用的存儲引擎,瞭解Innodb存儲引擎的索引對於平常工做有很大的益處,索引的存在即是爲了加速數據庫行記錄的檢索。如下是我對最近學習的知識的一些總結,以及對碰到的以及別人提到過的問題的一些分析,若有錯誤,請指正,我會及時更正。算法

 

1. Innodb表結構sql

此小結與索引其實沒有太多的關聯,可是爲了便於理解索引的內容,添加此小結做爲鋪墊知識。數據庫

1.1 Innodb邏輯存儲結構

Mysql表中的全部數據被存儲在一個空間內,稱之爲表空間,表空間內部又能夠分爲段(segment)、區(extent)、頁(page)、行(row),邏輯結構以下圖:編程

  • 段(segment)

表空間是由不一樣的段組成的,常見的段有:數據段,索引段,回滾段等等,在Mysql中,數據是按照B+樹來存儲,所以數據即索引,所以數據段即爲B+樹的葉子節點,索引段爲B+樹的非葉子節點,回滾段用於存儲undo日誌,用於事務失敗後數據回滾以及在事務未提交以前經過undo日誌獲取以前版本的數據,在Innodb1.1版本以前一個Innodb,只支持一個回滾段,支持1023個併發修改事務同時進行,在Innodb1.2版本,將回滾段數量提升到了128個,也就是說能夠同時進行128*1023個併發修改事務。緩存

  • 區(extent)

區是由連續頁組成的空間,每一個區的固定大小爲1MB,爲保證區中頁的連續性,Innodb會一次從磁盤中申請4~5個區,在默認不壓縮的狀況下,一個區能夠容納64個連續的頁。可是在開始新建表的時候,空表的默認大小爲96KB,是因爲爲了高效的利用磁盤空間,在開始插入數據時表會先利用32個頁大小的碎片頁來存儲數據,當這些碎片使用完後,表大小纔會按照MB倍數來增長。服務器

  • 頁(page)

頁是Innodb存儲引擎的最小管理單位,每頁大小默認是16KB,從Innodb 1.2.x版本開始,能夠利用innodb_page_size來改變頁size,可是改變只能在初始化Innodb實例前進行修改,以後便沒法進行修改,除非mysqldump導出建立新庫,常見的頁類型有:數據頁、undo頁、系統頁、事務數據頁、插入緩衝位圖頁、插入緩衝空閒列表頁、未壓縮的二進制大對象頁、壓縮的二進制大對象頁。併發

  • 行(row)

行對應的是表中的行記錄,每頁存儲最多的行記錄也是有硬性規定的最多16KB/2-200,即7992行(16KB是頁大小,我也不明白爲何要這麼算,聽說是內核定義)函數

1.2 Innodb行記錄格式高併發

Innodb提供了兩種格式來存儲行記錄:Redundant格式、Compact格式、Dynamic格式、Compressed格式,Redudant格式是爲了兼容保留的。

Redundant行格式(5.0版本以前的格式)

  • 字段長度偏移列表:存儲字段偏移量,與列字段順序相反存放,若列長度小於255字節,用一個字節表示,若大於255字節,用兩個字節表示
  • 記錄頭信息:固定用6字節表示,具體含義以下:

隱藏列:事務id和回滾列id,分別佔用六、7字節,若此表沒有主鍵,還會增長6字節的rowid列。

Compact行格式(5.6版本的默認行格式)

 

  • 變長字段長度列表:此字段標識列字段的長度,與列字段順序相反存放,若列長度小於255字節,用一個字節表示,若大於255字節,用兩個字節表示,這也是Mysql的VARCHAR類型最大長度限制爲65535
  • NULL標誌位:標識改列是否有空字段,有用1表示,不然爲0,該標誌位長度爲ceil(N/8)(此處是Mysql技術內幕-Innodb存儲引擎與官方文檔有出入的地方);
  • 記錄頭信息:固定用5字節表示,具體含義以下:
  • 列數據:此行存儲着列字段數據,Null是不佔存儲空間的;
  • 隱藏列:事務id和回滾列id,分別佔用六、7字節,若此表沒有主鍵,還會增長6字節的rowid列。
Dynamic格式(5.7版本默認行格式)和Compressed格式

Dynamic格式和Compressed格式與Compact的不一樣之處在於對於行溢出只會在該列處存放20字節的指針,指向該字符串的實際存儲位置,不會存儲768字節前綴,並且Compressed格式在存儲BLOB、TEXT、VARCHAR等類型會利用zlib算法進行壓縮,可以以很高的存儲效率來存儲字符串。

1.3 Innodb數據頁結構

《Mysql技術內幕-Innodb存儲引擎》書中對此有描述,可是應該不是太準確,書中有以下描述,此處不作詳細介紹,如有興趣請看此神書。

2. B樹與B+樹

2.1 B樹

定義:

B樹(B-TREE)知足以下條件,便可稱之爲m階B樹:

  • 每一個節點之多擁有m棵子樹;
  • 根結點至少擁有兩顆子樹(存在子樹的狀況下);
  • 除了根結點之外,其他每一個分支結點至少擁有 m/2 棵子樹;
  • 全部的葉結點都在同一層上;
  • 有 k 棵子樹的分支結點則存在 k-1 個關鍵碼,關鍵碼按照遞增次序進行排列;
  • 關鍵字數量須要知足ceil(m/2)-1 <= n <= m-1;

 

B樹插入

 

B樹刪除

 

 

2.2 B+樹

定義:

B+樹知足以下條件,便可稱之爲m階B+樹:

  • 根結點只有一個,分支數量範圍爲[2,m]
  • 分支結點,每一個結點包含分支數範圍爲[ceil(m/2), m];
  • 分支結點的關鍵字數量等於其子分支的數量減一,關鍵字的數量範圍爲[ceil(m/2)-1, m-1],關鍵字順序遞增;
  • 全部葉子結點都在同一層;
插入:

B+樹的插入必須保證插入後葉節點中的記錄依然排序,同時須要考慮插入B+樹的三種狀況,每種狀況均可能會致使不一樣的插入算法,插入算法入下圖:

插入舉例(未加入雙向鏈表):

一、 插入28這個鍵值,發現當前Leaf Page和Index Page都沒有滿,直接插入。

二、 插入70這個鍵值,Leaf Page已經滿了,可是Index Page尚未滿,根據中間的值60拆分葉節點。

三、 插入記錄95,Leaf Page和Index Page都滿了,這時須要作兩次拆分

四、 B+樹老是會保持平衡。可是爲了保持平衡,對於新插入的鍵值可能須要作大量的拆分頁(split)操做,而B+樹主要用於磁盤,所以頁的拆分意味着磁盤數據移動,應該在可能的狀況下儘可能減小頁的拆分。所以,B+樹提供了旋轉(rotation)的功能。旋轉發生在Leaf Page已經滿了、可是其左右兄弟節點沒有滿的狀況下。這時B+樹並不會急於去作拆分頁的操做,而是將記錄移到所在頁的兄弟節點上。一般狀況下,左兄弟被首先檢查用來作旋轉操做,在第一張圖狀況下,插入鍵值70,其實B+樹並不會急於去拆分葉節點,而是作旋轉,50,55,55旋轉。

刪除:

B+樹使用填充因子(fill factor)來控制樹的刪除變化,50%是填充因子可設的最小值。B+樹的刪除操做一樣必須保證刪除後葉節點中的記錄依然排序,同插入同樣,B+樹的刪除操做一樣須要考慮下圖所示的三種狀況,與插入不一樣的是,刪除根據填充因子的變化來衡量。 

刪除示例(未加入雙向鏈表): 一、刪除鍵值爲70的這條記錄,直接刪除(在插入第三點基礎上的圖)。

二、接着咱們刪除鍵值爲25的記錄,該值仍是Index Page中的值,所以在刪除Leaf Page中25的值後,還應將25的右兄弟節點的28更新到Page Index中。

 

 

三、刪除鍵值爲60的狀況,刪除Leaf Page中鍵值爲60的記錄後,填充因子小於50%,這時須要作合併操做,一樣,在刪除Index Page中相關記錄後須要作Index Page的合併操做。

 

 

B樹與B+樹區別:

以m階樹爲例:

  • 關鍵字不一樣:B+樹中分支結點有m個關鍵字,其葉子結點也有m個,可是B樹雖然也有m個子結點,可是其只擁有m-1個關鍵字。
  • 存儲位置不一樣:B+樹非葉子節點的關鍵字只起到索引做用,實際的關鍵字存儲在葉子節點,B樹的非葉子節點也存儲關鍵字。
  • 分支構造不一樣:B+樹的分支結點僅僅存儲着關鍵字信息和兒子的指針,也就是說內部結點僅僅包含着索引信息。
  • 查詢不一樣(穩定):B樹在找到具體的數值之後,則結束,而B+樹則須要經過索引找到葉子結點中的數據才結束,也就是說B+樹的搜索過程當中走了一條從根結點到葉子結點的路徑。

 

3. 聚簇索引和二級索引

3.1 聚簇索引

每一個Innodb的表都擁有一個索引,稱之爲聚簇索引,此索引中存儲着行記錄,通常來講,聚簇索引是根據主鍵生成的。爲了可以得到高性能的查詢、插入和其餘數據庫操做,理解Innodb聚簇索引是頗有必要的。

聚簇索引按照以下規則建立:

  • 當定義了主鍵後,innodb會利用主鍵來生成其聚簇索引;
  • 若是沒有主鍵,innodb會選擇一個非空的惟一索引來建立聚簇索引;
  • 若是這也沒有,Innodb會隱式的建立一個自增的列來做爲聚簇索引。

Note: 對於選擇惟一索引的順序是按照定義惟一索引的順序,而非表中列的順序, 同時選中的惟一索引字段會充當爲主鍵,或者Innodb隱式建立的自增列也能夠看作主鍵。

聚簇索引總體是一個b+樹,非葉子節點存放的是鍵值,葉子節點存放的是行數據,稱之爲數據頁,這就決定了表中的數據也是聚簇索引中的一部分,數據頁之間是經過一個雙向鏈表來連接的,上文說到B+樹是一棵平衡查找樹,也就是聚簇索引的數據存儲是有序的,可是這個是邏輯上的有序,可是在實際在數據的物理存儲上是,由於數據頁之間是經過雙向鏈表來鏈接,假如物理存儲是順序的話,那維護聚簇索引的成本很是的高。

3.2 輔助索引

除了聚簇索引以外的索引均可以稱之爲輔助索引,與聚簇索引的區別在於輔助索引的葉子節點中存放的是主鍵的鍵值。一張表能夠存在多個輔助索引,可是隻能有一個聚簇索引,經過輔助索引來查找對應的航記錄的話,須要進行兩步,第一步經過輔助索引來肯定對應的主鍵,第二步經過相應的主鍵值在聚簇索引中查詢到對應的行記錄,也就是進行兩次B+樹搜索。相反經過輔助索引來查詢主鍵的話,遍歷一次輔助索引就能夠肯定主鍵了,也就是所謂的索引覆蓋,不用回表(查詢聚簇索引)。

建立輔助索引,能夠建立單列的索引,也就是用一個字段來建立索引,也能夠用多個字段來建立副主索引稱爲聯合索引,建立聯合索引後,B+樹的節點存儲的鍵值數量不是1個,而是多個,以下圖:

  • 聯合索引的B+樹和單鍵輔助索引的B+樹是同樣的,鍵值都是排序的,經過葉子節點能夠邏輯順序的讀出全部的數據,好比上圖所存儲的數據時,按照(a,b)這種形式(1,1),(1,2),(2,1),(2,4),(3,1),(3,2)進行存放,這樣有個好處存放的數據時排了序的,當進行order by對某個字段進行排序時,能夠減小複雜度,加速進行查詢;
  • 當用select * from table where a=? and ?可使用索引(a,b)來加速查詢,可是在查詢時有一個原則,sql的where條件的順序必須和二級索引一致,並且還遵循索引最左原則,select * from table where b=?則沒法利用(a,b)索引來加速查詢。
  • 輔助索引還有一個概念即是索引覆蓋,索引覆蓋的一個好處即是輔助索引不高含行記錄,所以其大小遠遠小於聚簇索引,利用輔助索引進行查詢能夠減小大量的io操做。

 

 

4. sql執行順序

如下的每一步操做都會生成一個虛擬表,做爲下一個處理的輸入,在這個過程當中,這些虛擬表對於用戶都是透明的,只用最後一步執行完的虛擬表返回給用戶,在處理過程當中,沒有的步驟會直接跳過。

如下爲邏輯上的執行順序:

  • (1) from:對左表left-table和右表right-table執行笛卡爾積(a*b),造成虛擬表VT1;
  • (2) on: 對虛擬表VT1進行on條件進行篩選,只有符合條件的記錄纔會插入到虛擬表VT2中;
  • (3) join: 指定out join會將未匹配行添加到VT2產生VT3,如有多張表,則會重複(1)~(3);
  • (4) where: 對VT3進行條件過濾,造成VT4, where條件是從左向右執行的;
  • (5) group by: 對VT4進行分組操做獲得VT5;
  • (6) cube | rollup: 對VT5進行cube | rollup操做獲得VT6;
  • (7) having: 對VT6進行過濾獲得VT7;
  • (8) select: 執行選擇操做獲得VT8,本人看來VT7和VT8應該是同樣的;
  • (9) distinct: 對VT8進行去重,獲得VT9;
  • (10) order by: 對VT9進行排序,獲得VT10;
  • (11) limit: 對記錄進行截取,獲得VT11返回給用戶。

Note: on條件應用於連表過濾,where應用於on過濾後的結果(有on的話),having應用於分組過濾

 

5. sql優化建議

索引有以下有點:減小服務器掃描的數據量、避免排序和臨時表、將隨機I/O變爲順序I/O。

可以使用B+樹索引的查詢方式

 

  • 全值匹配:與索引中的全部列進行匹配,也就是條件字段與聯合索引的字段個數與順序相同;
  • 匹配最左前綴:只使用聯合索引的前幾個字段;
  • 匹配列前綴:好比like 'xx%'能夠走索引;
  • 匹配範圍值:範圍查詢,好比>,like等;
  • 匹配某一列並範圍匹配另一列:精確查找+範圍查找;
  • 只訪問索引查詢:索引覆蓋,select的字段爲主鍵;

範圍查詢後的條件不會走索引,具體緣由會在下一節進行介紹。

列的選擇性(區分度)

選擇性(區分度)是指不重複的列值個數/列值的總個數,通常意義上建索引的字段要區分度高,並且在建聯合索引的時候區分度高的列字段要放在前邊,這樣能夠在第一個條件就過濾掉大量的數據,有利用性能的提高,對於如何計算列的區分度,有以下兩種方法:

  • 根據定義,手動計算列的區分度,不重複的列值個數/列值的總個數
  • 經過mysql的carlinality,經過命令show index from <table_name>來查看 解釋一下此處的carlinality並非準確值,並且mysql在B+樹種選擇了8個數據頁來抽樣統計的值,也就是說carlinality=每一個數據頁記錄總和/8*全部的數據頁,所以也說明這個值是不許確的,由於在插入/更新記錄時,實時的去更新carlinality對於Mysql的負載是很高的,若是數據量很大的話,觸發mysql從新統計該值得條件是當表中的1/16數據發生變化時。

可是選擇區分度高的列做爲索引也不是百試百靈的,某些狀況仍是不合適的,下節會進行介紹。

Mysql查詢過程

當但願Mysql可以高性能運行的時候,最好的辦法就是明白Mysql是如何優化和執行的,一旦理解了這一點,不少查詢優化工做實際上就是遵循了一些原則讓優化器可以按照預想的合理的方式運行————《引用自高性能Mysql》

當想Mysql實例發送一個請求時,Mysql按照以下圖的方式進行查詢:

  • 客戶端先發送一條查詢給服務器;
  • 服務器先檢查查詢緩存,若是命中了緩存,則馬上返回給存儲在緩存中的結果,不然進入下一個階段;
  • 服務器端進行sql解析、預處理,再由優化器生成對應的執行計劃;
  • Mysql根據優化器生成的執行計劃,調用存儲引擎的API來執行查詢
  • 將結果返回客戶端

注意&建議

  • 主鍵推薦使用整型,避免索引分裂;
  • 查詢使用索引覆蓋可以提高很大的性能,由於避免了回表查詢;
  • 選擇合適的順序創建索引,有的場景並不是區分度越高的列字段放在前邊越好,聯合索引使用居多;
  • 合理使用in操做將範圍查詢轉換成多個等值查詢;
  • in操做至關於多個等值操做,可是要注意的是對於order by來講,這至關於範圍查詢,所以例如select * from t1 where c1 in (x,x) order by c2的sql是不走索引的;
  • 將大批量數據查詢任務分解爲分批查詢;
  • 將複雜查詢轉換爲簡單查詢;
  • 合理使用inner join,好比說分頁時候

 

6. 一些問題分析

這個部分是我在學習過程當中產生的一些疑問,以及在工做中碰到的或者同事提起的一些問題,對此我作了些調研,總結了一下並添加了些本身的理解,若有錯誤還請指正。

索引分裂

此處提一下索引分裂,就我我的理解,在Mysql插入記錄的同時會更新配置的相應索引文件,根據以上的瞭解,在插入索引時,可能會存在索引的頁的分裂,所以會致使磁盤數據的移動。當插入的主鍵是隨機字符串時,每次插入不會是在B+樹的最後插入,每次插入位置都是隨機的,每次均可能致使數據頁的移動,並且字符串的存儲空間佔用也很大,這樣重建索引不只僅效率低並且Mysql的負載也會很高,同時還會致使大量的磁盤碎片,磁盤碎片多了也會對查詢形成必定的性能開銷,由於存儲位置不連續致使更多的磁盤I/O,這就是爲何推薦定義主鍵爲遞增整型的一個緣由,Mysql索引頁默認大小是16KB,當有新紀錄插入的時候,Mysql會留下每頁空間的1/16用於將來索引記錄增加,避免過多的磁盤數據移動。

自增主鍵的弊端

對於高併發的場景,在Innodb中按照主鍵的順序插入可能會形成明顯的爭用,主鍵的上界會成爲「熱點」,由於全部的插入都發生在此處,索引併發的插入可能會形成間隙鎖競爭,何爲間隙鎖競爭,下個會詳細介紹;另一個緣由多是Auto_increment的鎖機制,在Mysql處理自增主鍵時,當innodb_autoinc_lock_mode爲0或1時,在不知道插入有多少行時,好比insert t1 xx select xx from t2,對於這個statement的執行會進行鎖表,只有這個statement執行完之後纔會釋放鎖,而後別的插入纔可以繼續執行,,可是在innodb_autoinc_lock_mode=2時,這種狀況不會存在表鎖,可是隻能保證全部併發執行的statement插入的記錄是惟一而且自增的,可是每一個statement作的多行插入之間是不鏈接的。

優化器不使用索引選擇全表掃描

好比一張order表中有聯合索引(order_id, goods_id),在此例子上來講明 這個問題是從兩個方面來講:

  • 查詢字段在索引中

select order_id from order where order_id > 1000,若是查看其執行計劃的話,發現是用use index condition,走的是索引覆蓋

  • 查詢字段不在索引中

select * from order where order_id > 1000, 此條語句查詢的是該表全部字段,有一部分字段並未在此聯合索引中,所以走聯合索引查詢會走兩步,首先經過聯合索引肯定符合條件的主鍵id,而後利用這些主鍵id再去聚簇索引中去查詢,而後獲得全部記錄,利用主鍵id在聚簇索引中查詢記錄的過程是無序的,在磁盤上就變成了離散讀取的操做,假如當讀取的記錄不少時(通常是整個表的20%左右),這個時候優化器會選擇直接使用聚簇索引,也就是掃全表,由於順序讀取要快於離散讀取,這也就是爲什麼通常不用區分度不大的字段單獨作索引,注意是單獨由於利用此字段查出來的數據會不少,有很大機率走全表掃描。

範圍查詢以後的條件不走索引

根據Mysql的查詢原理的話,當處理到where的範圍查詢條件後,會將查詢到的行所有返回到服務器端(查詢執行引擎),接下來的條件操做在服務器端進行處理,這也就是爲何範圍條件不走索引的緣由了,由於以後的條件過濾已經不在存儲引擎完成了。可是在Mysql 5.6之後假如了一個新的功能index condition pushdown(ICP),這個功能容許範圍查詢條件以後的條件繼續走索引,可是須要有幾個前提條件:

  • 查詢條件的第一個條件須要時有邊界的,好比select * from xx where c1=x and c2>x and c3<x,這樣c3是能夠走到索引的;
  • 支持InnoDB和MyISAM存儲引擎;
  • where條件的字段須要在索引中;
  • 分表ICP功能5.7開始支持;
  • 使用索引覆蓋時,ICP不起做用。

set @@optimizer_switch = "index_condition_pushdown=on" 開啓ICP set @@optimizer_switch = "index_condition_pushdown=off" 關閉ICP

範圍查詢統計函數不遵循Mysql索引最左原則

好比建立一個表:

create table `person`(
    `id` int not null auto_increment primary key,
    `uid` int not null,    
    `name` varchar(60) not null,
    `time` date not null,
    key `idx_uid_date` (uid, time) 
)engine=innodb default charset=utf8mb4;

當執行select count(*) from person where time > '2018-03-11' and time < '2018-03-16'時,time是能夠用到idx_uid_date`的索引的,看以下的執行計劃:

其中extra標識use index說明是走索引覆蓋的,通常意義來講是Mysql是沒法支持鬆散索引的,可是對於統計函數,是可使用索引覆蓋的,所以Mysql的優化器選擇利用該索引。

分頁offset值很大性能問題

在Mysql中,分頁當offset值很大的時候,性能會很是的差,好比limit 100000, 20,須要查詢100020條數據,而後取20條,拋棄前100000條,在這個過程當中產生了大量的隨機I/O,這是性能不好的緣由,爲了解決這個問題,切入點即是減小無用數據的查詢,減小隨機I/O。 解決的方法是利用索引覆蓋,也就是掃描索引獲得id而後再從聚簇索引中查詢行記錄,我知道有兩種方式:

好比從表t1中分頁查詢limit 1000000,5 

  • 利用inner join

 

select * from t1 inner join (select id from t1 where xxx order by xx limit 1000000,5) as t2 using(id),子查詢先走索引覆蓋查得id,而後根據獲得的id直接取5條得數據。

  • 利用範圍查詢條件來限制取出的數據

select * from t1 where id > 1000000 order by id limit 0, 5,即利用條件id > 1000000在掃描索引是跳過1000000條記錄,而後取5條便可,這種處理方式的offset值便成爲0了,但此種方式一般分頁不能用,可是能夠用來分批取數據。

索引合併

SELECT * FROM tbl_name WHERE key1 = 10 OR key2 = 20;
SELECT * FROM tbl_name WHERE (key1 = 10 OR key2 = 20) AND non_key=30;
SELECT * FROM t1, t2 WHERE (t1.key1 IN (1,2) OR t1.key2 LIKE 'value%') AND t2.key1=t1.some_col;
SELECT * FROM t1, t2 WHERE t1.key1=1 AND (t2.key1=t1.some_col OR t2.key2=t1.some_col2);

對於如上的sql在mysql 5.0版本以前,假如沒有創建相應的聯合索引,是要走全表掃描的,可是在Mysql 5.1後引入了一種優化策略爲索引合併,能夠在必定程度上利用表上的多個單列索引來定位指定行,其原理是將對每一個索引的掃描結果作運算,總共有:交集、並集以及他們的組合,可是索引合併並不是是一種合適的選擇,由於在作索引合併時可能會消耗大量的cpu和內存資源,通常用到索引合併的狀況也從側面反映了該表的索引須要優化。

 

7. 參考資料

  • 《Mysql技術內幕-Innodb存儲引擎》:此書對於Innodb的講解是比較全面並且細緻的,可是稍微有一點點老而且還有一點點錯誤地方,此書是基於Mysql 5.6版本的,裏邊會混雜一些5.7的知識。
  • 《MySQL技術內幕:SQL編程》:值得一看。
  • 《高性能Mysql 第三版》:此書是一本Mysql神書,裏邊有不少的Mysql優化建議以及一些案例
  • 官方文檔:這個是比較權威並且是最新的文檔,缺點是篇幅很長,內容不少,並且仍是純英文,在理解和閱讀速度上相對而言沒有中文來得快。

 

網易雲新用戶大禮包:https://www.163yun.com/gift

本文來自網易雲社區,經做者範鵬程受權發佈。

相關文章
相關標籤/搜索