Mysql高级部分系列(二)

1、索引的数据结构

1.1、索引概述

MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构

索引的本质: 索引是数据结构。简单理解为“排好序的快速查找数据结构”,满足特定查找算法。这些数据结构以某种方式指向数据, 这样就可以在这些数据结构的基础上实现高级查找算法

索引是在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。同时,存储引擎可以定义每个表的最大索引数和最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。

1.1.1、优点

  • 类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本,这也是创建索引最主要的原因。
  • 通过创建唯一索引,可以保证数据库表中每一行数据的唯一性
  • 在实现数据的参考完整性方面,可以加速表和表之间的连接。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度。
  • 在使用分组和排序子句进行数据查询时,可以显著减少查询中分组和排序的时间,降低了CPU的消耗。

1.1.2、缺点

  • 创建索引和维护索引要耗费时间,并且随着数据量的增加,所耗费的时间也会增加。
  • 索引需要占磁盘空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间存储在磁盘上,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。
  • 虽然索引大大提高了查询速度,同时却会降低更新表的速度。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。

1.2、InnoDB中索引的推演

1.2.1、索引之前的查找

SELECT [列名列表] FROM 表名 WHERE 列名 = xxx;

1.2.2、在一个页中的查找

假设目前表中的记录比较少,所有的记录都可以被存放在一个页中,在查找记录的时候可以根据搜索条件的不同分为两种情况:

  • 以主键为搜索条件
    • 可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
  • 以其他列作为搜索条件
    • 因为在数据页中并没有对非主键建立所谓的页目录,所以我们无法通过二分法快速定位相应的槽。这种情况下只能从最小记录开始依次遍历单链表中的每条记录,然后对比每条记录是不是符合搜索条件。很显然,这种查找的效率是非常低的。

1.2.3、在很多页中查找

大部分情况下我们表中存放的记录都是非常多的,需要好多的数据页来存储这些记录。在很多页中查找记录的话可以分为两个步骤:

  1. 定位到记录所在的页。
  2. 从所在的页内查找相应的记录。

在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录所在的页,所以只能从第一个页沿着双向链表一直往下找,在每一个页中根据我们上面的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式显然是超级耗时的。

1.2.4、设计索引

mysql> CREATE TABLE index_demo(
    -> c1 INT,
    -> c2 INT,
    -> c3 CHAR(1),
    -> PRIMARY KEY(c1)
    -> ) ROW_FORMAT = Compact;

