索引
- 前言
- 正式开始
- 磁盘、os、MySQL之间的IO
- MySQL与存储
- 扇区
- 结论
- 磁盘随机访问(Random Access)与连续访问(Sequential Access)
- MySQL 与磁盘交互基本单位
- 小总结
- 简单介绍一下内存池
- 谈回MySQL
- 简单理解MySQL中的page
- 为何IO交互基本单位是page
- page结构
- 页目录
- 单个page的页目录
- 多个page的页目录
- 内存池中的B+树
- MyISAM存储方式
- 聚簇索引和非聚簇索引
- 表中索引相关的操作
- 主键索引的操作
- 唯一索引(唯一键列)
- 普通索引
- 谈回InnoDB和MyISAM
- InnoDB的多个索引
- MyISAM的多个索引
- 查询索引
- 索引创建原则
- 全文索引
前言
索引是和效率挂钩的,它的价值在于提高海量数据的检索速度。
先来看看效率有多高。
我这里创建一个有800万条记录的表(只有数据足够大才能体现出索引有多快,不用看下面的sql,只要知道是一个搞很多数据的东西就行):
-- 库名字
drop database if exists `_index`;
create database if not exists `bit_index` default character set utf8;
use `bit_index`;--构建一个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 ;-- 创建一张雇员表,和我前面博客中讲的emp表结构一样。
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);
执行下来会获得EMP表:
看过我前面博客的同学因该是对于这个表结构很眼熟,这里的EMP就和我前面博客所讲过的emp表结构是一样的。
注意这里表结构中没有任何的主键唯一键什么的,这里是为了显示一下没有索引时查询有多慢。等会没有索引查完之后再加上索引进行查询,再来看看有多快。
现在里面就有800万条记录,我没法直接进行select *,机器配置不太行可能会直接卡死,不过能显示一下前几行:
这里显示了5行,其中ename和deptno都是随机数据。
我现在来查询员工编号为898376的员工:
很慢,花了4.3秒,再来几次:
可以看到稳定在4.1秒,比第一次快了差不多0.2秒。
这样也太慢了,绝对不能搞成这样,这还是在本机一个人来操作,在实际项目中,如果放在公网中,假如同时有1000个人并发查询,那很可能就死机。
那么加上索引来看看会有什么变化,加索引的语法:
alter table 表名 add index(列名);
我现在来给empno加上索引:
数据有点多,加上索引需要花费的时间还是很多的。等会就来说说为啥。
然后再来查询:
可以看到清一色的直接干到了0.00s,非常快。
ok,现在已经看到了加不加索引所产生的效果。下面本节课就来讲讲为什么查询效率会相差这么多。
正式开始
数据在存储的时候需要有特定的数据结构,而不同的数据结构查询的效率是不一样的。
当然不同的数据结构,都可以进行增删查改。而这增删改最重要的一点就是都要先查找,增的时候找位置,删的时候找数据在哪才能删,改的时候也是先找到才能改。
比如说你用一个顺序表来存储数据,如果数据没有序,那查询就是O(N)的,有点太慢了,如果有序还可以用用二分,能变成O( l o g 2 N log_2N log2N),这个效率ok。但是顺序表删除和插入不太方便。用链表,查询的时候只能是O(N)了,非常慢,但是链表很适合数据插入。用AVL数或者红黑树,查的时候为O( l o g 2 N log_2N log2N)。等等数据结构,不同的数据结构各有各的优势。
- 数据库中的数据也是数据,存储的时候也要有特定的数据结构进行存储。
为什么加上了索引之后效率会大大提升?
就是因为前后存储数据的数据结构发生了改变,从而改变了查找的效率。
那么加上了索引前后的数据的存储结构分别是什么呢?
这个问题先保留,直接说了也无法理解,慢慢来讲解。
下面我要一点有关IO效率的问题,涉及到三部分,一部分磁盘,一部分操作系统,一部分mysql本身。
先来简单说点,mysql启动了之后在os看来就就是一个用户层的一个应用进程,在网络看来就是一个应用层的进程。反正都是应用层的。三者就是这样:
当一个应用层进程想要进行数据IO的时候不是直接和磁盘进行IO的:
mysql和磁盘交互的数据必须要先经过os:
ok,三者关系先介绍到这里,下面来细说说磁盘。
磁盘、os、MySQL之间的IO
为啥要说磁盘呢,因为我们的数据就是直接存放在磁盘上的,得要简单了解了解磁盘。
其实我前面讲基础文件IO的时候也说过关于磁盘的知识,这里我就简单讲讲,如果想要了解的更详细一点的可以看看我前面这篇博客:【Linux】基础文件IO、动静态库的制作和使用
MySQL与存储
MySQL 给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设当中。磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率是比较低的,在加上IO本身的特征,可以知道,如何提高效率,是 MySQL 的一个重要话题。
看看完整磁盘长啥样:
其中有两个很重要的东西,一个叫磁盘(中间的那个圆片),一个叫磁头:
再来结合着侧面图来讲:
其中磁盘盘片可以分为好几片这里图中有3片,每一片又分为两个面,所以这里就有6个面,我们的数据就存放在每个面上,图中虚线画的看起来像圆柱的东西就叫做柱面,每个柱面在盘片上对应位置的东西叫做磁道,磁道会再细分为扇区,每个扇区就是存放数据的基本单元,为512字节。
当有数据进行IO的时候要先进行定位,此时磁头会进行左右摆动,盘片会进行旋转,二者一块配合来定位一个磁道中的某个扇区。
当找到指定位置的扇区之后就能进行IO了。
扇区
数据库文件,本质其实就是保存在磁盘的盘片当中。也就是上面的一个个小格子中,就是我们经常所说的扇区。当然,数据库文件很大,也很多,一定需要占据多个扇区。
来看看扇区:
题外话:
从上图可以看出来,在半径方向上,距离圆心越近,扇区越小,距离圆心越远,扇区越大。
那么,所有扇区都是默认512字节吗?目前是的,我们也这样认为。因为保证一个扇区多大,是由比特位密度决定的。不过最新的磁盘技术,已经慢慢的让扇区大小不同了,不过我们现在暂时不考虑。
我们在使用Linux,所看到的大部分目录或者文件,其实就是保存在硬盘当中的。(当然,有一些内存文件系统,如: proc , sys 之类,我们不考虑)
我前面博客中也讲过,创建数据库就是创建一个目录:
在库中创建表就是在创建文件:
所以,最基本的,找到一个文件的,本质上就是在磁盘找到所有保存文件的扇区。
而我们能够定位任何一个扇区,那么便能找到所有扇区,因为查找方式是一样的。
前面说了磁盘和磁头配合着来就能定位到某个扇区。
每个盘面都有一个磁头,那么磁头和盘面的对应关系便是1对1的。
所以,我们只需要知道,磁头(Heads)、柱面(Cylinder)(等价于磁道)、扇区(Sector)对应的编
号。即可在磁盘上定位所要访问的扇区。这种磁盘数据定位方式叫做 CHS 。
不过实际系统软件使用的并不是 CHS (但是硬件是),而是 LBA ,一种线性地址,就是将磁盘中的物理地址抽象化了,将一整个圆转化成了线性的,线中的每个小块就是扇区,可以想象成虚拟地址与物理地址间的关系。这个在我刚刚给的那篇博客中有,这里就不细说了,想要了解的同学可以看刚刚给的那篇。
系统将 LBA 地址最后会转化成为 CHS ,交给磁盘去进行数据读取。不过,现在不用关心转化细节,了解依稀这个东西,让我们逻辑自洽起来即可。
结论
我们现在已经能够在硬件层面定位,任何一个基本数据块了(扇区)。那么在系统软件上,就直接按照扇区(512字节,部分4096字节),进行IO交互吗?
不是。
如果操作系统直接使用硬件提供的数据大小进行交互,那么系统的IO代码,就和硬件强相关,换言
之,如果硬件发生变化,系统必须跟着变化。
从目前来看,单次IO 512字节,还是太小了。IO单位小,意味着读取同样的数据内容,需要进行多
次磁盘访问,会带来效率的降低。
之前学习文件系统,就是在磁盘的基本结构下建立的,文件系统读取基本单位,就不是扇区,而是
数据块。而一个数据块的大小为4KB。
我前面讲页表的时候说过页框和页帧的问题,就是内存和磁盘数据交互的基本单位,也就是4KB,想要了解的同学看看这篇:【Linux】页表讲解(一级、二级) 和 vm_area_struct ## 对于我前面博客内容的补充。
磁盘随机访问(Random Access)与连续访问(Sequential Access)
随机访问:本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需要作比较大的移动动作才能重新开始读/写数据。
连续访问:如果当次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次IO操作,这样的多个IO操作称为连续访问。
如果你没看懂的话,请看图:
假如说图中一格为一个扇区,不同行就代表不同的磁道,那么如果说前后两次的IO的扇区位置是这样的:
这里就是两个不连续的扇区,不再同一个磁道,当然不连续,那么两次IO都会需要在定位上花时间。
但是如果两次IO的位置是连续的:
这样第二次IO的时候就不需要再次重新定位扇区的位置, IO的效率就会比不连续的高一点。
因此尽管相邻的两次IO操作在同一时刻发出,但如果它们的请求的扇区地址相差很大的话也只能称为随机访问,而非连续访问。
磁盘是通过机械运动进行寻址的,也就是磁盘的旋转和磁头的摆动,连续访问不需要过多的定位,故效率比较高。
MySQL 与磁盘交互基本单位
MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB。 (后面统一使用 InnoDB存储引擎讲解,不同存储引擎IO的基本单位会不同,InnoDB引擎使用16KB进行IO交互)
前面说系统IO的基本单位是4KB,而MySQL和磁盘IO的数据必须要经过os,那么如果是MySQL进行读取,系统就会对磁盘连续读取4次,从而组成16KB,再交付给MySQL:
一个扇区的大小为512字节,因为局部性原理(就是加载的的指令附近指令很有可能最近还会被执行),一次多加载一点后续效率能高一点。所以系统选择用4KB作为IO的基本单位,这里的4KB可以被称作是一页(page)。
MySQL中需要进行很多IO,为了更高的效率,所以将其IO的基本单位设定为了16KB。这里的16KB也可看作是一页(page)。
磁盘中的一个扇区大小为512字节,4KB的页为系统IO的基本单位,16KB的一页为MySQL进行IO的一页,两个页不是一个东西。
我们可以查看MySQL中一页的大小:
SHOW GLOBAL STATUS LIKE 'innodb_page_size';
结果如下:
这里显示出来的16384指的是字节,来算一算,除以1024:
字节除以1024,单位就是KB,这里也就是16KB,那么就是用InnoDB存储引擎时MySQL进行IO的基本单位。
也就是说,磁盘这个硬件设备的基本单位是 512 字节,而 MySQL InnoDB引擎使用 16KB 进行IO交互。即, MySQL 和磁盘(中间要经过os)进行数据交互的基本单位是 16KB 。这个基本数据单元,在 MySQL 这里叫做page(注意和系统的page区分)。
小总结
-
MySQL 中的数据文件,是以page为单位保存在磁盘当中的(IO的单位为16KB,这也就决定了每次进行IO的时候都要加载16KB的内容,也就间接决定了MySQL文件的基本单位)。
-
MySQL 的 CURD(我前面博客讲的MySQL的增(create)删(delete)查(retrieve)改(update),四者首字母对应CURD) 操作,都需要通过计算来找到对应的插入位置,或者找到对应要修改或者查询的数据。
-
而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中。
-
所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。
-
为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为 Buffer Pool 的的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。其实就是内存池,等会来讨论这个话题。
-
为了更高的效率,一定要尽可能的减少系统和磁盘IO的次数,因为系统与磁盘IO的次数越多,磁盘就要进行越多的定位。假如有40KB的数据,以4KB为基本单位,那么就是进行10次IO,如果10次连续搞完,那就是连续定位,不需要多次定位扇区的位置,但如果分10次搞完,那么中间可能会还要重新定位扇区,相比而言,肯定是前者效率更高。
简单介绍一下内存池
这里只是简单说一下内存池是干啥的,等会再说MySQL中的内存池更详细的作用。
为了效率更高一点,MySQL中也搞了内存池,我前面没有写过关于内存池的博客,这里简单介绍一下功能是啥。
就拿喝水来举例子,假如你有一个杯子,每次喝水时必须要用这个杯子来喝水,但是接水的地方离你有点远,你不想每次喝水的时候都要拿着杯子去接水,于是你为了图个方便,买了个水壶,每次接水的时候就把水壶灌满,喝水的时候用用水壶往你的杯子里面倒一点,等喝完的时候再去接水的地方把水壶灌满。
其实内存池原理就和这个差不多,图个方便。每次用数据的时候不是向磁盘中要16KB用一用就完了,而是要很多个16KB放到一块,等这很多个16KB都用完了就再向磁盘要,同时把其中的一些修改后且不需要的数据返回给磁盘。这样与磁盘IO的次数就会减少效率就能高一点。
所以内存池也是为了效率而搞的。根据名字中的池就能理解,就像一个蓄水池一样,用水的时候从蓄水池中拿,而不是跑大老远拿一点再回来。
MySQL是用C/C++写的,内存池就是用new提前开出来的一大块空间,我们可以在配置文件(my.cnf,在/etc下)中看到这个内存池有多大:
可以看到InnoDB的内存池为128MB。不过这里注释掉了,就算没有注释默认也是有的,而且默认的大小就是128MB。我们把注释去掉之后能自己改这里内存池的大小,不过我这里就不搞了。
128有多少个KB呢?
这么多个。这么多KB能有多少个16KB呢?
注意,这里只是简单地计算了内存池中可以容纳的页数,实际使用中可能受到其他因素的影响,例如操作系统的内存管理、服务器的硬件配置等。
所以128MB的内存池,能存放好几千个page以用来进行读写。
谈回MySQL
到这里要确定三点:
- MySQL以16KB为基本单位进行MySQL级别的IO的。
- MySQL读取的时候会将数据先放到buffer pool中,写的时候也是将buffer pool中的数据交给系统,系统再将数据交给磁盘。
- IO时要尽量减少系统和磁盘IO的次数,一次IO数据越大,IO的次数越少,效率就会更高一点。
来搞一张表:
show 一下:
其中id为主键,我们来插入点数据:
注意我是按照id无序的方式插入的。
select:
这里查出来的结果却是有序的。
为啥?
这个问题也是暂时先搁置,后面再说。
如果没有主键,那存放的时候是按什么顺序的查看的就是按照什么顺序的。
简单理解MySQL中的page
如何理解MySQL中的一个page呢?
刚刚说了MySQL中的page为MySQLIO的基本单位,为16KB。
刚刚还说了MySQL中有一个内存池,为了效率,这个内存池中会存放很多的16KB,刚刚也说了其中至少能放好几千个page,那么这么多page,MySQL自身也是需要把这些page管理起来的。
怎么管理呢?
还是先描述,再组织(这个我前面博客中一直在说)。和os管理很多东西一样。给第一次听到先描述再组织的同学解释一下。
先描述,就是用一个结构体(或者类)来将一个抽象的物体具象化,比如说这里的page,大小为16KB,那么就实现一个大小为16KB的结构体就行。
再组织,就是用某种特定的数据结构将这些结构体组织起来,就比如说链表。
假如说MySQL中用的是链表(实际上并不是)来组织所有的page的,那么page结构体就可以是这样:
假如说上面的这个page有16KB,其中的buff就是用来存放多条记录的。
这个在内存池中的体现就是会new很多这样的结构体,有前后指针,这样每个page就能串起来了,这样在buffer pool内部就对page进行了建模。
为何IO交互基本单位是page
其实前面也比较含蓄的说过了。
为何MySQL和磁盘进行IO交互的时候,要采用Page的方案进行交互呢?用多少加载多少不香吗?
如上面刚插入的5条记录,如果MySQL要查找id=2的记录,第一次加载id=1,第二次加载id=2,一次一条记录,那么就需要2次IO。如果要找id=5,那么就需要5次IO。
但,如果这5条(或者更多)都被保存在一个Page中(16KB,更准确的是刚刚图中page的buff能保存很多记录),那么第一次IO查找id=2的时候,整个Page会被加载到MySQL的Buffer Pool中,这里完成了一次IO。但是往后如果在查找id=1,3,4,5等,完全不需要进行IO了,而是直接在内存中进行了。所以,就在单Page里面,大大减少了IO的次数。效率就提高了。
你怎么保证,用户一定下次找的数据,就在这个Page里面?我们不能严格保证,但是有很大概率,因为有局部性原理。
往往IO效率低下的最主要矛盾不是IO单次数据量的大小,而是IO的次数。
page结构
其实为了效率,page保存的数据都是有序且相关的,所以实际并不是我前面图中的buff,而是这样:
不同的Page ,在 MySQL 中,都是 16KB ,使用 prev 和 next 构成双向链表
因为有主键, MySQL 会默认按照主键给我们的数据进行排序。
为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?
插入数据时排序的目的,就是优化查询的效率。
页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的。
正是因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是可以提前结束查找过程的
而这些排序工作都是mysqld(MySQL的服务器)做的,这也就回答了前面的一个遗留问题。
此时page和page之间用链式链接,page内的各条记录也是用链式链接,也就是这样:
图太大了看不清,只搞了三个,实际上是可以有很多个的。
这样在找数据的时候就要在page间和page内进行查找,那么想要提高查找的效率就要考虑两个方面,一个是在page之间如何提高查找效率,二是在page内部如何提高查找效率。
页目录
通过上面的分析,我们知道,上面页模式中,只有一个功能,就是在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。但是,我们也可以看到,现在的页模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。
如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的。那么,查找特定一条记录,也一定是线性查找。这效率也太低了。
所以为了提高效率,就搞出来了页目录,这里先不说页目录是啥,先来说说我们日常生活中的目录是干啥的。
我们在看一本C语言书籍的时候,如果我们要看<指针章节>,找到该章节有两种做法
- 从头逐页的向后翻,直到找到目标内容
- 通过书提供的目录,发现指针章节在234页(假设),那么我们便直接翻到234页。同时,查找目录的方案,可以顺序找,不过因为目录肯定少,所以可以快速提高定位
- 本质上,书中的目录,是多花了纸张的,但是却提高了效率
- 所以,目录,是一种“空间换时间的做法”
一本书如果有五百多页,可能其目录有十来页,有了这十来页我们的查找某一个专题的效率就能大大提高。而设定目录的前提是书的页数是有序的。
MySQL中也是采用了同样的做法。
单个page的页目录
刚刚说page中的数据在存储时是这样的链式结构:
page有16KB,假如说一条记录有12字节的话,就能存放下:
1365条记录,不过去掉两个前后指针,假如说8个字节,那就是可以存放1264条记录,如果这1000多条记录以O(N)的时间复杂度去查找的话效率就会比较低。
那么可以少保存一点记录,把这些省下来的空间构建一个页目录,就像书本一样,查找的效率就会高很多:
上面目录1中存放的是1,表示主键的1,对应欧阳锋那条记录。
目录2中存放的是3,表示主键3,对应杨过那条记录。
查找的时候就不用再遍历每个记录了,而是遍历目录,通过目录来定位到记录所在的区间。
所以知道为啥排序了吧,主键有序可以方便我们引入页目录,从而方便查找。
上图中数据比较少,所以这也感觉不出来快多少,多了才能体现出来。
那么此时多个page连接起来就是这样:
需要注意,上面的图,是理想结构,大家也知道,目前要保证整体有序,那么新插入的数据,不一定会在新Page上面,这里仅仅做演示。
此时查找的就能快一些。不过page之间的效率还是有点低,因为数据是在某一个page中的,我们这里查找的时候没法直接定位page,还是得要遍历一个个page以找到数据所在的page。
如果查找的时候能够快速定位到想要的page就能大大提高效率。
多个page的页目录
MySQL 中每一页的大小只有 16KB ,单个Page大小固定,所以随着数据量不断增大, 16KB 不可能存下所有的数据,那么必定会有多个页来存储数据。也就是刚刚这张图:
在单表数据不断被插入的情况下, MySQL 会在容量不足的时候,自动开辟新的Page来保存新的数据,然后通过指针的方式,将所有的Page组织起来。
那么多表间的查询效率如何提升呢?
解决方案,其实就是我们之前的思路,给Page也带上目录。
- 使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值。
- 和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行。
- 其中,每个目录项的构成是:键值+指针。
看图:
用一个目录页来管理页目录,目录页中的数据存放的就是指向的那一页中最小的主键值和对应page的指针。查找的时候,就可通过比较目录页中存放的各,找到该访问那个Page,此时就可以直接在那个page中找了。
如果有连续的访问一大段数据,那么就可以先定位一个page,这个page中的数据访问完之后直接通过prev_next来访问下一个page,这样效率就会更高。
同样的假如说现在是在64位下,一个指针8B,主键用的是int,那么目录页中一对指针和主键就是12B,那么一个目录页也就可以存放下来1365个page的地址和对应主键,如果是这样的话一个目录页就能管理1265*16KB = 21840KB的数据,也就是21MB的数据,这样如果是128MB的内存池,就可以有多个目录页,那么假如说有6个目录页吧,这样查找的时候还是需要定位一下在哪个目录页,又得要遍历这6个目录页中存放的指针—主键对,有点慢,想要再快一点就可以再在上层加一个目录页来将这个6个目录页关联起来:
同样,最顶上的目录页存放的是中间目录页中最小的主键值,这样每次查找的时候就可以先查找最顶上的目录页,找到数据所在的中间层目录页,然后再通过中间层目录页找到数据记录所在的page,效率就会大大提升。
如果数据结构比较好的同学应该知道上面图中就是一颗B+树,用B+树来构建索引,这样每次查找都能排除一大批数据,查找的效率非常高。
内存池中的B+树
叶子结点保存有数据,非叶子结点不保存数据,只保存主键—指针对,这样非叶子节点就能保存更多的目录项,目录页也就可以管理更多的page,也就意味着这棵树一定会是一棵矮胖型的树,这样查找的时候路上途径的节点就会减少,所有的节点都是page(也就都是IO的基本单位),也就意味着找到目标数据需要的pageIO更少,也就是IO次数更少,这样就提高了IO的效率。这一点直接从IO上提高了效率。
刚开始查询只需要加载根节点page,再往下找的时候找到哪个page再加载对应的page。且每个page都有页目录,可以大大提高搜索效率。这一点就从算法上提高了效率。
这整个B+树结构就被称为MySQL InnoDB下的索引结构,不过其中非叶子结点是不会像链表一样穿起来的,也就是这样:
我们建表之后就是在这整个结构上进行CURD操作。
如果没有手动设置主键也会是这样的结构,后面讲事务的博客会细说这一点,现在简单了解一下,当我们没有手动设置主键的时候,MySQL会自动搞一个隐藏列作为主键,我们看不见,而对应B+树就是按照这个隐藏的主键列构建的,前面没有设置主键进行查询的时候用的都是非索引列进行查找,就会非常慢,因为是在线性遍历,但是设置了主键之后MySQL就会自动按照主键列作为索引的值来构建一棵B+树,而这一整棵B+树都是放在内存池中的。
一张表对应多棵B+树(表中可以不光有主键,唯一键也会有索引,而且普通列也可以通过add添加索引,等会会讲),多张表就会有更多的B+树。
为什么叶子结点要用链表连起来?
a. 这是B+树的特点,而不是MySQL自己实现成这样的,是MySQL选择了B+树。
b. 查找的时候我们比较希望进行范围查找(当查询的多条记录跨page的时候可以快速的定位到下一个page中的数据)。
InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?
- 链表?线性遍历,速度太慢。
- 二叉搜索树?退化问题,可能退化成为线性结构
- AVL &&红黑树?虽然是平衡或者近似平衡,但是毕竟是二叉结构,相比较多阶B+,AVL和红黑的层数太多了,二者肯定是一个瘦高型的树,查找的时候经过的节点会很多,IO的效率就比B+低。
- Hash?官方的索引实现方式中, MySQL 是支持HASH的,不过 InnoDB 和 MyISAM 并不支持。Hash跟进其算法特征,决定了虽然有时候也很快(O(1)),不过,在面对范围查找就明显不行,另外还有其他差别,有兴趣可以查一下。
这里有张存储引擎对应数据结构的图:
上面的BTREE指的是B+树,不是B树。
那为什么不用B树来存储数据呢?先来看看B树的结构:
再来看看B+树的结构:
上面的图,是在网上找的,大家也可以搜一下。
目前这两棵树,对我们最有意义的区别是:
- B树节点,既有数据,又有Page指针,单个目录页保存的目录项变少,相对于B+的高度会更高,就会经过更多次的IO,效率就低一点。而B+,只有叶子节点有数据,其他目录页,只有键值和Page指针。不过B树可能提前在飞叶子节点上找到数据。
- B+叶子节点,全部相连,而B没有,不利于范围查找。
为何选择B+?
- 节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。叶子节点相连,更便于进行范围查找。
相对而言B+更好。
有一个网站可以查看不同数据结构动态CURD的过程,感兴趣的同学可以看看:
Data Structure Visualizations
进去之后长这样:
比如其中的B+树:
可以选择不同的degree:
上面所讲的存储方式是InnoDB的默认存储方式,再来简单说说MyISAM的存储方式。
MyISAM存储方式
MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址,而不是直接存放数据记录。下图为 MyISAM表的主索引, Col1 为主键。
可以看到还是用B+树来构建索引的,只不过叶子结点存储的数据不一样,所有表中的数据记录是存放在其他地方的,B+树的叶子结点用来存放每一个数据记录对应的地址。这样找的时候过程和InnoDB差不多,只不过是直接找到了数据的地址,然后再通过地址访问到数据记录。
所以, MyISAM 最大的特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址。
相较于 InnoDB 索引, InnoDB 是将索引和数据放在一起的。
前面说了建库就是建文件,我现在来建一个库,里面搞两张表,一张存储引擎用InnoDB,一张存储引擎用MyISAM:
可以看到用InnoDB的有两个文件,一个.frm,是用来存放表结构的(表结构也是数据,也要进行存放),一个ibd,表示index(索引)block data(数据块),意思就是索引和数据是存在一起的。
再来看MyISAM,三个文件,一个.frm的存放表结构的文件,一个MYD,表示存放数据的文件,一个MYI,表示存放索引的文件。
存索引就是存放B+树。
聚簇索引和非聚簇索引
InnoDB 这种用户数据与索引数据在一起索引方案,叫做聚簇索引
而MyISAM就是非聚簇索引,因为数据和索引不在一块。
表中索引相关的操作
MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引。
先来看看主键索引,再来说普通索引。
主键索引的操作
直接上例子吧,搞一张表:
这张表中有一个主键,id即为主键列。那么id列最开始就会自动有一个索引,我们可以用下面的语法来查看某个表的索引信息:
show index from 表名;
比如说这里的user:
Non_unique值为1表示非唯一,即该索引可以包含重复的值;Non_unique值为0表示唯一,即该索引的值必须唯一。
因为当前只有一个主键索引,所以只会显示这一个。在逻辑上创建主键的同时会在底层以主键为关键字创建一个B+树。
当我把主键去掉之后就找不到了:
此时用id查找就会很慢。
上面创建表的时候就设置主键,是第一种创建主键索引的方式。还有一种创建表时设置主键的方式:
show index:
成功。
手动对已经创建且没有主键的表添加主键列就算是第三种创建主键索引的方式,刚刚删除了user表的主键列,下面手动添上:
show一下:
成功。
再来试试复合主键,再来创建一个表:
还没有加上主键,现在加上复合主键:
关于复合主键是啥我就不讲了,不懂的同学可以看我前面的这篇博客:复合主键
show index:
两个Column_name一个为name,一个email。就是复合主键索引。
复合索引用的时候有两点要注意:
- 索引最左匹配原则
- 索引覆盖
最左匹配,比如说刚刚的name和email作为复合索引,插入(张三, 1312335@qq.com)
此时如果查找(‘张三’, 1312335@qq.com),是按照索引来找的:
如果是查张三,会按照索引来查:
但是如果找的是email,是不会按照索引来找的:
不过这里就一行记录,看不出来差别。后面再说一下这个复合索引的用处。
主键索引的特点:
- 一个表中,最多有一个主键索引,当然可以使复合主键
- 主键索引的效率高(主键不可重复)
- 创建主键索引的列,它的值不能为null,且不能重复
- 主键索引的列基本上是int
唯一索引(唯一键列)
后面还有一个普通索引,其实唯一索引和普通索引没啥区别,至于为啥等会就知道了。
除了设置主键会自动构建索引以外,设置唯一键也会自动构建索引。
同样的,也是建表的时候就设置唯一键(两种方式),add添加唯一键。
建表:
show index:
唯一键不是主键,而且一个表中唯一键可能有多个,所以索引的名字就直接跟着唯一键列的列名了。
再来去掉这个唯一索引,注意去掉唯一索引用的不是alter table drop unique key(列名),这里的drop语法是用来删除某一列的,而且这里用的也不对。而是:
alter table drop index key(列名);
等会的普通索引也是这样删除的,所以说唯一键索引和普通索引没啥区别。
show:
加回来:
show:
创建表时在末尾添加unique:
show:
复合唯一索引。
建表:
show:
复合索引的名字为name,就是在创建的时候unique(name, qq)从左往右第一列的列名。查找对比的时候和刚刚讲的复合主键索引一样。
唯一索引的特点:
- 一个表中,可以有多个唯一索引
- 查询效率高
- 如果在某一列建立唯一索引,必须保证这列不能有重复数据
- 如果一个唯一索引上指定not null,等价于主键索引
普通索引
创建普通索引也有三种方式,不过和前面不太一样:
第一种
create table user6(id int primary key,name varchar(20),email varchar(30),index(name) --在表的定义最后,指定某列为索引
);
show:
第二种
create table user7(id int primary key,name varchar(20),email varchar(30)
);alter table user7 add index(name); --创建完表以后指定某列为普通索引
建表:
添加:
show:
第三种:
create table user8(id int primary key,name varchar(20),email varchar(30)
);-- 创建一个索引名为 idx_name 的索引
create index idx_name on user8(name);
创建索引:
show:
注意我刚刚创建索引的时候索引名字给的是myindex。所以这里索引名就是myindex。
来去掉这个索引:
注意去掉索引的时候用的是索引名,不是列名。
添加一个复合索引:
show:
注意二者的索引名字都是composite_index,两个和一块是一个复合索引。查找对比的时候和刚刚讲的复合主键索引一样。
谈回InnoDB和MyISAM
多个索引下B+树是什么样子的?
其实只要多一个索引就会多一棵B+树,也就是一个索引对应一个B+树,无论是主键索引还是普通索引(我这里就直接把唯一索引当做普通索引了)。
不过不同的存储引擎做法不太一样,这里就说一下InnoDB和MyISAM的。
InnoDB的多个索引
InnoDB是以主键所构建的B+树为主。
主键的那棵B+树,叶子结点存放了所有列的数据记录,这一点要搞清楚。
比如说id、name、qq和email,4列中id作为主键。那么以id为关键字的所有数据都会被记录在主键构建的B+树中。
假如说这是以主键id构建出来的B+树:
而普通索引的B+树,叶子结点中只会保存普通索引列和主键列的数据。
比如说以name列作为普通索引,就会构建一棵以name作为关键字的B+树,叶子结点中只会保存name和id列的数据,如图:
当进行查找的时候,如果是用name进行查找的话,比如说找张三,那么就会先通过以name为关键字构建出来的B+树找到对应的name和id,然后再通过id去主键构建出来的B+树中找到完整的数据。
整个流程就是这样:
而上面的这个操作用专业术语来说就叫做回表查询。
为什么要这样做呢?
因为如果普通索引构建出来的B+树叶子结点中也保存了所有的数据就会造成非常多的重复数据,空间上会很浪费。
再来说说复合索引的用处。
同样的还是上面的id、name、qq、email。
如果我现在以name、qq建一个复合的普通索引,那么就会建议对应的B+树,树中叶子结点保存的就是id、name和qq这三个列的数据。
那么复合索引用处在哪呢?
比如说我现在要找张三的qq,但是我不知道张三的id,那么我就必须得要按照人名找,此时找的时候直接通过复合索引的B+树找到对应的id、name和qq记录,然后就可以直接提取出来张三的qq,不需要再通过id来再去id构建的B+树中去找张三的完整数据记录了。
这一点就非常适合高频的通过非主键列(比如说name)来找其他列,不需要进行回表。这就是索引覆盖。
MyISAM的多个索引
MyISAM更为简单一点。
因为MyISAM的所有数据是单独存在一个地方的,不和索引在一块,主键对应的B+树叶子结点是直接存放完整数据地址的。
如果新建了其他的索引,同样会创建一棵B+树,但是树中叶子结点也是存放对应的完整数据的地址,那么这样普通索引就和逐渐索引没什么区别了,无非就是主键不能重复,而非主键可重复(如果非主键加上unique,也就是唯一索引,那么就和逐渐约束一样了)。
所有索引的B+树都是用叶子结点直接存放数据地址:
查询索引
其实刚刚已经说了一个,就是:
show index from 表名;
再演示一下:
还有一个:
show keys from 表名
两种方式得到的结果是完全一样的:
索引创建原则
-
比较频繁作为查询条件的字段应该创建索引
-
唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件,比如说性别,只有男女那就没必要搞成索引。
-
更新非常频繁的字段不适合作创建索引
-
不会出现在where子句中的字段不该创建索引
全文索引
这个是个边缘知识,了解一下就行。
当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文索引机制,但是有要求,要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版(coreseek)。
演示一下,建张表:
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT (title,body)
)engine=MyISAM;
插入点数据:
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 ...');
查询有没有database数据
如果使用如下查询方式,虽然查询出数据,但是没有使用到全文索引:
可以用explain工具看一下,是否使用到索引:
其中的key为NULL,表示没有用到索引。
如何使用全文索引呢?
通过explain来分析这个sql语句:
key用到了title。
就讲这么多。
到此结束。。。