MySQL - 第10节 - MySQL索引特性

目录

1.索引的概念

2.认识磁盘

2.1.磁盘的结构

2.1.1.磁盘的整体结构

2.1.2.磁盘中的一个盘片

2.1.3.扇区的定位方式

2.1.4.操作系统与磁盘交互的基本单位

2.2.磁盘的随机访问(Random Access)与连续访问(Sequential Access)

3.MySQL与磁盘交互的基本单位

4.建立共识

5.索引的理解

5.1观察主键索引现象

5.2.推导主键索引结构的构建

5.3.索引结构可以采用哪些数据结构

5.4.聚簇索引 VS 非聚簇索引

6.索引操作

6.1.创建主键索引

6.2.创建唯一索引

6.3.创建普通索引

6.4.创建全文索引

6.5.查询索引

6.6.删除索引

6.7.索引创建原则

7.其他概念(自行了解)


1.索引的概念

索引的概念:

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

常见的索引分为:

• 主键索引(primary key)。

• 唯一索引(unique)。

• 普通索引(index)。

• 全文索引(fulltext)。

索引的价值:

使用如下SQL创建一个海量数据表:

drop database if exists `index_demon`;
create database if not exists `index_demon` default character set utf8;
use `index_demon`;

-- 构建一个8000000条记录的数据
-- 构建的海量表数据需要有差异性,所以使用存储过程来创建

-- 产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
begin
declare chars_str varchar(100) default
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
declare return_str varchar(255) default '';
declare i int default 0;
while i < n do
set return_str =concat(return_str,substring(chars_str,floor(1+rand()*52),1));
set i = i + 1;
end while;
return return_str;
end $$
delimiter ;

-- 产生随机数字
delimiter $$
create function rand_num( )
returns int(5)
begin
declare i int default 0;
set i = floor(10+rand()*500);
return i;
end $$
delimiter ;

-- 创建存储过程,向雇员表添加海量数据
delimiter $$
create procedure insert_emp(in start int(10),in max_num int(10))
begin
declare i int default 0;
set autocommit = 0;
repeat
set i = i + 1;
insert into EMP values ((start+i)
,rand_string(6),'SALESMAN',0001,curdate(),2000,400,rand_num());
until i = max_num
end repeat;
commit;
end $$
delimiter ;

-- 雇员表
CREATE TABLE `EMP` (
  `empno` int(6) unsigned zerofill NOT NULL COMMENT '雇员编号',
  `ename` varchar(10) DEFAULT NULL COMMENT '雇员姓名',
  `job` varchar(9) DEFAULT NULL COMMENT '雇员职位',
  `mgr` int(4) unsigned zerofill DEFAULT NULL COMMENT '雇员领导编号',
  `hiredate` datetime DEFAULT NULL COMMENT '雇佣时间',
  `sal` decimal(7,2) DEFAULT NULL COMMENT '工资月薪',
  `comm` decimal(7,2) DEFAULT NULL COMMENT '奖金',
  `deptno` int(2) unsigned zerofill DEFAULT NULL COMMENT '部门编号'
);

-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);

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

将上述SQL保存到index_data.sql文件中,然后在MySQL中使用source依次执行文件中的SQL即可(该source依次执行文件的SQL命令需要一些时间)。如下:

SQL执行完毕后查看数据库就能看到一个名为index_demon的数据库。如下:

进入该数据库,在数据库中可以看到一个名为EMP的员工表。如下:

由于EMP表中有八百万条记录,因此在查看EMP表中的数据时可以带上limit子句。如下:

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

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

当我们给员工表中的工号建立索引后,数据库底层就会为员工表中的数据记录构建特定的数据结构,需要注意的是,由于当前员工表中的数据量较大,因此建立索引时也需要花费较长时间。如下:

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

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


2.认识磁盘

• mysqld中的所有操作全部都是在内存中进行的,后期mysqld才会进行持久化操作,将数据写入磁盘中。

• MySQL给用户提供存储服务,存储的数据在磁盘这个外设当中。

• 磁盘是计算机中的一个机械设备,相比于计算机的其他电子元件,磁盘的效率是比较低的。

• 而如何提高效率是MySQL的一个重要话题,因此我们有必要了解一下磁盘的相关内容。

2.1.磁盘的结构

2.1.1.磁盘的整体结构

磁盘的整体结构如下:

部分说明:

