MySQL——索引与EXPLAIN

版权声明:本文为博主原创文章,转载注明出处即可。 https://blog.csdn.net/bskfnvjtlyzmv867/article/details/84844229

前言

本文内容主要参考自《高性能MySQL》第5章以及《MySQL DBA 修炼之道》书中的第三章,算是原书的实践与补充。 上次主要讲了MySQL的基本操作,这次来谈谈索引与EXPLAIN

I. 什么是索引?

想要深入的学习MySQL相关技术,而不仅仅停留在简单CURD,能够写出百万数据中分分钟查出需要数据的SQL,首先就需要掌握索引技术。那么什么是索引呢?

要理解MySQL中索引是如何工作的,最简单的方法就是去看看一本书的“索引”部分:如果想在一本书中找到某个特定主题,一般会先看书的“索引”,找到对应的页码。所以当数据表中的数据越来越多时,挨个查找记录将会越来越慢,我们需要像查“字典”一样建立一种“目录”,来帮我们仍然能够快速的查找想要的记录。这种“目录”一样的存在便是索引。在MySQL中,存储引擎用类似的方法使用索引,其先在索引中找到对应值,然后根据匹配的索引记录找到数据库表中对应的数据行。

索引,在MySQL中也叫做“键(key)”,是存储引擎用于快速找到记录的一种数据结构。为什么是数据结构?因为本身索引是为了解决查找问题,查找排序在算法中是经常遇到的,实现查找我们通常有一些对应的算法,遍历、二分、二叉搜索树、红黑树、散列表等(详细可以看橙色的那本《算法》书)。而一些快速的查找算法都有其对应的数据结构来实现,索引就是存储引擎实现的一种数据结构能够快速用于查找数据库中记录。后面我们会知道数据结构具体可能是 B-Tree、哈希索引、R-Tree、全文索引等。

索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。创建一个真正“最优”的索引经常需要重写查询。

同一个表,可以创建多个索引。就像新华字典的索引,不仅仅只有拼音,还有笔画、偏旁部首等。除了允许不同的字段添加索引之外,还可以将字段组合添加索引,组合的先后顺序也影响到查询速度。甚至其实MySQL允许同一个字段上重复创建索引,但这并不可取

II. 索引类型

对于数据库索引相关问题来说,有许多计算机操作系统底层的名词需要了解并通晓其含义,下面将本文需要提前了解的概念列出如下:

  • CPU密集型:CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

  • IO密集型:IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,没有充分利用处理器能力。

  • 操纵系统页:磁盘的读写速度比主存慢很多,所以为了提高效率,要尽量减少磁盘I/O,减少读写操作。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存,这样做的理论依据是计算机科学中著名的局部性原理。预读的长度一般为页(page)的整倍数,页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。

  • 数据库页:数据库文件存储是页为存储单元,一个页可以存放N行数据。我们常用的页类型就是数据页和索引页。以InnoDB引擎为例,其逻辑存储结构如下图所示。所有数据都被逻辑地存放在一个称之为表空间 (tablespace)的空间中。表空间又由段(segment)区(extent)页(page) 组成。表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。InnoDB存储引擎表是索引组织的,因此数据即索引,索引即数据。那么数据段即为B+树的叶子节点 (leaf node segment),索引段即为B+树的非叶子节点 (non-leaf node segment)。区是由64个连续的页组成的,每个页大小为16KB,即每个区的大小为1MB。页是InnoDB磁盘管理的最小单位。InnoDB存储引擎是面向行的,也就是说数据的存放按行进行存放。每个页存放的行记录数目也是有硬性定义的,最多允许存放 16KB/2-200 行的记录,即7992行记录。
    InnoDB逻辑存储结构

  • 顺序IO:每次访问磁盘的一个目标块时,磁臂就需移动到正确的磁道上,耗费的这段时间为寻址时间;然后盘片就需旋转到正确的扇区上,这段时间又为旋转时延。很明显总共耗费的时间依赖于磁头的初使位置,还有要访问的扇区的位置。如果目标块刚好就在磁头下方,那不需要等待;如果刚刚经过磁头,那就不得不等上一个周期时间。 找到上一个目标块后,下一个目标块就在刚才访问的那一个磁盘块的后面,磁头能立刻遇到,不需等待,这种IO就叫顺序IO。

  • 随机IO:如果下一个目标块在磁盘的另一个地方,访问它会有同样的寻道和旋转时延,我们就把这种方式的IO叫做随机IO。数据库的很多设计也都是尽量充分利用顺序IO,传统的数据库架构对随机IO几乎没有还手之力,随机IO几乎令所有DBA谈虎色变,MySQL InnoDB则利用事务日志把随机I/O转成顺序I/O。

  • 主存存取过程:从抽象角度看,主存是一系列的存储单元组成的矩阵,每个存储单元存储固定大小的数据。每个存储单元有唯一的内存地址。当系统需要读取主存时,则将地址信号放到地址总线上传给主存,主存读到地址信号后,解析信号并定位到指定存储单元,然后将此存储单元数据放到数据总线上,供其它部件读取。写主存的过程类似,系统将要写入单元地址和数据分别放在地址总线和数据总线上,主存读取两个总线的内容,做相应的写操作。这里可以看出,主存存取的时间仅与存取次数有关,和存取的数据的位置没什么关系,因为读写数据相当于直接根据地址定位坐标。

  • 磁盘存取过程:当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在哪个磁道,哪个扇区。这个过程耗费的时间包括寻道和旋转时间。由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分之一,因此为了提高效率,要尽量减少磁盘I/O。

