【MySQL】索引

索引

  • 一、索引的介绍
    • 1、无索引情况下海量数据的查询
    • 2、有索引情况下海量数据的查询
  • 二、 认识磁盘
    • 1、磁盘的物理结构
    • 2、磁盘的一个盘片
    • 3、扇区的定位方式
    • 4、操作系统与磁盘交互的基本单位
    • 5、磁盘的随机访问与连续访问
    • 6、MySQL 与磁盘交互基本单位
  • 三、索引的理解
    • 1、主键索引现象
    • 2、主键索引结构的构建
    • 3、索引数据结构的选择
    • 4、聚簇索引 与 非聚簇索引
  • 四、索引操作
    • 1、创建索引
      • Ⅰ、创建主键索引
      • Ⅱ、唯一索引的创建
      • Ⅲ、普通索引的创建
    • 2、查询索引
    • 3、删除索引
    • 4、索引创建原则
    • 5、创建全文索引(选学)

一、索引的介绍

  • 数据库表中存储的数据都是以记录为单位的(也就是一行数据),如果在查询数据时直接一条条遍历表中的数据记录,那么查询的时间复杂度将会是 O ( N ) O ( N ) O(N)
  • 索引的价值在于提高海量数据的检索速度,只要执行了正确的创建索引的操作,查询速度就可能提高成百上千倍。当一张表创建索引后,在数据库底层就会为表中的数据记录构建特定的数据结构,后续在查询表中数据时就能通过查询该数据结构快速定位到目标数据。
  • 索引虽然提高了数据的查询速度,但在一定程度上也会降低数据增删改的效率,因为这时在对表中的数据进行增删改操作时,除了需要进行对应的增删改操作之外,可能还需要对底层建立的数据结构进行调整维护。

常见的索引类型:

  • 主键索引(primary key)。
  • 唯一索引(unique)。
  • 普通索引(index)。
  • 全文索引(fulltext)。

1、无索引情况下海量数据的查询

先建立一个海量数据的员工表,看看在查询的时候没有索引时有什么问题?

海量数据的员工表

上面的文件中构建一个8000000条记录的数据,因为构建的海量表数据需要有差异性,所以使用存储过程来创建。

还有因为数据量很大,所以我们在创建该数据库时,会很慢,大概要7-10分钟。

上述SQL中创建了一个名为index_demon的数据库,在该数据库中创建了一个名为EMP的员工表,并向表当中插入了八百万条记录。

【MySQL】索引_第1张图片

由于数据量很大,因此在查看EMP表中的数据时可以使用limit子句。如下:

select * from EMP limit 10;

【MySQL】索引_第2张图片

通过desc命令可以看到,目前EMP员工表中没有建立任何索引。

desc EMP;

【MySQL】索引_第3张图片

查询EMP表中指定工号的员工信息,可以看到每次查询过程都需要花费5秒左右。如下:

select * from EMP where empno=999877;

【MySQL】索引_第4张图片

这还是在本机一个人来操作,在实际项目中,如果放在公网中,假如同时有1000个人并发查询,那很可能就死机。

2、有索引情况下海量数据的查询

为了解决这个问题我们可以给这张表加上索引:

alter table EMP add index(empno);

给数据建立索引其实就是在底层去构建特定的数据结构,当然对海量数据构建特定数据结构也要花费一定的时间。比如我这里就花费了20.36

【MySQL】索引_第5张图片

这时再查询EMP表中指定工号的员工信息,可以看到几乎检测不到查询时耗费的时间。

select * from EMP where empno=1212349;
select * from EMP where empno=2212349;

【MySQL】索引_第6张图片

根本原因就是,给员工的empno创建索引后再根据员工工号来查询数据,这时就能够直接通过底层建立的数据结构来快速定位到目标数据,从而提高数据的检索速度,这就是索引的价值!

二、 认识磁盘

  • MySQL 给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设当中。
  • 磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率是比较低的。
  • 如何提交效率,是MySQL 的一个重要话题,因此我们有必要了解一下磁盘的相关内容。

1、磁盘的物理结构

在这里插入图片描述

其主要的核心物理结构有三个:

  • 磁盘片:磁盘片是硬盘中承载数据存储的介制,磁盘是由多个盘片叠加在一起,互相之间由垫圈隔开的。
  • 盘面:一片磁盘片是由两面的,每一面被称为盘面,磁盘片的两面都可以存储数据的。每一个盘面都有对应的磁头,也就是说一个磁盘片有两个磁头的。
  • 磁头:磁头是向磁盘读取数据的媒介,其通过磁性原理读取磁性介质上数据。所以磁头不与盘面接触,磁头悬浮在盘面上面或下面。其中还有重要的一点是:磁头是共进退的

