【MySQL】索引

思维导图

学习目标

MySQL的服务器本质是在内存中的,所有数据库的CRUD操作,全部是在内存中------索引也是这样。

提高算法效率的因素:1.组织数据的方式;2.算法本身。所以,索引是通过组织数据的方式来进行减少海量数据的检索速度,利用树的数据结构来进行优化效率。

一、没有索引,可能会有什么问题

1.1 先看一下索引是什么

我们先要创建一个海量的表,在创建完表之后,我们来进行一些操作:

  • 查询员工编号为998877的员工
cpp 复制代码
select * from emp where empno = 998877;

在未添加索引之前,我们可以看到查询耗时5.54秒,这还只是本机一个人在查询操作,在实际项目中, 假如同时有1000个人并发查询,那就有可能会死机,这是不能容忍的。

  • 创建索引
cpp 复制代码
alter table emp add index(empno);

创建索引也是需要时间的, 但是在创建完索引之后,查询所需要的时间就会大大减少。

  • 换一个员工编号,测试看看查询时间
cpp 复制代码
select * from emp where empno = 123456;

1.2 索引的概念

索引是用来提高数据库的性能,是一个物美价廉的东西。不需要添加内存,不需要修改程序,不需要调用sql,只用执行正确的create index,查询速度就可以提高成百上千倍。

但是,没有事物是完美的:查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作(插入、更新、删除)增加了大量的IO。所以其价值在于提高一个海量数据的检索速度。

1.3 索引的分类

常见的索引分为:

  • 主键索引(primary key)
  • 唯一索引(unique)
  • 普通索引(index)
  • 全文索引(fulltext)------解决中文索引的问题

二、认识磁盘

2.1 MySQL与存储

MySQL给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设中。

磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘的效率是比较低的,在加上IO本身的效率问题,可以知道,如何提高效率是MySQL的一个重要话题。

2.2 研究一下磁盘的构造

2.2.1 磁盘的介绍

  • 先来看一下磁盘的总体形状:
  • 再来看一下磁盘中的一个盘片:

磁盘表面被分为许多个同心圆,每一个同心圆称为一个磁道。每一个磁道都有一个编号嘴最外面的是0磁道。

每个磁道被划分为若干段(段又叫做扇区),每一个扇区的存储容量为512字节,每个扇区都有一个编号。

近三十年以来,扇区的大小一直是512字节。但是在最近几年中,正在迁移到更大、更高效的4096字节扇区,通常称为4K扇区。

  • 从上图可以看出,在半径方向上,距离圆心越近,扇区越小;距离圆心越远,扇区越大
  • 那么,所有扇区基本都是默认512字节的,因为保证一个扇区有多大,是有比特位密度决定的
  • 不过,最新的磁盘技术,已经慢慢让扇区的大小不同了,不过我们现在不考虑

2.2.2 扇区

存储在数据库中的数据,其实在Linux操作系统中,是以文件的形式进行存储的。数据库文件本质其实就是保存在磁盘的盘片中,也就是上图中的一个个小格子中(又称为扇区)。明显地,一个数据库文件很大,很多时,一定也需要占据多个扇区。

我们在使用Linux,所看到的大部分目录和文件都是在硬盘中进行保存的(一些内存文件系统,如:proc,sys之类的,我们不进行考虑)。

所以,最基本的,找到一个文件的全部,本质上就是在磁盘中找到所有保存文件的扇区。而我们能够定位到任何一个扇区,那么便可以找到所有扇区,因为查找方式是一样的。

2.2.3 定位扇区

我们想要定位扇区,就需要先定位盘面,再定位磁道,最后定位扇区。我们需要了解一下:

  • 柱面(磁道):多盘磁道,每一个盘都是双面,大小完全相等。所以同半径的磁道在整体上便构成了一个柱面。
  • 每一个盘面都有一个磁头,因此磁头和盘面的对应关系是一一对应的。