索引是一中数据结构,那么索引类型自然是指不同类型的数据结构。索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。我们主要来研究MySQL支持的索引类型。

B-Tree索引

当谈索引的时候,如果没有特别指明类型,那多半说的是B-Tree索引,它使用B-Tree数据结构来存储数据。那么首先我们先来了解B-Tree数据结构。

① B树,B-树,B+树

首先需要明确的是,B树就是B-(减)树,本文中凡是出现的B-Tree,都是B横杠Tree,而非减号,如果想要表示B减树则直接用汉字减或用B树代表相同含义。

B树

二叉查找树是非平衡树,极端情况下查找性能可能非常低,所以才有了红黑树这类平衡二叉树。B树也是一种平衡树,相比于红黑树之类的2-3平衡树而言,B树的阶,或者说是节点的最大出度不仅仅局限于2或3。从查找效率来说,一般阶大于等于3,用 m 表示。假设一个非空的B树,满足以下性质:

  • 根结点至少有两个子女;
  • 每个中间节点都包含 k-1 个元素和 k 个孩子,k 属于 [ceil(m/2),m];
  • 每个叶子节点都包含 k-1 个元素,k 属于 [ceil(m/2),m];
  • 所有的叶子节点都位于同一层(平衡树);
  • 每个节点中的元素从小到大排列,节点当中 k-1 个元素正好是 k 个孩子包含的元素的值域划分;
  • 每个节点包含了 k-1 个元素,以及 k 个孩子的指针。

一个标准的B树如下图:

标准的B树

可以看出当我们查找某一个元素时,最多需要读取 h (B树的高度) 次数即可。如果需要插入一条数据,B树为了维护上面的性质,需要对树的结构做一些调整。如果插入元素后某一节点的元素数目大于 m,则在插入前需要进行分裂。同样,如果删除一条数据,删除后节点的元素数目小于 ceil(m/2),也要进行相应的合并操作。

利用B树的数据结构来进行存储数据,我们可以将数据与对应的索引信息定义为一个组合[key, data],key是data的索引。那么一个简单的B树可以表示为:

B树存储数据

每个节点中包含了 k-1 个索引值、 k-1 个对应的数据 (除去了索引值之外的数据)以及 k 个指针指向子节点。

B+树

B+树其实是B树的一种变种,MySQL普遍使用B+Tree的数据结构来实现索引,当然包括主要存储引擎MyISAM和InnoDB。B+树与B树相比,主要有以下不同:

  • 每个中间节点都包含 k 个元素和 k 个孩子,相当于指针数目也是 k
  • 非叶子节点不存储数据data,只存储key;
  • 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

如下是一个B+树的示意图,可以看到完全满足上面的三条性质。

B+树的示意图

带有顺序访问指针的B+树

一般在数据库系统或文件系统中使用的B+树结构都在经典B+树的基础上进行了优化,增加了顺序访问指针。下图所示的带有顺序访问指针的B+树就是我们经常看到的B+树模样。

带有顺序访问指针的B+树

对比上一幅图,主要区别在于每个叶子节点增加一个指向相邻叶子节点的指针,这样就形成了带有顺序访问指针的B+树。这样优化的目的是为了提高区间访问的性能,如果要查询key为某个范围内的所有数据记录,当找到第一个数据后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。

② 数据库为什么使用B+树?

数据库的索引数据量也是很大的,所以它存储在磁盘中,而非内存。那么当进行增删改查数据时,需要读取索引内容,就进行了磁盘I/O。通过前面的相关概念介绍,磁盘I/O的耗时操作越少越好,所以磁盘I/O次数可以评价索引数据结构的优劣。