2、磁盘的一个盘片

在磁盘的盘面上,磁盘被一个个的同心圆以及射线进行分割,从而出现了:磁道扇面扇区

在这里插入图片描述

  • 扇区: 被一个个的同心圆以及射线进行分割出的一个个扇形区域。
  • 扇面: 两条相邻的射线之间夹的所有扇区构成扇面。
  • 磁道:盘面上半径相同的扇区构成一个磁道。

在这里插入图片描述

  • 柱面:由于现实世界中磁盘的立体结构,所以把空间中所有半径相同的磁道定义为一个柱面

其中扇区是存储的基本单元, 每个扇区其大小为:512 byte4kb ,一般来说都是512 byte(下面我们讨论时也是以512 byte为准)

由于扇区是最小的存储单元,所以在硬件的角度:一个文件(内容 + 属性)无非是占用一个或多个扇区进行数据的存储。

说明

  • 由于每个扇区的存储容量相同,因此最内侧磁道上的扇区数据密度最大,而最外侧磁道上的扇区数据密度最小。
  • 近三十年来,扇区大小一直是512字节,但最近几年正在迁移到更大、更高效的4096字节扇区,通常称为4K扇区。
  • 数据库文件就是保存在磁盘中的一个个扇区中的,因此找到一个文件本质就是,在磁盘上找到保存该文件的所有扇区

3、扇区的定位方式

磁盘的示意图:
【MySQL】索引_第7张图片

定位扇区时采用CHS寻址方式:

  • 磁头(Heads): 每个盘面都有一个对应的磁头,因此确定了磁头也就确定了数据在哪一个盘面。

  • 柱面(Cylinder): 所有盘面中半径相同的同心磁道构成柱面,因此在确定了数据在哪一个盘面的基础上,再确定柱面也就确定了数据在该盘面上的哪一个磁道。

  • 扇区(Sector): 每个磁道被划分成若干个扇区,因此在确定了数据在哪一个磁道的基础上,再确定扇区也就确定了数据在该磁道上的哪个扇区。

简单来说,CHS寻址方式就是先通过H(磁头)确定数据所在的盘面,再通过C(柱面)确定数据所在的磁道,最后通过S(扇区)定位到目标扇区。

说明

  • 因为CHS寻址方式是磁盘定位扇区的方式,但实际CHS寻址方式对磁盘以外的设备来说没什么作用,因此系统软件在定位磁盘上的数据时采用的是LBA(Logical Block Address,逻辑区块地址)。

  • LBA是描述计算机存储设备上数据所在区块的通用机制,LBA和CHS之间可以通过计算公式进行相互转换,LBA存在的意义就是对底层逻辑器件进行虚拟化,让系统软件可以不用关心底层硬件具体的寻址方式,而实际底层硬件采用的还是CHS寻址方式。

4、操作系统与磁盘交互的基本单位

操作系统与磁盘进行IO交互的基本单位是4KB,而不是扇区的大小512字节

原因如下:

  • 操作系统与磁盘进行IO交互时,如果直接以扇区的大小作为IO的基本单位,那么这时系统的IO代码和硬件就是强相关的,如果将来当硬件的扇区大小发生变化时就需要对应修改操作系统的IO代码。

  • 此外,以扇区的大小作为IO的基本单位太小了,这就意味着读取同样的数据内容,需要进行更多次的磁盘访问,而磁盘的效率是比较低的,这样IO效率就降低了。

  • 最后,操作系统也会对内存进行管理,物理内存实际是被划分成一个个4KB大小的页框的,为了提高数据加载和保存的效率,磁盘上的数据也会被划分成一个个4KB大小的页帧(由文件系统进行管理),因此操作系统与磁盘以4KB为单位进行IO交互。

总结:操作系统与磁盘以4KB作为IO交互的基本单位,一方面是为了提高IO效率,另一方面是为了实现硬件和系统的解耦。

5、磁盘的随机访问与连续访问

  • 随机访问: 本次IO所给出的扇区地址与上次IO给出的扇区地址不连续,磁头在两次IO操作之间需要做比较大的移动动作才能找到目标扇区进行IO。

  • 连续访问: 本次IO所给出的扇区地址与上次IO给出的扇区地址是连续的,磁头很快就能找到目标扇区进行IO。

因此,尽管两次IO是在同一时刻发出的,但如果它们请求的扇区地址相差很大,那也只能称为随机访问,因为连续访问中的连续指的是访问的扇区地址的连续,而不是访问时间的连续。

