前面咱們講了SQL分析和索引優化都涉及到了索引,那麼什麼是索引,它的模型有什麼,實現的機制是什麼,今天咱們來好好討論下。html
索引就至關書的目錄,好比一本500頁的書,若是你想快速找到其中的某一個知識點,在不借助目錄的狀況下,你得一點點慢慢的找,要找好一下子。一樣,對於數據庫的表,而言,索引就是它的「目錄」,提升了數據查詢的效率。算法
好比要運行下面的查詢:sql
select first_name from actor where actor_id=5;
若是在actor_id列上建有索引,則MySQL將使用該索引找到actor_id爲5的行,也就是說,MySQL先在索引上按值查找,而後返回全部包含該值的數據行。數據庫
哈希索引是一種以鍵-值(key-value)存儲數據的結構,只有精確匹配索引全部列的查詢纔有效。對於每一行數據,存儲引擎都會對全部的索引列計算一個哈希碼,哈希碼是一個較小的值,而且不一樣鍵值的行計算出來的哈希碼也不同。哈希索引將全部的哈希碼存儲在索引中,同時在哈希表中保存指向每一個數據行的指針。即咱們只要輸入待查找的值即key,就能夠找到其對應的值即value。數組
哈希的思路很簡單,把值放在數組裏,用一個哈希函數把key換算成一個肯定的位置,而後把value放在數組的這個位置。數據結構
固然不可避免地,多個key值通過哈希函數的換算,會出現同一個值的狀況。處理這種狀況的一種方法是,拉出一個鏈,即索引會以鏈表的方式存放多個記錄指針到同一個哈希條目中。函數
在MySQL中,只有Memory引擎顯式支持哈希索引,也是 Memory引擎表的默認索引類型。性能
下面來看一個例子:優化
create table testhash( fname varchar(50) not null, lname varchar(50) not null, key using HASH(fname) )ENGINE=MEMORY;
表中包含以下數據:ui
假設索引使用假想的哈希函數f(),它返回下面的值:
則哈希索引的數據結構以下:
注意每一個槽的編號是順序的,可是數據行不是。假如咱們進行下面的查詢:
select lname from testhash where fname='Peter';
MySQL先計算'Peter'的哈希值,並使用該值尋找對於的記錄指針。由於f('Peter')=8784,因此MySQL在索引中查找8784,能夠找到指向第三行的指針,最後一步是比較第三行的值是否爲'Peter',以確保就是要查找的行。
由於索引自身只需存儲對應的哈希值,因此索引的結構十分緊湊,因此哈希索引的查找很快。但它仍是有下面的缺點:
InnoDB引擎有一個特殊的功能叫「自適應哈希索引」,當InnoDB注意到某些索引列被使用得很是頻繁時,它會在內存中基於B-Tree索引之上再創建一個哈希索引,這樣就讓B-Tree索引也具備哈希索引的一些優勢,好比快速的哈希查找。這是一個徹底自動的、內部的行爲,用戶沒法控制或配置,不過若是有必要,徹底能夠關閉該功能。
有序數組索引在等值查詢和範圍查詢場景中的性能都很是優秀。好比咱們維護一個身份證信息和姓名的表,根據身份證號查找名字,咱們使用有序數組來實現的話,示意圖以下:
這裏咱們假設身份證號沒有重複,這個數組就是按照身份證號遞增的順序保存的。這個時候若是你要查ID_card_n2對應的名字,用二分法就能夠快速獲得,這個時間複雜度是O(log(N))。
同時很顯然,這個索引結構支持範圍查詢。若是你要查身份證號在[ID_card_X,ID_card_Y]區間的User,能夠先用二分法找到ID_card_X(若是不存在ID_card_X,就找到大於ID_card_X的第一個User),而後向右遍歷,直到找到第一個大於ID_card_Y的身份證號,退出循環。
若是僅僅看查詢效率,有序數組就是最好的數據結構了,可是,對於更新數據時就很麻煩了,你往中間插入一個記錄就必須挪動後面的全部記錄,成本過高了。因此有序數組索引只適用於靜態存儲引擎。
根據上面身份證號查名字的例子,使用二叉搜索樹來實現的示意圖:
二叉搜索樹的特色是:每一個節點的左兒子小於父節點,父節點又小於右節點。這樣若是你要查ID_card_n2的話,按照圖中的搜索順序就是按照UserA->UserC->UserF->User2這個路徑獲得。這個時間複雜度是O(log(N))。固然爲了維持O(log(N))的查詢複雜度,你就須要保持這棵樹是平衡二叉樹。爲了作這個保證,更新的時間複雜度也是O(log(N))。
樹能夠有二叉,也能夠有多叉。多叉樹就是每一個節點有多個兒子,兒子之間的大小保證從左到右遞增。二叉樹是搜索效率最高的,可是實際上大多數的數據庫存儲卻並不使用二叉樹。其緣由是,索引不止存在內存中,還要寫到磁盤上。
你能夠想象一下一棵 100 萬節點的平衡二叉樹,樹高 20。一次查詢可能須要訪問 20 個數據塊。在機械硬盤時代,從磁盤隨機讀一個數據塊須要 10 ms 左右的尋址時間。也就是說,對於一個 100 萬行的表,若是使用二叉樹來存儲,單獨訪問一個行可能須要 20 個 10 ms 的時間,這個查詢可真夠慢的。
爲了讓一個查詢儘可能少地讀磁盤,就必須讓查詢過程訪問儘可能少的數據塊。那麼,咱們就不該該使用二叉樹,而是要使用「N 叉」樹。這裏,「N 叉」樹中的「N」取決於數據塊的大小。
以 InnoDB 的一個整數字段索引爲例,這個 N 差很少是 1200。這棵樹高是 4 的時候,就能夠存 1200 的 3 次方個值,這已經 17 億了。考慮到樹根的數據塊老是在內存中的,一個 10 億行的表上一個整數字段的索引,查找一個值最多隻須要訪問 3 次磁盤。其實,樹的第二層也有很大機率在內存中,那麼訪問磁盤的平均次數就更少了。
N 叉樹因爲在讀寫上的性能優勢,以及適配磁盤的訪問模式,已經被普遍應用在數據庫引擎中了。
在InnoDB中使用的是B+ 樹索引,數據都是存儲在 B+ 樹中的。表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱爲索引組織表。
每個索引在 InnoDB 裏面對應一棵 B+ 樹。
假設,咱們有一個主鍵列爲 ID 的表,表中有字段 k,而且在 k 上有索引。下面是表建立語句:
create table T( id int primary key, k int not null, name varchar(16), index (k) )engine=InnoDB;
表中 R1~R5 的 (ID,k) 值分別爲 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),兩棵樹的示例示意圖以下:
從圖中不難看出,根據葉子節點的內容,索引類型分爲主鍵索引和非主鍵索引。
主鍵索引的葉子節點存的是整行數據,在 InnoDB 裏,主鍵索引也被稱爲聚簇索引(clustered index)。
非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB 裏,非主鍵索引也被稱爲二級索引(secondary index)。
根據上面的索引結構說明,咱們來討論一個問題:基於主鍵索引和普通索引的查詢有什麼區別?
也就是說,基於非主鍵索引的查詢須要多掃描一棵索引樹。所以,咱們在應用中應該儘可能使用主鍵查詢。
那麼額外討論下一個問題:在下面這個表 T 中,若是我執行 select * from T where k between 3 and 5,須要執行幾回樹的搜索操做,會掃描多少行?
如今,咱們一塊兒來看看這條SQL查詢語句的執行流程:
在這個過程當中,回到主鍵索引樹搜索的過程爲回表。能夠看到,這個查詢過程讀了k索引樹的3條記錄(步驟一、3和5),回表了兩次(步驟2和4)。
在這個例子中,因爲查詢結果所須要的數據只在主鍵索引上有,因此不得不回表。【能夠經過索引優化策略來避免回表,好比覆蓋索引、聚簇索引等】
B+ 樹爲了維護索引有序性,在插入新值的時候須要作必要的維護。以上面這個圖爲例,若是插入新的行 ID 值爲 700,則只須要在 R5 的記錄後面插入一個新記錄。若是新插入的 ID 值爲 400,就相對麻煩了,須要邏輯上挪動後面的數據,空出位置。
而更糟的狀況是,若是 R5 所在的數據頁已經滿了,根據 B+ 樹的算法,這時候須要申請一個新的數據頁,而後挪動部分數據過去。這個過程稱爲頁分裂。
在這種狀況下,性能天然會受影響。除了性能外,頁分裂操做還影響數據頁的利用率。本來放在一個頁的數據,如今分到兩個頁中,總體空間利用率下降大約 50%。
固然有分裂就有合併。當相鄰兩個頁因爲刪除了數據,利用率很低以後,會將數據頁作合併。合併的過程,能夠認爲是分裂過程的逆過程。
基於上面的索引維護過程說明,咱們來討論一個案例:
你可能在一些建表規範裏面見到過相似的描述,要求建表語句裏必定要有自增主鍵。固然事無絕對,咱們來分析一下哪些場景下應該使用自增主鍵,而哪些場景下不該該。
自增主鍵是指自增列上定義的主鍵,在建表語句中通常是這麼定義的: NOT NULL PRIMARY KEY AUTO_INCREMENT。
插入新記錄的時候能夠不指定 ID 的值,系統會獲取當前 ID 最大值加 1 做爲下一條記錄的 ID 值。
也就是說,自增主鍵的插入數據模式,正符合了咱們前面提到的遞增插入的場景。每次插入一條新記錄,都是追加操做,都不涉及到挪動其餘記錄,也不會觸發葉子節點的分裂。
而有業務邏輯的字段作主鍵,則每每不容易保證有序插入,這樣寫數據成本相對較高。
除了考慮性能外,咱們還能夠從存儲空間的角度來看。假設你的表中確實有一個惟一字段,好比字符串類型的身份證號,那應該用身份證號作主鍵,仍是用自增字段作主鍵呢?
因爲每一個非主鍵索引的葉子節點上都是主鍵的值。若是用身份證號作主鍵,那麼每一個二級索引的葉子節點佔用約 20 個字節,而若是用整型作主鍵,則只要 4 個字節,若是是長整型(bigint)則是 8 個字節。
顯然,主鍵長度越小,普通索引的葉子節點就越小,普通索引佔用的空間也就越小。
因此,從性能和存儲空間方面考量,自增主鍵每每是更合理的選擇。
有沒有什麼場景適合用業務字段直接作主鍵的呢?仍是有的。好比,有些業務的場景需求是這樣的:
你必定看出來了,這就是典型的 KV 場景。
因爲沒有其餘索引,因此也就不用考慮其餘索引的葉子節點大小的問題。
這時候咱們就要優先考慮上一段提到的「儘可能使用主鍵查詢」原則,直接將這個索引設置爲主鍵,能夠避免每次查詢須要搜索兩棵樹。
MyISAM表支持空間索引,能夠用做地理數據存儲,該索引無須前綴查詢。空間索引會從全部維度來索引數據。查詢時,能夠有效地使用任意維度來組合查詢。必須使用MySQL的GIS相關函數如MBRCONTAINS()等來維護數據。
全文索引是一種特殊類型的索引,它查找的是文本中的關鍵詞,而不是直接比較索引中的值。全文索引適用於MATCH AGAINST操做,而不是普通的WHERE條件操做。
全文索引支持各類字符內容的搜索(包括char、varchar和text類型),也支持天然語言搜索和布爾搜索。
對於上面例子中的 InnoDB 表 T,若是你要重建索引 k,你的兩個 SQL 語句能夠這麼寫:
alter table T drop index k; alter table T add index(k);
若是你要重建主鍵索引,也能夠這麼寫:
alter table T drop primary key; alter table T add primary key(id);
對於上面這兩個重建索引的做法,有什麼不合適的,爲何,更好的方法是什麼?
答案:重建索引 k 的作法是合理的,能夠達到省空間的目的。可是,重建主鍵的過程不合理。不管是刪除主鍵仍是建立主鍵,都會將整個表重建。因此連着執行這兩個語句的話,第一個語句就白作了。這兩個語句,你能夠用這個語句代替 : alter table T engine=InnoDB。