吐血整理和分享 MySql索引原理與優化

索引的原理

筆者認爲索引意義主要是爲了提升性能。
一方面通過高效的算法,提高性能。性能的關鍵是查詢數據的時間複雜度。減少時間複雜度是提高性能的關鍵。
一方面通過減少I/O延遲,減少時間消耗。索引本身很大,往往以文件的形式存儲的磁盤上。而減少硬盤I/O的次數成爲了優化的關鍵。

索引的算法

把數據進行排序,然後使用二分查找法查找。
爲什麼要排序呢?
舉件例子,小時玩猜數字的遊戲,小紅在1-100裏挑個數字,寫下來。然後小明猜寫下來的是什麼數字。
**A方案:**小明隨便報個數字,然後小紅告訴小明 正確或者不正確。
**B方案:**小明隨便報個數字,然後小紅告訴小明 正確或者不正確,並告訴比猜的數字大了,還是小了。
很明顯,B方案更容易猜中數字。因爲每次猜之後可以得到更加明確的方向,每次猜之後離目標更接近。

數庫據使用的索引就是二分查找法,思維方法累似於B方案。那爲索引爲什麼依賴排序呢?下面摸擬一下1~100兩組數據,分別是有序與無序。
A組:
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100
B組:
85,17,29,65,5,49,23,54,21,36,51,41,25,37,93,4,88,45,59,30,12,75,26,24,66,63,20,15,77,14,79,1,47,60,39,57,61,81,8,28,53,50,91,94,73,44,71,27,34,40,58,33,48,19,38,35,22,100,78,10,68,72,56,89,80,92,90,43,7,46,95,70,52,69,16,87,9,82,99,42,74,18,67,96,11,32,6,98,64,3,62,76,97,83,2,13,84,55,31,86

模擬查找數字85的位置。
A組數據,可以使用二分查找法,步驟如以下

1)在1~100取中間值,找到50,得出比50大
2)在50~50取中間值 ,找到75,得出比75大
3)在75~100取中間值,找到87,得出比87小
4)在75~87取中間值,找到81,得出比81大
5)在81~87取中間值,找到84,得出比84大
5)在84~87取中間值,找到85,剛好等於85,返回位置。

B組數據沒有排序,只能一個一個順序去找

1)取第1個,85與86不相等,繼續找
2)取第2個,17與86不相等,繼續找
省略96次
100)取第100個,86與86相等,返回地址

很明顯了,排序的數據利可二分查找法,比無序的數據效率高得多。

二分查找法的特點:數據越是大,效率提升的更明顯。
下面提供一個二分查找法的小程序,根據程序得出結果。

public static void main(String[] args) {
    Integer num=100000000;
    Integer ac=0;
    while (num>1)
    {
        ac++;
        System.out.print("第"+ac+"次:" +(num)+"/2=");
        num=num/2;
        System.out.println(num);
    }
}

1百萬個數字,只需要19次
1千萬個數字,只需要23次
1億個數字,只需要26次

二分查找法的效率是數據量越多效率越高。

結構的選型

mysql索引的結構用的是b+樹。那爲什麼有那麼多種數據結構,偏愛b+樹的。
索引 結構選形的要求包括:
1:硬盤I/O次數少。
2:支持排序。
3:插入,刪除高效。

接下來我們對數據結構一項一項分析。

鏈表
1:沒有排序,不合適
2:存放在內存,不合適

二叉樹
二叉樹解決了排序的問題,節點的左子樹小於節點,右子樹大於節點。
但是,在極端的情況下,它實際上就是一個鏈表,如下圖所示。這樣樹的深度將會非常深,不利於I/O尋址。
在這裏插入圖片描述
AVL樹(平衡二叉樹)
AVL樹就是爲解決了二叉樹的深度問題的,平衡樹歸定:最深的葉節點的高度(A),最淺的葉節點的高度(B),A-B <=1或者說A-B<2。
那麼是怎麼做的呢?在插入/刪除的時候,有違反A-B<=2的時候,需要對樹的結構進行調整。如下圖所示。
在這裏插入圖片描述
從上圖可以看出,樹的結構因爲插入動作頻繁地變動,每一次的插入都會引起節點多次調換位置,插入的性能然受到影響。而且是成指數增長。AVL樹不合適

紅黑樹
紅黑是AVL樹的改良版,兼顧了查詢的性能與插入的性能。
紅黑樹算法:最深的葉節點的高度(A),最淺的葉節點的高度(B),A-2B<0 或者A<2B 。相對AVL樹,放寬了葉子節點高度差的限制,這樣就就可以減少子插入的動作,如下圖所示。