这个新建的 index_demo 表中有2个INT类型的列,1个CHAR(1)类型的列,而且我们规定了c1列为主键,这个表使用 Compact 行格式来实际存储记录的。这里我们简化了index_demo表的行格式示意图:
在这里插入图片描述

  • record_type:记录头信息的一项属性,表示记录的类型,0表示普通记录、1表示目录项记录、2表示最小记录、3`表示最大记录。
  • next_record:记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量,我们用箭头来表明下一条记录是谁。
  • 各个列的值:这里只记录在index_demo表中的三个列,分别是c1c2c3
  • 其他信息:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。

将记录格式示意图的其他信息项暂时去掉并把它竖起来的效果就是这样:
在这里插入图片描述
把一些记录放到页里的示意图就是:
在这里插入图片描述

1.2.4.1、一个简单的索引设计方案

我们在根据某个搜索条件查找一些记录时为什么要遍历所有的数据页呢?因为各个页中的记录并没有规律,我们并不知道我们的搜索条件匹配哪些页中的记录,所以不得不依次遍历所有的数据页。所以如果我们想快速的定位到需要查找的记录在哪些数据页中该咋办?我们可以为快速定位记录所在的数据页而建立一个目录,建这个目录必须完成下边这些事:

1.2.4.1.1、下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。

假设:每个数据页最多能存放3条记录(实际上一个数据页非常大,可以存放下好多记录)。有了这个假设之后我们向index_demo表插入3条记录:
在这里插入图片描述
那么这些记录已经按照主键值的大小串联成一个单向链表了,如图所示:
在这里插入图片描述
从图中可以看出来, index_demo表中的3条记录都被插入到了编号为10的数据页中了。此时我们再来插入一条记录:
在这里插入图片描述
注意,新分配的数据页编号可能并不是连续的。它们只是通过维护着上一个页和下一个页的编号而建立了链表关系。
另外,页10中用户记录最大的主键值是5,而页28中有一条记录的主键值是4,因为5>4,所以这就不符合下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值的要求,
所以在插入主键值为4的记录的时候需要伴随着一次记录移动,也就是把主键值为5的记录移动到页28中,然后再把主键值为4的记录插入到页10中,这个过程的示意图如下:
在这里插入图片描述
这个过程表明了在对页中的记录进行增删改操作的过程中,我们必须通过一些诸如记录移动的操作来始终保证这个状态一直成立:下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值,这个过程我们称为页分裂

因为这些16KB的页在物理存储上是不连续的,所以如果想从这么多页中根据主键值快速定位某些记录所在的页,我们需要给它们做个目录,每个页对应一个目录项,每个目录项包括下边两个部分:

  • 页的用户记录中最小的主键值,我们用key来表示。
  • 页号,我们用page_no表示。
1.2.4.1.2、给所有的页建立一个目录项

所以我们为上边几个页做好的目录就像这样子:
在这里插入图片描述
以页28 为例,它对应目录项2 ,这个目录项中包含着该页的页号 28 以及该页中用户记录的最小主键值 5 。我们只需要把几个目录项在物理存储器上连续存储(比如:数组),就可以实现根据主键值快速查找某条记录的功能了。比如:查找主键值为 20 的记录,具体查找过程分两步:

  1. 先从目录项中根据 二分法 快速确定出主键值为 20 的记录在 目录项3 中(因为 12 < 20 < 209 ),它对应的页是页9 。
  2. 再根据前边说的在页中查找记录的方式去页9 中定位具体的记录。至此,针对数据页做的简易目录就搞定了。这个目录有一个别名,称为 索引 。

1.2.4.2、InnoDB中的索引方案

1.2.4.2.1、迭代1次:目录项纪录的页

我们把前边使用到的目录项放到数据页中的样子就是这样
在这里插入图片描述
从图中可以看出来,我们新分配了一个编号为30的页来专门存储目录项记录。这里再次强调目录项记录和普通的用户记录不同点

  • 目录项记录record_type值是1,而普通用户记录record_type值是0。
  • 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列。
  • 了解:记录头信息里还有一个叫min_rec_mask的属性,只有在存储目录项记录的页中的主键值最小的目录项记录min_rec_mask值为1,其他别的记录的min_rec_mask值都是0

相同点: 两者用的是一样的数据页,都会为主键值生成Page Directory(页目录),从而在按照主键值进行查找时可以使用二分法来加快查询速度。

现在以查找主键为20的记录为例,根据某个主键值去查找记录的步骤就可以大致拆分成下边两步:

  1. 先到存储目录项记录的页,也就是页30中通过二分法快速定位到对应目录项,因为 12 < 20 < 209 ,所以定位到对应的记录所在的页就是页9。
  2. 再到存储用户记录的页9中根据二分法快速定位到主键值为20的用户记录。
1.2.4.2.2、迭代2次:多个目录项纪录的页

在这里插入图片描述
从图中可以看出,我们插入了一条主键值为320的用户记录之后需要两个新的数据页:

  • 为存储该用户记录而新生成了页31 。
  • 因为原先存储目录项记录的 页30的容量已满 (我们前边假设只能存储4条目录项记录),所以不得不需要一个新的 页32 来存放 页31 对应的目录项。

现在因为存储目录项记录的页不止一个,所以如果我们想根据主键值查找一条用户记录大致需要3个步骤,以查找主键值为 20 的记录为例:

  • 确定目录项记录页
    我们现在的存储目录项记录的页有两个,即 页30 和 页32 ,又因为页30表示的目录项的主键值的范围是 [1, 320) ,页32表示的目录项的主键值不小于 320 ,所以主键值为 20 的记录对应的目录项记录在 页30 中。
  • 通过目录项记录页 确定用户记录真实所在的页 。在一个存储 目录项记录 的页中通过主键值定位一条目录项记录的方式说过了。
  • 在真实存储用户记录的页中定位到具体的记录。
1.2.4.2.3、迭代3次:目录项记录页的目录页

在这里插入图片描述
如图,我们生成了一个存储更高级目录项的 页33 ,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在 [1, 320) 之间,则到页30中查找更详细的目录项记录,如果主键值 不小于320 的话,就到页32中查找更详细的目录项记录。

我们可以用下边这个图来描述它:
在这里插入图片描述
这个数据结构,它的名称是 B+树 。

1.2.4.2.4、B+Tree

一个B+树的节点其实可以分成好多层,规定最下边的那层,也就是存放我们用户记录的那层为第0层,之后依次往上加。
之前我们做了一个非常极端的假设:存放用户记录的页最多存放3条记录,存放目录项记录的页最多存放4条记录。其实真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录的叶子节点代表的数据页可以存放100条用户记录,所有存放目录项记录的内节点代表的数据页可以存放1000条目录项记录,那么:

  • 如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放100条记录。
  • 如果B+树有2层,最多能存放1000×100=10,0000条记录。
  • 如果B+树有3层,最多能存放1000×1000×100=1,0000,0000条记录。
  • 如果B+树有4层,最多能存放1000×1000×1000×100=1000,0000,0000条记录。相当多的记录!!!

你的表里能存放100000000000条记录吗?所以一般情况下,我们用到的B+树都不会超过4层,那我们通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内有所谓的Page Directory(页目录),所以在页面内也可以通过二分法实现快速定位记录。

1.3、常见索引概念

索引按照物理实现方式,索引可以分为 2 种:

  • 聚簇(聚集)
  • 非聚簇(非聚集)索引。

我们也把非聚集索引称为二级索引或者辅助索引。

1.3.1、聚簇索引

1.3.1.1、使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:

  1. 页内的记录是按照主键字段,从小到大的顺序排成一个单向链表
  2. 各个存放用户记录的页也是根据页中用户记录的主键字段,从小到大的顺序排成一个双向链表
  3. 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表

B+树的叶子节点存储的是完整的用户记录。所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。

1.3.1.2、优缺点

1.3.1.2.1、优点:
  • 数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快
  • 聚簇索引对于主键的排序查找范围查找速度非常快
  • 按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以节省了大量的io操作
1.3.1.2.2、缺点:
  • 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增ID列为主键
  • 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新
  • 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据(回表操作)

1.3.2、二级索引(辅助索引、非聚簇索引)

在这里插入图片描述

  • 什么是回表?

回表是指我们根据这个以非主键字段大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据非主键字段的值查找到完整的用户记录的话,仍然需要到聚簇索引中再查一遍,这个过程称为回表 。也就是根据c2列的值查询一条完整的用户记录需要使用到 2 棵B+树!

因为这种按照非主键列建立的B+树需要一次回表操作才可以定位到完整的用户记录,所以这种B+树也被称为二级索引(英文名secondary index ),或者辅助索引。由于我们使用的是c2列的大小作为B+树的排序规则,所以我们也称这个B+树是为c2列建立的索引。非聚簇索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个非聚簇索引。

  • 问题:为什么我们还需要一次回表操作呢?直接把完整的用户记录放到叶子节点不OK吗?

如果把完整的用户记录放到叶子节点是可以不用回表。但是太占空间了,相当于每建立一棵B+树都需要把所有的用户记录再都拷贝一遍,这就有点太浪费存储空间了。

1.3.3、聚簇索引与非聚簇索引的区别

  • 聚簇索引的叶子节点存储的就是我们的数据记录,非聚簇索引的叶子节点存储的是数据位置,非聚簇索引不会影响数据表的物理存储顺序。
  • 一个表只能有一个聚簇索引,因为只能有一种排序存储的方式,但可以有多个非聚簇索引,也就是多个索引目录提供数据检索。
  • 使用聚簇索引的时候,数据的查询效率高,但如果对数据进行插入,删除,更新等操作,效率会比非聚簇索引低。

1.3.4、联合索引

我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照 c2和c3列 的大小进行排序,这个包含两层含义:

  • 先把各个记录和页按照c2列进行排序。
  • 在记录的c2列相同的情况下,采用c3列进行排序
    在这里插入图片描述
    注意一点,以c2和c3列的大小为排序规则建立的B+树称为 联合索引 ,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:
    • 建立联合索引只会建立如上图一样的1棵B+树。
    • 为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树。

1.4、InnoDB的B+树索引的注意事项

1.4.1、根页面位置万年不动

我们前边介绍B+索引的时候,为了大家理解上的方便,先把存储用户记录的叶子节点都画出来,然后接着画存储目录项记录的内节点,实际上B+树的形成过程是这样的:

  • 每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点页面中既没有用户记录,也没有目录项记录。
  • 随后向表中插入用户记录时,先把用户记录存储到这个根节点页面中。
  • 当根节点页面中的可用空间用完时继续插入记录,此时会将根节点页面中的所有记录复制到一个新分配的页,比如页a中,然后添加插入记录数据时,对这个新页进行页分裂的操作,得到另一个新页,比如页b。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页b中,而根节点页面便升级为存储目录项记录的页。

这个过程特别注意的是:一个B+树索引的根节点自诞生之日起,便不会再移动。这样只要我们对某个表建立一个索引,那么它的根节点的页号便会被记录到某个地方,然后凡是InnoDB存储引擎需要用到这个索引的时候,都会从那个固定的地方取出根节点的页号,从而来访问这个索引。

1.4.2、内节点中目录项记录的唯一性

我们知道B+树索引的内节点中目录项记录的内容是索引列+页号的搭配,但是这个搭配对于二级索引来说有点不严谨。还拿index_demo表为例,假设这个表中的数据是这样的:
在这里插入图片描述
如果二级索引中目录项的内容只是索引号+页号的搭配的话,那么为c2列建立索引后的B+树应该长这样:
在这里插入图片描述
如果我们想要新插入一行记录,其中c1c2c3的值分别是:91c,那么在修改这个为c2列建立的二级索引对应的B+树时便碰到了个大问题:由于页3中存储的目录项记录是由c2列+页号的值构成的,页3中的两条目录项记录对应的c2列的值都是1,那么我们这条新插入的记录到底应该放在页4中,还是应该放在页5中啊?

为了让新插入记录能找到自己在哪个页里,我们需要 保证在B+树的同一层内节点的目录项记录除页号这个字段以外是唯一的。 所以对于二级索引的内节点的目录项记录的内容实际上是由三个部分构成的:

  • 索引列的值
  • 主键值
  • 页号

也就是我们把主键值也添加到二级索引内节点中的目录项记录了,这样就能保证B+树每一层节点中各条目录项记录除页号这个字段外是唯一的,所以我们为c2列建立二级索引后的示意图实际上应该是这样子的:
在这里插入图片描述
这样我们再插入记录(9, 1, 'c')时,由于页3中存储的目录项记录是由c2列+主键+页号的值构成的,可以先把新记录的c2列的值和页3中各目录项记录的c2列的值作比较,如果c2列的值相同的话,可以接着比较主键值,因为B+树同一层中不同目录项记录的c2列+主键的值肯定是不一样的,所以最后肯定能定位唯一的一条目录项记录,在本例中最后确定新记录应该被插入到页5中。

1.4.3、一个页面最少可以存储2条记录

一个B+树只需要很少的层级就可以轻松存储数亿条记录,查询速度相当不错!这是因为B+树本质上就是一个大的多层级目录,每经过一个目录时都会过滤掉许多无效的子目录,直到最后访问的存储真实数据的目录。

那如果一个大的目录中只存放一个子目录是个啥效果呢?
那就是目录层级非常非常非常多,而且最后的那个存放真实数据的目录中存放一条记录。显然是并不合适的,所以InnoDB的一个数据页至少可以存放两条记录

1.5、MyISAM中的索引方案

B树索引适用存储引擎如表所示:在这里插入图片描述
即使多个存储引擎支持同一种类型的索引,但是他们的实现原理也是不同的。Innodb和MyISAM默认的索引是Btree索引;而Memory默认的索引是Hash索引。
MyISAM引擎使用 B+Tree 作为索引结构,叶子节点的data域存放的是 数据记录的地址 。

1.5.1、MyISAM索引的原理

我们知道InnoDB中索引即数据,也就是聚簇索引的那棵B+树的叶子节点中已经把所有完整的用户记录都包含了,而MyISAM的索引方案虽然也使用树形结构,但是却将索引和数据分开存储︰

  • 将表中的记录按照记录的插入顺序单独存储在一个文件中,称之为数据文件。这个文件并不划分为若干个数据页,有多少记录就往这个文件中塞多少记录就成了。由于在插入数据的时候并没有刻意按照主键大小排序,所以我们并不能在这些数据上使用二分法进行查找。
  • 使用MyISAM存储引擎的表会把索引信息另外仔储到一个创月索敦的F白记寻‘而是主键值+数据记录地址的主键创建一个索引,只不过在索引的叶子节点中存储的不是完整的用户记录,而是主键值+数据记录地址的组合。

在这里插入图片描述
这里设表一共有三列,假设我们以Col1为主键,上图是一个MylSAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主键索引和二级索引 (Secondary key)在结构上没有任何区别,只是主键索引要求key是唯一的,而二级索引的key可以重复。如果我们在col2上建立一个二级索引,则此索引的结构如下图所示:

在这里插入图片描述
同样也是一棵B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为:首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。

1.5.1、MyISAM与InnoDB对比

MyISAM的索引方式都是“非聚簇”的,与InnoDB包含1个聚簇索引是不同的。

小结两种引擎中索引的区别:

  • 在InnoDB存储引擎中,我们只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录,而在MyISAM中却需要进行一次回表操作,意味着MyISAM中建立的索引相当于全部都是二级索引

  • InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。

  • InnoDB的非聚簇索引data域存储相应记录主键的值,而MyISAM索引记录的是地址。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。

  • MyISAM的回表操作是十分快速的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。

  • InnoDB要求表必须有主键MyISAM可以没有)。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。

1.6、索引的代价

索引是个好东西,可不能乱建,它在空间和时间上都会有消耗:

1.6.1、空间上的代价

每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用 16KB 的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。

1.6.2、时间上的代价

每次对表中的数据进行 增、删、改 操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值 从小到大的顺序排序 而组成了 双向链表 。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些 记录移位 , 页面分裂 、 页面回收 等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。

1.7、MySQL数据结构选择的合理性

从MysQL的角度讲,不得不考虑一个现实问题就是磁盘IO。如果我们能让索引的数据结构尽量减少硬盘的I/O操作,所消耗的时间也就越小。可以说,磁盘的 I/0操作次数对索引的使用效率至关重要。

查找都是索引操作,一般来说索引非常大,尤其是关系型数据库,当数据量比较大的时候,索引的大小有可能几个G甚至更多,为了减少索引在内存的占用,数据库索引是存储在外部磁盘上的。当我们利用索引查询的时候,不可能把整个索引全部加载到内存,只能逐一加载,那么MySQL衡量查询效率的标准就是磁盘lO次数。

1.7.1、Hash结构

Hash本身是一个函数,又被称为散列函数,它可以帮助我们大幅提升检索数据的效率。

Hash算法是通过某种确定性的算法(比如MD5、SHA1、SHA2、SHA3)将输入转变为输出。相同的输入永远可以得到相同的输出,假设输入内容有微小偏差,在输出中通常会有不同的结果。

举例:如果你想要验证两个文件是否相同,那么你不需要把两份文件直接拿来比对,只需要让对方把 Hash 函数计算得到的结果告诉你即可,然后在本地同样对文件进行Hash 函数的运算,最后通过比较这两个Hash函数的结果是否相同,就可以知道这两个文件是否相同。

加速查找速度的数据结构,常见的有两类:

  • 树,例如平衡二叉搜索树,查询/插入/修改/删除的平均时间复杂度都是0(log2N);
  • 哈希,例如HashMap,查询/插入/修改/删除的平均时间复杂度都是0( 1) ;

在这里插入图片描述
采用Hash进行检索效率非常高,基本上一次检索就可以找到数据,而B+树需要自顶向下依次查找,多次访问节点才能找到数据,中间需要多次lO操作,从效率来说Hash比 B+树更快。
在哈希的方式下,一个元素k处于h(k)中,即利用哈希函数h,根据关键字k计算出槽的位置。函数h将关键字域映射到哈希表T[o…m-1]的槽位上。
在这里插入图片描述

1.7.1.1、Hash结构效率高,那为什么索引结构要设计成树型呢?

  • 原因1: Hash索引仅能满足(=) (<>)和IN查询。如果进行范围查询,哈希型的索引,时间复杂度会退化为o(n);而树型的“有序”特性,依然能够保持o(log2N)的高效率。
  • 原因2: Hash索引还有一个缺陷,数据的存储是没有顺序的,在ORDER BY的情况下,使用Hash索引还需要对数据重新排序。
  • 原因3:对于联合索引的情况,Hash值是将联合索引键合并后一起来计算的,无法对单独的一个键或者几个索引键进行查询。
  • 原因4:对于等值查询来说,通常Hash 索引的效率更高,不过也存在一种情况,就是索引列的重复值如果很多,效率就会降低。这是因为遇到Hash冲突时,需要遍历桶中的行指针来进行比较,找到查询的关键字非常耗时。所以,Hash索引通常不会用到重复值多的列上,比如列为性别、年龄的情况等。

1.7.1.2、Hash索引的适用性:

Hash索引存在着很多限制,相比之下在数据库中B+树索引的使用面会更广,不过也有一些场景采用Hash索引效率更高,比如在键值型(Key-value)数据库中,Redis存储的核心就是 Hash表。

MysQL中的Memory存储引擎支持Hash存储,如果我们需要用到查询的临时表时,就可以选择Memory存储引擎,把某个字段设置为Hash 索引,比如字符串类型的字段,进行Hash计算之后长度可以缩短到几个字节。当字段的重复度低,而且经常需要进行等值查询的时候,采用Hash索引是个不错的选择。

另外,InnoDB本身不支持 Hash索引,但是提供自适应Hash索引(Adaptive Hash Index)。

  • 什么情况下才会使用自适应Hash 索引呢?

如果某个数据经常被访问,当满足一定条件的时候,就会将这个数据页的地址存放到Hash表中。这样下次查询的时候,就可以直接找到这个页面的所在位置。这样证B+树也具备了Hash 索引的优点。
在这里插入图片描述
采用自适应 Hash 索引目的是方便根据 SQL 的查询条件加速定位到叶子节点,特别是当 B+ 树比较深的时候,通过自适应 Hash 索引可以明显提高数据的检索效率。

我们可以通过 innodb_adaptive_hash_index 变量来查看是否开启了自适应 Hash,比如:

mysql> show variables like '%adaptive_hash_index';

在这里插入图片描述

1.7.2、二叉搜索树

如果我们利用二叉树作为索引结构,那么磁盘的IO次数和索引树的高度是相关的。

1.7.2.1、二叉搜索树的特点

  • —个节点只能有两个子节点,也就是一个节点度不能超过2
  • 左子节点<本节点I右子节点>=本节点,比我大的向右,比我小的向左

1.7.2.2、查找规则

我们先来看下最基础的二叉搜索树(Binary Search Tree),搜索某个节点和插入节点的规则一样,我们假设搜索插入的数值为key:

  1. 如果key大于根节点,则在右子树中进行查找;
  2. 如果key小于根节点,则在左子树中进行查找;
  3. 如果key等于根节点,也就是找到了这个节点,返回根节点即可。

举个例子,我们对数列(34,22,89,5,23,77,91)创造出来的二分查找树如下图所示:
在这里插入图片描述
但是在极端的条件下,查询的时间复杂度会变成O(n)级别
在这里插入图片描述
为了提高查询效率,就需要 减少磁盘IO数 。为了减少磁盘IO的次数,就需要尽量 降低树的高度 ,需要把原来“瘦高”的树结构变的“矮胖”,树的每层的分叉越多越好。

1.7.3、AVL树

为了解决上面二叉查找树退化成链表的问题,人们提出了平衡二叉搜索树(Balanced Binary Tree),又称为AVL树(有别于AVL算法),它在二叉搜索树的基础上增加了约束,具有以下性质:

  • 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
  • 左右两个子树都是一棵平衡二叉树

这里说一下,常见的平衡二叉树有很多种,包括了平衡二叉搜索树、红黑树、数堆、伸展树。平衡二叉搜索树是最早提出来的自平衡二叉搜索树,当我们提到平衡二叉树时一般指的就是平衡二叉搜索树。事实上,第一棵树就属于平衡二叉搜索树,搜索时间复杂度就是o( log2n)。
数据查询的时间主要依赖于磁盘IO的次数,如果我们采用二叉树的形式,即使通过平衡二叉搜索树进行了改进,树的深度也是o(log2n),当n 比较大时,深度也是比较高的,比如下图的情况:
在这里插入图片描述
针对同样的数据,如果我们把二叉树改成 M 叉树 (M>2)呢?当 M=3 时,同样的 31 个节点可以由下面的三叉树来进行存储:
在这里插入图片描述

1.7.4、B-Tree

B 树的结构如下图所示:

在这里插入图片描述
B树作为多路平衡查找树,它的每一个节点最多可以包括M个子节点,M称为B树的阶。每个磁盘块中包括了关键字和子节点的指针。如果一个磁盘块中包括了x个关键字,那么指针数就是x+1。对于一个100阶的B树来说,如果有3层的话最多可以存储约100万的索引数据。对于大量的索引数据来说,采用B树的结构是非常适合的,因为树的高度要远小于二叉树的高度。

一个 M 阶的 B 树(M>2)有以下的特性:

  1. 根节点的儿子数的范围是 [2,M]。
  2. 每个中间节点包含 k-1 个关键字和 k 个孩子,孩子的数量 = 关键字的数量 +1,k 的取值范围为[ceil(M/2), M]。
  3. 叶子节点包括 k-1 个关键字(叶子节点没有孩子),k 的取值范围为 [ceil(M/2), M]。
  4. 假设中间节点节点的关键字为:Key[1], Key[2], …, Key[k-1],且关键字按照升序排序,即 Key[i] <Key[i+1]。此时 k-1 个关键字相当于划分了 k 个范围,也就是对应着 k 个指针,即为:P[1], P[2], …,P[k],其中 P[1] 指向关键字小于 Key[1] 的子树,P[i] 指向关键字属于 (Key[i-1], Key[i]) 的子树,P[k]指向关键字大于 Key[k-1] 的子树。
  5. 所有叶子节点位于同一层。

上面那张图所表示的 B 树就是一棵 3 阶的 B 树。我们可以看下磁盘块 2,里面的关键字为(8,12),它有 3 个孩子 (3,5),(9,10) 和 (13,15),你能看到 (3,5) 小于 8,(9,10) 在 8 和 12 之间,而 (13,15)大于 12,刚好符合刚才我们给出的特征。

然后我们来看下如何用 B 树进行查找。假设我们想要查找的关键字是 9,那么步骤可以分为以下几步:

  1. 我们与根节点的关键字 (17,35)进行比较,9 小于 17 那么得到指针 P1;
  2. 按照指针 P1 找到磁盘块 2,关键字为(8,12),因为 9 在 8 和 12 之间,所以我们得到指针 P2;
  3. 按照指针 P2 找到磁盘块 6,关键字为(9,10),然后我们找到了关键字 9。

你能看出来在 B 树的搜索过程中,我们比较的次数并不少,但如果把数据读取出来然后在内存中进行比较,这个时间就是可以忽略不计的。而读取磁盘块本身需要进行 I/O 操作,消耗的时间比在内存中进行比较所需要的时间要多,是数据查找用时的重要因素。B 树相比于平衡二叉树来说磁盘 I/O 操作要少,在数据查询中比平衡二叉树效率要高。所以只要树的高度足够低,IO次数足够少,就可以提高查询性能

小结:

  1. B树在插入和删除节点的时候如果导致例个平倒,就通过自动调整节点的位置来保持树的自平衡。
  2. 关键字集合分布在整棵树中,即叶子节点和非叶子节点都存放数据。搜索有可能在非叶子节点结束
  3. 其搜索性能等价于在关键字全集内做一次二分查找。

再举例1:在这里插入图片描述

1.7.5、 B+Tree

B+树也是一种多路搜索树,基于B树做出了改进,主流的DBMS都支持B+树的索引方式,比如MySQL。相比于B-Tree,B+Tree适合文件索引系统。

1.7.5.1、 B+树和B树的差异:

  1. 有 k 个孩子的节点就有 k 个关键字。也就是孩子数量 = 关键字数,而 B 树中,孩子数量 = 关键字数+1。
  2. 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
  3. 非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而 B 树中, 非叶子节点既保存索引,也保存数据记录
  4. 所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。

B 树和 B+ 树都可以作为索引的数据结构,在 MySQL 中采用的是 B+ 树。
但B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然。

1.7.5.2、思考题:为了减少IO,索引树会一次性加载吗?

1、数据库索引是存储在磁盘上的,如果数据量很大,必然导致索引的大小也会很大,超过几个G。
2、当我们利用索引查询时候,是不可能将全部几个G的索引都加载进内存的,我们能做的只能是:逐一加载每一个磁盘页,因为磁盘页对应着索引树的节点。

1.7.5.3、思考题:B+树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘IO

1、InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值,因为是估算,为了方便计算,这里的K取值为10^3。
也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿条记录。(这里假定一个数据页也存储10^3条行记录数据了)
2、实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2~4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作

1.7.5.4、思考题:为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?

1.B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对于B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
2、B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的节点,而只是叶子结点中关键字的索引。所有任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

1.7.5.5、思考题:Hash索引与B+树索引的区别

1、Hash索引不能进行范围查询,而B+树可以。这是因为Hash索引指向的数据是无序的,而B+树的叶子节点是个有序的链表。
2、Hash索引不支持联合索引的最左侧原则(即联合索引的部分索引无法使用),而B+树可以。对于联合索引来说,Hash索引在计算Hash值的时候是将索引键合并后再一起计算Hash值,所以不会针对每个索引单独计算Hash值。因此如果用到联合索引的一个或者几个索引时,联合索引无法被利用。
3、Hash索引不支持 ORDER BY 排序,因为Hash索引指向的数据是无序的,因此无法起到排序优化的作用,而B+树索引数据是有序的,可以起到对该字段ORDER BY 排序优化的作用。同理,我们也无法用Hash索引进行模糊查询,而B+树使用LIKE进行模糊查询的时候,LIKE后面后模糊查询(比如%结尾)的话就可以起到优化作用。
4、InnoDB不支持哈希索引

1.7.5.6、思考题:Hash 索引与 B+ 树索引是在建索引的时候手动指定的吗?

针对InnoDB和MylSAM存储引擎,都会默认采用B+树索引,无法使用Hash索引。InnoDB提供的自适应Hash是不需要手动指定的。如果是>Memory/Heap和NDB存储引擎,是可以进行选择Hash索引的

1.7.6、R树

R-Tree在MysQL很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有myisam、bdbinnodb、ndb、archive几种。
举个R树在现实领域中能够解决的例子:查找20英里以内所有的餐厅。

如果没有R树你会怎么解决?

一般情况下我们会把餐厅的坐标(x,y)分为两个字段存放在数据库中,一个字段记录经度,另一个字段记录纬度。这样的话我们就需要遍历所有的餐厅获取其位置信息,然后计算是否满足要求。如果一个地区有100家餐厅的话,我们就要进行100次位置计算操作了,如果应用到谷歌、百度地图这种超大数据库中,这种方法便必定不可行了。

R树就很好的解决了这种高维空间搜索问题。它把B树的思想很好的扩展到了多维空间,采用了B树分割空间的思想,并在添加、删除操作时采用合并、分解结点的方法,保证树的平衡性。因此,R树就是一棵用来存储高维数据的平衡树。相对于B-Tree,R-Tree的优势在于范围查找。

在这里插入图片描述
小结:

使用索引可以帮助我们从海量的数据中快速定位想要查找的数据,不过索引也存在一些不足,比如占用存储空间、降低数据库写操作的性能等,如果有多个索引还会增加索引选择的时间。当我们使用索引时,需要平衡索引的利(提升查询效率)和弊(维护索引所需的代价)。在实际工作中,我们还需要基于需求和数据本身的分布情况来确定是否使用索引,尽管索引不是万能的,但数据量大的时候不使用索引是不可想象的,毕竟索引的本质,是帮助我们提升数据检索的效率。

2、InnoDB数据存储结构

2.1、数据库的存储结构:页

索引结构给我们提供了高效的索引方式,不过索引信息以及数据记录都保存在文件上的,确切说是存储在页结构中。另一方面,索引是在存储引擎中实现的,MySQL服务器上的存储引擎负责对表中数据的读取和写入工作。不同存储引擎中存放的格式一般不同的,甚至有的存储引擎比如Memory都不用磁盘来存储数据。

由于InnoDB是MySQL的默认存储引擎,所以本章剖析InooDB存储引擎的数据存储结构。

2.1.1、 磁盘与内存交互基本单位:页

InnoDB将数据划分为若干个页,InnoDB中页的大小默认为16KB

作为磁盘和内存之间交互的基本单位,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。也就是说,**在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。也就是说,数据库管理存储空间的基本单位是页(Page),数据库I/O操作的最小单位是页。**一个页中可以存储多个行记录。

记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率会非常低。

2.1.2、页结构概述

页a、页b、页c…页n这些页可以不在物理结构上相连,只要通过双向链表相关联即可。每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应的分组中的记录即可快速找到指定的记录。

2.1.3、页的上层结构

在这里插入图片描述
区(Extent)是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB

段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。

表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间用户表空间撤销表空间临时表空间等。

2.1.4、页的内部结构

页如果按类型划分的话,常见的有数据页(保存B+树节点)、系统页、Undo页和事务数据页等。数据页是我们最常使用的页。

数据页的16KB大小的存储空间被划分为七个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum+supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer) 。

页结构的示意图如下所示:
在这里插入图片描述
这7个部分作用分别如下,我们简单梳理如下表所示:

在这里插入图片描述

2.1.4.1、为什么需要Page Directory(页目录)?

在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。因此在页结构中专门设计了页目录这个模块,专门给记录做一个目录,通过二分查找法的方式进行检索,提升效率。

2.1.4.1.1、需求:根据主键值查找页中的某条记录,如何实现快速查找呢?
例如:SELECT *FROM page_demo WHERE c1 = 3;
2.1.4.1.1.1、方式1:顺序查找

从Infimum记录(最小记录)开始,沿着链表一直往后找,总有一天会找到(或者找不到),在找的时候还能投机取巧,因为链表中各个记录的值是按照从小到大顺序排列的,所以当链表的某个节点代表的记录的主键值大于你想要查找的主键值时,你就可以停止查找了,因为该节点后边的节点的主键值依次递增。如果一个页中存储了非常多的记录,这么查找性能很差。

2.1.4.1.1.2、方式2:使用页目录,二分法查找
  1. 将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“己删除”的记录。
  2. 第1组,也就是最小记录所在的分组只有1个记录;最后一组,就是最大记录所在的分组,会有1-8条记录;其余的组记录数量在4-8条之间。
    这样做的好处是,除了第1组(最小记录所在组)以外,其余组的记录数会尽量平分。
  3. 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为n_owned字段。
  4. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。

在这里插入图片描述
举例2:
现在的page_demo表中正常的记录共有6条,InnoDB会把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的5条记录。如下图:
在这里插入图片描述
从这个图中我们需要注意这么几点:

  • 现在页目录部分中有两个槽,也就意味着我们的记录被分成了两个组,槽1中的值是112,代表最大记录的地址偏移量(就是从页面的O字节开始数,数112个字节)﹔槽O中的值是99,代表最小记录的地址偏移量。
  • 注意最小和最大记录的头信息中的n_owned属性
  • 最小记录的n_owned值为1,这就代表着以最小记录结尾的这个分组中只有1条
    记录,也就是最小记录本身。
  • 最大记录的n_owned值为5,这就代表着以最大记录结尾的这个分组中只有5条记录,包括最大记录本身还有我们自己插入的4条记录。
    用箭头指向的方式替代数字,这样更易干我们理解,修改后如下:
    在这里插入图片描述
    再换个角度看一下:(单纯从逻辑上看一下这些记录和页目录的关系)

在这里插入图片描述

2.1.4.2、从数据页的角度看B+树如何查询一棵B+树

按照节点类型可以分成两部分:

  1. 叶子节点,B+树最底层的节点,节点的高度为0,存储行记录。
  2. 非叶子节点,节点的高度大于o,存储索引键和页面指针,并不存储行记录本身。
    在这里插入图片描述
    当我们从页结构来理解B+树的结构的时候,可以帮我们理解一些通过索引进行检索的原理:
2.1.4.2.1、B+树是如何进行记录检索的?

如果通过B+树的索引查询行记录,首先是从B+树的根开始,逐层检索,直到找到叶子节点,也就是找到对应的数据页为止,将数据页加载到内存中,页目录中的槽(slot)采用二分查找的方式先找到一个粗略的记录分组,然后再在分组中通过链表遍历的方式查找记录。

2.1.4.2、普通索引和唯一索引在查询效率上有什么不同?

我们创建索引的时候可以是普通索引,也可以是唯一索引,那么这两个索引在查询效率上有什么不同呢?

  • 唯一索引就是在普通索引上增加了约束性,也就是关键字唯一,找到了关键字就停止检索。
  • 普通索引,可能会存在用户记录中的关键字相同的情况,根据页结构的原理,当我们读取一条记录的时候,不是单独将这条记录从磁盘中读出去,而是将这个记录所在的页加载到内存中进行读取。InnoDB存储引擎的页大小为16KB,在一个页中可能存储着上千个记录,因此在普通索引的字段上进行查找也就是在内存中多几次“判断下一条记录”的操作,对于CPU来说,这些操作所消耗的时间是可以忽略不计的。
  • 所以对一个索引字段进行检索,采用普通索引还是唯一索引在检索效率上基本上没有差别。

2.2、区、段和碎片区

2.2.1、为什么要有区?

B+树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。我们介绍B+树索引的使用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,指针在磁盘上寻道的时间就会增加,然后读取的速度就会变慢,就是所谓的随机I/O。再一次强调,磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O

引入的概念,一个区就是物理位置上连续的64个页。因为InnoDB中的页的大小默认是16KB,所以一个区的大小是64*16KB=1MB。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页的单位分配了,而是按照区为单位分配,甚至在表中的数据特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费(数据不足以填充满整个区),但是从性能角度看,可以消除很多的随机I/O,功大于过

2.2.1.1、随机读取

如果数据没有在内存中,就需要在磁盘上对该页进行查找,整体时间预估在_10ms 左右,这10ms 中有6ms是磁盘的实际繁忙时间(包括了寻道和半圈旋转时间),有3ms是对可能发生的排队时间的估计值,另外还有1ms的传输时间,将页从磁盘服务器缓冲区传输到数据库缓冲区中。这10ms看起来很快,但实际上对于数据库来说消耗的时间已经非常长了,因为这还只是一个页的读取时间。
在这里插入图片描述

2.2.1.2、顺序读取

顺序读取其实是一种批量读取的方式,因为我们请求的数据在磁盘上往往都是相邻存储的,顺序读取可以帮我们批量读取页面,这样的话,一次性加载到缓冲池中就不需要再对其他页面单独进行磁盘I/O操作了。如果一个磁盘的吞吐量是40MB/S,那么对于一个16KB大小的页来说,一次可以顺序读取2560 (4OMB/16KB)个页,相当于一个页的读取时间为0.4ms。采用批量读取的方式,即使是从磁盘上进行读取,效率也比从内存中只单独读取一个页的效率要高。

2.2.2、为什么要有段?

对于范围查询,其实是对B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以InnoDB对B+树的叶子节点非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段

除了索引的叶子节点段和非叶子节点段之外,InnoDB中还有为存储一些特殊的数据而定义的段,比如回滚段。所以,常见的段有数据段索引段回滚段。数据段即为B+树的叶子节点,索引段即为B+树的非叶子节点。

在InnoDB存储引擎中,对段的管理都是由引擎自身所完成,DBA不能也没有必要对其进行控制。这从一定程度上简化了DBA对于段的管理。

段其实不对应表空间中的某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。

2.2.3、为什么要有碎片区?

默认情况下,一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M(64*16KB=1024KB)存储空间,所以**默认情况下一个只存在几条记录的小表也需要2M的存储空间么?**以后每次添加一个索引都要多申请2M的存储空间么?这对于存储记录比较少的表简直是天大的浪费。这个问题的症结在于到现在为止我们介绍的区都是非常纯粹的,也就是一个区被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作他用。

为了考虑以完整的区为单位分配给某个段对于数据量较小的表太浪费存储空间的这种情况,InnoDB提出了一个碎片(fragment)区的概念。在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页面用于段A,有些页面用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。

所以此后为某个段分配存储空间的策略是这样的:

  • 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的,
  • 当某个段已经占用了32个碎片区页面之后,就会申请以完整的区为单位来分配存储空间。

所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面已经一些完整的区的集合。

如果把表空间比作是一个集团军,段就相当于师,区就相当于团。一般的团都是隶属于某个师的,就像是处于FSEG的区全都隶属于某个段,而处于FREE、FREE_FRAG以及FULL_FRAG这三种状态的区却直接隶属于表空间,就像独立团直接听命于军部一样。

2.2.4、区的分类

区大体上可以分为4种类型:

  • 空闲的区(FREE):现在还没有用到这个区中的任何页面。
  • 有剩余空间的碎片区(FREE_FRAG):表示碎片区中还有可用的页面。
  • 没有剩余空间的碎片区(FULL_FRAG):表示碎片区中的所有页面都被使用,没有空闲页面。
  • 附属于某个段的区(FSEG):每一索引都可以分为叶子节点段和非叶子节点段

处于FREEFREE_FRAG以及FULL_FRAG这三种状态的区都是独立的,直属于表空间。而处于FSEG状态的区是附属于某个段的。

3、索引的创建与设计原则

3.1、索引的分类

MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。

  • 从功能逻辑 上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。
  • 按照 物理实现方式 ,索引可以分为 2 种:聚簇索引和非聚簇索引。
  • 按照 作用字段个数 进行划分,分成单列索引和联合索引。

3.1.1、普通索引

在创建普通索引时,不附加任何限制条件,只是用于提高查询效率。这类索引可以创建在任何数据类型中,其值是否唯一和非空,要由字段本身的完整性约束条件决定。建立索引以后,可以通过索引进行查询。例如,在表student的字段name 上建立一个普通索引,查询记录时就可以根据该索引进行查询。

3.1.2、唯一性索引

使用UNIQUE参数可以设置索引为唯一性索引,在创建唯一性索引时,限制该索引的值必须是唯一的但允许有空值。在一张数据表里可以有多个唯一索引。例如,在表student的字段email中创建唯一性索引,那么字段email的值就必须是唯一的。通过唯一性索引,可以更快速地确定某条记录。

3.1.3、主键索引

主键索引就是一种特殊的唯一性索引,在唯一索引的基础上增加了不为空的约束,也就是NOT NULL+UNIQUE,一张表里最多只有一个主键索引。

3.1.4、单列索引

在表中的单个字段上创建索引。单列索引只根据该字段进行索引。单列索引可以是普通索引,也可以是唯一性索引,还可以是全文索引。只要保证该索引只对应一个字段即可。一个表可以有多个单列索引。

3.1.5、多列(组合、联合)索引

多列索引是在表的多个字段组合上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询,但是只有查询条件中使用了这些字段中的第一个字段时才会被使用。例如,在表中的字段id,name和,gender上建立一个多列索引idx_id_name_gender,只有在查询条件中使用了字段id时该索引才会被使用。使用组合索引时遵循最左前缀集合。

3.1.6、全文索引

全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果

全文索引非常适合大型数据集,对于小的数据集,它的用处比较小。使用参数FULLTEXT可以设置索引为全文索引。在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引只能创建在CHAR、VARCHAR 或TEXT类型及其系列类型的字段上,查询数据量较大的字符串类型的字段时,使用全文索引可以提高查询速度。

例如,表student的字段information是TEXT类型,该字段包含了很多文字信息。在字段information上建立全文索引后,可以提高查询字段information的速度。

全文索引典型的有两种类型:自然语言的全文索引和布尔全文索引。

  • 自然语言搜索引擎将计算每一个文档对象和查询的相关度。这里,相关度是基于匹配的关键词的个数,以及关键词在文档中出现的次数。在整个索引中出现次数越少的词语,匹配时的相关度就越高。相反,非常常见的单词将不会被搜索,如果一个词语的在超过50%的记录中都出现了,那么自然语言的搜索将不会搜索这类词语。

MySQL数据库从3.23.23版开始支持全文索引,但MySQL5.6.4以前只有Myisam支持,5.6.4版本以后innodb才支持,但是官方版本不支持中文分词,需要第三方分词插件。在5.7.6版本,MySQ L内置了ngram全文解析器,用来支持亚洲语种的分词。测试或使用全文索引时,要先看一下自己的MySQL版本、存储引擎和数据类型是否支持全文索引。
随着大数据时代的到来,关系型数据库应对全文索引的需求已力不从心,逐渐被solr、ElasticSearch等专门的搜索引擎所替代。

3.1.7、补充:空间索引

使用参数SPATIAL可以设置索引为空间索引。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间数据的效率。MySQL中的空间数据类型包括GEOMETRY、POINT、LINESTRING和POLYGON等。目前只有MyISAM存储引擎支持空间检索,而且索引的字段不能为空值。对于初学者来说,这类索引很少会用到。

3.1.8、小结

不同的存储引擎支持的索引类型也不一样

  • InnoDB:支持B-tree、Full-text等索引,不支持 Hash 索引;
  • MyISAM:支持B-tree、Full-text等索引,不支持Hash索引;
  • Memory :支持B-tree、Hash 等索引,不支持Full-text索引;
  • NDB:支持Hash索引,不支持B-tree、Full-text等索引;
  • Archive :不支持B-tree、Hash、Full-text等索引;

3.2、创建索引

CREATE TABLE table_name [col_name data_type] 
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC]
  • UNIQUEFULLTEXTSPATIAL为可选参数,分别表示唯一索引、全文索引和空间索引;
  • INDEXKEY为同义词,两者的作用相同,用来指定创建索引;
  • index_name指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名;
  • col_name为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择;
  • length为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;
  • ASCDESC指定升序或者降序的索引值存储。

3.2.1、创建普通索引

在book表中的year_publication字段上建立普通索引,SQL语句如下:

CREATE TABLE book( 
    book_id INT , 
    book_name VARCHAR(100), 
    authors VARCHAR(100), 
    info VARCHAR(100) , 
    comment VARCHAR(100), 
    year_publication YEAR, 
    INDEX(year_publication) 
);

3.2.2、创建唯一索引

举例:

CREATE TABLE test1( 
    id INT NOT NULL, 
    name varchar(30) NOT NULL, 
    UNIQUE INDEX uk_idx_id(id) 
);

3.2.3、创建主键索引

CREATE TABLE student ( 
    id INT(10) UNSIGNED AUTO_INCREMENT, 
    student_no VARCHAR(200),
    student_name VARCHAR(200), 
    PRIMARY KEY(id) 
);
# 删除主键索引
ALTER TABLE student drop PRIMARY KEY ;

3.2.4、创建单列索引

CREATE TABLE test2( 
    id INT NOT NULL, 
    name CHAR(50) NULL, 
    INDEX single_idx_name(name(20)) 
);

3.2.5、创建组合索引

CREATE TABLE test3( 
    id INT(11) NOT NULL, 
    name CHAR(30) NOT NULL, 
    age INT(11) NOT NULL, 
    info VARCHAR(255), 
    INDEX multi_idx(id,name,age) 
);

3.2.6、创建全文索引

CREATE TABLE `papers` ( 
    id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
    `title` varchar(200) DEFAULT NULL, 
    `content` text, PRIMARY KEY (`id`), 
    FULLTEXT KEY `title` (`title`,`content`) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’);

3.2.7、创建空间索引

CREATE TABLE test5( 
    geo GEOMETRY NOT NULL, 
    SPATIAL INDEX spa_idx_geo(geo) 
) ENGINE=MyISAM;

3.2.8、在已经存在的表上创建索引

3.2.8.1、使用ALTER TABLE语句创建索引

ALTER TABLE table_name 
ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name[length],...) [ASC | DESC]

3.2.8.2、使用CREATE INDEX创建索引

CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name 
ON table_name (col_name[length],...) [ASC | DESC]

3.2.9、删除索引

3.2.9.1、使用ALTER TABLE删除索引

ALTER TABLE table_name DROP INDEX index_name;

3.2.9.2、使用DROP INDEX语句删除索引

DROP INDEX index_name ON table_name;

提示 删除表中的列时,如果要删除的列为索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个索引将被删除。

3.3、MySQL8.0索引新特性

3.3.1、支持降序索引

CREATE TABLE ts1(a int,b int,index idx_a_b(a,b desc));

3.3.2、隐藏索引

在MySQL 5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。

从MySQL 8.x开始支持隐藏索引(invisible indexes),只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除

3.3.2.1、创建表时直接创建*
CREATE TABLE tablename( 
    propname1 type1[CONSTRAINT1], 
    propname2 type2[CONSTRAINT2], 
    ……
    propnamen typen, 
    INDEX [indexname](propname1 [(length)]) INVISIBLE 
);
3.3.2.2、在已经存在的表上创建
CREATE INDEX indexname 
ON tablename(propname[(length)]) INVISIBLE;
3.3.2.3、通过ALTER TABLE语句创建
ALTER TABLE tablename 
ADD INDEX indexname (propname [(length)]) INVISIBLE;
3.3.2.4、切换索引可见状态
ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引 
ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引

注意 当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。

3.4、索引的设计原则

为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引。索引设计不合理或者缺少索引都会对数据库和应用程序的性能造成障碍。高效的索引对于获得良好的性能非常重要。设计索引时,应该考虑相应准则。

3.4.1、哪些情况适合创建索引

3.4.1.1、字段的数值有唯一性的限制

索引本身可以起到约束的作用,比如唯一索引、主键索引都可以起到唯一性约束的,因此在我们的数据表中,如果某个字段是唯一的,就可以直接创建唯一性索引,或者主键索引。这样可以更快速地通过该索引来确定某条记录。
例如,学生表中学号是具有唯一性的字段,为该字段建立唯一性索引可以很快确定某个学生的信息,如果使用姓名的话,可能存在同名现象,从而降低查询速度。

业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)

说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但提高查找速度是明显的。

3.4.1.2、频繁作为WHERE查询条件的字段

某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。
比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。

3.4.1.3、经常GROUP BY和ORDER BY的列

索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要对分组或者排序的字段进行索引。如果待排序的列有多个,那么可以在这些列上建立组合索引

3.4.1.4、UPDATE、DELETE的WHERE条件列

对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。

3.4.1.5、DISTINCT字段需要创建索引

有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。

3.4.1.6、多表JOIN连接操作时,创建索引注意事项

首先,连接表的数量尽量不要超过 3 张,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。
其次,对 WHERE 条件创建索引,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。
最后,对用于连接的字段创建索引,并且该字段在多张表中的类型必须一致,因为不同的类型,会进行转换后在进行比较,转换会用到函数,使用函数,索引就会失效。

3.4.1.7、使用列的类型小的创建索引

我们这里所说的类型大小指的就是该类型表示的数据范围的大小。

  • 数据类型越小,在查询时进行的比较操作越快
  • 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/O带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。

这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键使用更小的数据类型,也就意味着节省更多的存储空间和更高效的I/O。

3.4.1.8、使用字符串前缀创建索引

假设我们的字符串很长,那存储一个字符串就需要占用很大的存储空间。在我们需要为这个字符串列建立索引时,那就意味着在对应的B+树中有这么两个问题:

  • B+树索引中的记录需要把该列的完整字符串存储起来,更费时。而且字符串越长,在索引中占用的存储空间越大。
  • 如果B+树索引中索引列存储的字符串很长,那在做字符串比较时会占用更多的时间。

我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。
这样在查找记录时虽然不能精确的定位到记录的位置,但是能定位到相应前缀所在的位置,然后根据前缀相同的记录的主键值回表查询完整的字符串值。既节约空间,又减少了字符串的比较时间,还大体能解决排序的问题。例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间,如果只检索字段前面的若干字符,这样可以提高检索速度。
创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引

create table shop ( address varchar ( 120) not null) ;
alter table shop add index ( address( 12));

问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多字段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?

  • 先看一下字段在全部数据中的选择度:
select count( distinct address) / count( * ) from shop ;
  • 通过不同长度去计算,与全表的选择性对比:
    区分度计算公式:
count(distinct left(列名, 索引长度))/count(*)

例如:

select count(distinct left(address,10)) / count(*) as sub10,--截取前10个字符的选择度
count(distinct left(address,15)) /l count(*) as sub11,--截取前15个字符的选择度
count(distinct left(address,2)) / count(*) as sub12,--截取前20个字符的选择度
count(distinct left(address,25)) / count(*) as sub13 -- 截取前25个字符的选择度
from shop;

引申另一个问题:索引列前缀对排序的影响

如果使用了索引列前缀,比方说前边只把address列的前12个字符放到了二级索引中,下边这个查询可能就有点儿尴尬了︰

SELECT *FROM shopORDER BY addressLIMIT 12;

因为二级索引中不包含完整的address列信息,所以无法对前12个字符相同,后边的字符不同的记录进行排序,也就是使用索引列前缀的方式无法支持使用索引排序,只能使用文件排序。
拓展:Alibaba《Java开发手册》
强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上

3.4.1.9、区分度高(散列性高)的列适合作为索引

列的基数指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8,虽然有9条记录,但该列的基数却是3。也就是说,**在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中的值越集中。**这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引,为基数太小的列建立索引效果可能不好。

可以使用公式select count(distinct a)/count(*) from t1计算区分度,越接近1越好,一般超过33%就算是比较高效的索引了。

拓展:联合索引把区分度高(散列性高)的列放在前面。

3.4.1.10、使用最频繁的列放到联合索引的左侧

这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。

3.4.1.11、在多个字段都要创建索引的情况下,联合索引优于单值索引
3.4.1.12、限制索引的数目

在实际工作中,我们也需要注意平衡,索引的数目不是越多越好。我们需要限制每张表上的索引数量,建议单张表索引数量不超过6个。原因:

  • 每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。
  • 索引会影响INSERT、DELETE、UPDATE等语句的性能,因为表中的数据更改的同时,索引也会进行调整和更新,会造成负担。
  • 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,会增加MySQL优化器生成执行计划时间,降低查询性能。

3.4.2、哪些情况不适合创建索引

3.4.2.1、在where中使用不到的字段,不要设置索引

WHERE条件(包括GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的。举个例子:

SELECT course_id, student_id, create_timeFROM student_info
WHERE student_id = 41251;

因为我们是按照student_id 来进行检索的,所以不需要对其他字段创建索引,即使这些字段出现在SELECT 字段

3.4.2.2、数据量小的表最好不要使用索引

如果表记录太少,比如少于100Q个,那么是不需要创建索引的。表记录太少,是否创建索引对查询效率的影响并不大。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。

3.4.2.3、有大量重复数据的列上不要建立索引

在条件表达式中经常用到的不同值较多的列上建立索引,但字段中如果有大量重复数据,也不用创建索引。比如在学生表的“性别"字段上只有“男”与“女”两个不同值,因此无须建立索引。如果建立索引,不但不会提高查询效率,反而会严重降低数据更新速度。

举例1:要在100万行数据中查找其中的50万行(比如性别为男的数据),一旦创建了索引,你需要先访问50万次索引,然后再访问50万次数据表,这样加起来的开销比不使用索引可能还要大。

3.4.2.4、避免对经常更新的表创建过多的索引
  • 第一层含义︰频繁更新的字段不一定要创建索引。因为更新数据的时候,也需要更新索引,如果索引太多,在更新索引的时候也会造成负担,从而影响效率。
  • 第二层含义:避免对经常更新的表创建过多的索引,并且索引中的列尽可能少。此时,虽然提高了查询速度,同时却会降低更新表的速度。
3.4.2.5、不建议用无序的值作为索引

例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等。

3.4.2.6、删除不再使用或者很少使用的索引

表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。

3.4.2.8、不要定义冗余或重复的索引

4、性能分析工具的使用

4.1、数据库服务器的优化步骤

当我们遇到数据库调优问题的时候,该如何思考呢?这里把思考的流程整理成下面这张图。

整个流程划分成了 观察(Show status) 和 行动(Action) 两个部分。字母 S 的部分代表观察(会使用相应的分析工具),字母 A 代表的部分是行动(对应分析可以采取的行动)。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们可以通过观察了解数据库整体的运行状态,通过性能分析工具可以让我们了解执行慢的SQL都有哪些,查看具体的SQL执行计划,甚至是sQL执行中的每一步的成本代价,这样才能定位问题所在,找到了问题,再采取相应的行动。

详细解释一下这张图:

首先在S1部分,我们需要观察服务器的状态是否存在周期性的波动。如果存在周期性波动,伺可能是同期性卫尽的原因,比如双十一、促销活动等。这样的话,我们可以通过A1这一步骤解决,也就是加缓存,或者更改缓存失效策略。
如果缓存策略没有解决,或者不是周期性波动的原因,我们就需要进一步分析查询延迟和卡顿的原因,接下来进入S2这一步,我们需要开启慢查询。

慢查询可以帮我们定立执行慢的SQL语句。我们可以通过设置long.query_time参数定义“慢”的阈值,如果SQL执行时间超过了long_query_time,则会认为是慢查询。当收集上来这些慢查询之后,我们就可以通过分析工具对慢查询日志进行分析。

在S3这一步骤中,我们就知道了执行慢的sQL,这样就可以针对性地用EXPLAIN查看对应sQL语句的执行计划,或者使用show profile查看sQL中每一个步骤的时间成本。这样我们就可以了解SQL查询慢是因为执行时间长,还是等待时间长。

如果是SQL等待时间长,我们进入A2步骤。在这一步骤中,我们可以调优服务器的参数,比如适当增加数据库缓冲池等。如果是SQL执行时间长,就进入A3步骤,这一步中我们需要考虑是索引设计的问题?还是查询关联的数据表过多?还是因为数据表的字段设计问题导致了这一现象。然后在这些维度上进行对应的调整。

如果A2和A3都不能解决问题,我们需要考虑数据库自身的sQL查询性能是否已经达到了瓶颈,如果确认没有达到性能瓶颈,就需要重新检查,重复以上的步骤。如果已经达到了性能瓶颈,进入A4阶段,需要考虑增加服务器,采用读写分离的架构,或者考虑对数据库进行分库分表,比如垂直分库、垂直分表和水平分表等。

以上就是数据库调优的流程思路。如果我们发现执行sQL时存在不规则延迟或卡顿的时候,就可以采用分析工具帮我们定位有问题的sQL,这三种分析工具你可以理解是SQL调优的三个步骤:慢查询、EXPLAIN和SHOWPROFILING。
在这里插入图片描述

4.2、查看系统性能参数

在MySQL中,可以使用 SHOW STATUS 语句查询一些MySQL数据库服务器的性能参数 、执行频率 。
SHOW STATUS语句语法如下:

SHOW [GLOBAL|SESSION] STATUS LIKE '参数';

一些常用的性能参数如下:

  • Connections:连接MySQL服务器的次数。
  • Uptime:MySQL服务器的上线时间。
  • Slow_queries:慢查询的次数。
  • Innodb_rows_read:Select查询返回的行数
  • Innodb_rows_inserted:执行INSERT操作插入的行数
  • Innodb_rows_updated:执行UPDATE操作更新的行数
  • Innodb_rows_deleted:执行DELETE操作删除的行数
  • Com_select:查询操作的次数。
  • Com_insert:插入操作的次数。对于批量插入的 INSERT 操作,只累加一次。
  • Com_update:更新操作的次数。
  • Com_delete:删除操作的次数。

4.3、统计SQL的查询成本:last_query_cost

一条SQL查询语句在执行前需要确定查询执行计划,如果存在多种执行计划的话,MySQL会计算每个执行计划所需要的成本,从中选择成本最小的一个作为最终执行的执行计划。
如果我们想要查看某SQL语句的查询成本,可以在执行完这条SQL语句之后,通过查看当前会话中的last_query_cost变量值来得到当前查询的成本。它通常也是我们评价一个查询的执行效率的一个常用指标。这个查询成本对应的是SOL语句所需要读取的页的数量。

SHOW STATUS LIKE 'last_query_cost';

使用场景:它对于比较开销是非常有用的,特别是我们有好几种查询方式可选的时候。

SQL 查询是一个动态的过程,从页加载的角度来看,我们可以得到以下两点结论:

  1. 位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。
  2. 批量决定效率。如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多10ms),而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。

所以说,遇到I/O并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是经常使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升。

5、定位执行慢的 SQL:慢查询日志

5.1、开启慢查询日志参数

MySQL的慢查询日志,用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过long_query_time的值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上(不含10秒)的语句,认为是超出了我们的最大忍耐时间值。

它的主要作用是,帮助我们发现那些执行时间特别长的sQL查询,并且有针对性地进行优化,从而提高系统的整体效率。当我们的数据库服务器发生阻塞、运行变慢的时候,检查一下慢查询日志,找到那些慢查询,对解决问题很有帮助。比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合explain进行全面分析。

默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。

慢查询日志支持将日志记录写入文件。

5.1.1、开启slow_query_log

set global slow_query_log='ON';

查看下慢查询日志是否开启,以及慢查询日志文件的位置:

show variables like `%slow_query_log%`;

5.1.2、修改long_query_time阈值

show variables like '%long_query_time%';

测试发现:设置global的方式对当前session的long_query_time失效。对新连接的客户端有效。所以可以一并执行下述语句

mysql > set global long_query_time = 1; 
mysql> show global variables like '%long_query_time%'; 

mysql> set long_query_time=1; 
mysql> show variables like '%long_query_time%';

5.1.3、查看慢查询数目

SHOW GLOBAL STATUS LIKE '%Slow_queries%';

5.1.4、慢查询日志分析工具:mysqldumpslow

在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具mysqldumpslow 。
查看mysqldumpslow的帮助信息

mysqldumpslow --help

在这里插入图片描述

mysqldumpslow 命令的具体参数如下:

  • -a: 不将数字抽象成N,字符串抽象成S
  • -s: 是表示按照何种方式排序:
    • c: 访问次数
    • l: 锁定时间
    • r: 返回记录
    • t: 查询时间
    • al:平均锁定时间
    • ar:平均返回记录数
    • at:平均查询时间 (默认方式)
    • ac:平均查询次数
  • -t: 即为返回前面多少条的数据;
  • -g: 后边搭配一个正则匹配模式,大小写不敏感的;

举例:我们想要按照查询时间排序,查看前五条 SQL 语句,这样写即可:

mysqldumpslow -s t -t 5 /var/lib/mysql/atguigu01-slow.log

[root@bogon ~]# mysqldumpslow -s t -t 5 /var/lib/mysql/atguigu01-slow.log 
Reading mysql slow query log from /var/lib/mysql/atguigu01-slow.log 
Count: 1 Time=2.39s (2s) Lock=0.00s (0s) Rows=13.0 (13), root[root]@localhost SELECT * FROM student WHERE name = 'S' 
Count: 1 Time=2.09s (2s) Lock=0.00s (0s) Rows=2.0 (2), root[root]@localhost SELECT * FROM student WHERE stuno = N 
Died at /usr/bin/mysqldumpslow line 162, <> chunk 2.
#得到返回记录集最多的10个SQL 
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log 
#得到访问次数最多的10个SQL 
mysqldumpslow -s c -t 10 /var/lib/mysql/atguigu-slow.log
#得到按照时间排序的前10条里面含有左连接的查询语句 
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/atguigu-slow.log 
#另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现爆屏情况 
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log | more

5.1.5、关闭慢查询日志

  • 方式1:永久性方式
[mysqld] 
slow_query_log=OFF
#或
[mysqld] 
#slow_query_log =OFF
  • 方式2:临时性方式
SET GLOBAL slow_query_log=off;

5.1.5、删除慢查询日志

使用SHOW语句显示慢查询日志信息,具体SQL语句如下。

SHOW VARIABLES LIKE 'slow_query_log% ';

使用命令mysqladmin flush-logs来重新生成查询日志文件,具体命令如下,执行完毕会在数据目录下重新生成慢查询日志文件。

mysqladmin -uroot -p fush-logs slow

5.2、查看SQL执行成本:SHOW PROFILE

show variables like 'profiling';
#开启
set profiling = 'ON';
#查看
show profiles;
show profile cpu,block io for query 2;

show profile的常用查询参数:

  • ALL:显示所有的开销信息。
  • BLOCK IO:显示块Io开销。
  • CONTEXT SWITCHES: 上下文切换开销。
  • CPU:显示CPU开销信息。
  • IPC:显示发送和接收开销信息。
  • MEMORY:显示内存开销信息。
  • PAGE FAULTS:显示页面错误开销信息。
  • SOURCE:显示和Source_function,Source_file,Source_line相关的开销信息。
  • SWAPS:显示交换次数开销信息。

5.3、分析查询语句:EXPLAIN

定位了查询慢的SQL之后,我们就可以使用EXPLAIN或DESCRIBE 工具做针对性的分析查询语句DESCRIBE语句的使用方法与EXPLAIN语句是一样的,并且分析结果也是一样的。

MySQL中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供它认为最优的执行计划(他认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)。

这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。MysQL为我们提供了EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划,大家看懂EXPLAIN语句的各个输出项,可以有针对性的提升我们查询语句的性能。

5.3.1、概要

EXPLAIN SELECT select_options 
#或者
DESCRIBE SELECT select_options

如果我们想看看某个查询的执行计划的话,可以在具体的查询语句前边加一个 EXPLAIN ,就像这样 :

mysql> EXPLAIN SELECT 1;

在这里插入图片描述

输出的上述信息就是所谓的执行计划。在这个执行计划的辅助下,我们需要知道应该怎样改进自己的查询语句以使查询执行起来更高效。其实除了以SELECT开头的查询语句,其余的DELETE、INSERT、REPLACE以及UPDATE语句等都可以加上 EXPLAIN,用来查看这些语句的执行计划,只是平时我们对SELECT语句更感兴趣。

注意:执行EXPLAIN时并没有真正的执行该后面的语句,因此可以安全的查看执行计划。

EXPLAIN 语句输出的各个列的作用如下:

列名 描述
id 在一个大的查询语句中每个SELECT关键字都对应一个唯一的id
select_type SELECT关键字对应的那个查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的访问方法
possible_keys 可能用到的索引
key 实际上使用的索引
key_len 实际使用到的索引长度
ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要读取的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
Extra 一些额外的信息

5.3.2、基本语法

测试用的表结构

CREATE TABLE s1 ( 
id INT AUTO_INCREMENT, 
key1 VARCHAR(100), 
key2 INT, 
key3 VARCHAR(100), 
key_part1 VARCHAR(100), 
key_part2 VARCHAR(100), 
key_part3 VARCHAR(100), 
common_field VARCHAR(100), 
PRIMARY KEY (id), 
INDEX idx_key1 (key1), 
UNIQUE INDEX idx_key2 (key2), 
INDEX idx_key3 (key3), 
INDEX idx_key_part(key_part1, key_part2, key_part3) ) ENGINE=INNODB CHARSET=utf8;

5.3.2.1、table

不论我们的查询语句有多复杂,包含了多少个表 ,到最后也是需要对每个表进行单表访问的,所以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名(有时会出现中间表)。

5.3.2.2、id n

  • id如果相同,可以认为是一组,从上往下顺序执行
  • 在所有组中,id值越大,优先级越高,越先执行
  • 关注点:id号每个号码,表示一趟独立的查询,一个sql的查询趟数越少越好

5.3.2.3、select_type

一条大的查询语句里边可以包含若干个SELECT关键字,每个SELECT关键字代表着一个小的查询语句而每个SELECT关键字的FROM子句中都可以包含若干张表(这些表用来做连接查询),每一张表都对应着执行计划输出中的一条记录,对于在同一个SELECT关键字中的表来说,它们的id值是相同的。

MySQL为每一个SELECT关键字代表的小查询都定义了一个称之为select_type的属性,意思是我们只要知道了某个小查询的select_type属性,就知道了这个小查询在整个大查询中扮演了一个什么角色,我们看一下select_type都能取哪些值.

请看官方文档:
在这里插入图片描述
select_type 属性 含义

  • SIMPLE 简单的 select 查询,查询中不包含子查询或者 UNION
  • PRIMARY 查询中若包含任何复杂的子部分,最外层查询则被标记为 Primary
  • DERIVED 在 FROM 列表中包含的子查询被标记为 DERIVED(衍生) MySQL 会递归执行这些子查询, 把结果放在临时表里。
  • SUBQUERY 在SELECT或WHERE列表中包含了子查询
  • DEPEDENT SUBQUERY 在SELECT或WHERE列表中包含了子查询,子查询基于外层
  • UNCACHEABLE SUBQUERY 无法使用缓存的子查询
  • UNION 若第二个SELECT出现在UNION之后,则被标记为UNION; 若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
  • UNION RESULT 从UNION表获取结果的SELECT

查询语句中不包含UNION或者子查询的查询都算作是SIMPLE类型,连接查询也算是SIMPLE类型

5.3.2.4、partitions

代表分区表中的命中情况,非分区表,该项为NULL 。一般情况下我们的查询语句的执行计划的partitions列的值都是NULL。

5.3.2.5、type ☆

执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法,又称“访问类型”,其中的type列就表明了这个访问方法是啥,是较为重要的一个指标。
比如,看到type列的值是ref,表明MySQL即将使用ref访问方法来执行对s1表的杳询。

完整的访问方法如下: system , const , eq_ref , ref , fulltext , ref_or_null , index_merge , unique_subquery , index_subquery , range , index , ALL 。

type 是查询的访问类型。是较为重要的一个指标,结果值从最好到最坏依次是: system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index >ALL ,一般来说,得保证查询至少达到 range 级别,最好能达到 ref。

5.3.2.5.1、 system

system:当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system。表只有一行记录(等于系统表),这是 const 类型的特列,平时不会出现,这个也可以忽略不计。

比方说我们新建一个MyISAM表,并为其插入一条记录:

mysql> CREATE TABLE t(i int) Engine=MyISAM; Query OK, 0 rows affected (0.05 sec) 
mysql> INSERT INTO t VALUES(1); Query OK, 1 row affected (0.01 sec)

然后我们看一下查询这个表的执行计划:mysql> EXPLAIN SELECT * FROM t;
在这里插入图片描述

5.3.2.5.2、const

表示通过索引一次就找到了,const 用于比较 primary key 或者 unique 索引。因为只匹配一行数据,所以很快 如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量

5.3.2.5.3、eq_ref

在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq ref

EXPIAIN SELECT*FROM s1 INNERJOIN s2 ON s1.id = s2.id;

在这里插入图片描述

5.3.2.5.4、ref

当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref

EXPLAIN SELECTFROM s1 WHERE key1 = 'a';

在这里插入图片描述

5.3.2.5.5、fulltext

全文索引

5.3.2.5.6、ref_or_null

当对普通二级索引进行等值匹配查询,该索引列的值也可以是NUL值时,那么对该表的访问方法#就可能是ref_or_null

EXPLAIN SELECT *FROM s1 WHERE key1 = 'a' OR key1 IS NOLL;

在这里插入图片描述

5.3.2.5.7、index_merge

单表访问方法时在某些场景下可以使用Intersectionunion.Sort-Union这三种索引合并的方式来执行香询

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';

在这里插入图片描述

5.3.2.5.8、unique_subquery

unique subquery是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询,转换为EXISTS子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的type列的值就是unique_subquery

 EXPLAIN SELECT * FROM s1 WHERE key2 IN (SELECT id FROM s2 where s1.key1 = s2.key1) OR key3 = 'a';

在这里插入图片描述

5.3.2.5.9、index_subquery

利用索引来关联子查询,不再全表扫描

EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT key3 FROM s2 where s1.key1 = s2.key1) OR key3 = 'a';

5.3.2.5.10、range

只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引一般就是在你的 where 语句中出现 了 between、<、>、in 等的查询这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而 结束语另一点,不用扫描全部索引

EXPLAIN SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c');

在这里插入图片描述

5.3.2.5.11、index

出现index是sql使用了索引,但是没有通过索引进行过滤,一般是使用了覆盖索引或者是利用索引进行了排序分组。覆盖索引是指不需要回表就能找到数据返回。key_part3 属于联合索引,key_part2 也属于联合索引

  EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = 'a';

在这里插入图片描述

5.3.2.5.12、all

Full Table Scan,将遍历全表以找到匹配的行。

 EXPLAIN SELECT * FROM s1;

在这里插入图片描述
SQL性能优化的目标:至少要达到 range级别,要求是ref级别,最好是consts级别。(阿里巴巴开发手册要求)

5.3.2.6、possible_keys和key

在EXPLAIN语句输出的执行计划中

  • possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。一般查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。
  • key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引。

比方说下边这个查询:

EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key3 = 'a';

在这里插入图片描述

5.3.2.7、key_len

表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。 key_len 字段能够帮你检查是否充分的利用上了索引。ken_len 越长,说明索引使用的越充分。主要针对于联合索引,有一定的参考意义。

key_len的长度计算公式:

varchar(10)变长字段且允许NULL = 10 * ( character set: utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段) 
varchar(10)变长字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+2(变长字段)
char(10)固定字段且允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+1(NULL) 
char(10)固定字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)

5.3.2.7、ref

当使用索引列等值查询时,与索引列进行等值匹配的对象信息。

5.3.2.8、rows

rows 列显示 MySQL 认为它执行查询时必须检查的行数。越少越好!

5.3.2.9、 filtered

某个表经过搜索条件过滤后剩余记录条数的百分比,如果使用的是索引执行的单表扫描,那么计算时需要估计出满足除使用到对应索引的搜索条件外的其他搜索条件的记录有多少条。

EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND common_field = 'a';

在这里插入图片描述

对于单表查询来说,这个filtered列的值没什么意义,我们更关注在连接查询中驱动表对应的执行计划记录的filtered值,它决定了被驱动表要执行的次数(即: rows * filtered)

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1 WHERE s1.common_field = ‘a’;
在这里插入图片描述

5.3.2.10、Extra

顾名思义,Extra列是用来说明一些额外信息的,包含不适合在其他列中显示但十分重要的额外信息。我们可以通过这些额外信息来更准确的理解MySQL到底将如何执行给定的查询语句。

5.3.2.10.0、No tables used

当查询语句的没有FROM子句时将会提示该额外信息
在这里插入图片描述

5.3.2.10.1、Impossible WHERE

查询语句的WHERE子句永远为FALSE时将会提示该额外信息

EXPLAIN SELECT * FROM s1 WHERE 1 != 1;
5.3.2.10.2、Using where

当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时,在Extra列中会提示上述额外信息。

EXPLAIN SELECT * FROM s1 WHERE common_field = 'a';

在这里插入图片描述
当使用索引访问来执行对某个表的查询,并且该语句的WHERE子句中,有除了该索引包含的列之外的其他搜索条件时,在`Extra '列中也会提示上述额外信息。

 EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' AND common_field = 'a';