磁盘是通过机械运动进行寻址的,由于连续访问不需要过多的定位,因此效率比较高。

6、MySQL 与磁盘交互基本单位

而MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。这种软件有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是16KB,这个基本数据单元,在MySQL 这里叫做Page(注意和系统的page区分)

说明: 本篇博客中没有做特殊说明的地方,都是以InnoDB存储引擎为例进行讲解的。

通过show命令查看系统中的全局变量,可以看到InnoDB存储引擎交互的基本单位是16KB。如下:

show global status like 'innodb_page_size';

【MySQL】索引_第8张图片

Buffer Pool

  • 在MySQL中进行的各种CRUD操作时,都需要先通过计算找到对应的操作的数据,只要涉及计算就需要CPU参与,而冯诺依曼体系结构决定了CPU只能和内存打交道,因此为了便于CPU参与,就需要先将数据加载到内存当中。

  • 所以在特定的时间内,MySQL中的数据一定是同时存在于磁盘和内存中的,当操作完内存数据后,再以特定的刷新策略将内存中的数据刷新到磁盘当中,这时MySQL和磁盘进行数据交互的基本单位就是Page。

  • 由于MySQL与磁盘交互的单位是Page,所以MySQL 中的数据文件,是以Page为单位保存在磁盘当中的

  • 为了更好的支持上述操作,MySQL服务器在启动的时候会预先申请一块内存空间来进行各种缓存,这块内存空间叫做Buffer Pool(一般情况下是128M,可以修改),后续磁盘中加载的数据就会保存在Buffer Pool中,刷新数据时也就是将Buffer Pool中的数据刷新到磁盘。

  • 由于操作系统内核中是有内核文件缓冲区的,因此MySQL从磁盘读取数据时,需要先将数据从磁盘读取到内核缓冲区,再将数据从内核缓冲区读取到Buffer Pool,MySQL将数据刷新到磁盘时,同样需要先将数据从Buffer Pool刷新到内核缓冲区,再将数据从内核缓冲区刷新到磁盘。

因为操作系统和磁盘交互的基本单位是4KB,所以内核缓冲区与磁盘之间是以4KB为单位进行交互的。而MySQL的Buffer Pool和磁盘实际并不是直接交互的,因此通常情况下我们说:MySQL与磁盘交互的基本单位是16KB,指的是MySQL的Buffer Pool与内核缓冲区之间是以16KB为单位进行交互的。只不过在说的时候更关注的是MySQL和磁盘之间的关系,所以直接说的是MySQL与磁盘交互的基本单位是16KB,相当于忽略了中间的内核缓冲区。

示意图如下:

【MySQL】索引_第9张图片

三、索引的理解

1、主键索引现象

创建一个用户表,表当中包含用户的id、年龄和姓名,并将用户的id设置成主键。如下:

【MySQL】索引_第10张图片

创建表完毕后向表中插入一些数据,并且插入数据时没有按照主键的大小顺序插入。如下:

insert into user (id, age, name) values(3, 18, '杨过'),
 (4, 16, '小龙女'), 
 (2, 26, '黄蓉'), 
 (5, 36, '郭靖'),
 (1, 56, '欧阳锋');

在这里插入图片描述

然后我们查看表中的数据:

select * from user;

【MySQL】索引_第11张图片

这时为什么呢,根本原因就是:因为我们创建表时设置了主键,即便向表中插入数据时是乱序插入的,MySQL底层也会自动按照主键对插入的数据进行排序。


接下啦我们先思考一个问题:

为什么MySQL与磁盘交互的基本单位是Page

MySQL与磁盘进行交互时为什么不是按需交互,而是以Page为基本单位进行交互的?

  • 当我们查询表中的某一条记录时,如果MySQL只从磁盘中将这一条记录加载到内存当中,那么当我们继续查询表中的其他记录时,MySQL就一定需要再次与磁盘进行IO交互。

  • 而如果当我们查询表中的某一条记录时,MySQL直接将这条记录所在的整个Page都加载到内存当中,那么当我们继续查询表中的其他记录时,MySQL很可能就不再需要与磁盘进行IO交互了,因为这条记录很可能也在被加载进来的Page当中,这时直接在内存中进行查询即可,大大减少了IO的次数。

  • 当然,我们不能保证用户下一次要访问的数据一定就在本次加载进来的Page当中,但是根据统计学原理,当一个数据正在被访问时,那么下一次有很大可能会访问其周边的数据(局部性原理),因此我们有较大概率保证用户下一次要访问的数据和本次访问的数据在同一个Page当中,如果局部性原理没有起作用,那就再把对应的Page加载到内存当中即可。

