本文來自網易雲社區。mysql
Innodb是Mysql最經常使用的存儲引擎,瞭解Innodb存儲引擎的索引對於平常工做有很大的益處,索引的存在即是爲了加速數據庫行記錄的檢索。如下是我對最近學習的知識的一些總結,以及對碰到的以及別人提到過的問題的一些分析,若有錯誤,請指正,我會及時更正。算法
1. Innodb表結構sql
此小結與索引其實沒有太多的關聯,可是爲了便於理解索引的內容,添加此小結做爲鋪墊知識。數據庫
Mysql表中的全部數據被存儲在一個空間內,稱之爲表空間,表空間內部又能夠分爲段(segment)、區(extent)、頁(page)、行(row),邏輯結構以下圖:編程
表空間是由不一樣的段組成的,常見的段有:數據段,索引段,回滾段等等,在Mysql中,數據是按照B+樹來存儲,所以數據即索引,所以數據段即爲B+樹的葉子節點,索引段爲B+樹的非葉子節點,回滾段用於存儲undo日誌,用於事務失敗後數據回滾以及在事務未提交以前經過undo日誌獲取以前版本的數據,在Innodb1.1版本以前一個Innodb,只支持一個回滾段,支持1023個併發修改事務同時進行,在Innodb1.2版本,將回滾段數量提升到了128個,也就是說能夠同時進行128*1023個併發修改事務。緩存
區是由連續頁組成的空間,每一個區的固定大小爲1MB,爲保證區中頁的連續性,Innodb會一次從磁盤中申請4~5個區,在默認不壓縮的狀況下,一個區能夠容納64個連續的頁。可是在開始新建表的時候,空表的默認大小爲96KB,是因爲爲了高效的利用磁盤空間,在開始插入數據時表會先利用32個頁大小的碎片頁來存儲數據,當這些碎片使用完後,表大小纔會按照MB倍數來增長。服務器
頁是Innodb存儲引擎的最小管理單位,每頁大小默認是16KB,從Innodb 1.2.x版本開始,能夠利用innodb_page_size
來改變頁size,可是改變只能在初始化Innodb實例前進行修改,以後便沒法進行修改,除非mysqldump導出建立新庫,常見的頁類型有:數據頁、undo頁、系統頁、事務數據頁、插入緩衝位圖頁、插入緩衝空閒列表頁、未壓縮的二進制大對象頁、壓縮的二進制大對象頁。併發
行對應的是表中的行記錄,每頁存儲最多的行記錄也是有硬性規定的最多16KB/2-200,即7992行(16KB是頁大小,我也不明白爲何要這麼算,聽說是內核定義)函數
1.2 Innodb行記錄格式高併發
Innodb提供了兩種格式來存儲行記錄:Redundant格式、Compact格式、Dynamic格式、Compressed格式,Redudant格式是爲了兼容保留的。
隱藏列:事務id和回滾列id,分別佔用六、7字節,若此表沒有主鍵,還會增長6字節的rowid列。
Dynamic格式和Compressed格式與Compact的不一樣之處在於對於行溢出只會在該列處存放20字節的指針,指向該字符串的實際存儲位置,不會存儲768字節前綴,並且Compressed格式在存儲BLOB、TEXT、VARCHAR等類型會利用zlib算法進行壓縮,可以以很高的存儲效率來存儲字符串。
《Mysql技術內幕-Innodb存儲引擎》書中對此有描述,可是應該不是太準確,書中有以下描述,此處不作詳細介紹,如有興趣請看此神書。
B樹(B-TREE)知足以下條件,便可稱之爲m階B樹:
B+樹知足以下條件,便可稱之爲m階B+樹:
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的合併操做。
以m階樹爲例:
每一個Innodb的表都擁有一個索引,稱之爲聚簇索引,此索引中存儲着行記錄,通常來講,聚簇索引是根據主鍵生成的。爲了可以得到高性能的查詢、插入和其餘數據庫操做,理解Innodb聚簇索引是頗有必要的。
聚簇索引按照以下規則建立:
Note: 對於選擇惟一索引的順序是按照定義惟一索引的順序,而非表中列的順序, 同時選中的惟一索引字段會充當爲主鍵,或者Innodb隱式建立的自增列也能夠看作主鍵。
聚簇索引總體是一個b+樹,非葉子節點存放的是鍵值,葉子節點存放的是行數據,稱之爲數據頁,這就決定了表中的數據也是聚簇索引中的一部分,數據頁之間是經過一個雙向鏈表來連接的,上文說到B+樹是一棵平衡查找樹,也就是聚簇索引的數據存儲是有序的,可是這個是邏輯上的有序,可是在實際在數據的物理存儲上是,由於數據頁之間是經過雙向鏈表來鏈接,假如物理存儲是順序的話,那維護聚簇索引的成本很是的高。
除了聚簇索引以外的索引均可以稱之爲輔助索引,與聚簇索引的區別在於輔助索引的葉子節點中存放的是主鍵的鍵值。一張表能夠存在多個輔助索引,可是隻能有一個聚簇索引,經過輔助索引來查找對應的航記錄的話,須要進行兩步,第一步經過輔助索引來肯定對應的主鍵,第二步經過相應的主鍵值在聚簇索引中查詢到對應的行記錄,也就是進行兩次B+樹搜索。相反經過輔助索引來查詢主鍵的話,遍歷一次輔助索引就能夠肯定主鍵了,也就是所謂的索引覆蓋,不用回表(查詢聚簇索引)。
建立輔助索引,能夠建立單列的索引,也就是用一個字段來建立索引,也能夠用多個字段來建立副主索引稱爲聯合索引,建立聯合索引後,B+樹的節點存儲的鍵值數量不是1個,而是多個,以下圖:
order by
對某個字段進行排序時,能夠減小複雜度,加速進行查詢;select * from table where a=? and ?
可使用索引(a,b)來加速查詢,可是在查詢時有一個原則,sql的where條件的順序必須和二級索引一致,並且還遵循索引最左原則,select * from table where b=?
則沒法利用(a,b)索引來加速查詢。
如下的每一步操做都會生成一個虛擬表,做爲下一個處理的輸入,在這個過程當中,這些虛擬表對於用戶都是透明的,只用最後一步執行完的虛擬表返回給用戶,在處理過程當中,沒有的步驟會直接跳過。
如下爲邏輯上的執行順序:
from
:對左表left-table
和右表right-table
執行笛卡爾積(a*b),造成虛擬表VT1;on
: 對虛擬表VT1進行on
條件進行篩選,只有符合條件的記錄纔會插入到虛擬表VT2中;join
: 指定out join
會將未匹配行添加到VT2產生VT3,如有多張表,則會重複(1)~(3);where
: 對VT3進行條件過濾,造成VT4, where
條件是從左向右執行的;group by
: 對VT4進行分組操做獲得VT5;cube | rollup
: 對VT5進行cube | rollup
操做獲得VT6;having
: 對VT6進行過濾獲得VT7;select
: 執行選擇操做獲得VT8,本人看來VT7和VT8應該是同樣的;distinct
: 對VT8進行去重,獲得VT9;order by
: 對VT9進行排序,獲得VT10;limit
: 對記錄進行截取,獲得VT11返回給用戶。Note: on
條件應用於連表過濾,where
應用於on過濾後的結果(有on
的話),having
應用於分組過濾
5. sql優化建議
索引有以下有點:減小服務器掃描的數據量、避免排序和臨時表、將隨機I/O變爲順序I/O。
like 'xx%'
能夠走索引;>
,like
等;select
的字段爲主鍵;範圍查詢後的條件不會走索引,具體緣由會在下一節進行介紹。
選擇性(區分度)是指不重複的列值個數/列值的總個數,通常意義上建索引的字段要區分度高,並且在建聯合索引的時候區分度高的列字段要放在前邊,這樣能夠在第一個條件就過濾掉大量的數據,有利用性能的提高,對於如何計算列的區分度,有以下兩種方法:
show index from <table_name>
來查看 解釋一下此處的carlinality並非準確值,並且mysql在B+樹種選擇了8個數據頁來抽樣統計的值,也就是說carlinality=每一個數據頁記錄總和/8*全部的數據頁,所以也說明這個值是不許確的,由於在插入/更新記錄時,實時的去更新carlinality對於Mysql的負載是很高的,若是數據量很大的話,觸發mysql從新統計該值得條件是當表中的1/16數據發生變化時。可是選擇區分度高的列做爲索引也不是百試百靈的,某些狀況仍是不合適的,下節會進行介紹。
Mysql查詢過程
當但願Mysql可以高性能運行的時候,最好的辦法就是明白Mysql是如何優化和執行的,一旦理解了這一點,不少查詢優化工做實際上就是遵循了一些原則讓優化器可以按照預想的合理的方式運行————《引用自高性能Mysql》
當想Mysql實例發送一個請求時,Mysql按照以下圖的方式進行查詢:
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是能夠走到索引的;where
條件的字段須要在索引中;set @@optimizer_switch = "index_condition_pushdown=on" 開啓ICP set @@optimizer_switch = "index_condition_pushdown=off"
關閉ICP
好比建立一個表:
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的優化器選擇利用該索引。
在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和內存資源,通常用到索引合併的狀況也從側面反映了該表的索引須要優化。
網易雲新用戶大禮包:https://www.163yun.com/gift
本文來自網易雲社區,經做者範鵬程受權發佈。