在这里插入图片描述

5.3.2.10.3、No matching min/max row

当查询列表处有MIN或者MAx聚合函数,但是并没有符合WHERE子句中的搜索条件的记录时,将会提示该额外信息

EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'abcdefg';

在这里插入图片描述

5.3.2.10.4、Using index

当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下,在Extra列将会提示该额外信息。不需要回表操作。

EXPLAIN SELECT key1 FROM s1 WHERE key1 = 'a';

在这里插入图片描述

5.3.2.10.5、Using index condition

有些搜索条件中虽然出现了索引列,但却不能使用到索引。

EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%b';

在这里插入图片描述

5.3.2.10.6、Using join buffer

在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一股会为其分配一块名叫join buffer的内存块来加快查询速度,也就是我们所讲的基于块的嵌套循环算法

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field;

在这里插入图片描述

5.3.2.10.7、Not exists

当我们使用左(外)连接时,如果WHERE子句中包含要求被驱动表的某个列等于NULL值的搜索条件,而且那个列又是不允许存储NOLL值的,那么在该表的执行计划的Extra列就会提示Not exists额外信息

EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;

在这里插入图片描述

5.3.2.10.8、Zero limit

当我们的TIMITT子句的参数为0’时,表示压根儿不打算从表中读出任何记录,将会提示该额外信息

 EXPLAIN SELECT * FROM s1 LIMIT 0;