先从二叉查找树以及红黑树说起,这两种树本身的阶数是固定的,每个节点的子节点数很小,导致了如果存在很多索引时,树的深度非常深,对应查找需要比较的次数也会非常多,性能必然受到严重影响。

再说B树,因为它的阶数是 m,可以设置的较大,这样可以使的决定查询比较次数的因素——树的深度可以很浅。根据B树的定义,可知检索一次最多需要访问 h 个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将树的每个节点的大小设为等于一个页,这样每个节点只需要一次磁盘I/O就可以完全载入。为了达到这个目的,在实际实现B树时还需要使用如下技巧:

  • 每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个节点只需一次磁盘I/O;
  • B树中一次查询最多需要 h-1 次磁盘I/O,因为根节点常驻于内存,渐进复杂度为 O ( h ) = O ( l o g d N ) O(h)=O(log_dN) 。一般实际应用中,出度 d 是非常大的数字,通常超过100,因此 h 非常小 (通常不超过3,3已经是 1 0 6 10^6 级别数据量)。

所以用B树作为数据库的索引效率远远高于红黑树等。

然而,MySQL的MyISAM和InnoDB都采用的是带有顺序访问指针的B+树去实现索引 ,这又是为何呢?比较B+树和B树的区别,除了叶子节点有顺序访问指针帮助范围查询之外,主要就是非叶子节点上B+树只存有索引(key),没有额外再存(data)。之前我们已经说过,一般树的每个节点的大小等于一个页的大小,容量固定的情况下,由于B树需要保存数据记录所以一个节点能包含的索引数目比B+树要小。也就是说,一个非叶子节点的出度 d,上限取决于节点内 keydata 的大小。具体的公式如下:

$d_{max}=一个节点中能容纳的索引数目=floor(pagesize/(keysize+datasize+pointsize)) $

由于B树非叶子节点去掉了 data,因此可以拥有更大的出度,拥有更好的性能。

③ MySQL中的B-Tree索引

《高性能MySQL》中一直使用的是B-Tree索引这样的描述,从技术实现角度,其实是B+树。

假设我们现在存在一个数据表,包含三个字段:主键Col1、辅助索引Col2以及字段Col3。

表格

MyISAM索引实现

从上面对于B+树的描述,我们可以大概的推测出索引的结构。我们先来看MyISAM对于主键索引的原理图:

MyISAM对于主键索引的原理图

可以看出MyISAM的B+树中,非叶子节点仅仅保存了主键值,非叶子节点上保存的是数据库对应记录的地址。通过地址我们可以定位到每一条记录。我们知道每个节点对应一页,每个节点中包含多行数据库记录 (图中为2个),需要注意的是逻辑上相邻的记录,物理上可能并不在同一页中,比如表中的第2行和第3行数据,它们在不同的页中。

我们再来看看辅助索引Col2的结构。辅助索引的叶子结点除了包含键值以外,每个叶子结点中的索引行还包含了一个书签,该书签用来告诉存储引擎可以在哪找到相应的数据行,MyISAM存储引擎的辅助索引的书签就是地址,其实和主键索引没什么差别。

MyISAM辅助索引

同样也是一颗B+树,data域保存数据库记录的地址。因此,MyISAM中索引检索的算法为首先按照B+树搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。

InnoDB索引实现

虽然InnoDB也使用B+树作为索引结构,但具体实现方式却与MyISAM截然不同。

首先来看主键索引的实现方式。

InnoDB主键索引

对比MyISAM的主键索引,最显著的区别就是在于叶子节点的保存内容。MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+树组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录,这个索引的key是数据表的主键,那么InnoDB引擎的数据文件本身就是主索引文件。这种数据与索引在一起的结构叫做聚簇索引,或者叫聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键。如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。

我们再来看看InnoDB的辅助索引实现结构,我们在表的Col3字段上添加上辅助索引。

InnoDB的辅助索引

与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域,因为通过主键我们同样可以查询到整个数据库记录。聚簇索引这种实现方式使得按主键的搜索十分高效,但是辅助索引查询需要二次查询:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

④ 正确使用和优化索引

知道了索引的实现方式对我们理解索引的正确使用方式和优化原理有着莫大的帮助。下面列举一些使用索引的常见策略。

假设现在有一个表:

CREATE TABLE People (
    last_name varchar(50) not null,
    first_name varchar(50) not null,
    birth date not null,
    gender enum('m', 'f') not null,
    key(last_name, first_name, birth)
);

