一篇搞懂mysql中的索引(大白话版)
容易來說,索引的出現其實就是為了提升數據查詢的效率,就像書的目錄一樣。一本 500 頁的書,如果你想快速找到其中的某一個知識點,在不借助目錄的情況下,那我估計你可得找一會兒。同樣,對于數據庫的表而言,索引其實就是它的“目錄”。
索引模型
索引的出現是為了提升查詢效率,但是實現索引的方式卻有很多種,所以這里也就引入了索引模型的概念,一下是常見的三種:
哈希表
哈希表是一種以鍵值對的方式進行存儲的。只要輸入key就可以查找到對應的value,哈希思路很容易,就是放在一個有序數組中,通過一個可以計算出hash值的函數算出key指定一個位置,然后再將value放到對應的位置上。
不可避免的還有哈希沖突,也就是算出了同一個哈希值作為key,發生哈希沖突時數據就會延升出一個鏈表,這個鏈表會存儲value,value會有一個next屬性指向下一個value。
圖中User1和User4算出了兩個相同的哈希值,不過沒關系,后面跟了一個鏈表,假設,這時候你要查 ID_1對應的名字是什么,處理步驟就是:首先,將 ID_1 通過哈希函數算出 N;然后,按順序遍歷,找到 User1。
圖中的ID不是自增的,這樣做的好處是增加新的 User 時速度會很快,只需要往后追加。但缺點是,因為不是有序的,所以哈希索引做區間查詢的速度是很慢的。
你可以設想下,如果你現在要找ID在[ID_X, ID_Y]這個區間的所有用戶,就一定要全部掃描一遍了。所以,哈希表這種結構適用于只有等值查詢的場景。
有序數組
但是有序數組在等值查詢的時候就特別優秀,同樣是上面的例子。
這個數組就是按照ID號遞增的順序保存的。這時候如果你要查 ID3 對應的名字,用二分法就可以快速得到,這個時間復雜度是 O(log(N))。
這個索引結構支持范圍查詢。你要查ID號在[ID_X, ID_Y]區間的 User,可以先用二分法找到 ID_X(如果不存在 ID_X,就找到大于 ID_X 的第一個 User),然后向右遍歷,直到查到第一個大于 ID_Y 的ID號,退出循環。
如果僅僅看查詢效率,有序數組就是最好的數據結構了,但是需要更新時就很麻煩了,如果要往中間插入數據的話就要挪動后面所有的數據。
一般可以使用在靜態數據或者常年不怎么改變的數據進行存儲。
搜索樹
二叉搜索樹的特點是:父節點左子樹所有結點的值小于父節點的值,右子樹所有結點的值大于父節點的值。
這樣如果你要查 ID_4 的話,按照圖中的搜索順序就是按照 UserA -> UserC ->User2 這個路徑得到。這個時間復雜度是 O(log(N))。
二叉樹是搜索效率最高的,但是實際上大多數的數據庫存儲卻并不使用二叉樹。其原因是,索引不止存在內存中,還要寫到磁盤上。
如果樹節點很多,還很高的話,這個查詢效率是非常底的。為了提升效率就一定要盡量的規避IO。
InnoDB 的索引模型
在 InnoDB 中,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱為索引組織表。InnoDB 使用了 B+ 樹索引模型,所以數據都是存儲在 B+ 樹中的。每一個索引在 InnoDB 里面對應一棵 B+ 樹。
假設,我們有一個主鍵列為 ID 的表,表中有字段 k,并且在 k 上有索引。
mysql> create table T(id int primary key, k int not null, name varchar(16),index (k))engine=InnoDB;然后假設表中有五行數據:(id,k) 值分別為 (100,1)、(200,2)、(300,3)、(400,4) 和 (500,5)。
兩棵樹的圖:
兩棵樹分為兩個索引:主鍵索引和非主鍵索引。
- 主鍵索引的葉子節點存儲的是整行的數據。(聚簇索引)
- 非主鍵索引的葉子節點內容是主鍵的值。(二級索引)
主鍵索引和非主鍵索引的區別
- 如果語句是 select * from T where ID=500,即主鍵查詢方式,則只需要搜索 ID 這棵 B+ 樹;
- 如果語句是 select * from T where k=5,即普通索引查詢方式,則需要先搜索 k 索引樹,得到 ID 的值為 500,再到 ID 索引樹搜索一次。這個過程稱為回表。
普通索引比主鍵索引多出了一次查詢,所有我們要盡可能的使用自增主鍵。
維護索引
如果需要在id500后面新增id600,就直接在后面插入,如果說我要中間的位置插入數據的話成本又會顯的很高。需要邏輯上挪動后面的數據,空出位置。
如果 R5 所在的數據頁已經滿了,根據 B+ 樹的算法,這時候需要申請一個新的數據頁,然后挪動部分數據過去。這個過程稱為頁分裂。在這種情況下,性能自然會受影響。
頁分裂操作還影響數據頁的利用率。原本放在一個頁的數據,現在分到兩個頁中,整體空間利用率降低大約 50%,也就是會挪動50%到新的數據頁中。
比如我R3,R4,R5,R6挪動50%,到新的數據頁里(新的數據頁中包含R7因為插入時候不夠了所以分頁了呀),R3,R4,R5,R6、R7(這是挪過來的數據)。
當然有分裂就有合并。
怎么選擇索引,使用自增還是普通的業務索引?
自增主鍵的插入數據模式,正符合了我們前面提到的遞增插入的場景。每次插入一條新記錄,都是追加操作,都不涉及到挪動其他記錄,也不會觸發葉子節點的分裂。
而有業務邏輯的字段做主鍵,則往往不容易保證有序插入,這樣寫數據成本相對較高。
還有一方面就是存儲空間的考慮:我們上面也說了普通索引的葉子節點存的主鍵索引的ID。假設身份證這個唯一的字符串做主鍵索引的話,其他的普通索引的葉子節點就會存儲主鍵索引的值,身份證一般18個子符,可以想象多占空間,如果用整型就是4字節。
主鍵長度越小,普通索引的葉子節點就越小,普通索引占用的空間也就越小
什么場景適合用業務字段直接做主鍵的呢?
- 只有一個索引;
- 該索引一定要是唯一索引。
典型的 KV 場景。
索引覆蓋
就是主鍵索引和非主鍵索引之間,非主鍵索引總是需要回表。這方面就可以采用索引覆蓋去優化,覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。
這條語句需要執行幾次樹的搜索操作,會掃描多少行?
可以看到這個過程非常的繁瑣,來會進行回表的操作,因為根據k查詢的結果都在主鍵中,所以不得不回表取數據。
如果這個語句只需要查詢ID的值,而ID的值也已經在k索引上了,因此可以直接提供查詢結果,不需要回表了,其實就是這個K已經覆蓋了我們需要的查詢結果,被稱為覆蓋索引。
聯合索引
舉個栗子:就是如果我有一個id和身份證號加姓名這幾個字段,這個時候我要根據身份證號去查詢姓名的話,我需不需要把身份證號和姓名建立聯合索引(多個索引合起來作為一個聯合索引,如(card,name,size>1單值索引是聯合索引size=1的特例)?
答案是肯定的,建立聯合索引后,查詢身份證號的時候就不用去回表去主鍵索引拿到整行數據再找到姓名這個字段了,而是直接將姓名這個字段結果返回了。(因為聯合索引存的是聯合字段+主鍵值)。
其實索引高多了成本也是很高的。
最左前綴
接上個栗子:給姓名和身份證號建立聯合索引(name,card_id)可以看的我是順序排列的,從左往右。
使用語句 like “name%” 這個索引就會生效:
- 顧名思義:最左優先,以最左邊的為起點任何連續的索引都能匹配上。同時遇到范圍查詢(>、<、between、like)就會停止匹配(like “name%”會生效,like “%name”不會生效)。
- 例如:b = 2 如果建立(a,b)順序的索引,是匹配不到(a,b)索引的;但是如果查詢條件是a = 1 and b = 2-或者a=1(又或者是a = 2 and b = 1)就可以,因為優化器會自動調整a,b的順序。
- 再比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)順序的索引,d是用不到索引的,因為c字段是一個范圍查詢,它之后的字段會停止匹配。
索引下垂
還是用(name,card_id)這個例子,這兩個是個聯合索引,查詢語句 like “name%” 就可以獲取到數據了,但是如果我加了一個條件,語句變成了 like “name%” and card_id=2;
再遍歷這兩個索引的時候先對索引列進行判斷,直接過濾掉不滿足條件的記錄,減少回表次數。
小結
如果有了(a,b)兩個聯合索引后就不需要去單獨維護一個a的索引了。
如果有一個基于a、b給自的查詢語句,比如條件里面只有b的話(a,b)索引就不會生效,這時候就不得不單獨維護一個單獨的索引了。
建立聯合索引可以有效的避免回表次數。比如(a,b)兩個索引,這和時候通過a去查詢到b的值的時候就可以直接獲取到b的數據而不用回表去查詢了。(聯合索引包含了字段值+主鍵id)
覆蓋索引是一種優化索引,可以減少回表,就是當我這個索引包含了要查詢的字段的值就可以不用回表查詢而是直接返回。
總結
以上是生活随笔為你收集整理的一篇搞懂mysql中的索引(大白话版)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为最快动脉线诊断 铁科院联合第四范式完成
- 下一篇: redis单线程原理___Redis为何