也就是说,MySQL与磁盘进行交互时以Page为基本单位,可以减少与磁盘IO交互的次数,进而提高IO的效率。


2、主键索引结构的构建

2.1 理解单个Page

  • MySQL启动后,其BufferPool中一定有许多数据文件,在运行期间一定有大量的Page需要被换入换出,因此MySQL一定需要将内存中大量的Page管理起来。

  • MySQL将内存中的每一个Page都用一个结构体描述起来,然后再将各个结构体以双链表的形式组织起来,因此一个Page结构体内部既包含数据字段,也包含属性字段。

  • 此外,为了方便后续数据的插入和删除,每个Page结构体内部存储的数据记录会以单链表的形式组织起来,并且各个记录之间会按照主键进行排序。

假设上述测试表中的记录都在同一个Page当中,那么该Page的结构大致如下:

【MySQL】索引_第12张图片

因为有主键的问题, MySQL 会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的。


2.2 单个Page内创建页内目录

Page结构体内部存储的数据记录是以单链表的形式组织起来的,当页内部的数据量增多时,本质在页内部进行的还是线性遍历,效率比较低下。

  • 为了解决这个问题,这时可以在Page结构体内部引入页内目录,将Page结构体内部存储的数据记录按照主键划分为若干区域,页内目录中就存储着这若干区域的最小键值。

  • 在Page结构体内部引入页内目录后,在页内部查询数据时就可以先通过页内目录找到目标数据所在区域的起始记录,然后再从该记录开始向后遍历找到目标记录。

比如在之前的Page内部引入页内目录后的结构大致如下:

【MySQL】索引_第13张图片

  • 在每个Page结构体内部引入页内目录,目的是为了加速在单个Page内部数据查询的效率。由于这个页内目录也是保存在Page内部的,而单个Page的大小是固定为16KB,因此添加页内目录后Page内部能够保存的数据记录变少了,所以在Page内部引入页内目录本质是一种空间换时间的做法,就像给书添加目录需要花费更多的纸张一样。

思考

为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?

  • 页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的!而插入数据时排序的目的,就是优化查询的效率!

  • 每个Page结构体内部的数据会按照主键进行排序,最重要就是:为了引入页内目录,因为只有数据按照主键排序后引入页内目录才有意义,就像书中每一页都是按照页码进行排序的一样,如果一本书的页码是乱序的,那么它的目录根本就没有意义。

2.3 多页情况

多个Page的示意图如下:

【MySQL】索引_第14张图片

随着数据量不断增大,单个Page中无法存下所有数据,这时就需要用多个Page来存储数据。

  • 这时在查询数据时就需要,先遍历由Page构成的双链表确定目标数据在哪一个Page中,然后再在该Page内部找到目标数据。

  • 虽然在单个Page内部能够通过页内目录来快速定位数据,但在遍历Page双链表寻找目标Page时本质进行的还是线性遍历。

  • 遍历意味着依旧需要进行大量的IO,将下一个Page加载到内存,进行线性检测。这样就显得我们之前的Page内部的目录,有点杯水车薪了。

2.4 给Page结构体创建页目录

那么如何解决呢?解决方案,其实就是我们之前的思路,给Page也带上目录!

  • 使用一个目录项来指向某一页,其中每个目录项的构成是:键值+指针。(图中没有画全)

  • 页目录中的每个目录项都指向一个Page,而这个目录项存放的就是其指向的Page中存放的最小数据的键值。

  • 在给各个Page结构体建立页目录后,在查询数据时就可以先通过遍历页目录找到目标数据所在的Page,然后再在该Page内部找到目标数据。

给各个Page建立页目录后的示意图如下:

【MySQL】索引_第15张图片

说明

  • 这里的「页目录」与之前的「页内目录」的区别在于,这种目录管理的级别是页,而「页内目录」管理的级别是行。此外,页内目录与其管理的多条记录是保存在同一个Page中的,而页目录是重新申请的一个Page结构体只用来保存的每个Page的地址

  • 随着数据量不断增大,Page变得越来越多,这时一个页目录无法管理所有的Page,这时就需要更多个的页目录。这些页目录也是一个个的Page结构体,只不过这些Page结构体中存放的不是数据记录,而是各个Page的目录信息。但是在MySQL看来,无论Page当中存储的是什么数据,都应该被管理起来,因此这些Page页目录也需要用双链表连接起来。

