4 索引

本文介绍了数据库索引的常见模型,包括哈希表、有序数组和搜索树,重点讲解了InnoDB存储引擎中B+树索引模型,以及其在主键索引和非主键索引的应用。此外,文章还探讨了索引的维护,如页分裂和合并,以及何时应该使用自增主键。最后,文章讨论了如何有效使用索引,如覆盖索引、最左前缀原则和索引下推优化,以及创建索引的时机和注意事项。
摘要由CSDN通过智能技术生成

来源:

《MySQL实战45讲》

为什么使用B+树:https://mp.weixin.qq.com/s/Mwh5T5wQNLrxORLpNvIZoA

页的概念:https://segmentfault.com/a/1190000008545713

sql语句执行顺序:https://www.cnblogs.com/yyjie/p/7788428.html

1 索引模型

索引是数据库表的“目录”。

常见的三种索引模型:哈希表、有序数组和搜索树。

1.1 哈希表

哈希表是⼀种以键-值(key-value)存储数据的结构,输⼊待查找的值,对应key,就可以找到其对应的值,对应Value。

存储的思路:把值放在数组里,用⼀个哈希函数把key换算成⼀个确定的位置,然后把value放在数组的这个位置即可

使用链地址法来解决哈希冲突,如下图:
在这里插入图片描述
User2和User4根据身份证号算出来的值都是N,所以还跟了⼀个链表。
假设,这时候要查ID_card_n2对应的名字是什么,处理步骤就是:
⾸先,将ID_card_n2通过哈希函数算出N;
然后,按顺序遍历,找到User2。

图中四个ID_card_n的值并不是递增的;
好处是增加新的User时速度会很快,只需要往后追加;
缺点是因为不是有序的,所以哈希索引做区间查询的速度很慢。

所以,哈希表这种结构适用于只有等值查询的场景,如Redis和其他一些NoSQL引擎

1.2 有序数组

有序数组在等值查询和范围查询场景中的性能都⾮常优秀:
在这里插入图片描述
要查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的身份证号,退出循环。

虽然查询的效率好,但是更新数据时很不好,如往中间插⼊⼀个记录就必须得挪动后面所有的记录,成本太高,所以有序数组索引只适用于静态存储引擎,即存储不会再修改的数据

1.3 搜索树

如果使用二叉搜索树,则如图:
在这里插入图片描述
要查ID_card_n2的话,按照图中的搜索顺序就是按照UserA -> UserC -> UserF -> User2这个路径得到。这个时间复杂度是O(log(N))。

为了维持O(log(N))的查询复杂度,需要保持这棵树是平衡⼆叉树;
为了保证这棵树是平衡⼆叉树,更新的时间复杂度也是O(log(N))。

实际上⼤多数的数据库存储却并不使用二叉树,因为索引不但存在内存中,还要写到磁盘上;
⼀棵100万节点的平衡二叉树,树高20,⼀次查询最多需要访问20个数据块(逐层加载,一层加载一个节点);
在机械硬盘时代,从磁盘随机读⼀个数据块需要10 ms左右的寻址时间。也就是说,对于⼀个100万行的表,如果使用二叉树来存储,单独访问⼀个行可能需要20个10ms的时间,慢得一批。

为了让⼀个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块;
所以要使用“N叉”树,让每一个节点尽可能多地加载块;
“N”取决于数据块的大小;
在读写上性能有很大的优点,其访问模式也很适配磁盘。

以InnoDB的⼀个整数字段索引为例,"N"差不多是1200;
树高是4的时候,就可以存1200的3次方个值,已经达到17亿了;
而根的数据块总是在内存中的,⼀个10亿行的表上⼀个整数字段的索引,查找⼀个值最多只需要访问3次磁盘。
其实,树的第⼆层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。

1.4 InnoDB的索引

1.4.1 索引模型

在InnoDB中,表是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表;
InnoDB使用了B+树索引模型,所以数据都是存储在B+树中的;
每⼀个索引在InnoDB里面对应⼀棵B+树。

例子:有一个主键列为ID的表,表中有字段k,并且在k上有索引;
表中第一行到第五行的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6):

create table T(
	id int primary key,
	k int not null,
	name varchar(16),
	index (k)
)engine=InnoDB;

