目录
[聚簇索引 VS 非聚簇索引](#聚簇索引 VS 非聚簇索引)
一、索引本质:
索引是用来进行高效查询的,其本质上是数据库中的一种高效数据结构,其主要是B+树,它能够加快数据的查询,排序的类似操作,类似于书本的目录,是通过减少所需要扫描的数据来提高性能的,就像二分查找那样,在二分查找中,能够查找一个数据就能够去掉一半的无效数据,索引也是类似于如此,
但是索引也并不是没有代价的,查询速度是提高了,但是插入,更新,删除的效率会有所降低,这些写操作,增加了大量的IO,在调整的时候会对其底层的所实现的数据结构如B+树进行维护
索引的核心作用
- 加速查询:通过跳过全表扫描,快速定位目标数据。
- 排序优化:索引本身有序,可加速ORDER BY,GROUP BY等操作
- 唯一性约束:唯一索引(如主键)确保列值的唯一性。
索引的优缺点
优点:
- 显著提升查询速度(尤其是大数据量)
- 加速表连接(如外键索引)
- 避免排序开销(索引已有序)
缺点:
- 占用额外存储空间
- 降低写操作(INSERT/UPDATE/DELETE)速度,需维护索引结构
- 索引设计不当可能导致未命中(如索引失效)
二、预备知识:
硬件理解:
MySQL给用户提供存储服务,是存储在磁盘上的,数据是存储在磁盘上的那一个个扇区中的, 关于磁盘的更详细了解可以看看我之前写的:
在系统软件上,并不是按照扇区(512字节或者4096字节/4kb)进行交互的,因为这样的话就会和硬件强相关了,也就是如果将硬件换了就会导致系统软件也必须跟着变换,这样其耦合度就会太高,不便于彼此之间的硬件或者系统各自进行升级或者更新,所以必须得将解耦工作做得更好
并且,单词IO 512字节的单位太小了,IO单位小意味着读取相同的内容的会多次进行IO,这样导致每次IO都会将磁盘进行转动,磁头进行选扇区,这些机械操作就肯定会带来效率的降低
所以系统进行读取磁盘是以块为单位进行存储的,基本单位是4kb(4096字节),4kb是为了能够让磁盘数据和内存数据进行更好的IO交互
那么4kb会不会多做了很多工作呢?实际上单纯这样理解是不够充分的,因为OS一次加载4kb也有其自己的考量 -----局部性原理,实际上本身就是预加载,预加载数据也就是一次加载4kb,有可能只使用几条指令,但是后面的部分,可能也有些指令,大概率还会访问的,所以因为局部性原理,一次预加载4kb空间是能够在一定意义上在宏观的概率提高整体的效率的
软件理解:
MySQL与磁盘交互基本单位:
MySQL是属于在OS上的应用层中的,也就是说MySQL之下是OS,MySQL是通过OS和硬件打交道的,不能够直接和硬件打交道

在MySQL中,MySQL和磁盘打交道的单位是page---其大小是16kb,
如上,这是以字节为单位的,将value=16384进行 /1024 就是16kb
这看起来是MySQL直接和磁盘打交道的,但是实际上并不是如此,实际上是MySQL先要经过OS,然后在让OS和磁盘打交道,这里MySQL一次性操作就是16kb,但是到了OS就是4次4kb,但是我们不需要关心这4次4kb是怎么进行交互的
MySQL中的page和OS中的page是1:4的关系
buffer pool:
首先要知道,MySQL中的数据文件都是以page为单位保存在磁盘中的,当对MySQL中的表进行各种CRUD的时候,首先要通过计算找到对应的位置,既然要进行计算,就需要CPU进行参与,既然要CPU进行参与,根据冯诺依曼体系结构,我们知道CPU和内存打交道的,所以就需要将数据从磁盘中移动到内存中,在内存上完成数据的CURD后,在以特定的刷新策略从内存刷新到磁盘中,此时就是内存到磁盘之间的数据交互了,也就是IO,其基本单位是page在这里是以4kb为大小的
但是这样的话效率是比较低的,为了提高效率,当MySQL服务器在其内存中运行的时候,在其服务器内部申请了buffer pool的大内存空间,来进行缓存,这样MySQL就能够直接和buffer pool进行数据交互,不需要等待OS和磁盘进行IO交互

三个共识:
1、MySQL以16kbpage为单位进行MySQL级别的IO
2、MySQL有自己的buffer pool,MySQL在进行IO的时候,它会把读到的数据写到buffer里,刷新的时候,也是把buffer里的数据刷新到自己的OS内部的缓冲区中,最后在刷新到磁盘的
3、一般在进行IO的时候要减少OS和磁盘的IO次数,所以一次IO次数越大,那么它的IO次数越少,效率就会越高
三、索引的理解:
我们首先看一个现象,这里创建一个学生表,然后把id设置为主键,

接着在进行插入的时候故意乱序插入:

然后在进行查找:

这里发现尽管我们是乱序进行插入的,但是在查找的时候却是有序的,这显然只有是MySQL进行处理了的,但是MySQL为什么要这样进行处理呢?
要想知道,我们得先理解page
理解page:
如何理解MySQL中page的概念:
page只是一个个内存块么?
在MySQL内部一定会存在大量的page,那么就需要将这些大量的page通过B+树进行管理-----先描述,再组织,所以这些page内也会存在指向下一个page的指针,所以不能将page认为仅仅是一个内存块,page内部也必须写入对应的管理信息:
cpp
struct page
{
struct page* next;
struct page* next;
char buffer[NUM];
}
那么为什么IO交互的单位是page呢?
这里为什么是通过以page为单位进行交互呢?为什么不用多少就申请多少呢?
因为存在**局部性原理:**CPU访问存储器时(包括指令和数据),所访问的地址倾向于聚集在较小的连续区域内,而非均匀分布
比如我们要查找学号为3的学生,就需要从学号为1的学生依次进行查找,这里就需要进行3次IO,那么如果是学号为5呢?50呢?500呢?这样就会进行很多次IO,那么如果将学号为5的学生都保存在同一个page中,在MySQL中一个page交互为16kb是很大的空间,能够保存很多记录,这时如果在查找学号为2的学生后,如果想查找学号为5或者在同一个page中的学生的学号的时候,就不需要进行IO了,直接读取即可,这样就能够大大减少IO的次数
但是怎么保证下次找到数据一定在同一个page呢?这是不能够严格保证的,但是因为存在局部性原理,会有很大的概率
这样因为IO次数的减少就能够提高IO的效率,因为往往IO效率的低下并不是IO单次的数据量,而是IO的次数
单个page:
通过上述分析,我们知道要管理好表的数据文件,我们目前可以理解这一个个文件是由一个或多个page构成的
所以其可以理解为:

在page里面,当进行数据的查找的时候,如果进行链式查找的话效率会很低下,所以page里面并不全是数据,还牺牲了一小部分的空间来存放目录,

这样当进行查找的时候就不从数据的最开始直接进行链式查找了,而是通过目录进行查找,这样的话能够大大增大效率
那么回到我们之前的问题,为什么乱序插入,MySQL会通过我们的主键进行排序呢?
这是因为要引入目录,那么就需要数据的有序性,这样方便通过目录进行查找
多个page:
当创建一个表的时候,这个表中所需的数据可能会很大,所以就需要多个page,那么我们解决了单个page的查找效率问题,但是多个page的查找效率问题呢
如果有多个page,并且在查找数据的时候,数据在最后一个page中,这个时候进行线性遍历的话需要遍历所有page,这个遍历操作是在内存中进行的,那么就需要将所有page从磁盘中IO交互到内存中,会增加IO次数,进而影响效率

如果仅仅给单个page加上目录有点杯茶水车薪了,所以就需要给所有page加上目录,进而大大提高效率
那么就需要创建目录page,这个page里面只存其管理的page的最小主键所对应的数据,如下就是示意图,最下面的就是里面有数据的page,然后上一层就是只存放目录的page,

当数据进一步增多,那么就需要再加上一层page了,这样就能够很快速的读取数据了

这就是一颗B+树,这是多叉树的结构,是innodb下的索引结构,当进行查找的时候就会首先在最上面的page中查找到一个范围,这样就会淘汰掉很多数据,像上述那样,仅仅只有三层但是其能够存储2000多万条数据了
这样当进行IO查找的时候,就仅仅只需要进行磁盘和内存之间交互3次即可
关于这可B+树:
1、叶子结点保存了数据,非叶子节点没有保存数据,只保存了目录,这是为了让非叶子节点存储更多的目录这样来管理更多的叶子page,对与这棵树其外形就是一棵矮胖型的树,这样就能够使其途径的节点数量少,只需通过更少的page,更少的IO来找到数据,这样大大提高了效率
2、叶子节点是全部用链表连接起来了,这是B+树的特点,并且这样能够便于进行范围查找
3、我们进行建表插入数据的时,就是在这棵B+树下进行CURD的,在进行目录的引入的时候,在之前的学习中我们了解需要让这些数据是有序的,就需要主键,但是如果我的表没有设置主键呢?那么会不会就是乱序的,这是不会的,尽管我们没有主动设置主键,但是MySQL会设置默认主键
4、MySQL底层会将我们的数据组合成这个B+树的,然后将其保存在buffer pool中,MySQL中会有大量的表被处理,所以在buffer pool中就会有多个索引结构,也就是存在多个B+树
5、为什么使用B+树,不使用B树或者其他数据结构呢?
链表:不行,这是链式存储,查找的效率低下
二叉树,AVL树,红黑树,这都是二叉式的结构,是瘦高型的树,其效率不如矮胖型的B+树,并且二叉树在极端情况下会退化为链式结构
B树也不行,B树不只是在叶子结点中进行数据的存储的,还有在非叶子节点中也会存储数据的,这样会导致范围查找不如B+树,哈希表这个数据结构也是因为范围查找不行而不被使用的
聚簇索引 VS 非聚簇索引
像上述所讲的那种将数据和索引都放在叶子结点中这种存储方式是我们innodb的存储形式
与这种不同的是Myisam引擎的底层,并未将索引和数据放在一起,其叶子节点存储的只是数据的地址,然后通过这个地址进行数据的查找的
如下:

- 将B+树和数据本身分离的方案称为非聚簇索引
- 将数据和B+树放到一起的称为聚簇索引
接下来看例子:

如上,通过innodb引擎和Myisam引擎创建两张表
我们是通过MySQL客户端进行创建的表,接着MySQL服务端就会将我们创建的表在磁盘中创建对应的文件.

其中test1也就是innodb只有一个文件ibd,索引和表是合起来的
其中test2这两个文件里面myd就是数据,myi就是索引结构
所以innodb为聚簇索引,Myisam为非聚簇索引
回表查询:
MySQL除了会建立主键索引外还会建立不止一种索引,其他的索引叫做普通(辅助)索引
对于Myisam,建立辅助索引和主键索引没有区别,唯一就是主键索引不能重复,非主键索引不能重复
对于Innodb,辅助索引就很有用了,辅助索引不在保存一个完整的数据了,而是只保存该表中的某一个字段和主键,比如保存的是主键和name列,未来查询某一个name信息,然后筛选条件是age,那么就会找到以这个age为key的辅助索引,然后就能够找到对应的主键了,然后就拿着这个主键,在主键索引中找到对应的name信息了
当根据普通索引查询数据时,会先查找普通索引对应的B+树找到目标记录的主键值,然后再查找主键索引对应的B+树找到目标记录,这个过程就叫做回表查询
索引的本质,就是一种数据结构
四、索引实操:
创建主键索引:
创建主键索引和创建主键的方式是一样的,在我们以前创建主键的时候不仅仅是创建了主键,并且还创建了主键索引

用如下SQL语句来进行查看
sql
show index from 表名

或者也可以使用如下的两种方法,其实就是创建主键的方法:
sql
create table 表名(id int, name varchar(20), primary key(id));
create table 表名(id int, name varchar(20));
// 创建表以后再添加主键
alter table 表名 add primary key(id);
主键索引的特点如下:
- 一个表中,最多只能有一个主键索引,一个主键可以由多个列同时承担。
- 主键索引的查询效率高。
- 创建主键索引的列,其列值不能为NULL,且不能重复。
- 主键索引的列一般是数字类型。
创建唯一键索引:
sql
alter table 表名 add unique(列名);
其实也就是创建唯一键的三种方法
sql
create table 表名(id int primary key, name varchar(30) unique);
create table 表名(id int primary key, name varchar(30), unique(name));
create table 表名(id int primary key, name varchar(30));
alter table 表名add unique(name);

唯一索引的特点如下:
- 一个表中,可以有多个唯一索引,一个唯一键可以由多个列同时承担。
- 唯一索引的查询效率高。
- 创建唯一索引的列,其列值可以为NULL,但是不能重复。
- 如果给唯一索引设置NOT NULL属性,则等价于主键索引。
创建普通索引:
sql
create table 表名(id int primary key,name varchar(20),email varchar(30),index(name));
create table 表名(id int primary key, name varchar(20), email varchar(30));
alter table 表名 add index(name);
create table 表名(id int primary key, name varchar(20), email varchar(30));
create index 自己取名字 on 表名(name)
sql
create table test(id int primary key,name varchar(20),ema,email varchar(30),index(name));


创建复合索引:

首先将name的普通索引删除
sql
alter table test1 add index(name, email);
如上,以name和email作为索引进行创建

接着会看到三个索引,这实际上是两个,只是name和email公用同一个B+树

删除索引:
删除主键索引:
sql
alter table 表名 drop primary key;
删除非主键索引:
sql
alter table 表名 drop index 索引名;
drop index 索引名 on 表名;
一个表只有一个主键索引,所以在删除主键索引的时候不用指明索引名,而一个表中可能有多个非主键索引,所以在删除非主键索引时需要指明索引名
创建索引原则:
- 比较频繁作为查询条件的字段应该创建索引。
- 唯一性太差不适合做索引。
- 更新太频繁不适合做索引。
- 永远不会出现在where子句中的。