2.5 页目录之上再创建页目录

  • 就算给各个Page结构体也建立了页目录,但随着数据量不断增大,页目录的数量也会越来越多,这时在遍历页目录寻找目标Page时本质进行的还是线性遍历。

  • 类似的,我们可以不断在页目录之上再创建页目录,最终就一定能够得到一个入口页目录,这时在查询数据时就可以从入口页目录开始不断查询页目录,最终找到目标数据所在的Page,然后再在该Page内部找到目标数据。

最终构建出来的结构如下:

【MySQL】索引_第16张图片

说明

  • 最终构建出来的实际就是一棵B+树,这棵B+树就是InnoDB索引结构,其中每一层Page的作用就是加速它的下一层的查找效率
  • 如果我们创建表时设置了主键,那么MySQL在底层就会自动将这张表中的的数据以B+树的形式组织起来,保存在Buffer Pool当中,当我们查询数据时就可以通过查询这棵B+树来提高查询效率。
  • MySQL中可能同时有大量的表正在被处理,因此Buffer Pool中可能会存在多个索引结构,也就是同时存在多个B+树结构,当我们查询表时访问的就是这张表对应的B+树结构。

思考:B+树中的Page结点是否需要全量加入到Buffer Pool中?

  • 当对MySQL中的某张表进行增删查改操作时,不需要将其对应B+树的所有结点全量加入到Buffer Pool中,甚至在刚开始时只需要将B+树的根结点加入到Buffer Pool中。

  • 当后续访问表中的数据时,再将该数据对应路径上的结点加入到Buffer Pool中即可,对于其他不需要的结点根本不用加入到Buffer Pool中,这一点和操作系统中的页表是很像的。

  • 此外,在刷新数据时也不需要将B+树中所有的结点都进行刷新,在Page结构体中有一个标记位用来标记当前Page是否被修改过,如果被修改过则说明这是一个脏数据,在刷新数据时只有脏数据才需要被刷新到磁盘上。

  • 由于B+树中的结点都是16KB大小的Page,因此无论是刷新数据到磁盘函数从磁盘加载数据到Buffer Pool,都是以Page为单位进行的,这也就是所谓的MySQL与磁盘交互的基本单位是Page。

如果把这棵B+树逆时针旋转90度,就会发现这其实就是操作系统中的页表结构,本质操作系统中的页表也是B+树结构。如下:

【MySQL】索引_第17张图片

以32位平台为例,页表将一个虚拟地址转换成物理地址的过程如下:

  • 选择虚拟地址的前10个比特位在页目录当中进行查找,找到对应的页表。
  • 再继续选择虚拟地址后续的10个比特位在对应的页表当中进行查找,找到物理内存中对应页框的起始地址。
  • 最后选择虚拟地址中剩下的12个比特位作为偏移量,从对应页框的起始地址处向后进行偏移,最终得到的就是转换后的物理地址。

说明

  • 12个比特位有 2 12 2^{12} 212 种取值,而 2 12 2^{12} 212字节对应就是4KB,所以物理内存中一个页框的大小就是4KB,这也就是为什么操作系统与磁盘交互的基本单位是4KB的原因。
  • 此外,页表中的各个B+树结点也不需要全量加入到内存中,而只需要加入访问到的结点即可,所以页表占用的内存大小实际是可控的,这也就是为什么二级页表可行的原因。

3、索引数据结构的选择

除了InnoDB存储引擎所采用的B+树结构,索引结构还可以采用哪些数据结构呢?

  • 链表:查找时是线性遍历,效率太低。
  • 普通二叉搜索树:可能退化成线性结构,这时查找还是线性遍历。
  • AVL树和红黑树:虽然保证了二叉树是绝对或近似平衡的,不会退化成线性结构,但AVL树和红黑树都是二叉树结构,这就意味着树的层高会比较高,而查询数据时都是从根结点开始向下进行查找的,这也就意味着在查询过程中需要遍历更多结点,如果这些结点还没有被加载到Buffer Pool中,这时就需要进行更多次的IO操作,影响效率。
  • 哈希表:官方的索引实现方式中MySQL是支持HASH的,只不过InnoDB和MyISAM存储引擎并不支持。哈希表的优点就是它的时间复杂度是 O ( 1 ) O ( 1 ) O(1) 的,但哈希表也有一个缺点就是不利于进行数据的范围查找,每次查找都要经过哈希运算。

下面是几个常见的存储引擎,与其所支持的索引类型:

存储引擎 支持的索引类型
InnoDB BTREE
MyISAM BTREE
MEMORY/HEAP HASH、BTREE
NDB HASH、BTREE

B树 VS B+树