在這裏插入圖片描述
紅黑樹解決了查詢與操作的問題,那I/O問題呢?
下面我們來算樹的深度。按最理想滿二叉樹來統計節點與深度的關係。公式: 節點數(m),深度(n) , m= 2^(n-1)

節點數 深度
2 2
4 3
16 4
32 5
64 6
64 7
128 8
256 9
512 10
1024 11
1,000,000 19
10,000,000 23
100,000,000 26

從上表得出結論,千量級的數據量時就需要IO尋址11次。過多的IO導致耗時太多,需要尋找更好的方案,解決I/O問題。

#b 樹
b 樹是是多叉的多路平衡查找樹,結構如下圖所示。
在這裏插入圖片描述
它的每個節點最多包含m個孩子,m稱爲b樹的階,m的大小取決於磁盤頁的大小(一般是4kb)。
因爲是多叉樹,每個節點最多能有1000個頁子節點(int佔4個字節算,不考慮其它開銷)。我們來整理一下節點與深度的關係整理。公式: 節點數(m),深度(n) , m= 1000^(n-1)

節點數 深度
1000 2
1,000,000 3
1,000,000,000 4

從上面表格可以看出,深度4的b樹就能滿足千萬數據量了。
假如,存的數據不是一個int。而是一條長達4kb的學生信息,而包括 學生號,姓名,性別,住址,電話,頭像。那麼一個節點也只能存儲一條件記,又回到二叉樹的深度了。。所以還需要更好的方案。

b+樹
b+樹,是b樹的一種變體。內節點不存儲data,只存儲key;葉子節點不存儲指針。如下圖所示。
記錄的大小對樹的結構是沒有影響的,所以b+樹更適合做索引的結構。
在這裏插入圖片描述

索引的的分類

聚簇索引

聚簇索引又稱主鍵,主鍵就是這棵樹的排序信息。
注意這棵樹的子節點存儲這是整條記錄的信息。

查詢過程

select id,user_name from t_user where id=10
1:根據主鍵id使用聚簇索引查到定位到聚簇索引根結點。
2:拿出data。
在這裏插入圖片描述

非聚簇索引

普通索引,結構也用的b+樹,排序的字段是索引字段。
樹的子節點存儲的是主鍵信息。
在這裏插入圖片描述

查詢過程

select id,user_name from t_user where id=name=‘Richy’
1:根據name對應的索引,查詢到名稱=Richy的記錄對應的根據點位置。
2:查出Data,這裏的data是主鍵id。
3: 再使用聚簇索引查到對應的數據。
相比聚簇索引的查詢,增加了一個再去查聚簇索引的步驟。叫回表

組合索引

組合索引多字段是有序的,並且是個完整的BTree 索引,查詢時有最左匹配原則。
在這裏插入圖片描述

查詢過程

select user_code,user_name from t_user where user_code and user_name=‘Richy’
1:根據最左匹配原則,首匹配user_code 字段,再匹配user_name字段。
2:判斷字段信息是否足夠。本例子select 的字段剛好在索引裏面,直接返回結果,稱爲覆蓋索引
3:如果不是覆蓋索引,再通過回表的方式查詢主鍵引。

索引使用的規則

我們以mysql提供 sakila 庫做例子。以下的演示都使用此庫。
在這裏插入圖片描述

全匹配

使用"="可以匹配到索引, 例如:

explain select city from city where city ='a';

在這裏插入圖片描述

匹配列前綴

使用"="可以匹配到索引, 例如:

explain select city from city where city like'a%';

在這裏插入圖片描述

匹配範圍值

使用"<“或者”>"可以匹配到索引,例如

explain select city from city where city>'a';

在這裏插入圖片描述

謂語下推

mysql智能根據條件去匹配索引,假如第一條件沒有可用索引,它將繼續去匹配下一個條件。我們用一個例子示範一下。從輸出的結果可以看出:使用的索引是第二個字段的。

explain select 1 from city where last_update >'2020-03-31' and city='aa';

在這裏插入圖片描述

最左匹配

最左優先主要是針對 組合索引匹配的情況,以最左邊的爲起點任何連續的索引都能匹配上。同時遇到範圍查詢(>、<、between、like)就會停止匹配。例如:b = 2 如果建立(a,b)順序的索引,是匹配不到(a,b)索引的;但是如果查詢條件是a = 1 and b = 2或者a=1(又或者是b = 2 and b = 1)就可以,因爲優化器會自動調整a,b的順序。再比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)順序的索引,d是用不到索引的,因爲c字段是一個範圍查詢,它之後的字段會停止匹配。

