MySQL的Innodb存儲引擎的索引分為聚集索引和非聚集索引兩大類,理解聚集索引和非聚集索引可通過對比漢語字典的索引。漢語字典提供了兩類檢索漢字的方式,第一類是拼音檢索(前提是知道該漢字讀音),比如拼音為cheng的漢字排在拼音chang的漢字后面,根據拼音找到對應漢字的頁碼(因為按拼音排序,二分查找很快就能定位),這就是我們通常所說的字典序;第二類是部首筆畫檢索,根據筆畫找到對應漢字,查到漢字對應的頁碼。拼音檢索就是聚集索引,因為存儲的記錄(數據庫中是行數據、字典中是漢字的詳情記錄)是按照該索引排序的;筆畫索引,雖然筆畫相同的字在筆畫索引中相鄰,但是實際存儲頁碼卻不相鄰,這是非聚集索引。
聚集索引
索引中鍵值的邏輯順序決定了表中相應行的物理順序。
聚集索引確定表中數據的物理順序。聚集索引類似於電話簿,后者按姓氏排列數據。 聚集索引對於那些經常要搜索范圍值的列特別有效。使用聚集索引找到包含第一個值的行后,便可以確保包含后續索引值的行在物理相鄰。例如,如果應用程序執行 的一個查詢經常檢索某一日期范圍內的記錄,則使用聚集索引可以迅速找到包含開始日期的行,然后檢索表中所有相鄰的行,直到到達結束日期。這樣有助於提高此 類查詢的性能。同樣,如果對從表中檢索的數據進行排序時經常要用到某一列,則可以將該表在該列上聚集(物理排序),避免每次查詢該列時都進行排序,從而節 省成本。
以上是innodb的b+tree索引結構
我們知道b+tree是從b-tree演變而來,一棵m階的B-Tree有如下特性:
1、每個結點最多m個子結點。
2、除了根結點和葉子結點外,每個結點最少有m/2(向上取整)個子結點。
3、如果根結點不是葉子結點,那根結點至少包含兩個子結點。
4、所有的葉子結點都位於同一層。
5、每個結點都包含k個元素(關鍵字),這里m/2≤k<m,這里m/2向下取整。
6、每個節點中的元素(關鍵字)從小到大排列。
7、每個元素(關鍵字)字左結點的值,都小於或等於該元素(關鍵字)。右結點的值都大於或等於該元素(關鍵字)。
b+tree的特點是:
1、所有的非葉子節點只存儲關鍵字信息。
2、所有衛星數據(具體數據)都存在葉子結點中。
3、所有的葉子結點中包含了全部元素的信息。
4、所有葉子節點之間都有一個鏈指針。
我們發現,b+trre有以下特性:
- 對一個范圍內的查詢特別有效快速(通過葉子的鏈指針);
- 對具體的key值查詢僅僅比b-tree低效一點(因為要到葉子一級),但也可以忽略;
非聚集索引
索引中索引的邏輯順序與磁盤上行的物理存儲順序不同。
其實按照定義,除了聚集索引以外的索引都是非聚集索引,只是人們想細分一下非聚集索引,分成普通索引,唯一索引,全文索引。如果非要把非聚集索引類比成現實生活中的東西,那么非聚集索引就像新華字典的偏旁字典,他結構順序與實際存放順序不一定一致。
非聚集索引的存儲結構與前面是一樣的,不同的是在葉子結點的數據部分存的不再是具體的數據,而數據的聚集索引的key。所以通過非聚集索引查找的過程是先找到該索引key對應的聚集索引的key,然后再拿聚集索引的key到主鍵索引樹上查找對應的數據,這個過程稱為回表!
舉個例子說明下:
create table student ( `id` INT UNSIGNED AUTO_INCREMENT,
`username` VARCHAR(255),
`score` INT,
PRIMARY KEY(`id`), KEY(`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
聚集索引clustered index(id), 非聚集索引index(username)。
使用以下語句進行查詢,不需要進行二次查詢,直接就可以從非聚集索引的節點里面就可以獲取到查詢列的數據。
select id, username from t1 where username = '小明' select username from t1 where username = '小明'
但是使用以下語句進行查詢,就需要二次的查詢去獲取原數據行的score:
select username, score from t1 where username = '小明'
如何解決非聚集索引的二次查詢問題
復合索引(覆蓋索引)
建立兩列以上的索引,即可查詢復合索引里的列的數據而不需要進行回表二次查詢,如index(col1, col2),執行下面的語句
select col1, col2 from t1 where col1 = '213';
要注意使用復合索引需要滿足最左側索引的原則,也就是查詢的時候如果where條件里面沒有最左邊的一到多列,索引就不會起作用。
何時使用聚集索引或非聚集索引:
動作描述 | 使用聚集索引 | 使用非聚集索引 |
列經常被分組排序 | 應 | 應 |
返回某范圍內的數據 | 應 | 不應 |
一個或極少不同值 | 不應 | 不應 |
小數目的不同值 | 應 | 不應 |
大數目的不同值 | 不應 | 應 |
頻繁更新的列 | 不應 | 應 |
外鍵列 | 應 | 應 |
主鍵列 | 應 | 應 |
頻繁修改索引列 | 不應 | 應 |
目前mysql中就是將自增Id強制設定為主鍵索引,這是為了b+tree和分頁。
mysql中每次新增數據,都是將一個頁寫滿,然后新創建一個頁繼續寫,這里其實是有個隱含條件的,那就是主鍵自增!主鍵自增寫入時新插入的數據不會影響到原有頁,插入效率高!且頁的利用率高!但是如果主鍵是無序的或者隨機的,那每次的插入可能會導致原有頁頻繁的分裂,影響插入效率!降低頁的利用率!這也是為什么在innodb中建議設置主鍵自增的原因!
這棵樹的非葉子結點上存的都是主鍵,那如果一個表沒有主鍵會怎么樣?在innodb中,如果一個表沒有主鍵,那默認會找建了唯一索引的列,如果也沒有,則會生成一個隱形的字段作為主鍵!
#1. 准備表 create table s1( id int, name varchar(20), gender char(6), email varchar(50) ); #2. 創建存儲過程,實現批量插入記錄 delimiter $$ #聲明存儲過程的結束符號為$$ create procedure auto_insert1() BEGIN declare i int default 1; while(i<3000000)do insert into s1 values(i,concat('egon',i),'male',concat('egon',i,'@oldboy')); set i=i+1; end while; END$$ #$$結束 delimiter ; #重新聲明分號為結束符號 #3. 查看存儲過程 show create procedure auto_insert1\G #4. 調用存儲過程 call auto_insert1(); #5、修改刪除索引 1、在創建表后創建索引 create index name on s1(name); #添加普通索引 create unique username on s1(username);添加唯一索引 alter table s1 add primary key(id); #添加住建索引,也就是給id字段增加一個主鍵約束 create index idandname on s1(id,name); #添加普通聯合索引 2.刪除索引 drop index id on s1; drop index name on s1; #刪除普通索引 drop index age on s1; #刪除唯一索引,就和普通索引一樣,不用在index前加unique來刪,直接就可以刪了 alter table s1 drop primary key; #刪除主鍵(因為它添加的時候是按照alter來增加的,那么我們也用alter來刪)
先只使用主鍵索引。
name未使用索引
索引覆蓋
select * from s1 where id=123; 該sql命中了索引,但未覆蓋索引。 利用id=123到索引的數據結構中定位到該id在硬盤中的位置,或者說再數據表中的位置。 但是我們select的字段為*,除了id以外還需要其他字段,這就意味着,我們通過索引結構取到id還不夠, 還需要利用該id再去找到該id所在行的其他字段值,這是需要時間的,很明顯,如果我們只select id, 就減去了這份苦惱,如下 select id from s1 where id=123; 這條就是覆蓋索引了,命中索引,且從索引的數據結構直接就取到了id在硬盤的地址,速度很快
設計原則
#1.最左前綴匹配原則,非常重要的原則, create index ix_name_email on s1(name,email,) - 最左前綴匹配:必須按照從左到右的順序匹配 select * from s1 where name='egon'; #可以 select * from s1 where name='egon' and email='asdf'; #可以 select * from s1 where email='alex@oldboy.com'; #不可以 mysql會一直向右匹配直到遇到范圍查詢(>、<、between、like)就停止匹配, 比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)順序的索引, d是用不到索引的,如果建立(a,b,d,c)的索引則都可以用到,a,b,d的順序可以任意調整。 #2.=和in可以亂序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意順序,mysql的查詢優化器 會幫你優化成索引可以識別的形式 #3.盡量選擇區分度高的列作為索引,區分度的公式是count(distinct col)/count(*), 表示字段不重復的比例,比例越大我們掃描的記錄數越少,唯一鍵的區分度是1,而一些狀態、 性別字段可能在大數據面前區分度就是0,那可能有人會問,這個比例有什么經驗值嗎?使用場景不同, 這個值也很難確定,一般需要join的字段我們都要求是0.1以上,即平均1條掃描10條記錄 #4.索引列不能參與計算,保持列“干凈”,比如from_unixtime(create_time) = ’2014-05-29’ 就不能使用到索引,原因很簡單,b+樹中存的都是數據表中的字段值, 但進行檢索時,需要把所有元素都應用函數才能比較,顯然成本太大。 所以語句應該寫成create_time = unix_timestamp(’2014-05-29’);
#5.其他
- 索引字段盡量使用數字型(簡單的數據類型)
- 盡量不要讓字段的默認值為NULL
- 使用組合索引代替多個列索引
- 使用唯一索引,考慮某列中值的分布。索引的列的基數越大,索引的效果越好。 例如,存放出生日期的列具有不同值,很容易區分各行。而用來記錄性別的列,只含有“ M” 和“F”,則對此列進行索引沒有多大用處,因為不管搜索哪個值,都會得出大約一半的行。
-
- 如果對大的文本進行搜索,使用全文索引而不要用使用 like ‘%…%’
- like語句不要以通配符開頭
對於LIKE:在以通配符%和_開頭作查詢時,MySQL不會使用索引。like操作一般在全文索引中會用到(InnoDB數據表不支持全文索引)。
例如下句會使用索引:
SELECT * FROM mytable WHERE username like'admin%'
而下句就不會使用:
SELECT * FROM mytable WHEREt Name like'%admin'
- 不要在列上進行運算
- 盡量不要使用NOT IN、<>、!=
- 任何地方都不要使用 select * from t ,用具體的字段列表代替“*”,不要返回用不到的任何字段如果 MySQL 估計使用索引比全表掃描更慢,則不使用索引。當索引列有大量數據重復時,查詢可能不會去利用索引,如一表中有字段sex,male、female幾乎各一半,那么即使在sex上建了索引也對查詢效率起不了作用
組合索引的左側命中

一個問題:
為什么 MySQL 索引要使用 B+樹而不是其它樹形結構?
InnoDB一棵B+樹可以存放多少行數據?這個問題的簡單回答是:約2千萬。
InnoDB存儲引擎也有自己的最小儲存單元——頁(Page),一個頁的大小是16K。innodb的所有數據文件(后綴為ibd的文件),他的大小始終都是16384(16k)的整數倍。數據表中的數據都是存儲在頁中的,所以一個頁中能存儲多少行數據呢?假設一行數據的大小是1k,那么一個頁可以存放16行這樣的數據。
因為B樹不管葉子節點還是非葉子節點,都會保存數據,這樣導致在非葉子節點中能保存的指針數量變少(有些資料也稱為扇出)。指針少的情況下要保存大量數據,只能增加樹的高度,導致IO操作變多,查詢性能變低;
一個二叉查找樹是由n個節點隨機構成,所以,對於某些情況,二叉查找樹會退化成一個有n個節點的線性鏈。和順序查找差不多。顯然這個二叉樹的查詢效率就很低,因此若想最大性能的構造一個二叉查找樹,需要這個二叉樹是平衡的(這里的平衡從一個顯著的特點可以看出這一棵樹的高度比上一個輸的高度要大,在相同節點的情況下也就是不平衡),從而引出了一個新的定義-平衡二叉樹AVL。AVL樹是帶有平衡條件的二叉查找樹,一般是用平衡因子差值判斷是否平衡並通過旋轉來實現平衡,左右子樹樹高不超過1,和紅黑樹相比,它是嚴格的平衡二叉樹,平衡條件必須滿足(所有節點的左右子樹高度差不超過1)。不管我們是執行插入還是刪除操作,只要不滿足上面的條件,就要通過旋轉來保持平衡,而旋轉是非常耗時的,由此我們可以知道AVL樹適合用於插入刪除次數比較少,但查找多的情況。
1、 B+樹的磁盤讀寫代價更低:B+樹的內部節點並沒有指向關鍵字具體信息的指針,因此其內部節點相對B樹更小,如果把所有同一內部節點的關鍵字存放在同一盤塊中,那么盤塊所能容納的關鍵字數量也越多,一次性讀入內存的需要查找的關鍵字也就越多,相對IO讀寫次數就降低了。
2、B+樹的查詢效率更加穩定:由於非終結點並不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。
3、由於B+樹的數據都存儲在葉子結點中,分支結點均為索引,方便掃庫,只需要掃一遍葉子結點即可,但是B樹因為其分支結點同樣存儲着數據,我們要找到具體的數據,需要進行一次中序遍歷按序來掃,所以B+樹更加適合在區間查詢的情況,所以通常B+樹用於數據庫索引。
一些聯合索引的實戰
實戰
OK,懂上面的基礎,我們就可以開始扯了~我舉了經典的五大題型,看完基本就懂!
題型一
如果sql為
SELECT * FROM table WHERE a = 1 and b = 2 and c = 3;
如何建立索引?
如果此題回答為對(a,b,c)建立索引,那都可以回去等通知了。
此題正確答法是,(a,b,c)或者(c,b,a)或者(b,a,c)都可以,重點要的是將區分度高的字段放在前面,區分度低的字段放后面。像性別、狀態這種字段區分度就很低,我們一般放后面。
例如假設區分度由大到小為b,a,c。那么我們就對(b,a,c)建立索引。在執行sql的時候,優化器會 幫我們調整where后a,b,c的順序,讓我們用上索引。
題型二
如果sql為
SELECT * FROM table WHERE a > 1 and b = 2;
如何建立索引?
如果此題回答為對(a,b)建立索引,那都可以回去等通知了。
此題正確答法是,對(b,a)建立索引。如果你建立的是(a,b)索引,那么只有a字段能用得上索引,畢竟最左匹配原則遇到范圍查詢就停止匹配。
如果對(b,a)建立索引那么兩個字段都能用上,優化器會幫我們調整where后a,b的順序,讓我們用上索引。
題型三
如果sql為
SELECT * FROM `table` WHERE a > 1 and b = 2 and c > 3;
如何建立索引?
此題回答也是不一定,(b,a)或者(b,c)都可以,要結合具體情況具體分析。
拓展一下
SELECT * FROM `table` WHERE a = 1 and b = 2 and c > 3;
怎么建索引?嗯,大家一定都懂了!
題型四
SELECT * FROM `table` WHERE a = 1 ORDER BY b;
如何建立索引?
這還需要想?一看就是對(a,b)建索引,當a = 1的時候,b相對有序,可以避免再次排序!
那么
SELECT * FROM `table` WHERE a > 1 ORDER BY b;
如何建立索引?
對(a)建立索引,因為a的值是一個范圍,這個范圍內b值是無序的,沒有必要對(a,b)建立索引。
拓展一下
SELECT * FROM `table` WHERE a = 1 AND b = 2 AND c > 3 ORDER BY c;
怎么建索引?
題型五
SELECT * FROM `table` WHERE a IN (1,2,3) and b > 1;
如何建立索引?
還是對(a,b)建立索引,因為IN在這里可以視為等值引用,不會中止索引匹配,所以還是(a,b)!
拓展一下
SELECT * FROM `table` WHERE a = 1 AND b IN (1,2,3) AND c > 3 ORDER BY c;
如何建立索引?此時c排序是用不到索引的。
行鎖與表鎖
1、只根據主鍵進行查詢,並且查詢到數據,主鍵字段產生行鎖。 begin; select * from goods where id = 1 for update; commit; 2、只根據主鍵進行查詢,沒有查詢到數據,不產生鎖。 begin; select * from goods where id = 1 for update; commit; 3、根據主鍵、非主鍵含索引(name)進行查詢,並且查詢到數據,主鍵字段產生行鎖,name字段產生行鎖。 begin; select * from goods where id = 1 and name='prod11' for update; commit; 4、根據主鍵、非主鍵含索引(name)進行查詢,沒有查詢到數據,不產生鎖。 begin; select * from goods where id = 1 and name='prod12' for update; commit; 5、根據主鍵、非主鍵不含索引(name)進行查詢,並且查詢到數據,如果其他線程按主鍵字段進行再次查詢,則主鍵字段產生行鎖,如果其他線程按非主鍵不含索引字段進行查詢,則非主鍵不含索引字段產生表鎖,如果其他線程按非主鍵含索引字段進行查詢,則非主鍵含索引字段產生行鎖,如果索引值是枚舉類型,mysql也會進行表鎖,這段話有點拗口,大家仔細理解一下。 begin; select * from goods where id = 1 and name='prod11' for update; commit; 6、根據主鍵、非主鍵不含索引(name)進行查詢,沒有查詢到數據,不產生鎖。 begin; select * from goods where id = 1 and name='prod12' for update; commit; 7、根據非主鍵含索引(name)進行查詢,並且查詢到數據,name字段產生行鎖。 begin; select * from goods where name='prod11' for update; commit; 8、根據非主鍵含索引(name)進行查詢,沒有查詢到數據,不產生鎖。 begin; select * from goods where name='prod11' for update; commit; 9、根據非主鍵不含索引(name)進行查詢,並且查詢到數據,name字段產生表鎖。 begin; select * from goods where name='prod11' for update; commit; 10、根據非主鍵不含索引(name)進行查詢,沒有查詢到數據,name字段產生表鎖。 begin; select * from goods where name='prod11' for update; commit; 11、只根據主鍵進行查詢,查詢條件為不等於,並且查詢到數據,主鍵字段產生表鎖。 begin; select * from goods where id <> 1 for update; commit; 12、只根據主鍵進行查詢,查詢條件為不等於,沒有查詢到數據,主鍵字段產生表鎖。 begin; select * from goods where id <> 1 for update; commit; 13、只根據主鍵進行查詢,查詢條件為 like,並且查詢到數據,主鍵字段產生表鎖。 begin; select * from goods where id like '1' for update; commit; 14、只根據主鍵進行查詢,查詢條件為 like,沒有查詢到數據,主鍵字段產生表鎖。 begin; select * from goods where id like '1' for update; commit;
解決數據一致性的悲觀鎖和樂觀鎖
悲觀鎖方案:
每次獲取商品時,對該商品加排他鎖。也就是在用戶A獲取獲取 id=1 的商品信息時對該行記錄加鎖,期間其他用戶阻塞等待訪問該記錄。悲觀鎖適合寫入頻繁的場景。
begin; select * from goods where id = 1 for update; update goods set stock = stock - 1 where id = 1; commit;
樂觀鎖方案:每次獲取商品時,不對該商品加鎖。在更新數據的時候需要比較程序中的庫存量與數據庫中的庫存量是否相等,如果相等則進行更新,反之程序重新獲取庫存量,再次進行比較,直到兩個庫存量的數值相等才進行數據更新。樂觀鎖適合讀取頻繁的場景。
#不加鎖獲取 id=1 的商品對象 select * from goods where id = 1 begin; #更新 stock 值,這里需要注意 where 條件 “stock = cur_stock”,只有程序中獲取到的庫存量與數據庫中的庫存量相等才執行更新 update goods set stock = stock - 1 where id = 1 and stock = cur_stock; commit;