在这里插入图片描述

5.3.2.10.9、Using filesort

但是很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MysQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名: filesort)。如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的Extra列中显示Using filesort提示,比如这样:

 EXPLAIN SELECT * FROM s1 ORDER BY key1 LIMIT 10;

在这里插入图片描述

5.3.2.10.10、Using temporary

在许多查询的执行过程中,MySQ可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含DISTINCT'GROUP BY'、‘UNION等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行计划的Extra列将会显示Using temporary提示

EXPLAIN SELECT DISTINCT common_field FROM s1;

在这里插入图片描述

5.3.2.11、EXPLAIN四种输出格式

这里谈谈EXPLAIN的输出格式。EXPLAIN可以输出四种格式:传统格式JSON格式TREE格式以及可视化输出。用户可以根据需要选择适用于自己的格式。

5.3.2.11.1、传统格式

传统格式简单明了,输出是一个表格形式,概要说明查询计划。

5.3.2.11.2、JSON格式

JSON格式:在EXPLAIN单词和真正的查询语句中间加上FORMAT=JSON。用于查看执行成本cost_info

5.3.2.11.3、TREE格式

TREE格式是8.0.16版本之后引入的新格式,主要根据查询的各个部分之间的关系各部分的执行顺序来描述如何查询。

5.3.2.11.4、可视化输出