• 永磁铁: 机械硬盘的存储方式与磁带比较类似,磁体具有记忆的功能,永磁铁是为了保证磁性的稳定。
• 音圈马达: 硬盘读取数据的关键部位,主要作用是将存储在磁盘上的信息转换为电信号向外传输。
• 主轴: 保证电机稳定的转动,磁盘转动才能读出数据。
• 空气滤波片: 过滤空气硬盘透气孔中进入的空气,保证硬盘内部清洁,同时还可以防止硬盘内部的零件氧化,确保硬盘安全使用。
• 磁盘: 硬盘一般都是铝合金制作的,主要是用来存储文件的。
• 磁头: 用来读取盘片上的信息。
• 串行接口: 用来连接电脑与硬盘的接口,起到传输的作用。

2.1.2.磁盘中的一个盘片

一个磁盘由多个盘片叠加而成,盘片的表面涂有磁性物质,这些磁性物质就是用来记录二进制数据的,因为盘片的正反两面都可涂上磁性物质,因此一个盘片有两个盘面。

磁盘中的一个盘片如下:

部分说明: 

• 磁道: 磁盘表面被分为许多同心圆,每个同心圆称为一个磁道,每个磁道都有一个编号,最外面的是0磁道。
• 扇区: 每个磁道被划分成若干个扇区,每个扇区的存储容量为512字节,每个扇区都有一个编号。
说明一下:

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

2.1.3.扇区的定位方式

• 一个磁盘由多个盘片叠加而成,每个盘片有两个盘面,所有盘面中半径相同的同心磁道构成一个柱面。

• 每个盘面都有一个对应的磁头,每个磁头都有一个编号,所有的磁头都是连在同一个磁臂上的。

示意图如下:

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

• 磁头(Heads): 每个盘面都有一个对应的磁头,因此确定了磁头也就确定了数据在哪一个盘面。
• 柱面(Cylinder): 所有盘面中半径相同的同心磁道构成柱面,因此在确定了数据在哪一个盘面的基础上,再确定柱面也就确定了数据在该盘面上的哪一个磁道。
• 扇区(Sector): 每个磁道被划分成若干个扇区,因此在确定了数据在哪一个磁道的基础上,再确定扇区也就确定了数据在该磁道上的哪个扇区。
简单来说,CHS寻址方式就是先通过H确定数据所在的盘面,再通过C确定数据所在的磁道,最后通过S定位到目标扇区。

说明一下:

• CHS寻址方式是磁盘定位扇区的方式,但实际CHS寻址方式对磁盘以外的设备来说没什么作用,因此系统软件在定位磁盘上的数据时采用的是LBA(Logical Block Address,逻辑区块地址)。
• LBA是描述计算机存储设备上数据所在区块的通用机制,LBA和CHS之间可以通过计算公式进行相互转换,LBA存在的意义就是对底层逻辑器件进行虚拟化,让系统软件可以不用关心底层硬件具体的寻址方式,而实际底层硬件采用的还是CHS寻址方式。

2.1.4.操作系统与磁盘交互的基本单位

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

• 物理内存实际是被划分成一个个4KB大小的页框的,磁盘上的数据也会被划分成一个个4KB大小的页帧,因此操作系统与磁盘以4KB为单位进行IO交互,就能提高数据加载和保存的效率。
• 操作系统与磁盘进行IO交互时,如果直接以扇区的大小作为IO的基本单位,那么这时系统的IO代码和硬件就是强相关的,将来当硬件的扇区大小发生变化时就需要对应修改操作系统的IO代码。
• 此外,以扇区的大小作为IO的基本单位太小了,这就意味着读取同样的数据内容,需要进行更多次的磁盘访问,而磁盘的效率是比较低的,这样IO效率就降低了。
因此操作系统与磁盘以4KB作为IO交互的基本单位,一方面是为了提高IO效率,另一方面是为了实现硬件和系统的解耦。

2.2.磁盘的随机访问(Random Access)与连续访问(Sequential Access)

随机访问与连续访问:

• 随机访问: 本次IO所给出的扇区地址与上次IO给出的扇区地址不连续,磁头在两次IO操作之间需要做比较大的移动动作才能找到目标扇区进行IO。
• 连续访问: 本次IO所给出的扇区地址与上次IO给出的扇区地址是连续的,磁头很快就能找到目标扇区进行IO。
需要注意的是,尽管两次IO是在同一时刻发出的,但如果它们请求的扇区地址相差很大,那也只能称为随机访问,因为连续访问中的连续指的是访问的扇区地址的连续,而不是访问时间的连续,由于连续访问不需要过多的定位,因此效率比较高。


