前言
前面我们学习了,数据库的增删改查等操作,在数据库中,用的最多的业务就是查找,在现实生活中,我们会发现有很多的关键字会被经常查到,为了可以更加高效地进行查询,我们为此引出索引
1.简介
1.1.索引是什么?
MySQL的索引是一种数据结构,它可以帮助数据库高效地查询,更新数据表中的数据。索引通过一定的规则排列数据表中的记录,使得对表的查询可以通过索引的搜索来加快速度。
MySQL索引类似于书籍的目录,通过指向数据行的位置,可以快速定位和访问表中的数据,比如汉语字典的目录(索引)页,我们可以按照笔画、偏旁部首、拼音等排序的目录(索引)快速查询到需要的字。
-
笔画索引
-
偏旁部首索引
-
拼音索引
1.2.为什么使用索引?
显而易见,使用索引的目的只有一个,就是要提升数据检索的效率,在应用程序的运行过程中,查询操作的频率远远高于增删改的频率。
2.索引应该使用什么数据结构?
2.1.Hash
时间复杂度是 O(1),查询速度非常快,但是MySQL并没有选择Hash做为索引的默认数据结构,主要原因是hash不支持范围查找
2.2.二叉搜索树
二叉搜索树的中序遍历是一个有序数组,但有几个问题导致它不适合用作索引的数据结构
- 最坏情况下时间复杂度为O(N),退化成了单边树
- 节点个数过多无法保证树高
AVL和红黑树,虽然是平衡或者近似平衡,但是毕竟是二叉结构
在检索数据时,每次访问某个节点的子节点时都会发生一次磁盘IO,而在整个数据库系统中,IO是性能的瓶颈,减少IO次数可以有效的推升性能。
2.3.N叉树
为了解决树高的问题,可以使用N叉树:
通过观察,相同数据量的情况下,N叉树的树高可以得到有效的控制,也就意味这在相同数据量的情况下可以减少IO的次数,从而提升效率。但是MySQL认为N叉树做为索引的数据结构还不够好。
2.4.B+树
2.4.1. 简介
B+树是⼀种经常用于数据库和文件系统等场合的平衡查找树,MySQL索引采用的数据结构,以4阶B+树为例,如下图所示:
2.4.2. B+树的特点
- 能够保持数据稳定有序,插入与修改有效为稳定的时间复杂度
- 非叶子节点尽有索引作用,不存储数据,所以叶子节点保真实数据
- 所有叶子节点构成一个有序链表,可以按照key排序的次序依次遍历全部数据
2.4.3. B+树和B树的对比
- 叶子节点中的数据是连续的,且相互链接,便于区间查找和搜索
- 非叶子节点的值都包含在叶子节点中
- 对于B+树而言,在相同树高的情况下,查找任意元素的时间复杂度都一样,性能均衡
索引在整个数据检索的过程中是如何工作的,要从MySQL存储结构说起
3.Mysql中的页
3.1.为什么要使用页
在 .ibd 文件中最重要的结构体就是Page(页),页是内存与磁盘交互的最小单元,默认大小为16KB,每次内存与磁盘的交互至少读取一页,所以在磁盘中每个页内部的地址都是连续的,之所以这样做,是因为在使用数据的过程中,根据局部性原理,将来要使用的数据大概率与当前访问的数据在空间上是临近的,所以依次从磁盘中读取一页的数据放入内存中,当下次查询的数据还在这个页中就可以从内存中直接读取,从而减少磁盘IO提高性能。
局部性原理:
是指程序在执行时呈现出局部性规律,在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储的存储空间也局限于某个内存区域,局部性通常有两种形式:时间局部性和空间局部性
时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期他很可能还会被再次访问
空间局部性(Spatial Locality):将来要用到的信息大概率与正在使用的信息在空间地址上是临近的
- 每一页中即使没有数据也会使用16KB的存储空间,同时与索引的B+树种的节点对应。查看页的大小,可以通过系统变量innodb_page_size查看。
- 在MySQL中有很多种不同类型的页,最常用的就是用来存储数据和索引的索引页,也叫做"数据页",但不论哪种类型的页都会包含页头(File Header)和页尾(File Trailer),页的主体信息使用**数据"行"**进行填充,数据页的基本结果如下图所示:
3.2.页文件头和页文件尾
页文件头和页文件尾中包含的信息,如下图所示:
这里我们只关注,上一页页号和下一页页号,通过这两个属性可以把页与页之间链接起来,形成⼀个双向链表。
3.3.页主体
页主体部分是保存真实数据的主要区域,每当创建一个新页,都会自动分配两个行,一个是页内最小行Infimun,另外一个是页内最大行Supremun,这两个行并不存储任何真实信息,而是作为数据行链表的头和尾,第一个数据行有一个记录下一行的地址偏移量的区域next_record 将页内所有数据行组成一个单向链表,此时新页的结构如下所示:
当向一个新页插入数据时,将Infimun连接第一个数据行,最后一个真实数据行连接Supremun,这样数据行就构建成了一个单向链表,更多的行数据插入后,会按照主键从小到大的顺序进行连接,如下图所示:
3.4.页目录
- 当按主键或索引查找某条数据时,最直接简单的方法就是从头行Infimun开始,沿着链表顺序逐个比对查找,但一页有16kb,通常会存在数百行数据,每次都要遍历数百行,无法满足高效查询,为了提高效率,InnoDB采用二分查找来解决查询效率的问题。
- 具体实现方式是,在每一个页中加入一个叫做页目录(Page Directory) 的结构,将页内包括头行、尾行在内的所有行进行分组,约定头行单独为一组,其他每个组最多8条数据,同时把每个组最后一行在页中的地址,按主键从小到大的顺序记录在页目录中,页目录中的每一个位置称为一个槽,每个槽都对应了一个分组,一旦分组中的数据行超过分组的上线8个时,就会分裂出一个新的分组。
- 后续在查询某行时,就可以通过二分查找,先找到对应的槽,然后在槽内最多8个数据行中进行遍历即可,从而大幅度提高了查询效率,这时一恶搞页的核心结构就完成了。
- 例如要查询主键为6的行,先比对槽中记录的主键值,定位到最后一个槽2,再从最后一个槽中的第一条记录遍历,第二条记录就是我们要查询的目标行。
3.5.数据页头
数据页头记录了当前页保存数据相关的信息,如下图所示:
4.B+树在MySQL索引中的应用
非叶子节点在保存索引数据,叶子结点保存真实数据
以查找id为5的记录,完整的检索过程如下:
- 首先判断B+树的根节点中的索引记录,此时 5<7,应该访问左孩子节点,找到索引2
- 在索引页2中判断id的大小,找到与5相等的记录,然后加载对应的数据页
以上的IO过程,加载索引页1 - - - > 加载索引页2 - - -> 加载数据页3
4.1.计算三层树高的B+树可以存放多少条记录
- 假设一条用户数据大小为1kb,在忽略数据页中数据页自身属性空间占用的情况下,一页可以存16条数据
- 索引页一条数据的大小为,主键用BIGINT类型占8byte,下一页地址6byte,一共是14byte,一个索引页可以保存 16*1024/14 = 1170 条索引记录
- 如果只有三层树高的情况下,综合只保存索引的根节点和二级节点的索引页以及保存真实数据的数据页,那么一共可以保存1170117016 = 21,902,400 条记录,也就是说在两千多万条数据表中,可以通过三次IO就完成了数据的检索
5.索引分类
5.1.主键索引
- 当在一个表上定义一个主键PRIMARY KEY时,自动创建索引,索引的值是主键列的值,InnoDB使用它作为聚集索引。
- 推荐为每个表定义一个主键,如果没有逻辑上唯一且非空的列集可以使用主键,则添加一个自排列。
5.2.普通索引
- 最基本的索引类型,没有唯一性的原则
- 可能为多列创建组合索引,称为复合索引。
5.3.唯一索引
- 当在一个表上定义一个唯一键UNIQUE时,自动创建唯一索引
- 与普通索引类似,但区别在于唯一索引的列不允许有重复值。
5.4.全文索引
- 基于文本列(CHAR,WARCHAR或TEXT列)上创建,以加快对这些列中包含的数据查询和DML操作
- 用于全文搜索,仅MylSAM和InnoDB引擎支持
5.5.聚集索引
- 与主键索引是同义词
- 如果没有为表定义PRIMARY KEY,InnoDB使用第一个UNIQUE和NOT NULL的列作为聚集索引。
- 如果表中没有PRIMARY KEY或合适的UNIQUE索引,InnoDB会为新插入的行生成一个行号并用6字节的ROW_ID字段记录,ROW_ID单调递增,并使用ROW_ID做为索引
5.6.非聚集索引
- 聚集索引以外的索引称为非聚集索引或二级索引
- 二级索引中的每条记录都包含该行的主键列,以及二级索引指定的列
- InnoDB使用这个主键值来搜索聚集索引中的行,这个过程称为回表查询
5.7.索引覆盖
当一个select语句使用了普通索引且查询列表中的列刚好是创建普通索引时的所有或部分列,这时就可以直接返回数据,而不用回表查询,这样的现象称为索引覆盖
6.使用索引
6.1.自动创建
当我们为一张表加主键约束(Primary Key),外键约束(Foreign Key),唯一约束(Unique)时,MySQL会为对应的列自动创建一个索引
如果表不指定任何约束时,MySQL会自动为每一列生成一个索引并用ROW_ID进行标识
6.2.手动创建
6.2.1.主键索引
方法一:
创建表时指定主键并查看表结构
create table t_pk1(
id bigint primary key auto_increment,
name varchar(50)
);
desc t_pk1;
查看索引内容
show index from t_pk1;
方法二:
创建表时单独指定主键列
create table t_pk2(
id bigint auto_increment,
name varchar(50),
primary key (id)
);
方法三:
修改表中的列为主键索引
create table t_pk3(
id bigint,
name varchar(50)
);
-- 修改表中的id列为主键索引
alter table t_pk3 add primary key (id);
alter table t_pk3 modify id bigint auto_increment;
6.2.2.唯一索引
唯一约束跟上面的操作流程都一样,下面只写部分代码,大家可以按照上面的流程进行操作。
# ⽅式一,创建表时创建唯⼀键
create table t_test_uk (
id bigint primary key auto_increment,
name varchar(20) unique
);
# ⽅式二,创建表时单独指定唯⼀列
create table t_test_uk1 (
id bigint primary key auto_increment,
name varchar(20),
unique (name)
);
# ⽅式三,修改表中的列为唯⼀索引
create table t_test_uk2 (
id bigint primary key auto_increment,
name varchar(20)
);
alter table t_test_uk2 add unique (name);
6.2.3.普通索引
方法一:创建表的时候创建普通索引
create table t_index1(
id bigint primary key auto_increment,
name varchar(50) unique,
sno varchar(20),
index(sno)
);
方法二:修改表中的列为普通索引
create table t_index2(
id bigint primary key auto_increment,
name varchar(20) unique,
sno varchar(20)
);
alter table t_index2 add index(sno);
方法三:单独创建索引并指定索引名
create table t_index3(
id bigint primary key auto_increment;
name varchar(20),
sno varchar(20)
);
-- 为name列建立索引,不指定索引名时失效,必须要指定名字
create index idx_t_index3_sno on t_index3(sno);
删除索引
alter table t_index3 drop index idx_t_index3_sno;
6.3.创建复合索引
创建语法与创建普通索引相同,只不过制定多个列,列与列之间用逗号隔开
方法一:创建表时指定索引列
create table t_index4(
id bigint primary key auto_increment,
name varchar(50),
sno varchar(20),
class_id bigint,
index(sno,name)
);
方法二:修改表中的列为复合索引
create table t_index5(
id bigint primary key auto_increment,
name varchar(50),
sno varchar(50),
class_id bigint
);
alter table t_index5 add index(sno,name);
方法三:单独创建索引并指定索引名
create table t_index6(
id bigint primary key auto_increment,
name varchar(20),
sno varchar(20),
class_id bigint
);
create index idx_t_index6_sno_name on t_index6 (sno,name);
6.4.查看索引
方法一:
show keys from 表名\G;
方法二:
show index from 表名;
方法三:
desc 表名;
6.5.删除索引
6.5.1.主键索引
如果直接删除自增列的主键索引,会发生下面的状态
alter table t_index6 drop primary key;
如果不出现错误呢?
先把id的自增性删除掉,再删除主键索引
alter table t_index6 modify id bigint;
alter table t_index6 drop primary key;
6.5.2.其他索引
alter table 表名 drop index 索引列名;
6.6.创建索引的注意事项
- 索引应该创建在高频查询的列上
- 索引需要占用额外的存储空间
- 对表进行插入、更新和删除操作,同时也会修索引,可能会影响性能
- 创建过多或者不合理的索引会导致性能下降,需要谨慎选择和规划索引
7. explain语句
explain 语句就是为了查看自己写的SQL语句是否使用了索引
7.1.type:访问类型
类型 | 说明 |
---|---|
ALL | 扫描全表 |
index | 扫描全部索引树 |
range | 扫描部分索引,索引范围扫描,对索引的扫描开始于某一点,返回匹配值域的行,常见于between,<,>等的查询 |
ref | 使用非唯一索引或非唯一索引前缀进行的查找,不是主键或不是唯一索引 |
eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 |
const | 单表中最多有一个匹配行,查询起来非常迅速,例如根据主键或唯一索引查询。 |
system | 是const类型的特例,当查询的表只有一行的情况下,使用system |
NULL | 不用访问表或者索引,直接就能得到结果 |
从上面到下面,其中性能由低到高 |
7.2.使用explain
首先先在student表中添加一个复合主键,然后在再查一下索引结构
create index idx_student_sn_name on student(sn,name);
show index from student;
- 不加条件,查询所有
explain select * from student;
- 使用主键
explain select * from student where student_id = 1;
- 子查询中使用索引
explain select * from student where student_id =(select studnet_id from student where student_id = 1);
- 使用普通索引
explain select * from student where sn = '09982';
- 使用复合索引
explain select * from student where sn = '09982' and name = '黑旋风李逵';
现在我们删除sn这个唯一索引
alter table student drop index sn;
explain select sn,name from student where sn ='09982' and name = '黑旋风李逵';
可能在写代码的时候会写这样的
explain select sn from student where name ='黑旋风李逵';
Extra:执行情况的说明和描述,包含不适合在其他列中显示但非常重要的额外消息。
Using index 和 Using where:
- Using index:表示使用索引,如果只有 Using index,说明没有查询到数据表,只用索引表就完成了这个查询,这个叫做覆盖索引
- Using where:表示条件查询,如果不读取表的所有数据,或不是仅仅通过索引就可以获取皆可以获取所有需要的数据,则会出现 Using index