MySQL的服务器,本质是在内存中的,所有的数据库的CURD操作,全都是在内存中进行的,所以索引也是如此。索引的作用是提高查找的效率。
对于我们所学到的算法,有提高算法效率的因素:
组织数据的方式,比如链表的结构,比较适合:增加,删除,修改,但是不适合排序;顺序表的结构比较适合数据的排序,但是不适合数据的删除;或者哈希等等。
而对于索引,实际上就是将组织数据的方式,即数据结构给改变了,将其进行重构了,那搜索的效率自然也就变了,这就叫做结构决定算法。
索引的本质:特定的数据结构导致搜索效率的改变。
常见索引分为:
索引的价值
使用如下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保存到文件中,然后在MySQL中使用source命令依次执行文件中的SQL即可。如下:(需要等很长时间,预计18min左右,因为数据多)
SQL执行完毕后查看数据库就能看到一个名为bit_index的数据库。如下:
进入该数据库,在数据库中可以看到一个名为EMP的员工表。如下:
由于EMP表中有八百万条记录,因此在查看EMP表中的数据时可以带上limit子句。如下:
通过desc命令可以看到,目前EMP员工表中没有建立任何索引。如下:
查询EMP表中指定工号的员工信息,可以看到每次查询过程都需要花费8秒左右。如下:
当我们给员工表中的工号建立索引后,数据库底层就会为员工表中的数据记录构建特定的数据结构,需要注意的是,由于当前员工表中的数据量较大,因此建立索引时也需要花费较长时间。如下:
这时再查询EMP表中指定工号的员工信息,可以看到几乎检测不到查询时耗费的时间。如下:
根本原因就是,给员工工号创建索引后再根据员工工号来查询数据,这时就能够直接通过底层建立的数据结构来快速定位到目标数据,从而提高数据的检索速度,这就是索引的价值。
磁盘的整体结构如下:
部件说明:
磁盘中的一个盘片
一个磁盘由多个盘片叠加而成,盘片的表面涂有磁性物质,这些磁性物质就是用来记录二进制数据的,因为盘片的正反两面都可涂上磁性物质,因此一个盘片有两个盘面。
磁盘中的一个盘片如下:
部分说明:
说明一下:
扇区的定位方式
一个磁盘由多个盘片叠加而成,每个盘片有两个盘面,所有盘面中半径相同的同心磁道构成一个柱面。
每个盘面都有一个对应的磁头,每个磁头都有一个编号,所有的磁头都是连在同一个磁臂上的。
示意图如下:
定位扇区时采用CHS寻址方式:
简单来说,CHS寻址方式就是先通过H确定数据所在的盘面,再通过C确定数据所在的磁道,最后通过S定位到目标扇区。
说明一下:
操作系统与磁盘交互的基本单位
操作系统与磁盘进行IO交互的基本单位是4KB,而不是扇区的大小512字节,原因如下:
因此操作系统与磁盘以4KB作为IO交互的基本单位,一方面是为了提高IO效率,另一方面是为了实现硬件和系统的解耦。
随机访问与连续访问
需要注意的是,尽管两次IO是在同一时刻发出的,但如果它们请求的扇区地址相差很大,那也只能称为随机访问,因为连续访问中的连续指的是访问的扇区地址的连续,而不是访问时间的连续,由于连续访问不需要过多的定位,因此效率比较高。
MySQL作为一款应用软件,可以想象成是一种特殊的文件系统,它有着更高频的IO场景,因此为了提高基本的IO效率,MySQL与磁盘交互的基本单位是16KB,这个基本数据单元在MySQL这里也叫做Page。
通过show命令查看系统中的全局变量,可以看到InnoDB存储引擎交互的基本单位是16KB。如下:
说明一下: 本篇博客中没有做特殊说明的地方,都是以InnoDB存储引擎为例进行讲解的。
Buffer Pool
在MySQL中进行的各种CRUD操作时,都需要先通过计算找到对应的操作位置,只要涉及计算就需要CPU参与,而冯诺依曼体系决定了CPU只能和内存打交道,因此为了便于CPU参与,就需要先将数据加载到内存中。
所以在特定的时间内,MySQL中的数据一定是同时存在于磁盘和内存中的,当操作完内存数据后,再以特定的刷新策略将内存中的数据刷新到磁盘当中,这时MySQL和磁盘进行数据交互的基本单位就是Page。
为了更好的支持上述操作,MySQL服务器在启动的时候会预先申请一块内存空间来进行各种缓存,这块空间叫做Buffer Pool,后续磁盘中加载的数据就会保存在Buffer Pool中,刷新数据时也就是将Buffer Pool中的数据刷新到磁盘。
所谓的操作系统和磁盘交互的基本单位是4KB,就是指内核缓冲区与磁盘之间是以4KB为单位进行交互的。而MySQL的Buffer Pool和磁盘实际并不是直接交互的,因此所谓的MySQL与磁盘交互的基本单位是16KB,指的是MySQL的Buffer Pool与内核缓冲区之间是以16KB为单位进行交互的。只不过在说的时候更关注的是MySQL和磁盘之间的关系,所以直接说的是MySQL与磁盘交互的基本单位是16KB,相当于忽略了中间的内核缓冲区。
示意图如下:
首先,建立一张测试表
将其插入一定的数据:
我们向一个具有主键的表中,乱序插入数据,发现数据会自动排序。
根本原因就是,因为我们创建表时设置了主键,即便向表中插入数据时是乱序插入的,MySQL底层也会自动按照主键对插入的数据进行排序。
为什么MySQL与磁盘交互的基本单位是Page?
MySQL与磁盘进行交互时为什么不是按需交互,而是以Page为基本单位进行交互的呢?
总结:MySQL与磁盘进行交互时以Page为基本单位,可以减少与磁盘IO交互的次数,进而提高IO的效率。
单个Page
假设上述测试表中的记录都在同一个Page当中,那么该Page的结构大致如下:
说明一下:
单个Page内创建页内目录
比如在之前的Page内部引入页内目录后的结构大致如下:
说明一下:
多个Page
多个Page的示意图如下:
Page之上创建页目录
说明一下:
页目录之上再创建页目录
最终构建出来的结构模型如下:
说明一下:
B+树中的Page结点是否需要全量加入到Buffer Pool中?
如果把这棵B+树逆时针旋转90度,就会发现这其实就是操作系统中的页表结构,本质操作系统中的页表也是B+树结构。如下:
以32位平台为例,页表将一个虚拟地址转换成物理地址的过程如下:
说明一下:
除了InnoDB存储引擎所采用的B+树结构,索引结构还可以采用哪些数据结构呢?
下面是几个常见的存储引擎,与其所支持的索引类型:
存储引擎 | 支持的索引类型 |
---|---|
InnoDB | BTREE |
MyISAM | BTREE |
MEMORY/HEAP | HASH、BTREE |
NDB | HASH、BTREE |
B树 VS B+树
B+树是B树的一种变形结构,那为什么我们没有采用普通的B树作为索引结构呢?
MyISAM 存储引擎-主键索引
MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为 MyISAM表的主索引, Col1 为主键。
MyISAM 最大的特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址。
此结构将B+树的搜索结构与具体数据进行分离,B+树的叶子结点记录实际数据的地址,我们将这种索引称为非聚簇索引。
因此,什么是非聚簇索引?
非聚簇索引: 我们把这种将数据和索引结构分离的索引称之为非聚簇索引。
与此同时,我们也就能推导出什么是聚簇索引:
聚簇索引: 我们把将数据和索引结构放在一起的索引称之为聚簇索引。
如上,聚簇索引与非聚簇索引的区别就是B+树的叶子结点存储的是数据还是数据的地址。如果是数据的地址,该结构就是非聚簇索引;如果是数据本身,该结构就是聚簇索引。
证明一下:
innodb的存储引擎,将索引和数据都放在了xxx.idb
文件中,这就是聚簇索引。
myisam的存储引擎,将索引和数据分离,数据存在xxx.MYD
文件,索引存在xxx.MYI
文件,这就是非聚簇索引。
由于MySQL默认的就是innodb存储结构,故默认是聚簇索引。
当然, MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引。
对于 MyISAM ,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复。
下图就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别:
说明一下:
一个索引代表一个B+树,即索引的名字,实际上也是对应的B+树的名字。
我们上面创建的test1和test2两张表,因为创建的主键,都有对应的索引,通过以下语句,就能查询索引:
方式一:
show keys from 表名
解释一下:
方式二:show index from 表名;
方式三:desc 表名; —信息比较简略
下面索引的创建,我们都演示方式三,因为方式三才是创建表之后才添加索引的,具有普适性。
方式一:在创建表的时候,直接在字段名后指定 primary key
create table user1(
id int primary key,
name varchar(20)
);
方式二:在创建表的最后,指定某列或某几列为主键索引
方式二与方式一本质上没有区别。
create table user1(
id int,
name varchar(20),
primary key(id)
);
方式三:创建表之后,再指定字段添加索引
create table test1 if not exists(
id int,
name varchar(20) not null
);
alter table test1 add primary key(id);
主键索引的特点:
在添加唯一键约束时,其默认也会创建B+树,构成索引。
方式一:在表定义时,在某列后直接指定unique唯一属性。
create table user4(
id int primary key,
name varchar(30) unique
);
方式二:创建表时,在表的后面指定某列或某几列为unique
create table user5(
id int primary key,
name varchar(30),
unique(name)
);
方式三:创建表之后,再指定字段添加索引
create table test1 if not exists(
id int,
name varchar(20) not null
);
alter table test1 add unique(name);
这就是给name增加了对应的索引。如果此时再将id设置成primary key,那么该表就有两棵B+树。
唯一索引的特点:
方式一:在表的定义最后,指定某列为索引
create table user8(
id int primary key,
name varchar(20),
email varchar(30),
index(name)
);
方式二:创建完表以后指定某列为普通索引(最常用)
create table user9(
id int primary key,
name varchar(20), email
varchar(30)
);
alter table user9 add index(name);
方式三:创建一个索引名为myindex的索引
create table test1(
id int primary key,
name varchar(20),
email varchar(30)
);
create index myindex on test1(name);
普通索引的特点:
全文索引比较常见的案例就是对文章中的词进行搜索,比如下面创建一个文章表,表当中包含文章的id、文章名称、文章内容,并在创建表的最后通过fulltext给title和body列创建全文索引。如下:
下面向表当中插入一些测试数据。如下:
如果要查询哪些文章中包含database关键字,我们可以通过模糊匹配进行查找。如下:
但实际这种查找方式并没有用到全文索引,在SQL语句前面加上explain,可以看到key对应的值为NULL,表示这条SQL在执行过程中没有用到任何索引。如下:
如果要通过全文索引来查询,需要使用match against进行搜索。如下:
在这条SQL语句前面加上explain,可以看到key对应的值为title,表示这条SQL在执行过程中用到了索引名为title的索引。如下:
说明一下:
删除索引实际上就和我们之前修改表结构一样。
删除主键索引
alter table 表名 drop primary key;
其他索引的删除:
alter table 表名 drop index 索引名;
索引名就是show keys from 表名中的 Key_name对应的字段。
此时就不存在name对应的索引了。删除主键索引,也可以以其他索引的方式删除,这种删除更具有普适性。
说明一下:
对于如下表,只有id存在索引:
若想同时将另外两个字段添加索引,就需要在index()的括号中同时添加两个字段:
这样,除了主键索引,也出现了另外两个索引。但是需要注意的是,不是又构建了两棵B+树,而是name和email同时看成一个整体作为一个索引,即这两个的数据是作用在一棵B+树上的,从key_name中的名字也可以看出,他们的B+树都是name,也就是以第一个字段的名字作为Key值。所以,此表目前仅仅存在两棵B+树。
如果要删掉name索引结构,这两个的索引同时都会消失,这就是因为同属于一个B+树。
当然,多列索引的创建不止一种方式,因为单列索引的创建不止一种方式。
我们将多列索引称之为复合索引,而复合索引实际上就是普通索引,只不过是将多个字段放在一起作为索引。
总结:创建索引的目的,就是为了提高查询的效率。