因此,我们只需要知道:磁头(Heads)、柱面(Cylinder)、扇区(Sector)对应的编号,就可以在磁盘中定位所要访问的扇区,这种数据定位方式为CHS。但是,在实际系统软件使用的并不是CHS(但是硬件是),而是LBA(一种线性结构,可以想象为虚拟地址与物理地址)系统将LBA地址最后转化为CHS,交给磁盘去进行数据读取。

2.2.4 总结

我们现在已经能够在硬件层面上定位任何一个基本数据块(扇区),那么在操作系统中,直接是按照扇区(512字节,部分4096字节)进行IO交互吗??答案:肯定不是的。

  • 如果操作系统直接使用硬件提供的数据大小进行交互,那么系统的IO代码就和硬件强相关; 换言之,如果系统发生变化,软件也需要发生变化。
  • 从目前来看,单次IO 512字节,还是太小了。IO单位小,意味着读取同样的数据内容,需要进行多次磁盘访问,会带来效率的降低。
  • 之前学习文件系统,就是在磁盘的基本结构下建立的,文件系统读取基本单位,就不是扇区,而是数据块。

因此:系统读取磁盘,是以块为单位进行的,基本单位是4KB。

2.3 磁盘的随机访问与连续访问

随机访问(Random Access):本次IO所给出的扇区地址和上次IO给出的扇区地址不连续,这样的话,磁头在两次IO操作之间需要进行比较大的移动才可以重新读/写数据。

连续访问(Sequential Access):如果当前IO操作给出的扇区地址与上次IO给出的扇区地址是连续的,那么磁头很快就可以开始本次IO,这样的多个IO操作称为连续访问。

三、MySQL与磁盘交互的基本单位

MySQL作为一款应用软件,可以想象成一种特殊的文件系统,它有着更高的IO场景。所以,为了提高基本的IO效率,MySQL进行IO的基本单位是16KB(后面统一使用InnoDB存储引擎)。也就是说,磁盘这个硬件设备的基本单位是512字节,而InnoDB存储引擎使用16KB进行IO交互,MySQL 和磁盘进行数据交互的基本单位是 16KB。这个数据基本单位在MySQL中称为page。

MySQL、操作系统与硬盘之间的关系:

四、建立共识

  • MySQL中的数据文件,是以page为单位保存在磁盘当中的。
  • 磁盘中的CRUD操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。
  • 然而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要先将数据移动到内存中。
  • 所以在特定时间中,数据一定是磁盘中有的,内存中也有,后续操作完数据之后,以特定的刷新策略,刷新到磁盘中。这时,就涉及到磁盘和内存的数据交互,也就是IO,此时IO的基本数据单位是page。
  • 为了更好地进行上面的操作,MySQL服务器在内存中运行时,在服务器的内部,申请了被称为:buffer pool的大内存空间,来进行各种缓存。其实就是一个更大的内存空间,来和磁盘数据进行IO交互。
  • 为了更高的效率,一定要尽可能的减少系统和磁盘IO的交互次数(局部性原理)。

五、索引的理解

5.1 建立测试表

cpp 复制代码
// 创建一个数据表
create table if not exists user (
    id int primary key, --一定要添加主键哦,只有这样才会默认生成主键索引
    age int not null,
    name varchar(16) not null
);

// 插入数据
// 插入多条数据,注意,我们并不是按照主键的大小顺序进行插入的
insert into user (id, age, name) values(3, 18, '杨过');
insert into user (id, age, name) values(4, 16, '小龙女');
insert into user (id, age, name) values(2, 26, '黄蓉');
insert into user (id, age, name) values(5, 36, '郭靖');
insert into user (id, age, name) values(1, 56, '欧阳锋');

5.1.1 我们向一个具有主键的表中,乱序插入数据,我们会发现数据会自动进行排序。为什么呢???

mysqld内部进行排序操作,只有创建的表中有主键,mysqld才会对其进行排序,使之后的查找效率更高。

插入数据时排序的目的是:优化查询的效率。