两棵树的示例示意图如下:
在这里插入图片描述
最后的叶子节点使用了链表进行连接

根据叶子节点的内容,索引类型分为主键索引和⾮主键索引:

主键索引的叶子节点存的是整⾏数据,在InnoDB里,主键索引也被称为聚簇索引(clustered index)。

非主键索引的叶子节点内容是主键的值,在InnoDB⾥,⾮主键索引也被称为⼆级索引(secondary index)。

基于主键索引和普通索引的查询的区别:基于非主键索引的查询需要多扫描⼀棵索引树

如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树。

如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索⼀次。这个过程称为回表。

每⼀个索引其实就是⼀个B+树;
主B+树是为主键索引建立的,节点的key值是某⼀行的主键,value是该行的其他数据;
新建索引就是增加⼀个B+树,查询如果不走索引的话就是遍历主B+树。

可以看到主键索引是默认排序的,所以建议不要使用联合主键,否则排序会很复杂,影响性能

1.4.2 索引维护

流程:

B+树为了维护索引有序性,在插⼊新值的时候需要做必要的维护。

如上图,如果插⼊新的行ID值为700,则只需要在R5的记录后面插⼊⼀个新记录。

如果新插⼊的ID值为400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置;
⽽更糟的情况是,如果R5所在的数据页已经满了,根据B+树的算法,这时候需要申请⼀个新的数据页,然后挪动部分数据过去,这个过程称为页分裂,这种情况下性能会受影响;

除了性能外,页分裂操作还影响数据页的利用率。
原本放在⼀个页的数据,现在分到两个页中,整体空间利用率降低⼤约50%;

分裂问题可以用合并来解决:当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。

为什么要重建索引:页分裂或因为删除数据等原因,导致数据页会有空洞,这时候可以重建索引,此过程会创建⼀个新的索引,把数据按顺序插⼊,这样页面的利用率最高,也就是索引更紧凑、更省空间。

如此处重建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);

应该如下:

alter table T engine=InnoDB

1.5 案例

基于索引模型和索引维护,讨论一下哪些场景下应该使用自增主键,哪些场景下不应该使用自增主键,而是业务字段做主键?

  • 性能考虑

⾃增主键是指自增列上定义的主键,在建表语句中⼀般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT;
插⼊新记录的时候可以不指定ID的值,系统会获取当前ID最⼤值加1作为下⼀条记录的ID值;
也就是说,⾃增主键的插⼊数据模式,符合了递增插⼊的场景(如前面所说的“插入的ID为700”),每次插⼊⼀条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。

用业务逻辑的字段做主键,往往不容易保证有序插⼊,这样写数据成本相对较高(成本如前面所说的“插入的ID为400”产生的数据挪动、页分裂)。

  • 存储空间考虑

假设表中有⼀个唯⼀字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?

由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个⼆级索引的叶子节点占用约20个字节。

而如果用整型做主键,则只要4个字节,如果是长整型(bigint)则是8个字节。

显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。

  • 例外

从性能和存储空间方面考量,自增主键往往是更合理的选择。

不过还是有场景适合用业务字段直接做主键的,比如,有些业务的场景需求是这样的:

  1. 只有⼀个索引;
  2. 该索引必须是唯⼀索引。

这就是典型的KV场景;
由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。

考虑到上⼀段提到的“尽量使用主键查询”原则,可以直接将这个索引设置为主键,避免每次查询需要搜索两棵树。

2 使用索引

2.1 覆盖索引

在这里插入图片描述
还是按照上图的树,如果sql语句为:

select * from T where k between 3 and 5

那么执行过程为:

  1. 在k索引树上找到k=3的记录,取得 ID = 300;
  2. 再到ID索引树查到ID=300对应的R3;
  3. 在k索引树取下⼀个值k=5,取得ID=500;
  4. 再回到ID索引树查到ID=500对应的R4;
  5. 在k索引树取下⼀个值k=6,不满足条件,循环结束。

这个查询过程读了k索引树的3条记录(步骤1、3和5),回表了两次(步骤2和4,回到主键索引树搜索的过程称为回表)。

而如果sql语句为:

select ID from T where k between 3 and 5

这时只需要查ID的值,而 ID的值已经在k索引树上了,因此可以直接提供查询结果,不需要回表;
也就是说,在这个查询里面,索引k已经“覆盖了”我们的查询需求,称为覆盖索引
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是⼀个常用的性能优化⼿段

