文章目录
理解索引机制及操作
本篇文章,我们将深入了解并学习mysql中的一个非常重要的特性------索引!
也正是有了索引,才会让我们进行搜索的时候更加高效!
索引的必要性
我们先不解释什么是索引,我们先明白一个最基本的道理:
在mysql中,正是因为索引,能够使我们搜索速度加快!
否则在面对大量数据的时候,mysql的查询速度是非常慢的!
我们现在需要来证明一下,有了索引是不是真的会变快呢?
------这就需要我们使用一个具有大量数据的数据库了!
如下图所示:这里我用了一个大数据量的数据库,光是恢复该数据库就用了6min 40.86sec:

使用聚合函数统计一下,发现足足由八百万条数据!这是非常大量的!
sql
mysql> select count(*) from EMP;
+----------+
| count(*) |
+----------+
| 8000000 |
+----------+
1 row in set (3.59 sec)
现在我们需要做的事情就是:验证索引是否能加速查询?
Tips:索引的操作我们先不谈,也不解释,我们知道可以对某一列加索引即可!
没有索引的时候:
sql
mysql> select empno, ename from EMP where empno=776651;
+--------+--------+
| empno | ename |
+--------+--------+
| 776651 | jeUmyH |
+--------+--------+
1 row in set (4.91 sec)
也可以尝试多搜索几次,我们会发现时间基本都是接近5s的。这已经是很慢的了!
如果我们在mysql中查询数据要查询5s的话,这效率就十分低下了。
给empno列加上一个索引(先不解释任何操作和原理):
sql
# 给EMP表的empno列创建索引
mysql> alter table EMP add index empno_idx (empno);
Query OK, 0 rows affected (28.69 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 连续查询三次
mysql> select empno, ename from EMP where empno=776651;
+--------+--------+
| empno | ename |
+--------+--------+
| 776651 | jeUmyH |
+--------+--------+
1 row in set (0.01 sec)
mysql> select empno, ename from EMP where empno=196485;
+--------+--------+
| empno | ename |
+--------+--------+
| 196485 | LHdsFY |
+--------+--------+
1 row in set (0.00 sec)
mysql> select empno, ename from EMP where empno=776415;
+--------+--------+
| empno | ename |
+--------+--------+
| 776415 | QPDxDA |
+--------+--------+
1 row in set (0.00 sec)
至此我们就发现了,加上索引就快的多了!这验证了之前的结论!也验证了索引的必要性!
索引的理解
我们先不直接学习索引的操作,这样子是不利于我们理解索引的特性的!
接下来我们要做的是:
- 理解硬件,从硬件角度理解如何加快查询速度
- 理解索引的本质
- 从底层理解索引的原理
- 具体到常用的存储引擎InnoDB、MyIsam
接下来学习的步骤,就是按照上述提出来的进行!
硬件层面------复习磁盘相关知识、mysql与磁盘交互
硬件层面其实就是磁盘的复习!因为最终我们在mysql中存储的数据,都是放在了磁盘中的!
首先,我们需要复习磁盘的相关知识,这个我们就不讲解了,具体参考这篇文章:磁盘的理解
我们也知道,在Linux系统底层,系统与磁盘交互的基本单位是:4KB!
这是系统做出权衡取舍的!
基本单位太小,读取次数就会多,IO是一件费时费力的工作,长久以往系统运行效率会低下。
基本单位太大,读取次数是少了,但是就可能会把更多地非相关数据带进来。也是比较浪费
所以,系统选择了4KB!
现在我们需要知道的是,在mysql内,和磁盘的交互基本单位是否也是4KB呢?
sql
# 通过指令进行查询
# 其中 Innodb_page_size是Ionodb存储引擎与磁盘的基本交互单位
mysql> SHOW GLOBAL STATUS LIKE 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.07 sec)
# 16384 Byte = 16 * 1024Byte = 16KB
我们发现,mysql与磁盘的基本交互单位是16KB!这是什么意思呢?