内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化效率是必须的。

正是因为有序,在查找的时候,从头到尾都是有效查找,没有任何一个查找是浪费的,而且如果运气好,是可以提前结束查找过程的。

5.1.2 如何理解mysql中的page的概念??

mysql内部,一定需要并且会大量存在page,也就决定了,mysql必须要将多个同时存在的page管理起来!要管理所有的mysql内的page,我们需要进行先描述,再组织。所以,不要简单地认为page就是一个内存块,page内部也必须写入对应的管理信息!

cpp 复制代码
struct page{
    struct page* next;
    string page* prev;
    char buffer[NUM];
};
// 将所有的page用一种特殊的数据结果管理起来,在 buffer pool 内部,对mysql中的page进行建模

5.2 为何IO交互要是page

为什么MySQL和磁盘进行交互时,要采用 page 的方案进行交互呢??我们可以用多少,加载多少。

举个例子:比如上面的 5 条记录,如果 MySQL 要查找 id = 2 的记录,第一次加载 id = 1,第二次加载 id = 2,一次加载一条记录,那么就需要加载 2 次 IO,如果要查找的是 id = 5,那么就需要 5 次 IO。但是,如果这5条记录被保存在一个 page 中(16KB,可以保存很多记录),那么第一次IO 查找 id = 2 的时候,整个 page 都会被加载到 Buffer Pool 中,这里完成了一次 IO。但是如果往后在寻找 id = 3, 4, 5等,完全不需要进行 IO,而是直接在内存中进行了。所以,在单 page中,大大减少了 IO 的次数。

因为计算机有局部性原理,有很大可能的概率用户下次找的数据在 page 中。往往 IO 效率低下的最主要矛盾不是 IO 单次数据量的大小,而是 IO 的次数。

5.3 理解单个 Page

在MySQL中一定有很多数据库表文件,因此,在MySQL中要管理很多数据库表文件,而要管理好这些数据库表文件,就要先描述,在组织。我们目前可以简单理解为一个个文件是有一个或者多个page构成的。

不同的page在MySQL中的大小都是16KB,使用prev和next构成双向链表。

因为有主键的问题,MySQL会默认按照主键给我们的数据进行排序,从上面的page内的数据记录可以看出,数据之间是有序并且彼此关联。

5.4 理解多个page

通过上面的分析,我们知道,在上面的页模式中,只有一个功能:**在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO的次数,从而提升性能。**但是,我们也可以看到,现在的页模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。

如果有一千万条数据,一定需要多个page来保存一千万条数据,多个page彼此使用双链表连接起来,而且每个page内部的数据也是基于链表的。那么,查找特定的一条记录,也一定是线式查找,其效率也是很低。

5.4.1 页目录

我们在生活中,想要看一本书的特定章节,我们有两种方式:

  • 从头到尾逐页向后翻,直到找到目标内容
  • 通过书本提供的目录,发现想要找的那个章节在235页,那么我们便可以直接翻到235页。同时,查找目录的方案,可以顺序找,不过因为目录数量少,所以可以快速提高定位。

在生活中,会发现基本上每一个书都有其目录,我们读者可以通过该目录很快的找到了我们想要看的那一页,不需要我们进行一页一页的寻找。而上述链表结构就是需要我们一一遍历寻找。所以,在mysql中的数据也有其对应的目录,为了提高搜索效率。

  • 本质上,书中的目录是多花了纸张,但是却提高了效率
  • 因此,目录是一种"空间换时间的做法"。

5.4.2 单页情况

针对单页的page,我们是否也可以引入目录呢?当然可以:

如上图所示,在一个page内部,我们引入了目录。比如,我们要查找的 id = 4 记录,按照之前的线性查找,需要进行线性遍历4次,才可以拿到结果。现在通过目录2可以直接定位到所要查找的记录,提高了效率。

5.4.3 多页情况

