目录
1. 索引的概念和价值
1.1 索引的概念
1.2 索引的价值
2. 磁盘的概念
2.1 磁盘的结构
2.2 操作系统与磁盘交互的基本单位
2.3 MySQL与磁盘交互的基本单位
3. 索引的理解
3.1 主键索引现象和推导
3.2 索引采用的数据结构:B+树
3.3 聚簇索引和非聚簇索引
4. 索引的操作
4.1 主键索引特点和创建
4.2 唯一索引特点和创建
4.3 普通索引特点和创建
4.4 全文索引创建案例
4.5 索引的查询
4.6 索引的删除
本篇完。
索引:可以提高数据库的性能,索引是物美价廉的东西了。不用加内存,不用改程序,不用调sql,只要执行正确的 create index ,查询速度就可能提高成百上千倍。但是天下没有免费的午餐,查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。所以它的价值,在于提高一个海量数据的检索速度。
常见的索引分为:
使用如下SQL创建一个海量数据表:
(可以复制粘贴到Windows文件夹(建议在桌面建一个index_demon.sql)然后Linux 用rz打开)
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保存到文件中,然后在MySQL中使用source命令依次执行文件中的SQL即可。如下:
执行了7分钟,SQL执行完毕后查看数据库就能看到一个名为index_demon的数据库。如下:
进入该数据库,在数据库中可以看到一个名为EMP的员工表。如下:
由于EMP表中有八百万条记录,因此在查看EMP表中的数据时可以带上limit子句。如下:
通过desc命令可以看到,目前EMP员工表中没有建立任何索引。如下:
查询EMP表中指定工号的员工信息,可以看到每次查询过程都需要花费4.5秒左右。如下:
当给员工表中的工号建立索引后,数据库底层就会为员工表中的数据记录构建特定的数据结构,需要注意的是,由于当前员工表中的数据量较大,因此建立索引时也要花费较长时间。如下:
这时再查询EMP表中指定工号的员工信息,可以看到几乎检测不到查询时耗费的时间。如下:
根本原因就是,给员工工号创建索引后再根据员工工号来查询数据,这时就能够直接通过底层建立的数据结构来快速定位到目标数据,从而提高数据的检索速度,这就是索引的价值。
之前Linux专栏有说过磁盘的物理结构和抽象结构,复习链接:
零基础Linux_14(基础IO_文件)缓冲区+文件系统inode等-CSDN博客
磁盘是电脑上的唯一的一个机械设备,目前的笔记本上很多已经不用磁盘了,而是固态硬盘(SSD)。相对而言用起来更快,效率更高。固态硬盘是另一种存储的方案,和磁盘的存储差别很大,单价比磁盘大很多。一般的固态基本上比同等的磁盘要贵1倍。
磁盘的物理结构:
之所以叫做磁盘,是因为它是盘状的,而且不止一片,有很多片叠放在一起。看到右上图:
磁盘中的一个盘片:
一个磁盘由多个盘片叠加而成,盘片的表面涂有磁性物质,这些磁性物质就是用来记录二进制数据的,因为盘片的正反两面都可涂上磁性物质,因此一个盘片有两个盘面。
磁盘中的一个盘片如下:
由于每个扇区的存储容量相同,因此最内侧磁道上的扇区数据密度最大,而最外侧磁道上的扇区数据密度最小。
近三十年来,扇区大小一直是512字节,但最近几年正在迁移到更大、更高效的4096字节扇区,通常称为4K扇区。
数据库文件就是保存在磁盘中的一个个扇区中的,因此找到一个文件本质就是,在磁盘上找到保存该文件的所有扇区。
扇区的定位方式:
定位扇区时采用CHS寻址方式:
简单来说,CHS寻址方式就是先通过H确定数据所在的盘面,再通过C确定数据所在的磁道,最后通过S定位到目标扇区。
操作系统与磁盘进行IO交互的基本单位是4KB,而不是扇区的大小512字节,原因如下:
因此操作系统与磁盘以4KB作为IO交互的基本单位,一方面是为了提高IO效率,另一方面是为了实现硬件和系统的解耦。
随机访问: 本次IO所给出的扇区地址与上次IO给出的扇区地址不连续,磁头在两次IO操作之间需要做比较大的移动动作才能找到目标扇区进行IO。
连续访问: 本次IO所给出的扇区地址与上次IO给出的扇区地址是连续的,磁头很快就能找到目标扇区进行IO。
需要注意的是,尽管两次IO是在同一时刻发出的,但如果它们请求的扇区地址相差很大,那也只能称为随机访问,因为连续访问中的连续指的是访问的扇区地址的连续,而不是访问时间的连续,由于连续访问不需要过多的定位,因此效率比较高。
MySQL作为一款应用软件,可以想象成是一种特殊的文件系统,它有着更高频的IO场景,因此为了提高基本的IO效率,MySQL与磁盘交互的基本单位是16KB,这个基本数据单元在MySQL这里也叫做Page。
通过show命令查看系统中的全局变量,可以看到InnoDB存储引擎交互的基本单位是16KB。如下:
次博客中没有做特殊说明的地方,都是以InnoDB存储引擎为例进行讲解的。
Buffer Pool介绍:
因此所谓的操作系统和磁盘交互的基本单位是4KB,就是指内核缓冲区与磁盘之间是以4KB为单位进行交互的。而MySQL的Buffer Pool和磁盘实际并不是直接交互的,因此所谓的MySQL与磁盘交互的基本单位是16KB,指的是MySQL的Buffer Pool与内核缓冲区之间是以16KB为单位进行交互的。只不过在说的时候更关注的是MySQL和磁盘之间的关系,所以直接说的是MySQL与磁盘交互的基本单位是16KB,相当于忽略了中间的内核缓冲区。
示意图:
观察主键索引现象:
创建一个用户表,表当中包含用户的id、年龄和姓名,并将用户的id设置成主键。如下:
创建表完毕后向表中插入一些数据,并且插入数据时没有按照主键的大小顺序插入。如下:
但最终当我们查看表中的数据时,却发现显示出来的数据是按照主键进行有序排列的。如下:
根本原因就是,因为我们创建表时设置了主键,即便向表中插入数据时是乱序插入的,MySQL底层也会自动按照主键对插入的数据进行排序。
为什么MySQL与磁盘交互的基本单位是Page:MySQL与磁盘进行交互时为什么不是按需交互,而是以Page为基本单位进行交互的?
也就是说,MySQL与磁盘进行交互时以Page为基本单位,可以减少与磁盘IO交互的次数,进而提高IO的效率。
推导主键索引结构的构建:
看看单个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+树(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+树结构。如下:
以32位平台为例,页表将一个虚拟地址转换成物理地址的过程如下:
12个比特位有2^12种取值,而2^12字节对应就是4KB,所以物理内存中一个页框的大小就是4KB,这也就是为什么操作系统与磁盘交互的基本单位是4KB的原因。
此外,页表中的各个B+树结点也不需要全量加入到内存中,而只需要加入访问到的结点即可,所以页表占用的内存大小实际是可控的,这也就是为什么二级页表可行的原因。
除了InnoDB存储引擎所采用的B+树结构,索引结构还可以采用哪些数据结构呢?
链表:查找时是线性遍历,效率太低。
普通二叉搜索树:可能退化成线性结构,这时查找还是线性遍历。
AVL树和红黑树:虽然保证了二叉树是绝对或近似平衡的,不会退化成线性结构,但AVL树和红黑树都是二叉树结构,这就意味着树的层高会比较高,而查询数据时都是从根结点开始向下进行查找的,这也就意味着在查询过程中需要遍历更多结点,如果这些结点还没有被加载到Buffer Pool中,这时就需要进行更多次的IO操作,所以最终没有选择其作为索引结构。
哈希表:官方的索引实现方式中MySQL是支持HASH的,只不过InnoDB和MyISAM存储引擎并不支持。哈希表的优点就是它的时间复杂度是O(1) 的,但哈希表也有一个缺点就是不利于进行数据的范围查找,但也有些存储引擎采用其作为存储结构。
下面是几个常见的存储引擎,与其所支持的索引类型:(BTREE就是B+树,HASH是哈希表)
存储引擎 | 支持的索引类型 |
---|---|
InnoDB | BTREE |
MyISAM | BTREE |
MEMORY/HEAP | HASH、BTREE |
NDB | HASH、BTREE |
还有一种树叫作B树:所有结点中都同时包括索引信息和数据信息。
B+树是B树的一种变形结构,那为什么我们没有采用普通的B树作为索引结构呢?
首先,普通B树中的所有结点中都同时包括索引信息和数据信息,由于一个Page的大小是固定的,因此非叶子结点中如果包含了数据信息,那么这些结点中能够存储的索引信息一定会变少,这时这棵树形结构一定会变得更高更瘦,当查询数据时就可能需要与磁盘进行更多次的IO操作。
其次,普通B树中的各个叶子结点之间没有连接起来,这将不利于进行数据的范围查找,而B+树的各个叶子结点之间是连接起来的,当我们进行范围查找时,直接先找到第一个数据然后继续向后遍历找到之后的数据即可,因此将各个叶子结点连接起来更有利于进行数据的范围查找。
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存储引擎建立的表必须有主键。
当采用InnoDB存储引擎创建表时,在数据库对应的目录下会新增两个文件。如下:
当采用MyISAM存储引擎创建表时,在数据库对应的目录下会新增三个文件。如下:
采用InnoDB和MyISAM存储引擎创建表时都会生成xxx.frm文件,该文件中存储的是表结构相关的信息。
采用InnoDB存储引擎创建表时会生成一个xxx.ibd文件,该文件中存储的是索引和数据相关的信息,这就是所谓的聚簇索引,索引和数据是存储在同一个文件中的。
采用MyISAM存储引擎创建表时会生成一个xxx.MYD文件和一个xxx.MYI文件,其中xxx.MYD文件中存储的是数据相关的信息,而xxx.MYI文件中存储的是索引相关的信息,这就是所谓的非聚簇索引,索引和数据是分开存储的。
索引创建的原则如下:
时刻要记住,创建索引的目的就是为了提高查询的效率。
主键索引的特点如下:
创建主键索引和创建主键一样:
创建主键索引方式一:
创建表时,直接在对应的字段名后指定primary key。如下:
创建主键索引方式二:
在创建表的最后,指定某列或某几列为主键索引。如下:
创建主键索引方式三:
创建表后,使用alter命令给指定字段添加主键索引。如下:
唯一索引的特点如下:
创建唯一索引和创建唯一键一样:
创建唯一索引方式一:
在创建表时,直接在对应的字段名后指定unique。如下:
创建唯一索引方式二:
在创建表的最后,指定某列或某几列为唯一索引。如下:
创建唯一索引方式三:
创建表后,使用alter命令给指定字段添加唯一索引。如下:
普通索引的特点如下:
创建普通索引方式一:
在创建表的最后,指定某列或某几列为普通索引。如下:
创建普通索引方式二:
创建表后,使用alter命令给指定字段添加普通索引。如下:
创建普通索引方式三:
创建表后,使用create命令给指定字段创建普通索引,并指定索引名。如下:
全文索引比较常见的案例就是对文章中的词进行搜索,比如下面创建一个文章表,表当中包含文章的id、文章名称、文章内容,并在创建表的最后通过fulltext给title和body列创建全文索引。如下:
下面向表当中插入一些测试数据。如下:
如果要查询哪些文章中包含database关键字,我们可以通过模糊匹配进行查找。如下:
但实际这种查找方式并没有用到全文索引,在SQL语句前面加上explain,可以看到key对应的值为NULL,表示这条SQL在执行过程中没有用到任何索引。如下:
如果要通过全文索引来查询,需要使用match against进行搜索。如下:
在这条SQL语句前面加上explain,可以看到key对应的值为title,表示这条SQL在执行过程中用到了索引名为title的索引。如下:
查询索引方式一:
使用show keys from 表名SQL查询,比如查询articles表中的索引信息。如下:
查询索引方式二:
使用show index from 表名SQL查询,比如查询articles表中的索引信息。如下:
查询索引方式三:
使用desc 表名SQL查询(信息比较简略),比如查询articles表中的索引信息。如下:
创建一个用户表用于测试索引的删除,表中包含用户的id、姓名和邮箱,并将这三列分别设置为主键索引、唯一索引和普通索引。如下:
删除主键索引:
使用alter table 表名 drop primary keySQL即可删除主键索引。如下:
删除非主键索引:
使用alter table 表名 drop index 索引名SQL即可删除指定的非主键索引。如下:
此外,也可以使用drop index 索引名 on 表名SQL也可以删除指定的非主键索引。如下:
一个表只有一个主键索引,所以在删除主键索引的时候不用指明索引名,而一个表中可能有多个非主键索引,所以在删除非主键索引时需要指明索引名。
下一篇:MySQL的事务。