所以,我们可以理解为mysql服务端在上层做多了一次封装即可!
但是我们需要注意的是:
我们不要混淆mysqld层的page和内核中的page,这是不一样的概念!需要注意区分。
最后,只需要通过系统中的4KB基本单位,找到对应inode节点,再找到存储数据的位置,就能够把数据从磁盘中读取到mysql的内存缓冲区中了!
建立共识------提高查找速度的思考
在建立共识前,我们再来理解两个概念:
磁盘随机访问(Random Access)与连续访问(Sequential Access)
- 随机访问:本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需要作比较大的移动动作才能重新开始读/写数据。
- 连续访问:如果当次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次IO操作,这样的多个IO操作称为连续访问。
磁盘是通过CHS定址法来进行定位读取的,是需要配合其机械结构操作的!
所以,为了提高读取速度,应该尽可能地选取连续地磁盘空间存储数据,这样子效率高一些!
建立共识:
- mysql中的数据,都是以page为基本单位,存储在磁盘中的!读写都是16KB!
- MySQL 的CURD 操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。所以,mysql会尽可能地选取磁盘中的连续空间进行存储!
- 由冯诺依曼体系结构可知:为了加快计算操作,需要先把磁盘中的数据读取到内存!
- 和磁盘进行IO是一件比较低效的事情,所以需要尽可能地降低IO的次数!
- 为了更好的进行上述操作, mysql服务器在内存中运行时,在服务器内部,就申请了被称为 Buffer Pool 的的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互! 这个就是我们刚刚看的服务端层专门存储page的一块连续内存!
在有了以上的共识后,我们才能更好地去理解,mysql是如何对搜索做优化的!
优化思想
我们做过很多算法题,我们经常面临的一个问题就是:如何降低时间复杂度?
在当前的计算机来看,存储空间的解决是一件比较简单的事情,所以提速更加关键!
我们通常都是往两个方面思考的:
1.对算法进行优化
2.使用数据结构
基本上就是上述的两种思想了!所以,后序我们在理解mysql索引对查找做优化时,也是要从这两个方面进行思考和分析的!
我们先创建一个带有主键的表,我们来做一些测试,以便后序引入概念:
sql
# 不要给主键带上 auto_increment 约束
mysql> create table t1( id int primary key, name varchar(20), score int );
Query OK, 0 rows affected (0.06 sec)

我们发现,虽然我们插入的时候,id的顺序是乱的,但是最后显示的时候,确实按照顺序排好的!这是为什么?正常来说显示顺序就应该是插入顺序。
这里很明显,mysql已经根据id做好排序了!然后展示给我们看的!
(绝对不能是展示的时候排序,因为这样只会增加mysql服务端的负担)
所以,我们这里可以知道,我们使用主键后,mysql自动地完成了一些工作!
这里先介绍一个结论:mysql的主键,本质上也是索引!
所以,我们就从上述的现象进行分析,mysql究竟是如何对搜索进行优化的?
基本思想:
1.使用算法优化 or 数据结构
2.mysql在进行CURD操作的时候,需要先把内容从磁盘中加载到内存的缓冲区中
3.mysql与磁盘交互的基本单位是16KB
4.局部性原理:mysql一次性读取、写入的数据都是以16KB为单位的,这是符合计算机局部原理的。因为下一次的操作有很大概率,在当前数据的附近!
基于上述四点思想,我们猜想:mysql最有可能的优化操作,应该是使用数据结构!
原因:搜索的算法其实很简单,最多是排好序后进行二分!没有特别多的优化角度!
但是使用数据结构就不一样了,这有很大的想象空间!
索引的本质
至此,我们通过上述的一系列分析,就可以引出索引的本质了:
索引的本质,其实就是数据结构!以某种数据结构组织要被查找的内容!
现在看不懂这个结论没有关系,下面我们将一一地对mysql中的索引进行分析!
我们先从单个mysql中的page来理解,这是mysql操作单次操作数据大小的基本单位!

一个page里面,必然是存着表中的相关数据的!
我们也必然能知道,mysql服务端的缓冲区Buffer Pool中,必然是存在着一系列的page的。我们学习过Linux系统,我们必然知道mysql只要对这些page做管理的!
先组织,再描述!我们就先假设Buffer Pool中的每个page是以链表的形式连接!
对单个page做优化:
一个page大概是16KB,如果是存储数据的话,还是能够存储不少的数据的!
如果我们要在一个page内进行查询,如何使用数据结构进行优化呢?
我们需要明白搜索的根本思想是什么?是淘汰非相关数据!
我们采用线性遍历,一次只能淘汰一个!有没有什么办法能够一次性淘汰一批呢?
答案是有的,我们类比看书:
我们要找到某个内容,我们会先选择看目录,这样就能淘汰其他所有的章,然后再选择某一节,淘汰其他节,最后在一节内容内找到我们要的内容!
上述看书的过程,其实就体现出来了,使用分页思想,是可以极大地提高搜索的效率的!
事实上,mysql也确实是这么做的:
在mysql的一个page内,有专门一些地方存放的是该page的"目录":