MySQL中每一页的大小只有16KB,单个page大小固定,所以随着数据量的不断增大,16KB不可能存下所有的数据,必定会有多个也来存储数据。

在单表数据不断被插入的情况下,MySQL会在容量不足的时候,自动开辟新的page来保存新的数据,然后通过指针的方式,将所有的page组织起来。

这样,我们可以通过多个page的遍历,page内部通过目录来快速定位数据。可是,这样也会有效率问题,在page之间,也是需要mysql遍历的,遍历意味着依旧需要进行大量的IO操作,将下一个page加载到内存,进行线性检测,这样显得我们之前的page内部的目录有点无用。

我们可以通过一些额外的空间建立目录来优化查找速度:

  • 使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值
  • 和页目录不同的地方在于:这种目录管理的级别是页,而页内目录管理的级别是行
  • 最后,每一个目录项的构成是:键值 + 指针

其中,目录页的本质是用户数据,而目录中的数据存放的就是指向的那一页的最小的数据。有数据,就可以通过该访问,进而通过指针找到下一个page。

其实目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。

5.4.4 复盘一下

  • page分为目录页和数据页,目录页只放各个下级page的最小键值
  • 查找的时候,自定向下找,只要加载部分电量到内存,即可完成算法的整个查询过程,大大减少; IO次数。

5.4.5 Inno_DB 在建立索引结果来管理数据的时候,我们可以使用其他数据结构吗??

  • 链表------线性遍历
  • 二叉搜索树------这个结构可能会发生退化,退化为线性结构
  • AVL && 红黑树------虽然是平衡或者是近似平衡,但是毕竟是二叉结构,相比较多阶B+树,意味着树的整体过高,大家都是自顶向下找,层高越低,意味着系统与硬盘之间的IO交互的次数会更少
  • Hash------官方的索引实现方式中,MySQL中是支持Hash,但是在InnoDB和MyIASM中并不支持Hash,Hash根据其算法特性,决定了虽然有时候速度是很快的(O(1)),不过,在面对范围查找的时候就明显不行,另外还有其他差别。
5.4.5.1 B树和B+树的对比

目前这两个树的区别是:

  • B树的节点上既有数据,又有page指针,而B+树中,只有叶子节点有数据,其他目录页中,只有键值和page指针
  • B+树中的叶子节点是全部相连的,而B树中的叶子节点是不相连的

为什么MySQL中的索引结构选择B+树??

  • B+树的节点不存放数据,不会占用一些空间,这样一个节点是可以存放更多的键值的,可以使得树更矮,IO次数就会变少
  • B+树的叶子节点是相连的,更便于进行范围查找(重要的)
5.4.5.2 聚簇索引和非聚簇索引

MyIASM引擎同样使用B+树作为索引结果,叶结点的data域存放的是数据记录的地址,简单的说就是将数据和索引是分来进行存储的。而InnoDB引擎中的叶节点是将数据和索引一样存储的。其中,MyIASM最大的特点是:将索引page和数据page进行分离,也就是叶子结点没有数据,只有对应数据的地址(如下图所示)。

在引擎中,我们除了主键索引,我们还可以建立其他索引。

在InnoDB中,如果建立普通索引,在非主键索引的叶子节点中并没有数据,而只有对应记录的Key值。所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录,这种过程就叫做回表查询。

为什么InnoDB针对这种辅助(普通)索引的场景,不给叶子结点附上数据呢?原因就是太浪费空间。

六、索引操作

6.1 创建索引

6.1.1 创建主键索引

  • 第一种方式
cpp 复制代码
// 在创建表的时候,直接在字段名后指定:primary key
create table user1(
    id int primary key,
    name varchar(30)
);
  • 第二种方式
cpp 复制代码
// 在创建表的最后,指定某列或某几列为主键索引
create table user2(
    id int,
    name varchar(30),
    primary key(id)
);
  • 第三种方式
cpp 复制代码
create table user3(
    id int,
    name varchar(30)
);
// 在创建表之后,在添加主键
alter table user3 add primary key(id);

