MySQL索引及其底层原理(上)

目录

前言

一、索引初步认识

二、再识磁盘

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

四、索引的理解

单个Page

多个Page

单个Page目录

多个Page目录


前言

从本篇开始我们就要进一步去理解mysql了,那么这一篇是mysql中非常重要的知识点---索引的上篇,相信大家看完能对mysql中的索引部分有新的认识和理解!

一、索引初步认识

  • 索引是提高数据库性能的关键工具。

  • 它通过改变数据的组织方式,显著提升查询速度,而无需增加硬件资源或修改应用程序代码。

  • 然而,索引的使用也并非没有代价。查询速度的提升是以插入、更新和删除操作的性能下降为代价的,因为这些写操作会增加大量的I/O操作。

  • 因此,索引的价值在于提高海量数据的检索速度。

MySQL的CURD操作与内存

  • 所有 MySQL 的 CURD 操作都在内存中进行。MySQL 在启动时会预先开辟一大块内存空间,用于缓存数据。

  • 这些数据会在适当的时候被刷新到磁盘中进行持久化。

  • 因此,MySQL服务器本质上是在内存中运行的,所有的数据库操作都在内存中进行。索引同样也是在内存中的一种特定结构。

  • 索引是提高效率的,一般我们知道提高算法效率的因素:1. 组织数据的方式 ,2. 算法本身。

    比如说在一个线性结构的数组中查找数据,那么我们可以通过不使用遍历而改为使用二分查找算法来提高效率,其次还可以将其结构改为非线性存储结构---搜索二叉、平衡二叉,此时查找的算法随之发生变化,查找效率也会提高

  • 索引是更改特定组织数据的方式,把以前数据的组织方式以新的数据结构组织起来。所以索引是内存中一种特定组织的一种数据结构,具体是什么结构后面再说

索引的类型

常见的索引类型包括:

  • 主键索引(Primary Key)

  • 唯一索引(Unique)

  • 普通索引(Index)

  • 全文索引(Fulltext)------主要用于解决中文索引问题

有无索引的比对

我们先创建一个包含800万条记录的表,并观察没有索引时的查询性能。

sql 复制代码
-- 产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
begin
    declare chars_str varchar(100) default 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    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 (empno, ename, job, mgr, hiredate, sal, comm, deptno)
        values (start + i, rand_string(6), 'SALESMAN', 0001, curdate(), 2000, 400, rand_num());
    until i = max_num
    end repeat;
    commit;
end $$
delimiter ;
 
-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);
复制代码

注意:创建表单前要注意,恢复默认结束符:使用 delimiter ; 将结束符恢复为默认的分号 ;

查询性能测试

查询员工编号为998877的员工

sql 复制代码
select * from EMP where empno = 998877;

可以看到没有索引时,查询非常慢,在本机上一人操作就花了5秒多,那在实际项目中,如果放在公网中,同时有1000人并发查询,可能会导致系统崩溃。

但是我们现在一旦加上了索引,查询速度就非常快了

sql 复制代码
alter table EMP add index(empno);

二、再识磁盘

硬件理解

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

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

我们先看看磁盘

我们再来看这个盘片的图:

数据库文件,本质其实就是保存在磁盘的盘片当中。也就是上面的一个个小格子中,就是我们经常所说的扇区。当然,数据库文件很大,也很多,一定需要占据多个扇区。

题外话:

  • 从上图可以看出来,在半径方向上,距离圆心越近,扇区越小,距离圆心越远,扇区越大

  • 那么,所有扇区都是默认 512 字节吗?目前是的,我们也这样认为。因为保证一个扇区多大,是由比特位密度决定的。

  • 不过最新的磁盘技术,已经慢慢的让扇区大小不同了,不过我们现在暂时不考虑。

其实,我们在使用 Linux ,所看到的大部分目录或者文件,其实就是保存在硬盘当中的。(当然,有一些内存文件系统,如: proc , sys 之类,我们不考虑)

建立数据库其实就是在 linux 下建立一个目录,建立一张表其实就是在 linux 建立一个文件,所以,最基本的,找到一个文件的全部,本质,就是在磁盘找到所有保存文件的扇区。

通过 CHS、LBA(在linux系统部分学过),我们现在已经能够在硬件层面定位,任何一个基本数据块了(扇区)。那么在系统软件上,就直接按照扇区进行IO交互吗?