我們做個示範,先創建個組合索引,如下圖所示。
在這裏插入圖片描述
第一次查詢,匹配第一個字段。從輸出結果可以看到ken_len的長度是2

explain select 1 from address where city_id='a';

在這裏插入圖片描述
第二次查詢,匹配第一,第二個字段。從輸出結果可以看到ken_len的長度是154。跟第一次查詢相比長度多了,說明第二個字段命中了索引匹配。

explain select 1 from address where city_id='a' and address='a';

在這裏插入圖片描述
第三次查詢 ,匹配第一,第二,第三個字段。從輸出結果可以看到ken_len的長度是307。跟第二次查詢相比長度多了,說明第三個字段命中了索引匹配。

explain select 1 from address where city_id='a' and address='a' and address2='a';

在這裏插入圖片描述
第四次查詢 ,匹配第一,第二個字段使用範圍查找,第三字段使用全部匹配。從輸出結果可以看到ken_len的長度是154。跟第二次查詢的結果一樣,說明第三個字段是沒有命中的,最左匹配的規則生效了。

explain select 1 from address where city_id='a' and address=>'a' and address2='a';

在這裏插入圖片描述

索引優化

一、一個表儘量不超過5個索引。

1:每個索引都是獨立的b+樹,索引的建立需要佔用大量的存儲空間。
2:數據的增加會導致b+樹分裂,刪除將導致 b+樹的合併,過多的索引將影響表的性能。

二、一個組合索引的字段儘量不要超過5個。

索引的字段的信息是存在於樹的中間節點,過多的組合字段將減少一個節點可以存儲的信息。數據增加/刪除,將會導致頻繁的頁裂變/合併。影響插入/刪除的性能。

三、聚簇索引與 非普通索引同時可選時,儘量使用聚簇索引。

使用聚簇索引不需要回表操作。查詢性能更高效。

四、儘量覆蓋索引

覆蓋索引是指是select 出來的字段在索引的範圍內。
舉個例子
組合索引字段:<a,b,c>,
使用: select a,b,c from table
因爲a,b,c都是在索引裏面。查詢的操作只需要在索引拿到數據就滿足要求,無需回表查詢

五、儘量少用 like ‘%aaa%’ 和 like ‘%aaa’ ;

使用這like ‘%aaa%’ 匹配不到索引,只能全表掃描。

explain select * from address where city_id like '%1%'

在這裏插入圖片描述

六、慎用 條件字段加計算

對比一下以下兩個語句執行的效果,可以看出第二個語句是沒有使用到索引的。

explain select amount from payment where amount>1;
explain select amount from payment where amount+1>1;

在這裏插入圖片描述
在這裏插入圖片描述
如果需要運算,可以調整成這樣子,達到同等效果,但卻能用到索引。

explain select amount from payment where amount>1-1;

在這裏插入圖片描述

七、索引需要建立在差異化的字段上

拿學生表來舉個例子
什麼間差異化大?比如學生編號,基本上每一個學生一個號,差異化大。
什麼是差異化小?比如學生性別,就只有“男”跟“女”兩個,存100W數據也就2種。

八、用有序的字段做主鍵比用無序性能更高

有序的主鍵,可以減少求插入時排序的消耗,更避免了插入時的頁分裂。

九、避免在查詢時做數據轉換

對比一下以下兩個語句執行的輸出,可以看出第二個語句是沒有使用到索引的。

explain select to_days(payment_date) From payment where payment_date='732456';
explain select to_days(payment_date) From payment where to_days(payment_date)='732456';

在這裏插入圖片描述

十、關聯查詢,字段類型不一致不會使用索引

舉個例子,select a.name,a.address from a inner join b on a.name=b.name 。
假如說a表的name是char ,而b表的name是varchar,那麼查詢是不會使用索引的。
所以建表的時候需要注意,字段類型需要有統一的標準。

十一、設置合適的索引長度

索引字段的信息是存在b+樹節點上的,過長的長度會導致頻繁的頁分裂。同時索引要求字段信息差異化(上面有講到)。
假如一個長度1000的字段,取前50位就能做到差異化,那麼可以就建一個長度50的索引,如下。

alter table address add index idx_address_address (address(50) asc) ;

本人能力有限,如果錯誤請指出,不勝感激。
寫作不易,請點贊鼓勵

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章