表中定义了四个字段,包含姓、名、出生日期以及性别,同时建立了一个组合索引包含了姓、名、出生日期三个字段。该索引的B+树结构某一小部分如下:

B+树结构某一部分

可以看到非叶子节点上存储了索引字段信息,B-Tree对索引列是顺序组织存储的,索引之间按照一定的排序规则进行有序的排序,这里就是按姓名的字母序以及日期的由小到大。依据这样的一个结构,编写合理的SQL语句,我们可以极为快速的寻找到我们需要的记录。

  • 全值匹配:全值匹配指的是和索引中的所有列进行匹配,也就是我们的查询语句的条件完全和索引列中一一对应,不仅仅是内容,而且要求顺序也一致。不难理解,这就是一个简单的递归查找树的过程。
  • 匹配最左前缀:当然我们想要从当前的索引树中获得好处,查询条件并不一定需要全值匹配,我们可以只包含索引列中的第一个字段,例如我们查找所有姓Allen的人。当然,如果只包含名或生日的查询条件,就不能利用当前索引树了。
  • 匹配列前缀:同样的,我们的查询条件甚至可以连第一列字段的信息都不完全,比如只匹配第一列值的开头部分。例如查找以姓All开头的人。
  • 精确匹配某一列并范围匹配另外一列:由前面的经验,我们自然就可以推演出,可以精确匹配前面部分的索引列,后面的索引列仅仅是最左前缀的形式。例如,我们要查找具体姓啥名啥但日期只要19xx年的人。
  • 匹配范围值:匹配范围值不光光能够从索引树整个结构获益,B+树的叶子节点额外增加了顺序访问指针,使得速度能够更快。这在之前我们已经有所提及。
  • 覆盖索引:覆盖索引是指查询只需要访问索引,而无须访问数据行。如果我们想要查询的信息索引列已经完全包含,那么我们就不需要再去叶子节点找到主键或者是记录的地址,然后再到对应的数据记录中查询信息。这样一个过程其实叫二次查询。
  • ORDER BY与GROUP BY:索引树中的节点是有序的,所以除了按值查找之外,索引还可以用于查询中的 ORDER BYGROUP BY 操作。

当然,我们从之前的索引实现方式也能想到一些关于B-Tree索引的限制

  • 如果不是按照索引的最左列开始查找,则无法使用索引。 例如上面例子中的索引无法用于直接查找名字为Bill的人,也无法直接查找某个特定生日的人,因为这两列都不是最左前缀。类似地,也无法查找姓氏以某个字母结尾的人。
  • 不能跳过索引中的列。 也就是说,前面所述的索引无法用于查找姓为Smith并且在某个特定日期出生的人。如果不指定名字,则MySQL只能使用索引的第一列——姓列。
  • 如果查询条件中有某个列是范围查询,则其右边所有列都无法使用索引优化查找。 如果范围查询列值的数量有限,那么可以通过使用多个等于条件来代替范围条件。

哈希索引

① 什么是哈希索引?