答案是 - 不是

  • 如果操作系统直接使用硬件提供的数据大小进行交互,那么系统的 IO 代码,就和硬件强相关,换言之,如果硬件发生变化,系统必须跟着变化

  • 从目前来看,单次IO 512字节,还是太小了。IO单位小,意味着读取同样的数据内容,需要进行多次磁盘访问,会带来效率的降低

  • 之前学习文件系统,就是在磁盘的基本结构下建立的,文件系统读取基本单位,就不是扇区,而是数据块

故系统读取磁盘,是以块为单位的,基本单位是 4KB 。

磁盘随机访问 (Random Access) 与连续访问 (Sequential Access):

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

连续访问:如果当次 IO 给出的扇区地址与上次 IO 结束的扇区地址是连续的,那磁头就能很快的开始这次 IO 操作,这样的多个 IO 操作称为连续访问。

因此尽管相邻的两次 IO 操作在同一时刻发出,但如果它们的请求的扇区地址相差很大的话也只能称为随机访问,而非连续访问。

磁盘是通过机械运动进行寻址的,连续访问时磁头无需大幅移动,效率更高;而随机访问需要磁头进行较大范围的移动和定位,因此效率比较低。

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

软件理解

MySQL是一个应用层软件,依赖操作系统进行 I/O 操作。操作系统和磁盘之间的 I/O 单位是 4KB ,而MySQL为了提高效率,以 16KB 为单位先和 OS 进行 I/O 操作(后面统一使用 InnoDB 存储引擎讲解 )

MySQL在内存中申请了一个大内存空间(Buffer Pool),用于缓存数据。默认大小为128MB。 Buffer Pool 用于减少磁盘 I/O 次数,提高查询性能。

I/O流程

注意:mysql并不能直接向磁盘去申请空间,因为其处在应用层的,申请只能由操作系统来进行!

  • MySQL向操作系统请求 16KB 数据。

  • 操作系统从磁盘读取 4 个 4KB 的数据块,加载到文件缓存区。

  • 数据从文件缓存区加载到MySQL的Buffer Pool。

  • MySQL在Buffer Pool中对数据进行处理。

  • 更新数据时,MySQL 将数据使用 write 写入 Buffer Pool,再将数据标记为 "脏页"(即内存中的数据与磁盘数据不一致),然后通过操作系统使用 fsync 函数传入 fd 将文件内部对应缓存区的数据刷新到磁盘中。

    解析:

    MySQL 的 Buffer Pool 是内存中的数据缓存区域,更新操作会先修改 Buffer Pool 中的数据页并标记为脏页(区别于未修改的干净页)。之后,MySQL 会通过后台线程(如 checkpoint 机制)或主动触发(如事务提交时),调用操作系统的 fsync 函数,将脏页对应的磁盘文件缓存数据强制刷新到物理磁盘,确保数据持久化,避免内存数据丢失。

  • 16 * 1024 = 16384

  • 磁盘这个硬件设备的基本单位是 512 字节,而 MySQL InnoDB 引擎 使用 16KB 进行IO交互。

  • 即, MySQL 和磁盘进行数据交互的基本单位是 16KB 。

  • 这个 16KB 的基本数据单元,在 MySQL 这里叫做 page(注意和系统的page区分),它们是 1 : 4 的关系

所以说,有三点是我们要牢记于心的

  • MySQL以 16KB page为单位进行I/O。

  • MySQL有 Buffer Pool ,数据先加载到 Buffer Pool ,再进行处理。

  • 减少系统和磁盘 I/O 次数,一次 I/O 的数据量越大,效率越高。

我们也就建立了下面几点共识:

  • MySQL 中的数据文件,是以 page 为单位保存在磁盘当中的。

  • MySQL 的 CURD 操作(增删查改),都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。

  • 而只要涉及计算,就需要 CPU 参与,而为了便于 CPU 参与,一定要能够先将数据移动到内存当中。

  • 所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是 IO 了。而此时 IO 的基本单位就是 Page。

  • 为了更好的进行上面的操作,MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为 Buffer Pool 的大内存空间(mysql是c、c++语言写的,所以本质就是malloc或者new出这样的空间),来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行 IO 交互。

  • 为何更高的效率,一定要尽可能的减少系统和磁盘 IO 的次数

