B+树与InnoDB中索引详解

什么是索引

索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。

举个例子,小学时我们使用字典进行文字查找时,根据目录进行查找,目录这个时候就是索引的作用。如果没有目录,我们要查找一个汉字时就只能从第一个开始顺序遍历了。因此索引就是提高检索速度的一个工具。

InnoDB中的索引结构

回顾一下B+树

在介绍数据库索引之前我们先来回顾一下B+树,为什么要回顾它哪?因为InnoDB中索引的实现就是以B+树的形式来实现的。

关于B+树的相关概念同学可以查看相关资料。这里我们回顾一下他的几个适于用作数据库索引结构的几个特征

  1. 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。因为中间节点不保存数据,因此相同空间内存储的数据更多,索引的范围更大,单次磁盘访问获取到的信息更多,能够有效的减少磁盘的io访问。由于只有在叶子节点上存储数据,因此每次查找的性能十分稳定。
  2. 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。Mysql是一种关系数据库,因此区间的访问是一个常见的场景,子节点根据大小等关系通过链表串起来可以有效的提高区间访问的效率。

我们来看下图来熟悉一下B+树的查找过程。

假设我们要查找数字4,那么查找过程如下。

第一次磁盘访问

第二次磁盘访问

第三次磁盘访问

最基本的索引-主键索引

数据库中的数据在数据页中按照主键形成一个单链表。每个数据页有一定的大小,如果一个数据页存满了那么就需要多个数据页来存储数据,并通过链表将数据页串起来。假设现在一个数据页能够存储的数据上限是两个。 那么我们来猜测一下数据在数据库中的存储方式可能如下所示。

但是在上述结构中我们的查找只能存头开始按照链表指向进行全局扫描来找到我们需要的数据。

那么我们将数据结构增加部分索引构成B+树是否就可以提高查询效率了?答案当然是肯定的。那么我们将其转换为下图所示。

这里有同学问不是一个页存储两个数据么?怎么上图有的有三个?请忽略这个细节由于这个是b+树,中间节点存储的数据可能多点。(根本问题是我懒,复用了最初的图)

上面的叶子节点的数据在页面中按照某一个值的大小进行排列,这个值通常就是我们的数据库主键,在建表的时候主键通常为该表的聚簇索引,表中的数据在数据页中按照聚簇索引的从小到大排列。

这个时候我们考虑一下,如果我们插入一条数值为8的数据,这个结构会如何变化哪?

我们首先找到8应该存储的位置,发现这个页面已经存满了,为了保证我们子节点的有序性,就只能进行页面的分裂操作了。这样效率明显会降低。因此通常我们都会使用数据库默认的自增序列作为主键。但是主键索引不一定是聚簇索引。(这个问题留给同学自己考虑吧)

普通索引

单字段索引,从名字上看就是只对一个字段建立索引。 联合索引就是多个字段一起作为索引呗。 我们来看下面的表结构