我们从主键的显示结果来看也知道,插入数据的时候是按照顺序的!所以每个page内的数据一定是按照某个索引排好序的了!
所以,只需要在每个page内,留出一小部分专门的空间用来存放整个page的索引信息即可!
目录内的内容就是:对应数据索引的起始数据!
👉这样,就可以很轻松地通过索引的信息 + 目录,快速地搜索到要找的内容!
这里想要说明的是:
1.只要有索引,就会按照索引来进行排序!
2.索引的本质就是创建数据结构,优化算法思想,加快搜索速度!
索引深入理解
但是上述展示的,仅仅只是一页情况!
mysql中必然不可能只有这么一个page,如果page有很多个呢?
如下图所示:

我们会发现,每个page内要进行查找是很简单的,只需通过page内目录结构进行查询即可!
但是,如果page的数量非常多呢?
mysql服务端的Buffer_Pool一般是128MB = 8192 * 16KB
万一就是八千多个page,那如何搜索到要哪一个page呢?
我们发现,这又得从头遍历每个page,并且还要查询一下page内目录是否有要找的索引值!
这又是十分耗费时间的!这该怎么办?
刚刚既然能在一个page内进行目录的分级操作,我们能否让page也起到一样的效果呢?

当然可以,我们可以让个别的page,只充当page目录的作用,而不存储数据!
目录page内部存放的就是指向下一层page的一个指针,其实就是一个page的开头的索引!
基本思想是:
使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值。
和页内目录不同的地方在于,这种目录管理的级别是页(一条数据),而页内目录管理的级别是行(一个page)。
Tips: 每个目录项的构成是:键值+指针。图中没有画全。
我们可以粗略计算一下:
一个page是16KB,假设64位系统下,指针是8个字节(忽略掉其他因素):
每个索引条目大小 = 主键大小 + 指针大小 = 8B + 6B = 14字节
单个Page能存储的索引条目数量:
可用空间 ≈ 16KB × 80%(扣除页头等信息)≈ 13KB = 13312字节
最大索引条目数 = 13312 ÷ 14 ≈ 951个
这里我是让AI计算的,我们就大概算成1000个!这算是比较恐怖的了!
也就是说,我们在搜索索引的时候,只需要先判断一下在哪个目录下,就可以一次性淘汰成千上万的数据!这是非常恐怖的效率!
而判断一个要找的索引在不在某个目录page内是很简单的:
只需要判断一下索引与 当前目录page的最大指向索引 and 下一个目录page的最小索引 的关系即可!
但是,如果说第二层还是比较多怎么办呢?这很简单,我们再给目录page再做一层目录:

通过这样的三层结构,mysql就可以进行快速搜索了:
1.先通过顶层的page,判断寻找的索引应该处于哪一个目录page -> 淘汰了其它的所有的分分支
2.再目录page内,判断寻找的索引应该处于哪一个page页 -> 淘汰其他所有的page页
3.最后根据页内的局部目录,先找到页内的某一段位置,最后进行检索!
上述提到的这样一个数据结构,其实就是大名鼎鼎的B+树!!!!
而我们又知:
一个目录page能管理很多的页,一个目录page的目录page也可管理多个目录。
这样子,就会导致这样的一颗B+树是矮胖形的!
对于一棵树而言,条件分支越多,越矮,它的搜索效率越是高!
我们也不用担心一张表对应的B+树很大,Buffer_Pool不够存!
因为mysql不一定要把所有的表中的数据从磁盘中导入内存!
选用数据结构的思考
我们现在理解了一张表的索引是什么,也知道了索引是如何被组织起来管理的!
现在我们有一个问题:为什么就选中了B+树呢?其他数据结构为什么不行?
1.链表,数组等:这种简单的线性容器基本都是被淘汰的!因为搜索效率是O(N)。
2.二叉树:极端情况下可能面临着效率退化,变成单支链表,且二分支必然导致高度过大!
3.AVL树、红黑树:它们的平衡控制的很好,但是也是二叉!不如多叉树是必然的!
4.哈希:哈希的搜索时O(1),这非常好!但是哈希最大的问题是不连续!
假设今天每个位置都是哈希进行映射,我们如何进行局部的线性遍历呢?而B+树一层的节点都有专门的链表节点连接,符合数据库的需求!
所以,最后会选择B+树!
B树 vs B+树
其实,B树是非常类似于B+树的,但是为什么我们不选用B树呢?
B树:

B+树:

B树和B+树最大的区别就在于:
- B+树只有再最下面一层的叶子节点才会存储真正的数据,其他位置存储的都是目录信息!
而且B+树最后一层是形成了双链表的,和上面理解的还是有一些出入的! - 而B树一个节点内不仅要存储指针,还要存储数据!最后一层不形成双向链表!
为什么要选用B+树而不是B树呢?我们从以下几个方面进行分析:
1.B+树一个目录page内可以管理更多的page(不用存储数据) -> 树更加矮平,效率高!
2.B+树最后一层形成了双链表,对于局部内的线性搜索时很方便的!用B树就得重新遍历!
基本就是上述两点原因,导致mysql选用B+树作为索引的结构!
聚簇、非聚簇索引
当然,我们上面讲的基于B+树的索引,是一个大致的概念轮廓!
具体到mysql的不同存储引擎(engine)是有区别的!
下面,我们将针对于mysql中比较常用的两个存储引擎------InnoDB和MyISAM来进行理解!
我们首先来了解聚簇索引和非聚簇索引:
聚簇索引:
这个我们不用解释了,刚刚已经看过了,就是B+树!最后一层每个节点内存储的是数据!
非聚簇索引:
非聚簇索引其实和聚簇索引仅仅就是最后一层存储的内容上有区别:
非聚簇索引存储的是:存放数据的地址!

这就是聚簇索引和非聚簇索引的最大区别!
具体到我们要学习的具体的存储引擎,我们下面需要知道一个结论:
InnoDB存储引擎用的是聚簇索引,而MyISAM是非聚簇索引!
我们尝试着创建两个表,一个用InnoDB,一个用MyISAM:

在早期的mysql中,
我们是能看到InnoDB对应的表会有两个文件:db.frm db.ibd,分别是表格,数据。
MyISAM则会有三个文件:my.frm my.MYI my.MYD。分别是表格,索引,数据。
由此我们就能看出来:InnoDB是聚簇索引,MyISAM是非聚簇索引!
但是我的系统内装的是mysql 8.x,效果是这样的:
shell
root@hcss-ecs-1643:/var/lib/mysql/test# ls db*
db.ibd
root@hcss-ecs-1643:/var/lib/mysql/test# ls my*
my_438.sdi my.MYD my.MYI
存储引擎的重识
讲解辅助索引前,我们需要先讲解存储引擎的基本知识!
既然说,InnoDB用的是聚簇,MyISAM用的是非聚簇,那么一张表创建的时候就要用吗?
我们这里给出的答案是:当然!而且是根据主键来进行创建索引!
👉所以这就解释了为什么,我们在创建带有索引的表的时候,显示出来的是自动排好序的!
也就是说:一张表如果使用InnoDB,那么就会创建一个聚簇索引;反之创建非聚簇索引!
但是,这里还有一个问题,可是如果我没有设置主键呢?也会有索引吗?
答案是当然,只不过此时的对应的主键索引,就是mysql自动给我们设置的了,我们看不见!
所以这就是为什么,即使有索引,我们查询的也很慢!因为我们查询的依据不是索引!
InnoDB和MyISAM的区别
二者最基本的区别就是:InnoDB是聚簇索引,MyISAM是非聚簇索引!
但是除此之外,二者在另外一个方面上也是有显著区别的------创建辅助索引。
没错,mysql的表,不仅仅可以只使用一个索引,也可以创建其他索引:
只不过,InnoDB和MyISAM在行为上有一些差别!
对于MyISAM来说,它用的是非聚簇索引!除了主键索引之外,它在创建其他索引的时候:
也是根据对印的索引,创建好一个B+树,然后最后一层存储的是数据的地址!
但是,我们重点要说的是InnoDB!它不是这样子的:

InnoDB在创建辅助索引的B+树的时候,最后一层存储的是数据对应的主键值!
所以,想要一句辅助索引查询到其他字段的值,在InnoDB下:
1.根据索引获取对应的主键值
2.根据主键值,回表到主索引对应的B+树,重新进行搜索!
这就是InnoDB和MyISAM在创建辅助索引的时候的区别!
索引的相关操作
上述我们就基本讲完了,mysql内索引的相关细节!
接下来,我们的重点就是聚焦于索引的相关操作了!
主键索引的创建
方法1:创建表的时候直接声明主键
sql
create table t (id int primary key, namevarchar(30));
方法2:创建表结构的最后声明主键
sql
create table t (id int, namevarchar(30), primary key(id));
方法3:创建表完成后添加主键
sql
alter table t add primary key(id);
这样子,看似是只给表添加一个主键作为唯一值,其实也是根据主键创建了索引!
一个表中,最多有一个主键索引,当然可以使复合主键主键索引的效率高(主键不可重复)
创建主键索引的列,它的值不能为null,且不能重复
主键索引的列基本上是int
唯一索引的创建
我们还学习过唯一键unique,它其实也是索引!但不是主键索引!
其创建方法和主键如出一辙,只需要把primary key替换为unique即可!
这里就不做过多展示了!
一个表中,可以有多个唯一索引
查询效率高
如果在某一列建立唯一索引,必须保证这列不能有重复数据如果一个唯一索引上指定not null,等价于主键索引
普通索引的创建
但是,我们既不想创建主键索引,也不想创建唯一索引呢?也是有办法的:
方法1:在表的定义最后,指定某列为索引
sql
create table user8(id int primary key,
name varchar(20),
email varchar(30),
index(name)
);
方法2:创建完表以后指定某列为普通索引
sql
create table user8(id int primary key,
name varchar(20),
email varchar(30),
);
alter table user8 add index(id);
方法3:直接创建一个名为xxx的普通索引
sql
# 其中idx_name是给这个索引取的名字!
create index idx_name on user8(name);
# mysql旧版本下 如果不取也是可以的!
# 但是我这里用的mysql 8.x是不行的!
create index on user8(name);
一个表中可以有多个普通索引,普通索引在实际开发中用的比较多
如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引
复合索引
我们在之前学过复合的主键,我们现在其实就能知道:这是复合的索引!
所以,索引也是可以复合起来进行使用的!(操作在这里就不演示了,只要把索引替换为复合的即可!)
针对于InnoDB引擎,如果有时候我们不想进行回表操作去查询其他字段,我们可以这样做:
设置复合索引(x1, y1),这样子,在搜索到x1的时候,就立马能搜索到y1的内容!
就不用再去回表操作了,这样就快得多!
全文索引的创建
在mysql中,还存在一个特殊的索引:全文索引!
当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文索引机制,但是有要求,要求表的存储引擎必须是 MyISAM,而且默认的全文索引支持英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版(coreseek)
我们来试着看一下:
sql
# 先创建一个表
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 ...');
# 查询表
mysql> select * from articles;
+----+-----------------------+------------------------------------------+
| id | title | body |
+----+-----------------------+------------------------------------------+
| 1 | MySQL Tutorial | DBMS stands for DataBase ... |
| 2 | How To Use MySQL Well | After you went through a ... |
| 3 | Optimizing MySQL | In this tutorial we will show ... |
| 4 | 1001 MySQL Tricks | 1. Never run mysqld as root. 2. ... |
| 5 | MySQL vs. YourSQL | In the following database comparison ... |
| 6 | MySQL Security | When configured properly, MySQL ... |
+----+-----------------------+------------------------------------------+
6 rows in set (0.00 sec)
这里介绍一个新的关键字:explain,它可以用来分析sql语句的执行过程:
因为没有用到key,所以只能无脑暴力查询文本,匹配格式 ‘%database%’:
sql
mysql> explain select * from articles where body like '%database%' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: articles
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL # 这里是没有用到key去查询的
key_len: NULL
ref: NULL
rows: 6
filtered: 16.67
Extra: Using where
1 row in set, 1 warning (0.00 sec)
使用全文索引,就不用无脑匹配了:
sql
mysql> explain select * from articles where match (title, body) against ('database') \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: articles
partitions: NULL
type: fulltext
possible_keys: title
key: title # 此时用到了索引
key_len: 0
ref: const
rows: 1
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
其实全文索引的作用:就是对大文本再次进行索引,这样子我们就可以直接查询需要的字符串了!而不需要去匹配!
全文索引的使用有些复杂,我们一起来理解一下:
使用全文索引的语法:
select ... from table 表名 where (全文索引) against ('xxxx');
如果全文索引是复合的索引,不能缺少任何一个!它匹配的时候,索引内的每个字段都会匹配!匹配的原则就是:出现’ '的词!
但是这个词,必须是单独出现的!
索引的查询
索引的查询我们提供如下两种方法:
sql
#1
show index from table_name;
#2
show keys from table_name;
其余的方法不太推荐,我们就是用这两种方法即可:

删除索引
总的来说,删除索引就没有创建索引那么复杂了!
1.删除主键索引:
sql
alter table 表名 drop primary key;
2.删除普通索引(包括unique索引------本质也是普通索引)
sql
alter table 表名 drop index 索引名;
drop index 索引名 on 表名;
# 需要特别注意unique key:不能这样写
alter table 表名 drop unique key;
# 因为对mysql来说,unique就是一个普通索引
使用索引的总结
1.比较频繁作为查询条件的字段应该创建索引
2.唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件
3.更新非常频繁的字段不适合作创建索引
4.不会出现在where子句中的字段不该创建索引