3.MySQL与磁盘交互的基本单位

MySQL与磁盘交互的基本单位:

MySQL作为一款应用软件,可以想象成是一种特殊的文件系统(mysql的文件系统和linux的文件系统之间属于上下层关系),它有着更高频的IO场景,因此为了提高基本的IO效率,MySQL与磁盘交互的基本单位是16KB,这个基本数据单元在MySQL这里也叫做Page。

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

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

Buffer Pool:

• 在MySQL中进行的各种CRUD数据操作时,都需要先通过计算找到对应的操作位置,只要涉及计算就需要CPU参与,而冯诺依曼体系结构决定了CPU只能和内存打交道,因此为了便于CPU参与,就需要先将数据加载到内存当中。
• 所以在特定的时间内,MySQL中的数据一定是同时存在于磁盘和内存中的,当执行SQL命令操作完内存数据后(对数据库的数据做的任何操作,都是在内存中进行的),再以特定的刷新策略将内存中的数据刷新到磁盘当中,这时MySQL和磁盘进行数据交互的基本单位就是Page。
• 为了更好的支持上述操作,MySQL服务器在启动的时候会预先申请一块内存空间来进行各种缓存,这块内存空间叫做Buffer Pool,后续磁盘中加载的数据就会保存在Buffer Pool中,刷新数据时也就是将Buffer Pool中的数据刷新到磁盘。
• 由于内核中是有内核缓冲区的(任何被打开的文件都有自己的内核缓冲区),因此MySQL从磁盘读取数据时,需要先将数据从磁盘读取到内核缓冲区,再将数据从内核缓冲区读取到Buffer Pool,MySQL将数据刷新到磁盘时,同样需要先将数据从Buffer Pool刷新到内核缓冲区,再将数据从内核缓冲区刷新到磁盘。
因此所谓的操作系统和磁盘交互的基本单位是4KB,就是指内核缓冲区与磁盘之间是以4KB为单位进行交互的。而MySQL的Buffer Pool和磁盘实际并不是直接交互的,因此所谓的MySQL与磁盘交互的基本单位是16KB,指的是MySQL的Buffer Pool与内核缓冲区之间是以16KB为单位进行交互的。只不过在说的时候更关注的是MySQL和磁盘之间的关系,所以直接说的是MySQL与磁盘交互的基本单位是16KB,相当于忽略了中间的内核缓冲区。

示意图如下:

注:

1.MySQL其实是在操作系统之上构建的一个应用层服务。

2.MySQL的Buffer Pool中会充满大量16KB的page,MySQL要管理这里所有的page,要管理就要先描述再组织,创建结构体对page描述(page结构体后面会详细介绍),然后使用某个数据结构对结构体进行管理,那么此时对page的管理就转换成了对该数据结构的增删查改。

3.MySQL在选取数据结构对page结构体进行管理时,需要采用高效的数据结构提高MySQL数据搜索的速度,该高效数据结构的选取也是索引的基础(具体采用何种高效数据结构下面进行讲解)。


4.建立共识

• MySQL中的数据文件,是以page为单位保存在磁盘当中的。
• MySQL的CURD操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。
• 而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中。
• 所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。
• 为了更好的进行上面的操作,MySQL服务器在内存中运行的时候,在服务器内部,就申请了被称为Buffer Pool的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。
• 为何更高的效率,一定要尽可能的减少系统和磁盘IO的次数。


5.索引的理解

5.1观察主键索引现象

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

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

但最终当我们查看表中的数据时,却发现显示出来的数据是按照主键进行有序排列的。如下:

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

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

答:当我们查询表中的某一条记录时,如果MySQL只从磁盘中将这一条记录加载到内存当中,那么当我们继续查询表中的其他记录时,MySQL就一定需要再次与磁盘进行IO交互。
而如果当我们查询表中的某一条记录时,MySQL直接将这条记录所在的整个Page都加载到内存当中,那么当我们继续查询表中的其他记录时,MySQL很可能就不再需要与磁盘进行IO交互了,因为这条记录很可能也在被加载进来的Page当中,这时直接在内存中进行查询即可,大大减少了IO的次数。
当然,我们不能保证用户下一次要访问的数据一定就在本次加载进来的Page当中,但是根据统计学原理,当一个数据正在被访问时,那么下一次有很大可能会访问其周围的数据(局部性原理),因此我们有较大概率保证用户下一次要访问的数据和本次访问的数据在同一个Page当中,如果局部性原理没有起作用,那就再把对应的Page加载到内存当中即可。
也就是说,MySQL与磁盘进行交互时以Page为基本单位,可以减少与磁盘IO交互的次数,进而提高IO的效率。