哈希索引 (hash index) 是基于哈希表实现的,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码 (hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。

在MySQL中,只有Memory引擎显式支持哈希索引。这也是Memory引擎表的默认索引类型,Memory引擎同时也支持B-Tree索引。值得一提的是,Memory引擎是支持非唯一哈希索引的,这在数据库世界里面是比较与众不同的。如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中。

哈希索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。从实现原理上,我们可以将其类比为一个巨大的 HashMap 集合。所以哈希索引也自然就有它的限制:

  • 哈希索引只包含哈希值和数据行指针,而不存储字段值,所以不能使用索引中的值来避免读取行,也就是不会有覆盖索引了。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。
  • 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序。
  • 哈希索引也不支持部分索引列匹配查找,也就是必须全值匹配,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。索引列少一点点哈希码就不一样,所以不可能进行部分匹配。
  • 哈希索引只支持等值比较查询,包括=、IN()、<=>,不支持任何范围查询。
  • 访问哈希索引的数据非常快,除非有很多哈希冲突。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。
  • 如果哈希冲突很多的话,一些索引维护操作的代价也会很高。比如在性别列上添加哈希索引,由于只存在两种常规性别,所以哈希冲突非常严重,这样的哈希索引价值也不大。

InnoDB引擎有一个特殊的功能叫做 “自适应哈希索引(adaptive hash index)”。当InnoDB注意到某些索引值被使用得非常频繁时,它会在内存中基于B-Tree索引之上再创建一个哈希索引,这样就让B-Tree索引也具有哈希索引的一些优点,比如快速的哈希查找。这是一个完全自动的、内部的行为,用户无法控制或者配置,不过如果有必要,完全可以关闭该功能。

② 利用自定义哈希索引提高性能

创建自定义哈希索引。如果存储引擎不支持哈希索引,则可以模拟像InnoDB一样创建哈希索引,这可以享受一些哈希索引的便利,例如只需要很小的索引就可以为超长的键创建索引。

思路很简单:在B-Tree基础上创建一个伪哈希索引。这和真正的哈希索引不是一回事,因为还是使用B-Tree进行查找,但是它使用哈希值而不是键本身进行索引查找。你需要做的就是在查询的WHERE子句中手动指定使用哈希函数。

举个例子,我们在数据库中经常会插入一些链接,这些链接往往很长,选择性也一般,如果使用B-Tree来存储URL,存储的内容就会很大。原来我们的查询语句如下:

mysql> SELECT id FROM url WHERE url="http://www.mysql.com";

若删除原来URL列上的索引,而新增一个被索引的 url_crc 字段,存放URL的哈希码值,不仅仅能够压缩字符串的大小,性能也因此提升很快。

mysql> SELECT id FROM url WHERE url="http://www.mysql.com" AND url_crc=CRC32("http://www.mysql.com");

MySQL优化器会使用这个选择性很高而体积很小的基于 url_crc 列的索引来完成查找。即使有多个记录有相同的索引值,查找仍然很快,只需要根据哈希值做快速的整数比较就能找到索引条目,然后一一比较返回对应的行。而原本则是对完整的URL字符串做索引,那样会非常慢。

当然,这样实现的缺陷是需要维护哈希值。可以手动维护,也可以使用触发器实现。除此以外,哈希算法的优劣也需要注意,因为它影响着哈希索引的选择性。索引的选择性是指索引列中不同值的数目与表中记录总数的比值。

III. 使用EXPLAIN

EXPLAIN工具可以确认执行计划是否良好,查询是否走了合理的索引。不同版本的MySQL优化器各有不同,一些优化规则随着版本的发展可能会有变化,查询的执行计划可能会随着数据的变化而变化。对于这种情况,我们可以使用EXPLAIN工具验证自己的判断。

使用方式

语法形式为:

explain select ·····

除此之外还有两种变体:

explain extended select ·····
show warnings

加上 extend 可以将执行计划反编译成 select 语句,通过 show warnings 即可得到被MySQL优化后的查询语句。

另一种变体是:

explain partitions select ·····

该命令用于分区表的EXPLAIN命令。分区是将数据分段划分在多个位置存放,可以是同一块磁盘也可以在不同的机器。分区后,表面上还是一张表,但数据散列到多个位置了。程序读写的时候操作的还是大表名字,MySQL服务器自动去组织分区的数据。

我们以MySQL官方文档中提供的示例数据库 employees 中的 titles 为例。首先先查看它的全部索引,可以看到前三列组成主键索引 <emp_no, title, from_date> ,同时单独又创建了一个辅助索引 <emp_no>。考虑图片宽度,下图的索引信息部分列被删除。
titles索引
我们进行一个查询,并用EXPLAIN进行分析。
explain
表格中告诉我们MySQL访问了哪些表,以及它是如何访问数据的。里面包含很重要的索引使用信息,据此可以判断出索引是否需要优化。

返回信息解读

针对上面EXPLAIN返回的表格,我们对每一列的含义进行具体的研究。所有的信息大致可以用下面思维导图表示:

MySQL EXPLAIN id:表示查询中SELECT子句或操作表的顺序 select_type:表示查询中SELECT子句的类型 table:表示从哪个表 (或查询结果表) 中进行查询 type:表示MySQL在表中查找所需行的方式,也称"访问类型" possible_keys:指出MySQL能使用哪个索引在表中找到行,查询涉及的字段上如果存在索引,则索引被列出,但不一定会被查询使用 key:显示MySQL在查询中实际使用的索引,如果没有则为NULL。查询中如果使用了覆盖索引,则该索引仅出现在key的列表中 key_len:表示经计算得到的索引最大可能使用的字节数,并非实际使用字节数。根据字节数可以推测索引最大使用长度 ref:表示table中的表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 rows:表示MySQL根据表统计信息以及索引的选用情况,估算出找到目标记录所需要读取的行数 Extra:展示那些不适合在其他列中显示又十分重要的备注信息 <dervied3> 3表示的是该查询结果衍生自id为3的select <union1,4> 1和4表示对第1个和第4个select结果进行union操作 1. SIMPLE:查询中不包含子查询或UNION 2. PARIMARY:最外层查询中包含任何复杂的子部分 3. SUBQUERY:在select和where列表中包含的子查询 4. DERIVED (衍生):在from列表中包含的子查询 5. UNION:出现在UNION之后的查询select 6. UNION RESULT:从union合并的表中select 1. 相同则顺序由上至下; 2. 子查询id序号会递增,id越大越优先执行。 All:MySQL通过遍历全表以找到匹配的行 index:只遍历索引树找到匹配行 range:索引范围扫描,对索引的扫描开始于某一点,返回匹配值域的行 ref:非唯一性索引扫描,将返回匹配某个单独值的所有行。常用于使用非唯一索引或唯一索引的非唯一前缀进行的查找 eq_ref:唯一索引扫描,对于每个索引键,表中只有一个记录与之匹配。常见于主键索引或唯一索引扫描 const:MySQL对查询的某部分进行优化,并转化为一个常量。如将主键置于where列表中,MySQL就能将该查询转换为一个常量。 system:是const类型的特例,当查询的表只有一行的情况下,即可使用system NULL:MySQL在优化过程中分解语句,执行时甚至不用访问表或索引 Using index:表示相应的select操作中使用了覆盖索引 Using where:表示MySQL服务器在存储引擎收到记录后进行"后过滤" Using temportary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询 Using filesort:文件排序,MySQL无法用索引完成的排序操作

① id

id 包含一组数字,表示查询中执行 select 子句或操作表的顺序。如果 id 相同,则为一组,执行顺序由上至下,如果是子查询,id 的序号会递增,id 值越大优先级越高,越先被执行。
id

② select_type

select_type 表示查询中每个select子句的类型,一共有如下几种情况:

  • SIMPLE:查询中不包含子查询或者 UNION
  • PRIMARY:查询中若包含任何复杂的子部分,最外层查询则被标记为 PRIMARY
  • SUBQUERY:在 SELECTWHERE 列表中包含了子查询,该子查询被标记为 SUBQUERY
  • DERIVED:用来表示包含在 FROM 子句中的子查询的 SELECT,MySQL会递归执行并将结果放到一个临时表中。服务器内部称为"派生表",因为该临时表是从子查询中派生出来的。
  • UNION:若第二个 SELECT 出现在 UNION 之后,则被标记为 UNION
  • UNION RESULT:从 UNION 的结果表中进行的 SELECT

下面举个例子,SQL语句如下

select vt1.dept_no
from (select emp_no, dept_no from dept_emp where emp_no < (select emp_no from employees where emp_no = 10010)) vt1
union
(select vt2.emp_no from (select emp_no from dept_manager) vt2 where emp_no < 110300);

执行对应的 EXPLAIN 语句查看执行计划。这里要关闭MySQL5.7开始的优化器引入 derived_merge,处理 from 语句中的派生表和视图能更好地避免不必要的物化并能够通过条件下放产生更有效的执行计划。比如上面SQL中 union 后的一句话,子查询 select emp_no from dept_manager 没有条件,正常情况下需要全部遍历输出产生派生表,然后再从派生表的所有记录中进行筛选 emp_no < 110300,这样其实很慢,在产生派生表的时候就利用上筛选条件 emp_no < 110300,派生表的结果也会变小很多。

为了完整的显示所有的查询,我们将这种优化先关闭。
select_type
从上图可以看出,一共有6个查询,基本包含了几种常见的 select_type,按顺序分析:

  • 最先执行 id=5 的查询,看 table 列中指明的是从 dept_manager 中查询,可以确定是 select emp_no from dept_manager,由于该子查询在 from 中,所以为 DERIVED
  • 然后执行 id=4 的查询,table 列中指明的是从 derived5 中查询,5代表 id=5,所以确定就是 union 后面部分的查询,外层查询查的是派生表。由于其在 union 之后,所以该查询标记为 UNION
  • 继续执行 id=3 的查询,table 列中指明的是从 employees 中查询,可以确定是 select emp_no from employees where emp_no = 10010,该子查询在 where 条件中,所以是个子查询标记为 SUBQUERY
  • id=2 的查询 table 列中指明的是从 dept_emp 中查询,可以确定是 select emp_no, dept_no from dept_emp where ····,同样它的结果也是一个派生表,所以标记为 DERIVED
  • union 前的查询为复杂查询,标记为 PRIMARY,其 table 列中指明的是从 derived2 中查询;
  • 最后将 union 前后的查询结果合并,标记为 UNION RESULT

③ type

MySQL中 explaintype 类型包括如下几种,从上到下,由最差到最好。

Type 含义
All 全表扫描, MySQL将遍历全表以找到匹配的行。
index 索引全扫描,indexALL 区别为 index 类型只遍历索引树。
range 索引范围扫描,对索引的扫描开始于某一点,返回匹配值域的行。显而易见的索引范围扫描是带有between或者where子句里带有<, >查询。当MySQL使用索引去查找一系列值时,例如 IN()OR 列表,也会显示 range (范围扫描),这种情况查询性能往往因为结果少性能更高。
ref 使用非唯一索引扫描或者唯一索引的前缀扫描,返回匹配某个单独值的所有记录行。
eq_ref 类似 ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用主键或者唯一索引作为关联条件。
const/system 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where 列表中,MySQL就能将该查询转换为一个常量。system是const类型的特例,当查询的表只有一条记录的情况下,即可使用system。
NULL MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。

type

④ possible_keys和key

possible_keys 指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。

key 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为 NULL。查询中如果使用了覆盖索引,则该索引仅出现在 key 的列表中。例如上面的例子中演示 typeindex 的查询。possible_keysNULLkey 为辅助索引 emp_no

⑤ key_len

key_len 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。注意 key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表的定义计算而得,不是通过表内检索出的。

举个例子,如下图所示,我们使用 titles 表的主键索引——一个组合索引,分别进行减少查询使用的索引列,emp_noINT 类型,占4个字节;titleVARCHAR 类型,50个字符,由于是utf-8字符集,每个字符3个字节,所以50个字符150个字节,加上2个字节存储长度,所以占据了152字节;最后 from_dateDATE 类型占据了3个字节。
key_len

⑥ ref

表示表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。查看下面图,表的内外连接使用了过滤匹配条件。先看外连接,被驱动表 (a left join bb) 使用了主键索引,驱动表作为外层循环先执行 (id相同顺序由上至下) ,需要全表扫描不走索引。对于内连接,MySQL以数据记录少的表作为被驱动表 (笛卡尔积的内层循环),所以后四列都一样。ref 的含义则是指用于索引的值来源于哪里,即内存循环走的索引值是来源于外层循环的
ref

⑦ rows

表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数。

⑧ Extra

显示那些不适合在其他列中显示但十分重要的额外信息。可能包含四种信息,如表格所示。

Extra信息 含义
Using index 该值表示相应的 select 操作中使用了覆盖索引。
Using where 表示MySQL服务器将在存储引擎检索行后再进行过滤。许多 where 条件里涉及索引中的列,当(并且如果)它读取索引时,就能被存储引擎检验,因此不是所有带 where 的查询都会显示"Using where"。有时"Using where"的出现就是一个暗示:查询可受益于不同的索引。
Using temporary 表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询。
Using filesort MySQL中无法利用索引完成的排序操作称为“文件排序”。

IV. 高性能索引策略

正确地创建和使用索引是实现高性能查询的基础。前面已经着重介绍了MySQL的B-Tree索引,现在我们一起来看看如何真正地发挥索引的优势。

独立的列

“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。如下面的例子:
独立的列

前缀索引vs选择性

有时候需要索引很长的字符列,这会让索引变得大且慢。通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率,这样做其实是牺牲了索引的选择性。选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOB、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。
前缀索引
前缀长度到6时选择性提升已经很微小了,基本接近0.0055。当然只看平均选择性是不够的,也有例外的情况,需要考虑最坏情况下的选择性。比如虽然 count(distinct left(last_name, 6)) 较大,但不代表每一种 last_name 的记录数量是均匀分布的,可以某些 last_name 数据特别多,那么这种特定的 last_name 查询的选择性就很低了。

前缀索引是一种能使索引更小、更快的有效办法,但另一方面也有其缺点:MySQL无法使用前缀索引做 ORDER BYGROUP BY,也无法使用前缀索引做覆盖扫描。

多列索引与顺序

多列索引并不是给每一个列创建一个索引,而是多个列创建一个组合索引,当然多个列的排列顺序也很有讲究。在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。正确的索引列顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要。

在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的 ORDER BYGROUP BYDISTINCT 等子句的查询需求,所以多列索引的列顺序至关重要。

当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化WHERE条件的查找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在WHERE子句中只使用了索引部分前缀列的查询来说选择性也更高。然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关。这和前面介绍的选择前缀的长度需要考虑的地方一样。可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。

聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。

一个表只能有一个聚簇索引,因为无法同时把数据行存放在两个不同的地方。MySQL不允许手动指定那个索引为聚簇索引,InnoDB主键是聚簇索引,如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。

聚簇主键可能对性能有帮助,但也可能导致严重的性能问题。

优点主要如下

  • 可以把相关数据保存在一起。聚簇索引本身就是包含数据的,而不需要在获取其他数据的时候再去对应的磁盘地址进行读取,发生磁盘IO。
  • 数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。
  • 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。也就是说,覆盖索引默认包含了主键的字段,设计覆盖索引的时候可以考虑不添加上主键字段,因为这是必然要添加的。

当然也存在缺点

  • 聚簇数据最大限度地提高了I/O密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了。
  • 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。
  • 更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分裂”的问题。因为B-tree索引每个节点占一页用来存放索引,当新来的一行数据记录的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。
  • 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
  • 二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。非覆盖索引的二级索引访问需要二次查询。

在介绍MyISAM和InnoDB两种存储引擎的时候,我们了解到两种引擎对于数据的组织方式。MyISAM叶子节点存放了“行指针”,指向具体的数据记录地址。MyISAM按照数据插入的顺序存储在磁盘上,对应的地址被叶子节点记录即可。反观InnoDB引擎,数据本身就记录在主键索引的叶子节点上,数据插入的顺序完全依据主键在整个B-Tree树该有的位置,如果主键是乱序的,那么插入数据的时候就会出现树的左边插一个,右边插一个,这边插一个,那边插一个的现象。这么做有什么隐患呢,首先插入时间长,其次占据空间可能更大,碎片化严重。具体如下:

  • 写入的目标页可能已经刷到磁盘上并从缓存中移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机I/O。
  • 因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。
  • 由于频繁的页分裂,页会变得稀疏并被不规则地填充,所以最终数据会有碎片。

那么,如何利用好InnoDB主键顺序插入数据的特点呢?

那就是有序的主键,比如自增长列。因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改),下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果。