四、索引的理解

建立测试表

sql 复制代码
create table if not exists user (
    id int primary key,
    age int not null,
    name varchar(16) not null
) engine=InnoDB;

插入多条记录

sql 复制代码
insert into user (id, age, name) values (3, 25, 'Alice');
insert into user (id, age, name) values (1, 20, 'Bob');
insert into user (id, age, name) values (2, 22, 'Charlie');

查看插入结果

发现数据是有序的,这是因为 MySQL 会默认按照主键进行排序。

这很有意思,明明我们是乱序插入,怎么查询就变得有序起来了?是谁做得,怎么做的?

在这里我们再来重谈 Page

  • 磁盘上的文件数据:首先会被读到操作系统的文件缓存区中。

  • MySQL的 Buffer Pool :MySQL在启动时会为自己申请一个 Buffer Pool ,用于缓存数据。MySQL与操作系统之间进行 I/O 交互的基本单位是 16KB ,这是为了提高效率,减少 I/O 成本。

  • Page: MySQL的基本数据单位,大小为 16KB,mysql内部一定需要并且会存在大量的page,也就是决定了mysql必须要将多个同时存在的page管理起来

  • Buffer Pool:可以加载多个 Page ,使用双向链表连接。

  • 先描述,再组织:不要简单地将Page认为是一个内存块,Page内部也必须写入对应的管理信息。

  • 链表管理:所谓在 MySQL 中申请一个 Page ,实际上是 new 一个 Page对象 。然后将所有 Page 用 "链表" 的形式管理起来。我们在 Buffer Pool 内部对 MySQL 中的 Page 进行了建模。

中断一下 --- 为何 IO 交互要是 Page

为何 MySQL 和磁盘进行 IO 交互的时候,要采用 Page 的方案进行交互呢?用多少,加载多少不香吗?

如上面的 3 条记录,如果 MySQL 要查找 id=2 的记录,第一次加载 id=1,第二次加载 id=2,一次一条记录,那么就需要 2 次 IO。如果要找 id=3,那么就需要 3 次 IO。

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

你怎么保证,用户一定下次找的数据,就在这个 Page 里面?我们不能严格保证,但是有很大概率,因为有局部性原理。

往往 IO 效率低下的最主要矛盾不是 IO 单次数据量的大小,而是 IO 的次数。

乱序插入,有序查询:我们向一个具有主键的表中,乱序插入数据,发现数据会自动排序。

结论:数据最终以Page为单位进行管理,而Page是先描述后组织的。

单个Page

Page:一个 Page 可以看作是一个大的结构体,里面可以放很多数据,也有它自己的属性。   数据承载:一个 Page 可以承载一部分数据,而一个文件可能很小也可能很大,因此 MySQL 建立的表可能会是一个或多个 Page 构成。

  • 大小:在 MySQL 中,每个 Page 的大小都是 16KB 。

  • 双向链表:Page 使用 prev 和 next 指针构成双向链表,方便管理和遍历。

  • 主键:MySQL 会默认按照主键对数据进行排序。

  • 无主键:如果没有主键,默认插入的顺序就是查询时的顺序。

很显然,这个排序的操作是数据库做的

那我们又要思考了,为什么数据库插入的时候要对其进行排序,按正常插入顺序不好吗?

  • 目的:插入数据时排序的目的是优化查询的效率。 Page 内部的数据记录实质上是一个链表结构,链表的特点是增删快,查询修改慢。因此,有序的结构在查找时更加高效。

  • 优势:有序的数据在查找时从头到尾都是有效查找,没有任何一个查找是浪费的,而且如果运气好,可以提前结束查找过程。

多个Page

单Page的功能:在查询某条数据时,直接将一整页的数据加载到内存中,以减少硬盘 I/O 次数,从而提高性能。

而如果有1千万条数据,需要多个 Page 来保存,多个 Page 彼此使用双链表链接起来,每个Page内部的数据也是基于链表的,这样子说白了在遍历的时候还是线性遍历,较慢

我们看到图中已经出现目录这一名词了

页目录的引入

  • 目录的作用:类似于书籍的目录,多花了空间但提高了效率。

  • 所以,目录,是一种"空间换时间的做法"

单个Page目录