5.2.推导主键索引结构的构建

单个Page:

• MySQL中要管理很多数据文件,在运行期间一定有大量的Page需要被换入换出,因此MySQL一定需要将内存中大量的Page管理起来。
• MySQL将内存中的每一个Page都用一个结构体描述起来,然后再将各个结构体以双链表的形式组织起来,因此一个Page结构体内部既包含数据字段,也包含属性字段。
• 此外,为了方便后续数据的插入和删除,每个Page结构体内部存储的数据记录会以单链表的形式组织起来,并且各个记录之间会按照主键进行排序。
假设上述测试表中的记录都在同一个Page当中,那么该Page的结构大致如下:

说明一下:

• 每个Page结构体内部的数据会按照主键进行排序,目的是为了优化数据查询的效率,因为单链表在查找的时候是顺序查找的,有序就意味着在查找的过程中有机会提前结束查询过程。
• 这也就是前面所说的,只要设置了主键,即便向表中插入的数据是乱序的,MySQL底层也会自动按照主键对插入的数据进行排序,因此查询得到的数据是按照主键进行有序排序的。

单个Page内创建页内目录:

• Page结构体内部存储的数据记录是以单链表的形式组织起来的,当页内部的数据量增多时,本质在页内部进行的还是线性遍历,效率低下。
• 这时可以在Page结构体内部引入页内目录,将Page结构体内部存储的数据记录按照主键划分为若干区域,页内目录中就存储着这若干区域的最小键值。
• 在Page结构体内部引入页内目录后,在页内部查询数据时就可以先通过页内目录找到目标数据所在区域的起始记录,然后再从该记录开始向后遍历找到目标记录。
比如在之前的Page内部引入页内目录后的结构大致如下:

说明一下:

• 在每个Page结构体内部引入页内目录,目的是为了加速在单个Page内部数据查询的效率。由于这个页内目录也是保存在Page内部的,而单个Page的大小是固定的,因此添加页内目录后Page内部能够保存的数据记录变少了,所以在Page内部引入页内目录本质是一种空间换时间的做法,就像给书添加目录需要花费更多的纸张一样。
• 每个Page结构体内部的数据会按照主键进行排序,其实就是为了引入页内目录,因为只有数据按照主键排序后引入页内目录才有意义,就像书中每一页都是按照页码进行排序的一样,如果一本书的页码是乱序的,那么它的目录根本就没有意义。

多个Page:

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

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

多个Page的示意图如下:

Page之上创建页目录:

• 虽然在单个Page内部能够通过页内目录来快速定位数据,但在遍历Page双链表寻找目标Page时本质进行的还是线性遍历。
• 这时可以给各个Page结构体也建立页目录,页目录中的每个目录项都指向一个Page,而这个目录项存放的就是其指向的Page中存放的最小数据的键值。
• 在给各个Page结构体建立页目录后,在查询数据时就可以先通过遍历页目录找到目标数据所在的Page,然后再在该Page内部找到目标数据。
给各个Page建立页目录后的示意图如下:

说明一下:

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

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

• 就算给各个Page结构体也建立了页目录,但随着数据量不断增大,页目录的数量也会越来越多,这时在遍历页目录寻找目标Page时本质进行的还是线性遍历。
• 类似的,我们可以不断在页目录之上再创建页目录,最终就一定能够得到一个入口页目录,这时在查询数据时就可以从入口页目录开始不断查询页目录,最终找到目标数据所在的Page,然后再在该Page内部找到目标数据。
最终构建出来的结构如下:

说明一下:

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