B+树是B树的一种变形结构,那为什么我们没有采用普通的B树作为索引结构呢?

  • 首先,普通B树中的所有结点中都同时包括索引信息和数据信息,由于一个Page的大小是固定的,因此非叶子结点中如果包含了数据信息,那么这些结点中能够存储的索引信息一定会变少,这时这棵树形结构一定会变高变瘦,当查询数据时就可能需要与磁盘进行更多次的IO操作。

  • 其次,普通B树中的各个叶子结点之间没有连接起来,这将不利于进行数据的范围查找,而B+树的各个叶子结点之间是连接起来的,当我们进行范围查找时,直接先找到第一个数据然后继续向后遍历找到之后的数据即可,因此将各个叶子结点连接起来更有利于进行数据的范围查找。

B树

【MySQL】索引_第18张图片

B+树
【MySQL】索引_第19张图片

4、聚簇索引 与 非聚簇索引

之前推导的主键索引结构是InnoDB存储引擎的主键索引结构,而MyISAM存储引擎同样采用B+树作为索引的基本数据结构。

  • 与InnoDB存储引擎的B+树不同的是,MyISAM存储引擎的B+树的叶子结点存放的不是数据记录,而是数据记录对应的地址

MyISAM存储引擎 - 主键索引结构

比如下图为MyISAM存储引擎的主键索引结构,其中Col1为主键。如下:

【MySQL】索引_第20张图片

MyISAM存储引擎 - 普通索引结构

  • MyISAM存储引擎的普通索引采用的也是B+树结构,与主键索引唯一不同的地方就是普通索引的B+树中的键值可以重复。

  • 因此一张表可能会同时存在多个B+树结构,但由于MyISAM存储引擎的B+树叶子结点中,存储的是对应的数据记录的地址,因此有效数据只会存储一份。

【MySQL】索引_第21张图片

InnoDB存储引擎 - 普通索引结构

  • InnoDB存储引擎的普通索引采用的也是B+树结构,为了减少数据的重复存储,普通索引的B+树的叶子结点中存储的不是数据记录,而是对应数据记录的主键值。

  • 当根据普通索引查询数据时,会先查找普通索引对应的B+树,找到目标记录的主键值,然后再查找主键索引对应的B+树找到目标记录,这个过程就叫做回表查询

比如下图为InnoDB存储引擎的普通索引结构,其中Col3为索引列。如下:

【MySQL】索引_第22张图片


说明

  • InnoDB存储引擎的普通索引的B+树叶子结点中没有保存整条数据记录,是为了节省空间,因为同一张表可能会创建多个普通索引,每个普通索引的B+树中都保存一份数据会造成数据冗余,所以通过回表查询主键索引对应的B+来获取整个数据记录,该做法本质一种以时间换取空间的做法。

  • 当根据普通索引查询数据时,其实也不一定需要进行回表查询,因为有可能我们想要查询的就是这条记录对应的主键的值,因此查询完普通索引对应B+树后即可完成查询。

  • 采用InnoDB存储引擎建立的每张表都会有一个主键,就算用户没有设置,InnoDB也会自动帮你创建一个不可见的主键,因为完整数据记录只会存储在主键索引对应的B+树中的,因此采用InnoDB存储引擎建立的表必须有主键。


讲到这里我们终于可以回答这个问题了:聚簇索引 VS 非聚簇索引

  • 聚簇索引: 像InnoDB存储引擎这种,将数据记录与索引结构放在一起的索引方案,叫做聚簇索引。
  • 非聚簇索引: 像MyISAM存储引擎这种,将数据记录与索引结构分离的索引方案,叫做非聚簇索引。

当采用InnoDB存储引擎创建表时,在数据库对应的目录下会新增两个文件。如下:

【MySQL】索引_第23张图片

当采用MyISAM存储引擎创建表时,在数据库对应的目录下会新增三个文件。如下:

【MySQL】索引_第24张图片

说明

  • 采用InnoDB和MyISAM存储引擎创建表时都会生成xxx.frm文件,该文件中存储的是表结构相关的信息。

  • 采用InnoDB存储引擎创建表时会生成一个xxx.ibd文件,该文件中存储的是索引和数据相关的信息,这就是所谓的聚簇索引,索引和数据是存储在同一个文件中的。

  • 采用MyISAM存储引擎创建表时会生成一个xxx.MYD文件和一个xxx.MYI文件,其中xxx.MYD文件中存储的是数据相关的信息,而xxx.MYI文件中存储的是索引相关的信息,这就是所谓的非聚簇索引,索引和数据是分开存储的。

四、索引操作

1、创建索引

Ⅰ、创建主键索引

  • 方式一
    在创建表的时候,直接在字段名后指定 primary key
create table user1(
     id int primary key,
     name varchar(20)
     );