可视化输出,可以通过MySQL Workbench可视化查看MySQL的执行计划。

5.3.2.12、小结

  • EXPLAIN不考虑各种cache
  • EXPLAIN不能显示MySQL在执行查询时所作的优化工作
  • EXPLAN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况。部分统计信息是估算的,并非精确值

5.4、SHOW WARNINGS的使用

mysql> EXPLAIN SELECT s1.key1, s2.key1 FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.common_field IS NOT NULL;
# 查看优化后的执行语句
mysql> SHOW WARNINGS\G

5.5、分析优化器执行计划:trace

# 开启
SET optimizer_trace="enabled=on",end_markers_in_json=on; 
# 设置大小
set optimizer_trace_max_mem_size=1000000;
# 使用
select * from student where id < 10;
select * from information_schema.optimizer_trace\G

5.6、MySQL监控分析视图-sys schema

5.6.1、Sys schema视图使用场景

5.6.1.1、索引情况
#1. 查询冗余索引 
select * from sys.schema_redundant_indexes; 
#2. 查询未使用过的索引 
select * from sys.schema_unused_indexes; 
#3. 查询索引的使用情况 
select index_name,rows_selected,rows_inserted,rows_updated,rows_deleted from sys.schema_index_statistics where table_schema='dbname' ;
5.6.1.2、表相关
# 1. 查询表的访问量 
select table_schema,table_name,sum(io_read_requests+io_write_requests) as io from sys.schema_table_statistics group by table_schema,table_name order by io desc; 
# 2. 查询占用bufferpool较多的表 
select object_schema,object_name,allocated,data
from sys.innodb_buffer_stats_by_table order by allocated limit 10; 
# 3. 查看表的全表扫描情况 
select * from sys.statements_with_full_table_scans where db='dbname';
5.6.1.3、语句相关
#1. 监控SQL执行的频率 
select db,exec_count,query from sys.statement_analysis order by exec_count desc; 
#2. 监控使用了排序的SQL 
select db,exec_count,first_seen,last_seen,query
from sys.statements_with_sorting limit 1; 
#3. 监控使用了临时表或者磁盘临时表的SQL 
select db,exec_count,tmp_tables,tmp_disk_tables,query
from sys.statement_analysis where tmp_tables>0 or tmp_disk_tables >0 order by (tmp_tables+tmp_disk_tables) desc;
5.6.1.4、IO相关
#1. 查看消耗磁盘IO的文件 
select file,avg_read,avg_write,avg_read+avg_write as avg_io
from sys.io_global_by_file_by_bytes order by avg_read limit 10;
5.6.1.5、Innodb相关
#1. 行锁阻塞情况 
select * from sys.innodb_lock_waits;

猜你喜欢

转载自blog.csdn.net/prefect_start/article/details/124203377