覆盖索引

使用覆盖索引的好处在于:

  • 索引字段数目通常远小于数据记录的所有字段数目,所以如果只需要读取索引,那MySQL就可以极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中(这对于MyISAM尤其正确,因为MyISAM能压缩索引以变得更小)。
  • 由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。

不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引。

InnoDB的二级索引的叶子节点都包含了主键的值,这意味着InnoDB的二级索引可以有效地利用这些“额外”的主键列来覆盖查询。换句话说,二级索引查询主键也是覆盖索引
二级索引查询主键

利用索引扫描进行排序

通过B-Tree索引的实现原理,我们可以知道 ORDER BY 排序也是可以从索引获益的。ORDER BY 子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则,MySQL都需要执行排序操作,而无法利用索引进行排序。

当然,有一种情况下 ORDER BY 子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果 WHERE 子句或者 JOIN 子句中对这些列指定了常量,就可以“弥补”索引的不足。

如下图举出一些例子。
索引排序

冗余重复不使用的索引

MySQL允许在相同列上创建多个索引,无论是有意的还是无意的。MySQL需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。

重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样创建重复索引,发现以后也应该立即移除。

冗余索引和重复索引有一些不同。如果创建了索引(A,B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。因此索引(A,B)也可以当作索引(A)来使用(这种冗余只是对B-Tree索引来说的)。但是如果再创建索引(B,A),则不是冗余索引,索引(B)也不是,因为B不是索引(A,B)的最左前缀列。

除了冗余索引和重复索引,可能还会有一些服务器永远不用的索引。这样的索引完全是累赘,建议考虑删除。

V. 总结

在MySQL中,大多数情况下都会使用B-Tree索引。其他类型的索引大多只适用于特殊的目的。如果在合适的场景中使用索引,将大大提高查询的响应时间。本章将不再介绍更多这方面的内容了,最后值得总的回顾一下这些特性以及如何使用B-Tree索引。

在选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住:

  • 单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机I/O要快很多,不过这一点仍然成立)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的块中能包含尽可能多所需要的行。使用索引可以创建位置引用以提升效率。
  • 按顺序访问范围数据是很快的。这有两个原因。第一,顺序I/O不需要多次磁盘寻道,所以比随机I/O要快很多,特别是对机械硬盘。第二,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUP BY 查询也无须再做排序和将行按组进行聚合计算了。
  • 索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎就不需要再回表查找行。这避免了大量的单行访问。

除了这些,对于 EXPLAIN 的使用也是至关重要的。

参考阅读

猜你喜欢

转载自blog.csdn.net/bskfnvjtlyzmv867/article/details/84844229