desc user1;

【MySQL】索引_第25张图片

  • 方式二

在创建表的最后,指定某列或某几列为主键索引

 create table user2(
     id int,
     name varchar(20),
     primary key(id)
     );

desc user2;

【MySQL】索引_第26张图片

  • 方式三

创建表后,使用alter命令给指定字段添加主键索引。如下:

create table user3(id int, name varchar(20));
alter table user3 add primary key(id);
desc user3;

【MySQL】索引_第27张图片

主键索引的特点:

  • 一个表中,最多有一个主键索引,当然可以使复合主键
  • 主键索引的效率高(主键不可重复)
  • 创建主键索引的列,它的值不能为null,且不能重复
  • 主键索引的列基本上是int

Ⅱ、唯一索引的创建

  • 方式一
    在创建表时,直接在对应的字段名后指定unique。如下:
create table user4(id int, name varchar(20) unique);
desc user4;

【MySQL】索引_第28张图片

  • 方式二

创建表时,在表的后面指定某列或某几列为unique

create table user5(id int, name varchar(20), unique(name));

在这里插入图片描述

  • 方式三

创建表后,使用alter命令给指定字段添加唯一索引。如下:

create table user6(id int, name varchar(20));

alter table user6 add unique(name);

【MySQL】索引_第29张图片

唯一索引的特点:

  • 一个表中,可以有多个唯一索引
  • 查询效率高
  • 如果在某一列建立唯一索引,必须保证这列不能有重复数据
  • 如果一个唯一索引上指定not null,等价于主键索引

Ⅲ、普通索引的创建

  • 方式一

在表的定义最后,指定某列为索引index

create table user7(id int, name varchar(20), email varchar(30), index(email));

desc user7;

【MySQL】索引_第30张图片

  • 方式二

创建表后,使用alter命令给指定字段添加普通索引。如下:

create table user8(id int, name varchar(20), email varchar(30));

alter table user8 add index(email);

【MySQL】索引_第31张图片

  • 方式三

创建表后,使用create命令给指定字段创建普通索引,并指定索引名。如下:

create table user9(id int, name varchar(20), email varchar(30));

create index email_idx on user9(email);

desc user9;

【MySQL】索引_第32张图片

普通索引的特点:

  • 一个表中可以有多个普通索引,普通索引在实际开发中用的比较多
  • 如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引

2、查询索引

  • 方法一
    (信息比较简略): 使用desc表名
desc user9;

【MySQL】索引_第33张图片

  • 方法二
    show keys from 表名
show keys from user1\G

【MySQL】索引_第34张图片


  • Table: 表示创建索引的表的名称。
  • Non_unique: 表示该索引是否是唯一索引,如果是则为0,如果不是则为1。
  • Key_name: 表示索引的名称。
  • Seq_in_index: 表示该列在索引中的位置,如果索引是单列的,则该列的值为1,如果索引是复合索引,则该列的值为每列在索引定义中的顺序。
  • Column_name: 表示定义索引的列字段。
  • Collation: 表示列以何种顺序存储在索引中,“A”表示升序,NULL表示无分类。
  • Cardinality: 索引中唯一值数目的估计值。基数根据被存储为整数的统计数据计数,所以即使对于小型表,该值也没有必要是精确的。基数越大,当进行联合时,MySQL使用该索引的机会就越大。
  • Sub_part: 表示列中被编入索引的字符的数量,若列只是部分被编入索引,则该列的值为被编入索引的字符的数目,若整列被编入索引,则该列的值为NULL。
  • Packed: 指示关键字如何被压缩。若没有被压缩,则值为NULL。
  • Null: 用于显示索引列中是否包含NULL,若包含则为YES,若不包含则为NO。
  • Index_type: 显示索引使用的类型和方法(BTREE、FULLTEXT、HASH、RTREE)。
  • Comment: 显示评注。

  • 方法三
    show index from 表名
show index from user2\G

【MySQL】索引_第35张图片

3、删除索引

  • 删除主键索引
alter table 表名 drop primary key

下面是user1的结构:

desc user1;

【MySQL】索引_第36张图片

删除主键索引:

alter table user1 drop primary key;

【MySQL】索引_第37张图片

  • 其他索引的删除
alter table 表名 drop index 索引名;

或者:

drop index 索引名 on 表名

索引名就是show keys from 表名中的 Key_name 字段。

删除user4的索引:

alter table user4 drop index name;

【MySQL】索引_第38张图片

删除user8的索引:

drop index email on user8;

【MySQL】索引_第39张图片