牺牲 Page 一部分保存数据的空间,把腾出来的空间用来保存目录,这所谓的目录里面只有两个字段

  • 第一个是它所指向起始位置的 key 值

  • 第二它有一个指针字段指向这条记录的起始位置。

所以未来在查找 key 的时候,不需要在数据记录里面查找了,而是去目录中找

  • 先找到目录中对应 key 值的所处的起始位置

  • 然后再根据指针找到这条记录

  • 然后根据这条记录再向下遍历

  • 虽然最后我们依旧需要遍历,但是是一个很小的子序列遍历了,效率也就大大提高了。

那么当前,在一个Page内部,我们引入了目录。比如,我们要查找 id=4 记录,之前必须线性遍历4次,才能拿到结果。现在直接通过目录 2[3] ,直接进行定位新的起始位置,提高了效率,这数据还太小不够明显,如果数据更大就很明白了!

现在我们可以再次正式回答上面的问题了,为何通过键值 MySQL 会自动排序?

答案是可以很方便引入目录。

多个Page目录

Page大小固定: MySQL 中每一页的大小只有 16KB ,单个 Page 大小固定。   数据量增长:随着数据量的不断增大, 16KB 不可能存下所有的数据,因此需要多个 Page 来存储数据

Page 有 动态管理机制 ,在单表数据不断被插入的情况下,MySQL 会在容量不足时,自动开辟新的 Page 来保存新的数据。

组织方式:通过指针的方式,将所有的 Page 组织起来,形成一个链表结构。

但是你会发现,这样又要线性遍历了,这样就显得我们之前的Page内部的目录,有点杯水车薪了 那么解决方法就是继承前面的思想:增加目录!!

  • 使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值,此时16kb的目录最多可管理映射好16384/(4+8)(这里是4字节整形key加8字节指针value)=1365个Page

  • 和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行。

  • 其中,每个目录项的构成是:键值+指针。图中没有画全。

存在一个目录页来管理页目录,目录页中的数据存放的就是指向的那一页中最小的数据。有数据,就可通过比较,找到该访问那个Page,进而通过指针,找到下一个Page。

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

可是,我们每次检索数据的时候,该从哪里开始呢?虽然顶层的目录页少了,但是还要遍历啊?不用担心,可以再加目录页

其实这就是传说中的 B+ 树,现在查找的 Page 数一定减少了,也就意味着 I/O 次数减少了,那么效率也就 提高了

需要注意的是,我们上图中其实在有多个节点时无论是目录还是Page都使用链表连接起来了,但实际上只有叶子节点的Page才是使用链表连接的,上面的目录都是通过上层父节点索引结构或者目录结构关联起来的

细节:

  1. 叶子节点保存有数据,路上节点没有,也就是非叶子节点不要数据,只要目录

    非叶子节点不存数据意味着其可以存储更多的目录项,那么这个目录页可以管理更多的叶子page,那么这棵树一定是一个 矮胖型 的树,这样的树使得途径的路上节点减少,也就是找到目标数据只需要更少的page,io次数更少,在io层面上,提高了效率;同时这样每一个节点都有目录项,可以大大提高搜索效率,和上面一起整体提高了我们的搜索效率

  2. 叶子节点全部用链表级联起来

    首先这个本身就是B+树的特点

相关推荐
怣505 小时前
MySQL子查询零基础入门教程:从小白到上手(零基础入门版)
数据库·mysql
猫头虎6 小时前
基于信创openEuler系统安装部署OpenTeleDB开源数据库的实战教程
数据库·redis·sql·mysql·开源·nosql·database
Nandeska6 小时前
17、MySQL InnoDB ReplicaSet
数据库·mysql
hlABgYML6 小时前
基于NGSIM数据的Wiedemann99跟驰模型标定
mysql
墨理学AI7 小时前
一文学会一点python数据分析-小白原地进阶(mysql 安装 - mysql - python 数据分析 - 学习阶段梳理)
python·mysql·数据分析
洛豳枭薰7 小时前
MySQL 并行复制
数据库·mysql
纤纡.7 小时前
Linux 下 MySQL 数据类型与约束:第三章核心表格归纳与实战应用
linux·mysql
czlczl200209257 小时前
增删改查时如何提高Mysql与Redis的一致性
数据库·redis·mysql
打工的小王7 小时前
MySql(二)索引
数据库·mysql