可以利用组合索引来完成覆盖索引,这也是尽可能按需查询字段的原因:将需要查询的字段做成一个组合索引即可

在引擎内部使用覆盖索引在索引k上其实读了三个记录:R3~R5(对应的索引k上的记录项);
但是对于MySQL的Server层来说,它就是找引擎拿到了两条记录,因此MySQL认为扫描行数是2。

2.2 最左前缀原则

B+树这种索引结构,可以利用索引的“最左前缀”,来定位记录。

  • 此原则的使用

假设有一个市民表,此表有name和age字段,为这两个字段建立联合索引(name,age),并插入一些记录,索引如下图,索引项是按照索引定义里面出现的字段顺序排序的:
在这里插入图片描述
需求1:查到所有名字是“张三”的⼈
过程:可以快速定位到ID4,然后向后遍历得到所有需要的结果。

需求2:所有名字第⼀个字是“张”的⼈,SQL语句的条件是"where name like ‘张%’"
过程:查找到第⼀个符合条件的记录是ID3,然后向后遍历,直到不满足条件为⽌。

可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索;
这个最左前缀可以是联合索引的最左N个字段(需求1的情况),也可以是字符串索引的最左M个字符(需求2的情况)。

  • 在建立联合索引的时候,如何安排索引内的字段顺序?

评估标准:索引的复用能力

因为可以支持最左前缀,所以当已经有了(a,b)这个联合索引后,⼀般就不需要单独在a上建立索引了。
因此,第⼀原则是,如果通过调整顺序,可以少维护⼀个索引,那么这个顺序往往就是需要优先考虑采用的。

例子:假如有一个高频请求为“根据市民的身份证号查询他的姓名”,和一个低频请求“根据市民的身份证号查询地址”,这三个字段也在市民表中。
解决:可以为这个请求创建(身份证号,姓名)这个联合索引,这样高频请求根据索引就能得出结果,低频请求则找到索引后,通过回表就能查询到地址

那么,如果既有联合查询,⼜有基于a、b各自的查询呢?查询条件里面只有b的语句,是无法使用(a,b)这个联合索引的,这时候不得不维护另外⼀个索引,也就是说需要同时维护(a,b)、(b) 这两个索引;
这时候要考虑的原则就是空间了。比如上面例子这个市民表的情况,假设name字段比age字段⼤,那就建议创建⼀个(name,age)的联合索引和⼀个(age)的单字段索引。

需要注意的是,组合索引从左向右匹配直到遇到范围查询( > < between)时索引会失效:如下索引顺序为a、b、c、d

select * from t1 where a=1 and b=1 and c>1 and d=1 ;

如此sql,c、d不会走索引,就算d在c前面,但是优化器会将c弄在前面,因此还是不会用上,只能通过改索引顺序来优化:改为a、b、d、c

2.3 索引下推

问题:以市民表的联合索引(name, age)为例,需求为:检索出表中“名字第⼀个字是张,而且年龄是10岁
的所有男孩”。SQL语句是这么写的:

select * from people where name like '张%' and age=10 and ismale=1;

MySQL5.6之前的执行流程:从ID3开始⼀个个回表。到主键索引上找出数据行,再对比字段值
在这里插入图片描述
MySQL5.6之后的执行流程:引入了**索引下推优化(index condition pushdown),**可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满⾜条件的记录,减少回表次数
在这里插入图片描述
可以看到,MySQL5.6之前的InnoDB不会去看age的值,需要把所有的数据行都取出来再判断字段是否匹配。

而MySQL5.6之后则会先判断字段值是否匹配,再去回表。

执行顺序的不同导致了效率相差很多

2.4 需要创建索引的情况

1、主键自动建立唯一索引

2、频繁作为查询条件的字段应该创建索引

3、多表关联查询中,关联字段应该创建索引 on 两边都要创建索引

4、查询中排序的字段,应该创建索引 B + tree 有顺序

5、可以使用到覆盖索引的地方

6、统计或者分组字段,应该创建索引

2.5 不需要创建索引的情况

1、表记录太少 索引是要有存储的开销

2、频繁更新 索引要维护

3、查询字段使用频率不高

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值