[MySQL#10] 索引底层(1) | Page | 页目录
目录
1. 初识索引
2. 认识磁盘
3. MySQL与磁盘交互基本单位
4. 索引的理解
1. 重谈Page
2. 为什么IO交互要用Page
3. 有主键的表插入数据时的排序
4. 单个Page与多个Page
4.1 单个Page
4.2 多个Page
目录
单Page目录
多Page目录
在看本文之前,可以回顾一下这两篇前文~
[MySQL#5] 表约束(2) | 唯一键 | 外键 | 简易商店数据库
【Linux】(26) 详解磁盘与文件系统:从物理结构到inode机制
1. 初识索引
- 索引是提高数据库性能的关键工具。
- 它通过改变数据的组织方式,显著提升查询速度,而无需增加硬件资源或修改应用程序代码。
- 然而,索引的使用也并非没有代价。
- 查询速度的提升是以插入、更新和删除操作的性能下降为代价的,因为这些写操作会增加大量的I/O操作。
- 因此,索引的价值在于提高海量数据的检索速度。
MySQL的CURD操作与内存
- 所有MySQL的CURD操作都在内存中进行。MySQL在启动时会预先开辟一大块内存空间,用于缓存数据。
- 这些数据会在适当的时候被刷新到磁盘中进行持久化。
- 因此,MySQL服务器本质上是在内存中运行的,所有的数据库操作都在内存中进行。索引同样也是在内存中的一种特定结构。
- 索引是提高效率的,一般我们知道提高算法效率的因素:1. 组织数据的方式 ,2. 算法本身。
- 索引是更改特定组织数据的方式,把以前数据的组织方式以新的数据结构组织起来。所以索引是内存中一种特定组织的一种数据结构,具体是什么结构后面再说,
索引的类型
常见的索引类型包括:
- 主键索引(Primary Key)
- 唯一索引(Unique)
- 普通索引(Index)
- 全文索引(Fulltext)——主要用于解决中文索引问题
创建海量数据表
为了演示索引的效果,我们先创建一个包含800万条记录的表,并观察没有索引时的查询性能。
-- 产生随机字符串
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的员工:
select * from EMP where empno = 998877;
没有索引时,查询非常慢,耗时约4.29秒。在实际项目中,如果放在公网中,同时有1000人并发查询,可能会导致系统崩溃。
解决方法:创建索引
alter table EMP add index(empno);
创建索引后,查询速度大幅提升。
2. 认识磁盘
硬件理解
MySQL 给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设当中。
磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率是比较低的,在加上IO本身的特征,可以知道,如何提升效率,是 MySQL 的一个重要话题。
磁盘结构
- 扇区:数据库文件保存在磁盘的扇区中。每个扇区通常是512字节。
- 柱面(Cylinder):同半径的磁道构成一个柱面。
- 磁头(Heads):每个盘面有一个磁头,磁头和盘面的对应关系是1对1的。
- CHS(Cylinder, Head, Sector):通过磁头、柱面和扇区编号定位扇区。
- LBA(Logical Block Addressing):系统使用线性地址,最终转换为CHS。
再细看磁盘中一个盘片
扇区
数据库文件,本质其实就是保存在磁盘的盘片当中。也就是上面的一个个小格中,就是我们经常所说的扇区。当然,数据库文件很大,也很多,一定需要占据多个扇区。
题外话:
- 从上图可以看出来,在半径方向上,距离圆心越近,扇区越小,距离圆心越远,扇区越大
- 那么,所有扇区都是默认512字节吗?目前是的,我们也这样认为。因为保证一个扇区多大,是由比特位密度决定的。
- 不过最新的磁盘技术,已经慢慢的让扇区大小不同了,不过我们现在暂时不考虑。
- 我们在使用Linux,所看到的大部分目录或者文件,其实就是保存在硬盘当中的。(当然,有一些内存文件系统,如: proc , sys 之类,我们不考虑)
- 建立数据库其实就是在linux下建立一个目录,建立一张表其就是在linux建立一个文件。
所以,最基本的,找到一个文件的全部,本质,就是在磁盘找到所有保存文件的扇区。
而我们能够定位任何一个扇区,那么便能找到所有扇区,因为查找方式是一样的。
定位扇区
- 柱面(磁道): 多盘磁盘,每盘都是双面,大小完全相等。那么同半径的磁道,整体上便构成了一个柱面
- 每个盘面都有一个磁头,那么磁头和盘面的对应关系便是1对1的
- 所以,我们只需要知道,磁头(Heads)、柱面(Cylinder)(等价于磁道)、扇区(Sector)对应的编号。即可在磁盘上定位所要访问的扇区。这种磁盘数据定位方式叫做 CHS 。
- 不过实际系统软件使用的并不是 CHS (但是硬件是),而是 LBA ,一种线性地址,可以想象成虚拟地址与物理地址。系统将 LBA 地址最后会转化成为 CHS ,交给磁盘去进行数据读取。不过,我们现在不关心转化细节,知道这个东西,让我们逻辑自洽起来即可。
结论
我们现在已经能够在硬件层面定位,任何一个基本数据块了(扇区)。那么在系统软件上,就直接按照扇区进行IO交互吗?不是
- 如果操作系统直接使用硬件提供的数据大小进行交互,那么系统的IO代码,就和硬件强相关,换言之,如果硬件发生变化,系统必须跟着变化
- 从目前来看,单次IO 512字节,还是太小了。IO单位小,意味着读取同样的数据内容,需要进行多次磁盘访问,会带来效率的降低。
- 之前学习文件系统,就是在磁盘的基本结构下建立的,文件系统读取基本单位,就不是扇区,而是数据块。
故,系统读取磁盘,是以块为单位的,基本单位是 4KB 。
磁盘访问方式
- 随机访问(Random Access):扇区地址不连续,磁头需要移动。
- 连续访问(Sequential Access):扇区地址连续,磁头移动较少,效率较高。
3. MySQL与磁盘交互基本单位
软件理解
MySQL是一个应用层软件,依赖操作系统进行I/O操作。操作系统和磁盘之间的I/O单位是4KB,而MySQL为了提高效率,以16KB为单位先和 OS 进行I/O操作。
Buffer Pool
MySQL在内存中申请了一个大内存空间(Buffer Pool),用于缓存数据。默认大小为128MB。Buffer Pool用于减少磁盘I/O次数,提高查询性能。
I/O流程
- MySQL向操作系统请求16KB数据。
- 操作系统从磁盘读取4个4KB的数据块,加载到文件缓存区。
- 数据从文件缓存区加载到MySQL的Buffer Pool。
- MySQL在Buffer Pool中对数据进行处理。
- 更新数据时,MySQL将数据写入Buffer Pool,然后通过操作系统刷新到磁盘。
验证
- 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的数据量越大,效率越高。
以上就是我们学习索引的预备工作,下面正式开始学习索引~
4. 索引的理解
建立测试表
create table if not exists user (
id int primary key,
age int not null,
name varchar(16) not null
) engine=InnoDB;
插入多条记录
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会默认按照主键进行排序。
❓我们向一个具有主键的表中,乱序插入数据,发现数据会自动排序。谁做的?为什么这么做?
为了解决这个问题,我们需要重新探讨一下MySQL中的Page概念。
1. 重谈Page
数据读取流程
- 磁盘上的文件数据:首先会被读到操作系统的文件缓存区中。
- MySQL的Buffer Pool:MySQL在启动时会为自己申请一个Buffer Pool,用于缓存数据。MySQL与操作系统之间进行I/O交互的基本单位是16KB,这是为了提高效率,减少I/O成本。
Page的概念
- Page:MySQL的基本数据单位,大小为16KB。
- Buffer Pool:可以加载多个Page,使用双向链表连接。
管理Page的方法
- 先描述,再组织:不要简单地将Page认为是一个内存块,Page内部也必须写入对应的管理信息。
- 链表管理:所谓在MySQL中申请一个Page,实际上是new一个Page对象。然后将所有Page用“链表”的形式管理起来。我们在Buffer Pool内部对MySQL中的Page进行了建模。
2. 为什么IO交互要用Page
Page的重要性
- 问题: MySQL和磁盘进行I/O交互时,采用Page方案的原因是为了提高效率。如果每次只加载需要的数据,例如查找id=2的记录,第一次加载id=1,第二次加载id=2,一次一条记录,那么就需要2次I/O。如果要找id=5,那么就需要5次I/O。
- 批量加载:但如果这5条记录(或更多)都被保存在一个Page中(16KB,能保存很多记录),那么第一次I/O查找id=2时,整个Page会被加载到MySQL的Buffer Pool中,完成一次I/O。此后如果查找id=1,3,4,5等,完全不需要进行I/O,而是在内存中进行。因此,在单Page内大大减少了I/O的次数。
局部性原理
- 不能严格保证:我们不能严格保证用户下次查找的数据一定在同一个Page内,但有很大概率,因为有局部性原理。
- 主要矛盾:IO效率低下的主要矛盾不是单次I/O数据量的大小,而是I/O的次数。
3. 有主键的表插入数据时的排序
现象
- 乱序插入,有序查询:我们向一个具有主键的表中,乱序插入数据,发现数据会自动排序。
- 结论:数据最终以Page为单位进行管理,而Page是先描述后组织的。
4. 单个Page与多个Page
4.1 单个Page
Page的定义
- Page:一个Page可以看作是一个大的结构体,里面可以放很多数据,也有它自己的属性。
- 数据承载:一个Page可以承载一部分数据,而一个文件可能很小也可能很大,因此MySQL建立的表可能会是一个或多个Page构成。
Page的特性
- 大小:在MySQL中,每个Page的大小都是16KB。
- 双向链表:Page使用
prev
和next
指针构成双向链表,方便管理和遍历。
主键与排序
- 主键:MySQL会默认按照主键对数据进行排序。
- 无主键:如果没有主键,默认插入的顺序就是查询时的顺序。
❓为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?
- 目的:插入数据时排序的目的是优化查询的效率。Page内部的数据记录实质上是一个链表结构,链表的特点是增删快,查询修改慢。因此,有序的结构在查找时更加高效。
- 优势:有序的数据在查找时从头到尾都是有效查找,没有任何一个查找是浪费的,而且如果运气好,可以提前结束查找过程。
4.2 多个Page
单Page的功能:在查询某条数据时,直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。
多Page的管理
- 数据量大:如果有1千万条数据,需要多个Page来保存,多个Page彼此使用双链表链接起来,每个Page内部的数据也是基于链表的。
- 线性查找:查找特定一条记录时,需要进行线性查找,效率较低,要遍历一万多条。
页目录的引入
- 目录的作用:类似于书籍的目录,多花了空间但提高了效率。
- 所以,目录,是一种“空间换时间的做法”
目录
单Page目录
牺牲page一部分保存数据的空间,把腾出来的空间用来保存目录,这所谓的目录里面只有两个字段
- 第一个是它所指向起始位置的key值
- 第二它有一个指针字段指向这条记录的起始位置。
所以未来在查找key的时候,不需要在数据记录里面查找了,而是去目录中找
- 先找到目录中对应key值的所处的起始位置
- 然后在根据指针找到这条记录
- 然后根据这条记录在向下遍历。
- 虽然最后我们依旧需要遍历,但是是一个很小的子序列遍历了。效率也就大大提高了。
那么当前,在一个Page内部,我们引入了目录。比如,我们要查找id=4记录,之前必须线性遍历4次,才能拿到结果。现在直接通过目录2[3],直接进行定位新的起始位置,提高了效率。
现在我们可以再次正式回答上面的问题了,为何通过键值 MySQL 会自动排序?
可以很方便引入目录。
多Page目录
多Page的必要性
- Page大小固定:MySQL中每一页的大小只有16KB,单个Page大小固定。
- 数据量增长:随着数据量的不断增大,16KB不可能存下所有的数据,因此需要多个Page来存储数据。
Page的动态管理
- 自动扩展:在单表数据不断被插入的情况下,MySQL会在容量不足时,自动开辟新的Page来保存新的数据。
- 组织方式:通过指针的方式,将所有的Page组织起来,形成一个链表结构。
我们每次检索数据的时候,该从哪里开始呢?虽然顶层的目录页少了,但是还要遍历啊?
不用担心,可以再加目录页
多级目录:如果保存数据的Page变得非常多,上层的页目录也会变得非常多
- 可以通过给上层页目录再加一层目录,形成多级目录结构。
这就是传说中的B+树呀!没错,至此,我们已经给我们的表user构建完了主键索引。
随便找一个id=?
我们发现,现在查找的Page数一定减少了,也就意味着IO次数减少了,那么效率也就 提高了。
本篇文章小结:
- Page:MySQL的基本数据单位,大小为16KB,用于提高I/O效率。
- 主键与排序:MySQL会默认按照主键对数据进行排序,优化查询效率。
- 多Page管理:通过页目录和B+树结构,提高多Page间的查找效率。
下篇文章继续讲解
- B+树:节点不存储数据,叶子节点相连,适合范围查找。
- 聚簇索引与非聚簇索引:InnoDB使用聚簇索引,MyISAM使用非聚簇索引,各有优劣。