主键索引的特点:

  • 一个表中,最多有一个主键索引,当然也可以是复合主键
  • 主键索引的效率高(主键不可重复)
  • 创建主键索引的列,它的值不能为NULL,且不能重复
  • 主键索引的列基本上是int

6.1.2 创建唯一键索引

  • 第一种方式
cpp 复制代码
// 在表定义时,在某列后直接指定unique唯一属性
create table user4(
    id int primary key,
    name varchar(30) unique
);
  • 第二种方式
cpp 复制代码
// 创建表时,在表的后面指定某列或者某几列为unique
create table user5(
    id int primary key,
    name varchar(30),
    unique(name)
);
  • 第三种方式
cpp 复制代码
create table user6(
    id int primary key,
    name varchar(30)
);

alter table user6 add unique(name);

唯一索引的特点:

  • 一个表中,可以有多个唯一索引
  • 查询效率高
  • 如果在某一列建立唯一索引,必须保证这列不能有重复数据
  • 如果一个唯一索引上指定not null,等价于主键索引

6.1.3 创建普通索引

  • 第一种方式
cpp 复制代码
create table user8(
    id int primary key,
    name varchar(30),
    index(name)  // 在表的定义最后,指定某列为索引
);
  • 第二种方式
cpp 复制代码
create table user10(
    id int primary key,
    name varchar(30)
);
alter table user10 add index(name); 
  • 第三种方式
cpp 复制代码
create table user11(
    id int primary key,
    name varchar(30)
);
create index idx_name on user11(name);

普通索引的特点:

  • 一个表中可以有多个普通索引,普通索引在实际开发中用的比较多
  • 如果某列需要创建索引,但是该列有重复值,那么我们就应该使用普通索引

6.1.4 创建全文索引(不重要)

当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL 提供全文索引机制,但是有要求,要求表的存储引擎必须是MyISAM ,而且默认的全文索引支持英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版。

  • 查询有没有database数据

如果使用如下查询方式,虽然查询出数据,但是没有使用到全文索引:

  • 如何使用全文索引?
cpp 复制代码
select * from articals
>> where match (title, body) against ('database');

我们可以通过explain来分析这个sql语句,来看一下是否使用到索引:

6.2 查询索引

  • 第一种方式
cpp 复制代码
show keys from 表名\G
  • 第二种方式
cpp 复制代码
show index from 表名
  • 第三种方式
cpp 复制代码
desc 表名

6.3 删除索引

  • 第一种方式------删除主键索引
cpp 复制代码
alter table 表名 drop primary key;
  • 第二种方式------删除其他索引
cpp 复制代码
alter table 表名 drop index 索引名;  索引名就是show index from 表名中的key_name字段
  • 第三种方式
cpp 复制代码
drop index 索引名 on 表名;

6.4 索引的创建原则

  • 比较频繁作为查询条件的字段应该创建索引
  • 唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件
  • 更新非常频繁的字段不适合作创建索引
  • 不会出现在where子句中的字段不该创建索引

6.5 其他概念

  • 复合索引
  • 索引最左匹配原则
  • 索引覆盖
相关推荐
奶糖趣多多28 分钟前
Redis知识点
数据库·redis·缓存
CoderIsArt1 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
师太,答应老衲吧3 小时前
SQL实战训练之,力扣:2020. 无流量的帐户数(递归)
数据库·sql·leetcode
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
Channing Lewis5 小时前
salesforce case可以新建一个roll up 字段,统计出这个case下的email数量吗
数据库·salesforce
追风林5 小时前
mac 本地docker-mysql主从复制部署
mysql·macos·docker
毕业设计制作和分享6 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
ketil276 小时前
Redis - String 字符串
数据库·redis·缓存
Hsu_kk7 小时前
MySQL 批量删除海量数据的几种方法
数据库·mysql
编程学无止境7 小时前
第02章 MySQL环境搭建
数据库·mysql