注:具有主键的表,一个表就是一个B+树。如果表中没有主键,我们可以认为该表中所有的数据是线性组织的(但其实表中没有主键,mysql会自动形成隐藏主键,因此即使表中没有主键其实其本质也是一个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+树结构。如下:

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

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

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

5.3.索引结构可以采用哪些数据结构

索引结构可以采用哪些数据结构:

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

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

B树 VS B+树:

目前对于B树和B+树,对我们最有意义的区别是:
(1) B树节点,既有数据,又有 Page 指针,而 B+ ,只有叶子节点有数据,其他目录页,只有键值和 Page 指针。
(2) B+叶子节点,全部相连,而B 没有。

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

• 首先,普通B树中的所有结点中都同时包括索引信息和数据信息,由于一个Page的大小是固定的,因此非叶子结点中如果包含了数据信息,那么这些结点中能够存储的索引信息一定会变少,这时这棵树形结构一定会变得更高更瘦,当查询数据时就可能需要与磁盘进行更多次的IO操作。
• 其次,普通B树中的各个叶子结点之间没有连接起来,这将不利于进行数据的范围查找,而B+树的各个叶子结点之间是连接起来的,当我们进行范围查找时,直接先找到第一个数据然后继续向后遍历找到之后的数据即可,因此将各个叶子结点连接起来更有利于进行数据的范围查找。

5.4.聚簇索引 VS 非聚簇索引

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

• 之前推导的主键索引结构是InnoDB存储引擎的主键索引结构,而MyISAM存储引擎同样采用B+树作为索引的基本数据结构。
• 与InnoDB存储引擎的B+树不同的是,MyISAM存储引擎的B+树的叶子结点存放的不是数据记录,而是数据记录对应的地址。
比如下图为MyISAM存储引擎的主键索引结构,其中Col1为主键。如下:

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

• MyISAM存储引擎的普通索引采用的也是B+树结构,与主键索引唯一不同的地方就是普通索引的B+树中的键值可以重复。
• 因此一张表可能会同时存在多个B+树结构,但由于MyISAM存储引擎的B+树叶子结点中,存储的是对应的数据记录的地址,因此有效数据只会存储一份。
比如下图为MyISAM存储引擎的普通索引结构,其中Col2为索引列。如下:

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

• InnoDB存储引擎的普通索引采用的也是B+树结构,但普通索引的B+树中的键值可以重复,并且B+树的叶子结点中存储的不是数据记录,而是对应数据记录的主键值。
• 当根据普通索引查询数据时,会先查找普通索引对应的B+树找到目标记录的主键值,然后再查找主键索引对应的B+树找到目标记录,这个过程就叫做回表查询。
比如下图为InnoDB存储引擎的普通索引结构,其中Col3为索引列。如下:

说明一下:

• InnoDB存储引擎的普通索引的B+树叶子结点中没有保存整条数据记录,是为了节省空间,因为同一张表可能会创建多个普通索引,每个普通索引的B+树中都保存一份数据会造成数据冗余,所以通过回表查询主键索引对应的B+来获取整个数据记录,该做法本质一种以时间换取空间的做法。
• 当根据普通索引查询数据时,其实也不一定需要进行回表查询,因为有可能我们要查询的就是这条记录对应的主键值,因此查询完普通索引对应B+树后即可完成查询。
• 采用InnoDB存储引擎建立的每张表都会有一个主键,就算用户没有设置,InnoDB也会自动帮你创建一个不可见的主键,因为完整数据记录只会存储在主键索引对应的B+树中的,因此采用InnoDB存储引擎建立的表必须有主键。

聚簇索引 VS 非聚簇索引:

• 聚簇索引:像InnoDB存储引擎这种,将数据记录与索引结构放在一起的索引方案(数据在叶子节点中),叫做聚簇索引。

• 非聚簇索引:像MyISAM存储引擎这种,将数据记录与索引结构分离的索引方案(数据不在叶子节点中),叫做非聚簇索引。

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

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

说明一下:

• 采用InnoDB和MyISAM存储引擎创建表时都会生成xxx.frm文件,该文件中存储的是表结构相关的信息(表中有几列,列的名称,列的属性等信息)。
• 采用InnoDB存储引擎创建表时会生成一个xxx.ibd文件,该文件中存储的是索引和数据相关的信息,这就是所谓的聚簇索引,索引和数据是存储在同一个文件中的(将整个B+树存储在一个文件中)。
• 采用MyISAM存储引擎创建表时会生成一个xxx.MYD文件和一个xxx.MYI文件,其中xxx.MYD文件中存储的是数据相关的信息,而xxx.MYI文件中存储的是索引相关的信息,这就是所谓的非聚簇索引,索引和数据是分开存储的(将B+树中的索引和数据分别存储在两个文件中)。

注:无论是InnoDB还是MyISAM存储引擎,都要将索引和数据存储在磁盘中(将整个B+树存储在磁盘中)。正是因为整个B+树被持久化存储在了磁盘中,这就支持了后续在访问表中数据的时候,只需要将该数据对应路径上的结点从磁盘中载入到内存Buffer Pool中即可。


6.索引操作

6.1.创建主键索引

方式一:

创建表时,直接在对应的字段名后指定primary key。如下:

方式二:

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

方式三:

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

主键索引的特点:

主键索引的特点如下:

• 一个表中,最多只能有一个主键索引,一个主键可以由多个列同时承担。

• 主键索引的查询效率高。

• 创建主键索引的列,其列值不能为NULL,且不能重复。

• 主键索引的列一般是数字类型。

6.2.创建唯一索引

唯一索引其实就是普通索引,只不过其是通过唯一键创建的。

方式一:

在创建表时,直接在对应的字段名后指定unique。如下:

方式二:

在创建表的最后,指定某列或某几列为唯一索引。如下:

方式三:

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

唯一索引的特点:

唯一索引的特点如下:

• 一个表中,可以有多个唯一索引,一个唯一键可以由多个列同时承担。

• 唯一索引的查询效率高。

• 创建唯一索引的列,其列值可以为NULL,但是不能重复。

• 如果给唯一索引设置NOT NULL属性,则等价于主键索引。

6.3.创建普通索引

方式一:

在创建表的最后,指定某列或某几列为普通索引。如下:

方式二:

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

方式三:

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

注:索引也是有索引名的,创建主键索引时默认主键索引名为primary(如果主键是多列复合键,则其索引名仍为primary),创建唯一键和普通键时默认将对应列名设置为索引名(如果唯一键和普通键是多列复合键,则其索引名为创建符合键时第一列的列明)。这里普通索引方式三的创建方法是在创建时手动指定了普通索引名为idx_name,因此最终该普通索引名为idx_name,如下图所示。

普通索引的特点:

普通索引的特点如下:

• 一个表中,可以有多个普通索引,一个普通索引可以由多个列同时承担。

• 创建普通索引的列,其列值可以为NULL,也可以重复。

6.4.创建全文索引

当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。

全文索引案例:

全文索引比较常见的案例就是对文章中的词进行搜索,比如下面创建一个文章表,表当中包含文章的id、文章名称、文章内容,并在创建表的最后通过fulltext给title和body列创建全文索引。如下:

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

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

但实际这种查找方式并没有用到全文索引,在SQL语句前面加上explain,可以看到key对应的值为NULL,表示这条SQL在执行过程中没有用到任何索引。如下:

如果要通过全文索引来查询,需要使用match against进行搜索。如下:

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

说明一下:

• MyISAM存储引擎是支持全文索引的,而InnoDB存储引擎是在5.6以后才开始支持全文索引的。默认的全文索引支持英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版(coreseek)
• 同时使用title和body建立全文索引时,相当于建立了一个复合索引,默认会选择fulltext中的第一个列名作为这个复合索引的索引名,所以这里explain中key对应的索引名为title。
• 由于是title和body共同建立的全文索引,所以如果文章当中没有出现关键字,但文章名称中出现了关键字则也会被筛选出来(当前示例没有体现出来)。

6.5.查询索引

方式一:

使用 show keys from 表名 命令查询,比如查询articles表中的索引信息。如下:

说明一下:

• Table: 表示创建索引的表的名称。
• Non_unique: 表示该索引是否是唯一索引,如果是则为0,如果不是则为1。
• Key_name: 表示索引的名称(主键索引的索引名默认为primary,唯一键索引和普通索引的索引名默认为对应列明)(如果主键是多列复合键,则其索引名仍为primary)(如果唯一键和普通键是多列复合键,则其索引名为创建符合键时第一列的列明)。
• 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 表名 命令 查询,比如查询articles表中的索引信息。如下:

方式三:

使用 desc 表名 命令查询(信息比较简略),比如查询articles表中的索引信息。如下:

6.6.删除索引

创建测试表:

创建一个用户表用于测试索引的删除,表中包含用户的id、姓名和邮箱,并将这三列分别设置为主键索引、唯一索引和普通索引。如下:

删除主键索引:

使用 alter table 表名 drop primary key 命令即可删除主键索引。如下:

删除非主键索引:

使用 alter table 表名 drop index 索引名 命令即可删除指定的非主键索引。如下:

此外,也可以使用 drop index 索引名 on 表名 命令也可以删除指定的非主键索引。如下:

说明一下:

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

6.7.索引创建原则

索引创建的原则如下:

• 比较频繁作为查询条件的字段应该创建索引。

• 唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件。

• 更新非常频繁的字段不适合创建索引。

• 不会出现在where子句中的字段不应该创建索引。

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


7.其他概念自行了解)

(1)复合索引(上面已讲)
(2)索引最左匹配原则
(3)索引覆盖

猜你喜欢

转载自blog.csdn.net/qq_45113223/article/details/131381248