0一、索引究竟是怎麼回事?mysql
前面兩篇說到了索引原理即數據結構,同時還講到了其索引原則。那麼查詢一條SQL語句究竟是怎麼執行的呢?或者說除了主鍵索引,其餘的索引究竟是怎麼一回事?今天的這篇主要任務就是理清這個關係,而後說說怎麼優化建立的索引?這個很重要,絕對對你的工做有很大幫助,但願能細心的體會,而且深刻理解,那麼就開始吧。算法
先來了解概念,以下:sql
主鍵索引的葉子節點存的是整行數據。在 InnoDB 裏,主鍵也被稱爲聚簇索引(clustered index)。數據庫
非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB 裏,非主鍵索引也被稱爲二級索引(secondary index)。緩存
根據上面的索引結構說明,咱們來討論一個問題:基於主鍵索引和普通索引的查詢有什麼區別?服務器
若是語句是 ,下面這條:數據結構
SELECT * from userinfo where id= 655;
即主鍵查詢方式,則只須要搜索 ID 這棵 B+ 樹;併發
若是語句是,下面這條:函數
SELECT * from userinfo where name= 'aaron';
即普通索引查詢方式,則須要先搜索name 索引樹,獲得 ID 的值爲 655,再到 ID 索引樹搜索一次。這個過程稱爲回表。性能
也就是說,基於非主鍵索引的查詢須要多掃描一棵索引樹。所以,咱們在應用中應該儘可能使用主鍵查詢。
索引維護
先從問題開始入手考慮,索引維護這個主題。
爲何公司規定主鍵ID通常是自增加,且是整型?
性能方面考慮:
B+ 樹爲了維護索引有序性,在插入新值的時候須要作必要的維護.若是插入新的行 ID 值爲 700,則只須要在 R5 的記錄後面插入一個新記錄。若是新插入的 的 ID 值爲 400,就相對麻煩了。須要邏輯上挪動後面的數據,空出位置。
而更糟的狀況是,若是 R5 所在的數據頁已經滿了,根據 B+ 樹的算法,這時候須要申請一個新的數據頁,而後挪動部分數據過去。這個過程稱爲頁分裂。在這種狀況下,性能天然會受影響。
固然有分裂就有合併。當相鄰兩個頁因爲刪除了數據,利用率很低以後,會將數據頁作合併。合併的過程,能夠認爲是分裂過程的逆過程。
自增主鍵的插入數據模式,正符合了咱們前面提到的遞增插入的場景。每次插入一條新記錄,都是追加操做,也不會觸發葉子節點的分裂。而有業務邏輯的字段作主鍵,則每每不容易保證有序插入,這樣寫數據成本相對較高。
存儲方面考慮:
除了考慮性能外,咱們還能夠從存儲空間的角度來看。假設你的表中確實有一個惟一字段,好比字符串類型的身份證號,那應該用身份證號作主鍵,仍是用自增字段作主鍵呢?
因爲每一個非主鍵索引的葉子節點上都是主鍵的值。若是用身份證號作主鍵,那麼每一個二級索引的葉子節點佔用約 20 個字節,而若是用整型作主鍵,若是是長整型(bigint)則是 8 個字節。
顯然,主鍵長度越小,普通索引的葉子節點就越小,普通索引佔用的空間也就越小。
因此,從性能和存儲空間方面考量,自增主鍵每每是更合理的選擇。
有沒有什麼場景適合用業務字段直接作主鍵的呢?
好比,有些業務的場景需求是這樣的:
1.只有一個索引;
2.該索引必須是惟一索引。
這就是典型的 KV 場景。直接將這個索引設置爲主鍵,能夠避免每次查詢須要搜索兩棵樹。
0二、高性能的索引策略
高效地選擇和使用索引有不少種方式,其中有些是針對特殊案列的優化方式,有些則是針對特定行爲的優化。
1.獨立的列
是指索引列不能是表達式的一部分,也不能是函數的參數。
錯誤的方式,肉眼能夠看出,可是mysql並不能識別出。
SELECT id FROM userinfo where id + 1 = 2;
正確的方式是:
SELECT id FROM userinfo where id = 1;
2.前綴索引和索引選擇性
有時候須要索引很長的字符串,這會讓索引變得大且慢。一個策略是哈希索引。一般能夠索引開始的部分字符,這樣能夠大大節約索引空間,從而提升索引效果。但這樣會下降索引的選擇性。索引的選擇性是指,不重複的索引值(也稱爲基數)和數據表的記錄總數(#T)的比值,範圍從1/#T到1之間。索引的選擇性越高則查詢效率越高,由於選擇性高的索引讓mysql在查找時過濾掉更多的行。
3.多列索引
一個常見的錯誤就是,爲每一個列建立獨立的索引,或者按照錯誤的順序建立多列索引。
creat table t(c1 int,c2 int,c3 int,key(c1),key(c2),key(c3));
把"where 條件裏的列都建上索引",這個是有點錯誤的想法,這個可能只是"一星"索引,其性能比起真正最優的索引可能差幾個數量級。有時若是沒法設計一個"三星"索引,那麼不如忽略掉where子句,集中精力優化索引列的順序,或者建立一個全覆蓋索引。進而,引出"索引合併":
SELECT id FROM userinfo where age > 10 or sex ='M';
通常會使用全表掃描的,除非改寫成以下的兩個表查詢union的方式:
SELECT id FROM userinfo where age > 10union allSELECT id FROM userinfo where sex ='M';
若是對 age和sex進行索引合併,仍是同樣的查詢:
SELECT id FROM userinfo where age > 10 or sex ='M';
可能將會大大改善其結果。
索引合併策略有時候是一種優化的結果,但實際上更多時候說明了表上的索引建的很糟糕:
1.當出現服務器對多個索引作相交操做時(一般有多個AND條件),一般意味着須要一個包含全部相關列的多列索引,而不是多個獨立的單列索引。
2.當服務器須要對多個索引作聯合操做時(一般有多個or條件),一般須要耗費大量CPU和內存資源在算法的緩存、排序和合並操做上。特別是當其中有些索引的選擇性不高,須要合併掃描返回的大量數據的時候。
4.選擇合適的索引列順序
在一個多列B-Tree索引中,索引列的順序意味着索引首先按照最左列進行排序,其次是第二列等等。因此,索引能夠按照升序或者降序進行掃描。以知足精確符合順序的order by、Group by 和 distinct 等子句的查詢需求。
當不須要考慮排序和分組時,將選擇性最高的列放在前面一般是很好的。這時候索引的做用只是用於優化where條件的查找。在這種狀況下,這樣的設計的索引確實可以最快地過濾出須要的行,對於where子句中只使用了索引部分前綴的查詢來講選擇性也更高。然而,性能不僅是依賴於全部索引列的選擇性(總體基數),也和查詢條件的具體值有關,也就是和值的分佈有關。
那麼咱們應該怎麼選擇組合索引字段的順序呢?
select * from userinfo where id = 2 and customer_id = 584;
是應該建立一個(id,customer_id)索引仍是應該顛倒一下順序?能夠跑一些查詢來肯定在這個表中值的分佈狀況,並卻肯定那個列的選擇性更高。
先使用sum函數來看看where條件的分支對應的數據基數有多大:
select sum(id = 2 ), sum (customer_id = 584) from userinfo ;
結果爲:
sum(id = 2 ): 7992 ; sum (customer_id = 584) : 30
根據前面的法則,應該將索引列 customer_id 放到前面,由於對應條件customer_id 數量更小。最後,儘管關於選擇性和基數的經驗法則值得去研究和分析,但必定要記住別忘記了where子句的排序、分組和範圍條件等其餘因素。
5.聚族索引
聚族索引並非一種單獨的索引類型,而是一種數據存儲方式。當表有聚族索引時,它的數據行實際上存放在索引的葉子頁中。
InnoDB將經過主鍵聚族數據,若是沒有定義主鍵,InnoDB會選擇一個惟一的非空索引代替。若是沒有這樣的索引,InnoDB會隱式定義一個主鍵來做爲聚族索引。InnoDB只彙集在同一個頁面中的記錄。包含相鄰鍵值的頁面可能會相距甚遠。
優勢:
1.能夠把相關的數據保存在一塊兒。例如實現電子郵箱時,能夠根據用戶id來彙集數據,這樣只須要從磁盤讀取少數的數據頁就能獲取某個用戶的所有郵件。若是沒有使用聚族索引,則每封郵件均可能致使一次磁盤i/o。
2.數據訪問更快。
3.使用覆蓋索引掃描的查詢能夠直接使用頁節點中的主鍵值。
缺點:
1.聚族數據最大限度地提升了i/o密集型應用的性能,但若是數據所有都放在內存中,則訪問的順序就沒那麼重要了,聚族索引也就沒什麼優點了。
2.插入速度嚴重依賴插入順序。按照主鍵的順序插入是加載數據到InnoDB表中最快的方式。
3.更新聚族索引列的代價很高。
4.聚族索引可能致使全表掃描行,尤爲是行比較稀疏,或者因爲頁分裂致使數據不連續的時候。
建議:最好儘量使用單調增長的聚族鍵的值來插入新行。這樣能夠減小寫入的時候分頁相關操做。
6.覆蓋索引
一般你們都會根據查詢的where條件來建立合適的索引,不過這只是索引優化的一個方面。設計優秀的索引應該考慮到整個查詢,而不是單單是where條件部分。若是一個索引包含(或者說覆蓋)全部須要查詢的字段的值,咱們就從稱之爲"覆蓋索引".
優勢:
1.索引條目一般遠小於數據行大小,因此若是隻須要讀取索引,那mysql就會極大地減小數據訪問量。
2.由於索引是按照列值順序存儲的,因此對於i/o密集型的範圍查詢會比隨機從磁盤讀取每一行數據的i/o要少得多。
3.因爲InnoDB的聚族索引,覆蓋索引對InnoDB表特別有用。InnoDB的二級索引在葉子節點中保存了行的主鍵值,因此若是二級主鍵可以覆蓋查詢,則能夠避免對主鍵索引的二次查詢。
發起一個被被覆蓋的查詢,在EXPLAIN 的 Extra列能夠看到"Using index" 的信息。那就可使用這個索引作覆蓋索引。
注意:Extra列 的 "Usering where" 是不能夠作索引的。有多是字段使用了like %%。
EXPLAIN SELECT ua.account_id,ua.user_id from user_account ua
能夠根據account_id,user_id 作覆蓋索引,也能夠根據where條件全部值作覆蓋索引。
7.使用索引掃描來作排序
mysql有兩種方式能夠生成有序的結果:經過排序操做;或者按照索引順序掃描。若是EXPLAIN 出來的type列的值爲"index",則說明mysql使用了索引掃描來作排序。
只要當索引的列順序和order by子句的順序徹底一致,而且全部列的排序方向都同樣時,mysql纔可以使用索引來對結果作排序。若是查詢須要關聯多張表,則只有當order by 子句引用的字段所有爲第一個表時,才能使用索引作排序order by子句和查找型查詢的限制是同樣的。
對於有這樣一個表 rental 在列(rental_date,inventory_id,customer_id)建立這樣一個索引。
where rental_date='2019-04-05' order by inventory_id,customer_id ;
即便order by 子句不知足索引的最左前綴的要求,也能夠用於查詢排序,這是由於索引的第一列被指定爲一個常數。
where rental_date = '2019-04-05' order by inventory_id desc;
能夠利用查詢爲索引的第一列提供了常量條件,而使用第二列進行排序,將兩列組合在一塊兒,就造成了索引的最左前綴。
#下面這個也是最左前綴索引:where rental_date = '2019-04-05' order by rental_date , inventory_id ;#下面是一些不能使用索引作排序的查詢:#使用了兩種不一樣的排序方向,可是索引列都是正序排序的:where rental_date = '2019-04-05' order by inventory_id desc , customer_id asc;#查詢的order by 子句中引用了一個不在索引中的列:where rental_date = '2019-04-05' order by inventory_id , staff_id;#查詢的where 和 order by 中的列沒法組合成索引的最左前綴:where rental_date = '2019-04-05' order by customer_id;#查詢在索引的第一列上是範圍條件,因此mysql沒法使用索引的其他列:where rental_date > '2019-04-05' order by inventory_id ,customer_id;#查詢在inventory_id 列上有多個條件,對於排序來講,這也是一種範圍查詢:where rental_date = '2019-04-05' and inventory_id in (1,2) order by customer_id ;
8. 冗餘和重複索引
mysql 容許在相同列上建立多個索引,mysql須要單獨維護重複的索引,而且優化器在優化查詢的時候也須要逐個地進行考慮,這會影響性能。重複索引是指在相同的列上按照相同的順序建立的相同類型的索引。應該避免這樣建立重複索引,發現之後也應該當即移除。
creat table test(ID int not null primary key,A int not null,UNIQUE(ID),index(ID))
這個在建立的時候,就已經建立了重複的索引。
大多數狀況下都不須要冗餘索引,應該儘可能擴展已有的索引而不是建立新索引。通常來講表中的索引越多插入速度會越慢(這個在項目中已經實驗過了,數據遷移項目),同時增長新索引將會致使insert / update / delete 等操做的速度變慢,特別是當新增索引後致使達到了內存瓶頸的時候。
9.索引和鎖
索引可讓查詢鎖定更少的行。若是你的查詢從不訪問哪些不須要的行,那麼就會鎖定更少的行。首先,雖然InnoDB的行鎖效率很高,內存使用頁不多,可是鎖定行的時候,仍然會帶來額外開銷;其次,鎖定超過須要的行會增長鎖爭用並減小併發性。
InnoDB只有在訪問行的時候纔會對其加鎖。而索引可以減小InnoDB訪問的行數,從而減小鎖的數量。但只有當InnoDB在存儲引擎層可以過濾掉全部不須要的行時纔有效。
實戰,案例一:
1. 支持多種過濾條件
好比有個表國家(country)列,這個列選擇性一般不高,但可能會查詢都會用到。sex的列選擇性確定也很低,但也會在不少查詢中用到。因此考慮到使用的頻率,仍是建議在建立不一樣組合索引的時候將(sex,country)列做爲前綴。
問題是,若是某個查詢不限制性別,那麼咱們應該怎麼作?
能夠經過在查詢條件中新增and sex in('m','f')來讓mysql選擇該索引。mysql可以匹配索引的最左前綴。但若是列有太多的值,就會讓in()列表太長(這篇關於in語句與between對比文章),這樣作久不太行了。
設計的原則:
考慮表上全部的選項。
當設計索引時,不要只考慮須要哪些索引,還須要考慮對查詢進行優化。若是發現某些查詢須要建立新索引,可是這個索引會下降另外一些查詢的效率,那麼應該想一下是否能優化原來的查詢。應該同時優化查詢和索引以找到最佳的平衡。
接下來,須要考慮其餘常見的where 條件的組合,並須要瞭解哪些組合在沒有合適索引狀況下會很慢。其實這個索引(sex,country)還能夠加上(sex,country,region,age).
2.避免多個範圍條件
什麼是範圍查詢?
從explain的輸出很難區分mysql是要查詢範圍值。可是咱們能夠從值的範圍和多個等於條件來得出不一樣。
假設有這樣一個語句:
where eye_color in('blue')and hair_color in('black','red')and sex in('m','f')and last_online > DATE_SUB(NOW(),INTERVAL 7 DAY)and age between 18 and 20
這個查詢有一個問題,它有兩個範圍條件,last_online and age , mysql last_online 列索引 and age 列索引,但沒法同時使用它們。這個是沒法解決的,建議多建立幾個組合索引,可是也不要建立太多,索引建立太多,可能致使插入很慢。
3.優化排序
例如,若是where 子句只有sex 列,如何排序?
對於那些選擇性很是低的列,能夠增長一些特殊的索引來作排序。 例如,能夠建立(sex, rating) 索引用於下面的查詢:
select * from profiles where sex = 'm' order by rating limit 10;
若是須要翻頁:
select * from profiles where sex = 'm' order by rating limit 100000, 10;
不管如何建立索引,這種查詢都是個嚴重的問題。由於隨着偏移量的增長,mysql 須要花費大量的時間來掃描須要丟棄的數據。反範式化,預先計算和緩存多是解決這類查詢的僅有策略,一個更好的辦法是限制用戶可以翻頁查詢的數量,實際上這對用戶體驗的影響不大,由於用戶不多真正在意搜索結果的第10000頁。
優化這類索引的另外一個比較好的策略是使用延遲關聯,經過使用覆蓋索引查詢返回須要的主鍵,在根據主鍵關聯原表得到須要的行。這能夠減小mysql掃描哪些須要丟棄的行數。
如何高效的利用(sex, rating)索引進行排序和分頁:
select * from profiles inner join ( select (primary key) from profiles where x.sex = 'm' order by rating limit 100000, 10 ) as x using (primary key) ;
實戰演練二:如何給字符串創建索引?
如今,幾乎全部的系統都支持郵箱登陸,如何在郵箱這樣的字段上創建合理的索引?
create table SUser(ID bigint unsigned primary key,email varchar(64))engine=innodb;
通常的sql是這樣的:
select f1, f2 from SUser where email='xxx';
若是 email 這個字段上沒有索引,那麼這個語句就只能作全表掃描。建立索引:
alter table SUser add index index1(email);#或 alter table SUser add index index2(email(6));
第一個語句建立的 index1 索引裏面,包含了每一個記錄的整個字符串;而第二個語句建立的index2 索引裏面,對於每一個記錄都是隻取前 6 個字節。
那麼,這兩種不一樣的定義在數據結構和存儲上有什麼區別呢?
最大的區別是:存儲的數據變大。
email(6) 這個索引結構中每一個郵箱字段都只取前 6 個字節,因此佔用的空間會更小,這就是使用前綴索引的優點。但,這同時帶來的損失是,可能會增長額外的記錄掃描次數。
select id,name,email from SUser where email='fanrongaaron@xxx.com';
若是使用的是 index1(即 email 整個字符串的索引結構),執行順序是這樣的:
1.從 index1 索引樹找到知足索引值是’fanrongaaron@xxx.com’的這條記錄,取得 ID2 的值;
2.到主鍵上查到主鍵值是 ID2 的行,判斷 email 的值是正確的,將這行記錄加入結果集;
3.取 index1 索引樹上剛剛查到的位置的下一條記錄,發現已經不知足 email='fanrongaaron@xxx.com’的條件了,循環結束。
這個過程當中,只須要回主鍵索引取一次數據。
若是使用的是 index2(即 email(6) 索引結構),執行順序是這樣的:
1.從 index2 索引樹找到知足索引值是’fanron’的記錄,找到的第一個是 ID1;
2.到主鍵上查到主鍵值是 ID1 的行,判斷出 email 的值不是’fanrongaaron@xxx.com’,這行記錄丟棄;
3.取 index2 上剛剛查到的位置的下一條記錄,發現仍然是’fanron’,取出 ID2,再到 ID 索引上取整行而後判斷,此次值對了,將這行記錄加入結果集;
4.重複上一步,直到在 idxe2 上取到的值不是’fanron’時,循環結束。
在這個過程當中,要回主鍵索引取 4 次數據,也就是掃描了 4 行。對於這個查詢語句來講,若是你定義的 index2而是 email(7),也就是說取 email 字段的前 7 個字節來構建索引的話,即知足前綴’fanrong’的記錄只有一個,也可以直接查到 ID2,只掃描一行就結束了。使用前綴索引,定義好長度,就能夠作到既節省空間,又不用額外增長太多的查詢成本。
那麼問題來了,當要給字符串建立前綴索引時,有什麼方法可以肯定我應該使用多長的前綴呢?
實際上,咱們在創建索引時關注的是區分度,區分度越高越好。由於區分度越高,意味着重複的鍵值越少。所以,咱們能夠經過統計索引上有多少個不一樣的值來判斷要使用多長的前綴。
首先,你可使用下面這個語句,算出這個列上有多少個不一樣的值:
select count(distinct email) as L from SUser;#依次選取不一樣長度的前綴來看這個值,好比咱們要看一下 4~7 個字節的前綴索引,能夠用這個語句:selectcount(distinct left(email,4))as L4,count(distinct left(email,5))as L5,count(distinct left(email,6))as L6,count(distinct left(email,7))as L7,from SUser;
前綴索引對覆蓋索引的影響:
select id,email from SUser where email='fanrongaaron@xxx.com';#要求返回 id 和 email 字段。select id,name,email from SUser where email='fanrongaaron@xxx.com';
若是使用 index1(即 email 整個字符串的索引結構)的話,能夠利用覆蓋索引,從index1 查到結果後直接就返回了,不須要回到 ID 索引再去查一次。而若是使用 index2(即email(6) 索引結構)的話,就不得不回到 ID 索引再去判斷 email 字段的值。
也就是說,使用前綴索引就用不上覆蓋索引對查詢性能的優化了,這也是你在選擇是否使用前綴索引時須要考慮的一個因素。
那麼有沒有更好的方式呢?其它方式:
好比,咱們國家的身份證號,一共 18 位,其中前 6 位是地址碼,因此同一個縣的人的身份證號前 6 位通常會是相同的。按照咱們前面說的方法,可能你須要建立長度爲 12 以上的前綴索引,纔可以知足區分度要求。
可是,索引選取的越長,佔用的磁盤空間就越大,相同的數據頁能放下的索引值就越少,搜索的效率也就會越低。
若是咱們可以肯定業務需求裏面只有按照身份證進行等值查詢的需求還有沒有別的處理方法呢?
第一種方式是使用倒序存儲。若是你存儲身份證號的時候把它倒過來存,每次查詢的時候,你能夠這麼寫:
select field_list from t where id_card = reverse('input_id_card_string');
第二種方式是使用 hash 字段。你能夠在表上再建立一個整數字段,來保存身份證的校驗碼,時在這個字段上建立索引。
alter table t add id_card_crc int unsigned, add index(id_card_crc);
而後每次插入新記錄的時候,都同時用 crc32() 這個函數獲得校驗碼填到這個新字段。因爲校驗碼可能存在衝突,也就是說兩個不一樣的身份證號經過 crc32() 函數獲得的結果多是相同的,因此你的查詢語句 where 部分要判斷 id_card 的值是否精確相同。
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
這樣,索引的長度變成了 4 個字節,比原來小了不少。使用倒序存儲和使用 hash 字段這兩種方法的異同點。
首先,它們的相同點是,都不支持範圍查詢。倒序存儲的字段上建立的索引是按照倒序字符串的方式排序的,已經沒有辦法利用索引方式。
1.從佔用的額外空間來看,倒序存儲方式在主鍵索引上不會消耗額外的存儲空間,而 hash 字段方法須要增長一個字段。固然,倒序存儲方式使用 4 個字節的前綴長度應該是不夠的,若是再長一點,這個消耗跟額外這個 hash 字段也差很少抵消了。
2.在 CPU 消耗方面,倒序方式每次寫和讀的時候,都須要額外調用一次 reverse 函數,而 hash字段的方式須要額外調用一次 crc32() 函數。若是隻從這兩個函數的計算複雜度來看的話,reverse 函數額外消耗的 CPU 資源會更小些。
3.從查詢效率上看,使用 hash 字段方式的查詢性能相對更穩定一些。由於 crc32 算出來的值雖然有衝突的機率,可是機率很是小,能夠認爲每次查詢的平均掃描行數接近 1。而倒序存儲方式畢竟仍是用的前綴索引的方式也就是說仍是會增長掃描行數。
總結:
1.直接建立完整索引,這樣可能比較佔用空間;
2.建立前綴索引,節省空間,但會增長查詢掃描次數,而且不能使用覆蓋索引。
3.倒序存儲,再建立前綴索引,用於繞過字符串自己前綴的區度不夠的問題;
4.建立 hash 字段索引,查詢性能穩定,有額外的存儲和計算消算消耗,跟第三種方式同樣,都不支持範圍掃描。
0三、選擇索引依據是什麼?
前面都是直接給出索引規則,這個應該怎麼操做,這個應該使用什麼索引。可是到底怎麼會選擇這個索引呢,主鍵索引?
假設你在維護一個市民系統,每一個人都有一個惟一的身份證號,並且業務代碼已經保證了不會寫入兩個重複的身份證號。若是市民系統須要按照身份證號查姓名,就會執行相似這樣的 SQL 語
select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';
因此,你必定會考慮在 id_card 字段上建索引。因爲身份證號字段比較大,我不建議你把身份證號當作主鍵,那麼如今你有兩個選擇,要麼給id_card 字段建立惟一索引,要麼建立一個普通索引。若是業務代碼已經保證了不會寫入重複的身份證號,那麼這兩個選擇邏輯上都是正確的。
1.從性能的角度考慮,你選擇惟一索引仍是普通索引呢?選擇的依據是什麼呢?
第一個節點之後說了主鍵索引和普通索引的區別。假設,執行查詢的語句是:
SELECT * from userinfo where name='1000';
這個查詢語句在索引樹上查找的過程,先是經過 B+ 樹從樹根開始,按層搜索到葉子節點,而後能夠認爲數據頁內部經過二分法來定位記錄。
對於普通索引來講,查找到知足條件的第一個記錄 (1000,5000)後,須要查找下一個記錄,直到碰到第一個不知足 id=5 條件的記錄。對於惟一索引來講,因爲索引定義了惟一性,查找到第一個知足條件的記錄後,就會中止繼續檢索。
那麼,這個不一樣帶來的性能差距會有多少呢?答案是,微乎其微。
InnoDB 的數據是按數據頁爲單位來讀寫的。也就是說,當須要讀一條記錄的時候,並非將這個記錄自己從磁盤讀出來,而是以頁爲單位,將其總體讀入內存。在 InnoDB 中,每一個數據頁的大小默認是 16KB。
由於引擎是按頁讀寫的,因此說,當找到 name='1000' 的記錄的時候,它所在的數據頁就都在內存裏了。那麼,對於普通索引來講,要多作的那一次「查找和判斷下一條記錄」的操做,就只須要一次指針尋找和一次計算。
2.更新過程
當須要更新一個數據頁時,若是數據頁在內存中就直接更新,而若是這個數據頁尚未在內存中的話,在不影響數據一致性的前提下,InooDB 會將這些更新操做緩存在change buffer 中,這樣就不須要從磁盤中讀入這個數據頁了。在下次查詢須要訪問這個數據頁的時候,將數據頁讀入內存,而後執行 change buffer 中與這個頁有關的操做。經過這種方式就能保證這個數據邏輯的正確性。
change buffer,實際上它是能夠持久化的數據。也就是說,change buffer 在內存中有拷貝,也會被寫入到磁盤上。將 change buffer 中的操做應用到原數據頁,獲得最新結果的過程稱爲 merge。除了訪問這個數據頁會觸發 merge 外,系統有後臺線程會按期 merge。在數據庫正常關閉(shutdown)的過程當中,也會執行 merge。
顯然,若是可以將更新操做先記錄在 change buffer.,減小讀磁盤,語句的執行速度會獲得明顯的提高。並且,數據讀入內存是須要佔用 buffer pool的,因此這種方式還可以避免佔用內存,提升內存利用率。
2.1 什麼條件下可使用 change buffer 呢?
對於惟一索引來講,全部的更新操做都要先判斷這個操做是否違反惟一性約束。好比,要插入(4,400) 這個記錄,就要先判斷如今表中是否已經存在 name=4 的記錄,而這必需要將數據頁讀入內存才能判斷。若是都已經讀入到內存了,那直接更新內存會更快,就不必使用 change buffe了。
所以,惟一索引的更新就不能使用 change buffer,實際上也只有普通索引可使用。再一塊兒來看看若是要在這張表中插入一個新記錄 (4,400) 的話,InnoDB 的處理流程是怎樣的。
第一種狀況是,這個記錄要更新的目標頁在內存中。這時,InnoDB 的處理流程以下:
1.對於惟一索引來講,找到 3 和 5 之間的位置,判斷到沒有衝突,插入這個值,語句執行結束;
2.對於普通索引來講,找到 3 和 5 之間的位置,插入這個值,語句執行結束。
第二種狀況是,這個記錄要更新的目標頁不在內存中。這時,InnoDB 的處理流程以下:
對於惟一索引來講,須要將數據頁讀入內存,判斷到沒有衝突,插入這個值,語句執行結束;
對於普通索引來講,則是將更新記錄在 change buffe,語句執行就結束了。
change buffer 由於減小了隨機磁盤訪問,因此對更新性能的提高是會很明顯的。
2.2 change buffer 的使用場景
change buffer 只限於用在普通索引的場景下,而不適用於惟一索引。
注意:所以,對於寫多讀少的業務來講,頁面在寫完之後立刻被訪問到的機率比較小,此時 change buffer 的使用效果最好。這種業務模型常見的就是帳單類、日誌類的系統。反過來,假設一個業務的更新模式是寫入以後立刻會作查詢,將更新先記錄在 change buffer,但以後因爲立刻要訪問這個數據頁,會當即觸發 merge 過程。這樣change buffer 反而起到了反作用。
redo log 主要節省的是隨機寫磁盤的 IO 消耗(轉成順序寫),而 change buffer 主要節省的則是隨機讀磁盤的 IO 消耗。
0四、總結
在選擇索引和編寫利用這些索引的查詢時,有以下三個原則始終須要記住:
1.單訪問時很慢的。特別是在機械鍵盤存儲中。若是服務器從存儲中讀取一個數據塊只是爲了獲取其中一行,那麼久浪費了不少工做。最好讀取的塊中能包含儘量多所須要的行。使用索引能夠建立位置引用以提高效率。
2.按順序訪問範圍數據是很快的,這有兩個緣由。第一,順序I/O不須要多行磁盤尋道,因此比隨機I/O要快不少。第二是,若是服務器可以按須要順序讀取數據,那麼就再也不須要額外的排序操做,而且Group By 查詢也無須再作排序和將行按組進行聚合計算了。
3.索引覆蓋查詢是快的。若是一個索引包含了查詢須要的全部列,那麼存儲引擎就不須要再回表查找行。這避免了大量的單行訪問。