说明

  • 一个表只有一个主键索引,所以在删除主键索引的时候不用指明索引名,而一个表中可能有多个非主键索引,所以在删除非主键索引时需要指明索引名。

4、索引创建原则

  • 比较频繁作为查询条件的字段应该创建索引
  • 唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件,因为这样的信息构建出来的B+树效率并不是很高。
  • 更新非常频繁的字段不适合作创建索引
  • 不会出现在where子句中的字段不该创建索引

时刻要记住,创建索引的目的就是为了提高查询的效率!


5、创建全文索引(选学)


当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文索引机制,但是要求表的存储引擎必须是MyISAM,而且默认的全文索引支持只英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版(coreseek)。


案例 对文章中的词进行搜索

比如下面创建一个文章表,表当中包含文章的id、文章名称、文章内容,并在创建表的最后通过fulltexttitlebody列创建全文索引。如下:

create table articles( 
id int primary key auto_increment, 
title varchar(200) not null, 
body text, fulltext(title, body)
)engine=myisam;

【MySQL】索引_第40张图片

下面向表当中插入一些测试数据。如下:

INSERT INTO articles (title,body) VALUES
('MySQL Tutorial','DBMS stands for DataBase ...'),
('How To Use MySQL Well','After you went through a ...'),
('Optimizing MySQL','In this tutorial we will show ...'),
('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),
('MySQL vs. YourSQL','In the following database comparison ...'),
('MySQL Security','When configured properly, MySQL ...');

【MySQL】索引_第41张图片

如果要查询哪些文章中包含database关键字,我们可以通过模糊匹配进行查找。如下:

select * from articles where body like '%database%';

【MySQL】索引_第42张图片

但实际这种查找方式并没有用到全文索引,如何证明呢?

在SQL语句前面加上explain,就可以看到MySQL对于此条SQL的执行计划。

explain select * from articles where body like '%database%'\G

【MySQL】索引_第43张图片

key对应的值为NULL,表示这条SQL在执行过程中没有用到任何索引。


explain中的列的含义

  • id : 查询执行顺序,id 值越高,优先级越高,id为NULL最后执行。

  • select_type: 查询类型,simple表示简单查询中不包含子查询或者 union

  • table: 显示这一行的数据是关于哪张表的。

  • type: 这是重要的列,显示连接使用了何种类型。ALL:即全表扫描,扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了。

  • possible_keys: 查询条件字段涉及到的索引,可能没有使用。

  • key: 实际使用的索引。如果为 NULL,则没有使用索引。

  • key_len: 表示索引中使用的字节数,查询中使用的索引的长度(最大可能长度),并非实际使用长度,理论上长度越短越好。

  • ref: 显示索引的哪一列被使用了。

  • rows: 根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数。注意这个不是结果集里的行数。

  • filtered: 显示了通过条件过滤出的行数的百分比估计值。

  • Extra: MYSQL 如何解析查询的额外信息。


如果要通过全文索引来查询,需要使用matchagainst关键字进行搜索。如下:

select * from articles where match(title, body) against('database');

match后面填写我们以前设置全文索引的列名,against后面填写我们要匹配的内容。

【MySQL】索引_第44张图片

在这条SQL语句前面加上explain,可以看到key对应的值为title,表示这条SQL在执行过程中用到了索引名为title的索引。如下:

explain select * from articles where match(title, body) against('database')\G

【MySQL】索引_第45张图片

  • 由于我们同时使用title和body建立全文索引时,相当于建立了一个复合索引,默认会选择fulltext中的第一个列名作为这个复合索引的索引名,所以这里explain中key对应的索引名为title。
  • 由于是title和body共同建立的全文索引,所以如果文章当中没有出现关键字,但文章名称中出现了关键字则也会被筛选出来(当前示例没有体现出来)。

说明

  • MyISAM存储引擎是支持全文索引的,而InnoDB存储引擎是在5.6以后才开始支持全文索引的。

  • 目前,使用MySQL自带的全文索引时,如果查询字符串的长度过短将无法得到期望的搜索结果。MySQL全文索引所能找到的词默认最小长度为4个字符。另外,如果查询的字符串包含停止词,那么该停止词将会被忽略。

  • 如果可能,请尽量先创建表并插入所有数据后再创建全文索引,而不要在创建表时就直接创建全文索引,因为前者比后者的全文索引效率要高。

如果我们要给已经存在的表的指定字段创建全文索引,同样以articles表为例,我们可以使用如下SQL语句进行创建:

ALTER TABLE article ADD FULLTEXT INDEX fulltext_article(title,content);



参考文章:

MySQL索引特性

你可能感兴趣的:(MySQL,mysql,性能优化,数据库)