CREATE TABLE `tb_predict_user_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键id,赛季id',
  `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户ID',
  `app_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户的appID',
  `bonus_amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户红包的总金额,单位分',
  `is_del` tinyint(4) NOT NULL DEFAULT '0' COMMENT '软删除标记,0:未删除1:已删除',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '记录最后更新时间',
  PRIMARY KEY (`id`),
  KEY `index_user` (`user_id`)
  KEY `index_user_app` (`app_id`,`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
复制代码
  • index_user就是一个单字段索引
  • index_user_app是一个联合索引

针对与上面的表,在数据库中的存储结构是什么样的那?

针对于每一个索引,InnoDB都会建立一个B+树来对数据进行索引,但是为了节省空间等原因,只有聚簇索引的叶子节点才会保存完整的数据,其他的B+树的叶子节点只保存对应索引字段的数据。

在数据库中行格式可以简略如下图所示

record_type:数据的类型

  • 0:普通的用户记录
  • 1:目录项记录
  • 2:最小记录
  • 3:最大记录

索引页如下图

数据页如下图

InnoDB中索引在查询中的使用

上面我们说了,数据在数据库中已B+树的方式进行数据存储。那么我们在查找数据的时候如何查找的哪?

通过主键索引查找过程是不是可以简单理解为下面的过程哪?

  1. 假设查找主键为10000的数据。
  2. 由于同一个页中的数据已经是排好序的,在页中可以通过二分查找。
  3. 在根中通过二分查找找到下一级索引所在页面。
  4. 以此类推找到叶子节点。
  5. 在叶子节点中找到目标数据所在。

上面我们说了在Innodb中有多少个索引就有多少个B+树,那么这样的话数据会不会重复?如何解决这个问题哪?

答案当然是不会重复啊!只有主键索引(聚簇索引)的叶子节点才会保存所有的数据,而其他的索引中并不会保存所有的数据,只保存了作为索引的值以及该条记录所在的主键值。

那么我们根据普通索引的过程又是如何哪?

其实跟我们通过主键索引查找很类似,只是由于叶子节点不保存记录的所有数据,所以需要根据主键再次进行一次查找,这个过程就是我们通常说的回表(回表:再次回到表中进行一次查询)。

既然又回表,那么如何避免回表哪?

叶子节点不保存记录的所有字段,但是保存了索引字段的值啊,那么如果我们查询的字段只有索引字段,是不是就可以避免回表了哪?

答对了,如果我们的查询字段的所有字段都可以被使用到的索引字段所覆盖,那么就可以避免回表,这就是我们通常说的覆盖索引。

范围查询走索引么?

上面我们说的都是指定某个索引条件进行查询,但是在日常开发中我们不可避免的会遇到范围查询(>,<,!=)等等,网上查找相关资料,有人说不走索引,有人说走索引(网络是开放的,大家需要自行识别真伪),那么范围查询到底走不走索引?如果走索引,这个时候索引在我们的查询中又有什么作用哪?

我们还是回到B+树的查找过程。 大家先自己想一下,如果你在B+树中查找 索引值>5000某个值时会如何操作哪?

  1. 由于在数据库的B+树中,数据都是从大到小进行排列。
  2. 我们先查找id=5000的值所在的叶子节点,然后通过next指针顺序遍历查找符合条件的值
  3. 通过回表进行其他字段的查找。

上面的过程就是在>,<的情况下如如何走索引的 注:由于数据库查询引擎的自动优化,同学在测试这个场景的时候最好加上limit来限定查询个数,如果查询个数过多可能会触发全表扫描。

还有一种类似于!=的操作,那么这个走索引么? 我们大家来思考一下,通过B+树来查找一个!=某个值的数据要如何操作哪? 1、先找到等于这个值的数据,然后查找其他的数据?

是不是感觉跟全表扫描差不多(全表扫描还不需要回表哪!),我们可以认为这样操作的效率要低于全表扫描,没表要使用到索引啊,所以这类查询并不会用到索引。

索引除了在where条件中会用到,在order by的字段中会用到么?

还是之前的方式,我们来思考一下,如果我们的查询是 select * from tb_table order by id asc limit 100。这个时候要如何查询哪? 1、由于表中的数据都是按照id进行从大到小排序的。 2、所以我们只要找到id最小的,然后顺序遍历出符合条件个数的值。 3、如果需要回表在进行回表操作

order by一定会走索引么?

order by原来也会用到索引啊!那么如果我们的索引是userid_tradeDay,查询条件是 select * from table order by userid desc, tradeDay asc,两个排序条件不一致。这个时候还会走索引么? 老方式,再来看一下这个查询我们在B+树中应该如何操作。

B+树中的索引字段都是按照从小到大排列的,我们要查询这两个索引的排序方式不一致,,,,,,,,,,,无法用到索引啊。

在来看一种情况

如果我们的索引是userid_tradeDay,查询条件是 select * from table where user_id = ?, userid和tradeDay作为联合索引,但是查询条件中的where条件只有user_id, 第二个查询条件是 select * from table where trade_day = ?,查询条件只有trade_day,没有user_id。

上面两个查询会走索引么? 还是从B+树出发。 1、我们的索引是先按照user_id从小到大排序,在按照trade_day从小到大排序。 2、我们的查询条件是user_id,那么当作trade_day没有呗,这样不就走索引进行查询了么。 3、我们的查询条件是trade_day,先按照。。。。。。啊啊啊啊啊啊啊,走不了索引我,我想不出啊啊啊啊啊啊啊啊啊啊。

上面所说的就是左前缀匹配原则,那么我们的where条件中使用了user_id和trade_day,但是两个顺序在where条件中颠倒了会走索引么。 会的,查询优化器会帮我们做的。

我们来看下面的表

CREATE TABLE `tb_user_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键id,赛季id',
  `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户ID',
  `user_id_c` varchar(200) NOT NULL DEFAULT '' COMMENT '用户ID',
  PRIMARY KEY (`id`),
  KEY `index_user` (`user_id`)
  KEY `index_userc` (`user_id_c`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
复制代码

上面的表主要有两列int类型的user_id和varchar类型的user_id_c以及相关索引。 我们表中存在一下数据

带引号是为了说明是字符串 我们看下面几个查询语句。 A:select * from tb_user_info where user_id = 0; B:select * from tb_user_info where user_id = '0'; C:select * from tb_user_info where user_id = 'abc'; D:select * from tb_user_info where user_id_c = 0; E:select * from tb_user_info where user_id_c = '0'; F:select * from tb_user_info where user_id_c = 'abc'; 上面6条语句的查询结果是什么,以及上面查询是否走索引了哪? A:查询结果为id=1的数据,走了index_user索引。

B:查询结果为id=1的数据,走了index_user索引。

C:查询结果为id=1的数据,走了index_user索引。

D:查询结果为id=1的数据,走了index_user_c索引

E:查询结果为id=1的数据,走了index_user_c索引

F:查询结果为空,走了index_user_c索引

看到这里是不是有疑问? 1、我都建立索引了,怎么有时候走,有时候不走? 2、我查=’abc‘的怎么返回了=0的数据?

这里我们引入了一个概念:隐式转换

在我们查询条件的类型和数据库字段的类型不一致时,mysql会进行会进行以下操作:

  1. 两个参数至少有一个是 NULL 时,比较的结果也是 NULL,例外是使用 ⇔ 对1. 两个 NULL 做比较时会返回 1,这两种情况都不需要做类型转换 两个参数都是字符串,会按照字符串来比较,不做类型转换
  2. 两个参数都是整数,按照整数来比较,不做类型转换
  3. 十六进制的值和非数字做比较时,会被当做二进制串
    1. 有一个参数是 TIMESTAMP 或 DATETIME,并且另外一个参数是常量,常量会被转换为 timestamp 有一个参数是 decimal 类型,如果另外一个参数是 decimal 或者整数,会将整数转换为 decimal 后进行比较,如果另外一个参数是浮点数,则会把 decimal 转换为浮点数进行比较
  4. 所有其他情况下,两个参数都会被转换为浮点数再进行比较

针对上面的原则我们来看刚才我们的疑问

1、我都建立索引了,怎么有时候走,有时候不走? 发现了没,走索引的都是类型一致或者数据库类型是int类型的? 类型一致走索引没有什么疑问,那为什么类型转换后有可能不走索引哪? 我们来想以下float数字的排序和字母的排序: 3,21 '21','3', 类型不一致时排序结果不一样啊同学!。 所以按照我的理解,可以简单认为发生隐式类型转换的时候,如果转换方是数据库的字段类型,这个时候索引就不生效了。(也不知道对不对,欢迎同学Diss) 2、我查=’abc‘的怎么返回了=0的数据? 类型不一致会进行转换啊!‘abc’转换的时候转为为数字是多少哪?无法转换啊,当然就是默认的0了,所以~你懂的。

总结:

当不知道走不走索引的时候,就会想一下B+树,如果查询条件是xxx,索引是xxx,我来查询的时候如何能够最优。。。。。。

猜你喜欢

转载自juejin.im/post/5df64970f265da339d106059