吐血整理和分享 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) ;

本人能力有限,如果错误请指出,不胜感激。
写作不易,请点赞鼓励

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