MySQL 第十一讲---进阶篇 索引(上)
前言:
在第九讲与第十讲中,我们深入探讨了 复合查询,掌握了如何通过子查询、联合查询(UNION)以及内连接(INNER JOIN)、外连接(LEFT/RIGHT JOIN)等高效整合多表数据。这些技能帮助我们应对复杂的业务场景,但随之而来的问题也逐渐浮现:当数据量激增时,查询效率如何保障?
试想,若一张表存储了百万甚至千万级的数据,频繁的 WHERE
条件筛选、JOIN
操作或 ORDER BY
排序可能导致数据库陷入全表扫描的泥潭,性能急剧下降。这时,索引(Index) 便成了优化查询性能的关键利器。它如同书籍的目录,能够帮助数据库引擎快速定位目标数据,避免“逐页翻找”的低效操作。
然而,索引并非“万能”。何时该建索引?如何选择索引类型?索引又潜藏哪些代价? 这些问题都需要我们系统性地学习。本节课程将开启索引的探索之旅。
一、索引的概念
1.1 索引的概念
数据库表中存储的数据都是以记录为单位的,如果在查询数据时直接一条条遍历表中的数据记录,那么查询的时间复杂度将会是 O ( N ) ,这对于存储大量数据的数据库来说太慢了。正因如此索引诞生了。
索引的价值在于提高海量数据的检索速度,只要执行了正确的创建索引的操作,查询速度就可能提高成百上千倍。当一张表创建索引后,在数据库底层就会为表中的数据记录构建特定的数据结构,后续在查询表中数据时就能通过查询该数据结构快速定位到目标数据。
索引虽然提高了数据的查询速度,但在一定程度上也会降低数据增删改的效率,因为这时在对表中的数据进行增删改操作时,除了需要进行对应的增删改操作之外,可能还需要对底层建立的数据结构进行调整维护。
常见的索引分为:
- 主键索引(primary key)。
- 唯一索引(unique)。
- 普通索引(index)。
- 全文索引(fulltext)。
我们也不说废话,直接实例演示。
演示:
使用如下SQL创建一个海量数据表:
drop database if exists `index_demon`;
create database if not exists `index_demon` default character set utf8;
use `index_demon`;-- 构建一个8000000条记录的数据
-- 构建的海量表数据需要有差异性,所以使用存储过程来创建-- 产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
begin
declare chars_str varchar(100) default
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
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 values ((start+i)
,rand_string(6),'SALESMAN',0001,curdate(),2000,400,rand_num());
until i = max_num
end repeat;
commit;
end $$
delimiter ;-- 雇员表
CREATE TABLE `EMP` (`empno` int(6) unsigned zerofill NOT NULL COMMENT '雇员编号',`ename` varchar(10) DEFAULT NULL COMMENT '雇员姓名',`job` varchar(9) DEFAULT NULL COMMENT '雇员职位',`mgr` int(4) unsigned zerofill DEFAULT NULL COMMENT '雇员领导编号',`hiredate` datetime DEFAULT NULL COMMENT '雇佣时间',`sal` decimal(7,2) DEFAULT NULL COMMENT '工资月薪',`comm` decimal(7,2) DEFAULT NULL COMMENT '奖金',`deptno` int(2) unsigned zerofill DEFAULT NULL COMMENT '部门编号'
);-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);
这个程序是这样的但是由于博主的机器配置太低防止卡死,这里使用200万的数据量。
上述SQL中创建了一个名为index_demon的数据库,在该数据库中创建了一个名为EMP的员工表,并向表当中插入了八百万条记录。
将上述SQL保存到文件中,然后在MySQL中使用source命令依次执行文件中的SQL即可。如下:
通过desc命令可以看到,目前EMP员工表中没有建立任何索引
这里我们看到每次查询指定工号的员工信息,都需要花费5秒多的时间。
这时再查询EMP表中指定工号的员工信息,可以看到几乎检测不到查询时耗费的时间:
这是因为给员工工号创建索引后再根据员工工号来查询数据,这时就可以直接通过底层建立的数据结构来快速定位到目标数据,从而提高数据的检索速度,这就是索引的价值。
现在我们看完了这个例子,我们有什么启发,我们对索引有什么认识呢?
第一感受是:索引可以提高我们查询数据库的速度。
可这又是为什么呢?
我们想一想如果想要提高数据库查询数据的效率,我们可以从那个方面入手呢?
我们是不是可以从两个方向上去思考,一是优化数据底层的存储结构,二是优化数据查找的算法。
例如我们有一堆以链表的形式存储的数据,我们想要查找某部分的数据,那是不是只能以O(N)的方式遍历一边,但是我们如果使用红黑树组织,那是不是查询的时间就降到了O(logN)了,同样的如果数据有序,我们使用二分查找同样可以降低算法复杂度。
当然我们现在想一想索引是通过什么降低时间复杂度的呢?
关于这个问题,大家可以直接去MySQL 第十一讲---进阶篇 索引(下)看,但是这讲的内容是有利于我们下一讲的理解的。
我们数据库的数据都是存储在磁盘当中,我们访问数据的速度一定会是收到磁盘结构的影响,所以我们下面见识一下磁盘。
二、认识硬件--磁盘
- MySQL给用户提供存储服务,存储的数据在磁盘这个外设当中。
- 磁盘是计算机中的一个机械设备,相比于计算机的其他电子元件,磁盘的效率是比较低的。
- 而如何提高效率是MySQL的一个重要话题,因此我们有必要了解一下磁盘的相关内容。
2.1 磁盘的结构
磁盘的整体结构如下:
部分说明:
- 永磁铁: 机械硬盘的存储方式与磁带比较类似,磁体具有记忆的功能,永磁铁是为了保证磁性的稳定。
- 音圈马达: 硬盘读取数据的关键部位,主要作用是将存储在磁盘上的信息转换为电信号向外传输。
- 主轴: 保证电机稳定的转动,磁盘转动才能读出数据。
- 空气滤波片: 过滤空气硬盘透气孔中进入的空气,保证硬盘内部清洁,同时还可以防止硬盘内部的零件氧化,确保硬盘安全使用。
- 磁盘: 硬盘一般都是铝合金制作的,主要是用来存储文件的。
- 磁头: 用来读取盘片上的信息。
- 串行接口: 用来连接电脑与硬盘的接口,起到传输的作用。
一个磁盘由多个盘片叠加而成,盘片的表面涂有磁性物质,这些磁性物质就是用来记录二进制数据的,因为盘片的正反两面都可涂上磁性物质,因此一个盘片有两个盘面。
2.2 磁盘中的一个盘片
部分说明:
- 磁道: 磁盘表面被分为许多同心圆,每个同心圆称为一个磁道,每个磁道都有一个编号,最外面的是0磁道。
- 扇区: 每个磁道被划分成若干个扇区,每个扇区的存储容量为512字节,每个扇区都有一个编号。
说明一下:
由于每个扇区的存储容量相同,因此最内侧磁道上的扇区数据密度最大,而最外侧磁道上的扇区数据密度最小(因为外侧的区域大,但是存储数据一样多)。
近三十年来,扇区大小一直是512字节,但最近几年正在迁移到更大、更高效的4096字节扇区,通常称为4K扇区。
数据库文件就是保存在磁盘中的一个个扇区中的,因此找到一个文件本质就是,在磁盘上找到保存该文件的所有扇区。
2.3 定位扇区
- 一个磁盘由多个盘片叠加而成,每个盘片有两个盘面,所有盘面中半径相同的同心磁道构成一个柱面。
- 每个盘面都有一个对应的磁头,每个磁头都有一个编号,所有的磁头都是连在同一个磁臂上的。
定位扇区时采用CHS寻址方式:
- 磁头(Heads): 每个盘面都有一个对应的磁头,因此确定了磁头也就确定了数据在哪一个盘面。
- 柱面(Cylinder): 所有盘面中半径相同的同心磁道构成柱面,因此在确定了数据在哪一个盘面的基础上,再确定柱面也就确定了数据在该盘面上的哪一个磁道。
- 扇区(Sector): 每个磁道被划分成若干个扇区,因此在确定了数据在哪一个磁道的基础上,再确定扇区也就确定了数据在该磁道上的哪个扇区。
- 简单来说,CHS寻址方式就是先通过H确定数据所在的盘面,再通过C确定数据所在的磁道,最后通过S定位到目标扇区。
说明一下:
CHS寻址方式是磁盘定位扇区的方式,但实际CHS寻址方式对磁盘以外的设备来说没什么作用,因此系统软件在定位磁盘上的数据时采用的是LBA(Logical Block Address,逻辑区块地址)。
LBA是描述计算机存储设备上数据所在区块的通用机制,LBA和CHS之间可以通过计算公式进行相互转换,LBA存在的意义就是对底层逻辑器件进行虚拟化,让系统软件可以不用关心底层硬件具体的寻址方式,而实际底层硬件采用的还是CHS寻址方式。
2.4磁盘的随机访问与连续访问
- 随机访问: 本次IO所给出的扇区地址与上次IO给出的扇区地址不连续,磁头在两次IO操作之间需要做比较大的移动动作才能找到目标扇区进行IO。
- 连续访问: 本次IO所给出的扇区地址与上次IO给出的扇区地址是连续的,磁头很快就能找到目标扇区进行IO。
需要注意的是,尽管两次IO是在同一时刻发出的,但如果它们请求的扇区地址相差很大,那也只能称为随机访问,因为连续访问中的连续指的是访问的扇区地址的连续,而不是访问时间的连续,由于连续访问不需要过多的定位,因此效率比较高。
三、认识软件
在上面我们简单的了解一下关于磁盘的概念后,我们再来看MySQL这个软件会与磁盘发生什么。
MySQL与磁盘交互的基本单位
MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的 IO 场景,所以为了提高基本的 IO 效率, MySQL 进行 IO 的基本单位是 16KB(后面统一使用 InnoDB 存储引擎讲解)。
mysql> show global status like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_page_size | 16384 | -- 16*1024=16384
+------------------+-------+
1 row in set (0.00 sec)
也就是说,磁盘这个硬件设备的基本单位是 512 字节,而 MySQL InnoDB 引擎使用 16KB 进行 IO 交互。即 MySQL 和磁盘进行数据交互的基本单位是 16KB 。这个基本数据单元,在 MySQL 这里叫做 page(注意和系统的 page 区分)。
但是我们知道mysql说到底也就是个应用程序,不可能越过操作系统直接与硬件交互,中间是一定要卡层操作系统的。
操作系统与磁盘交互的基本单位
而操作系统与磁盘进行IO交互的基本单位是4KB,而不是扇区的大小512字节,原因如下:
物理内存实际是被划分成一个个4KB大小的页框的,磁盘上的数据也会被划分成一个个4KB大小的页帧,因此操作系统与磁盘以4KB为单位进行IO交互,就能提高数据加载和保存的效率。
操作系统与磁盘进行IO交互时,如果直接以扇区的大小作为IO的基本单位,那么这时系统的IO代码和硬件就是强相关的,将来当硬件的扇区大小发生变化时就需要对应修改操作系统的IO代码。
此外,以扇区的大小作为IO的基本单位太小了,这就意味着读取同样的数据内容,需要进行更多次的磁盘访问,而磁盘的效率是比较低的,这样IO效率就降低了。
因此操作系统与磁盘以4KB作为IO交互的基本单位,一方面是为了提高IO效率,另一方面是为了实现硬件和系统的解耦。
Buffer Pool
在MySQL中进行的各种CRUD操作时,都需要先通过计算找到对应的操作位置,只要涉及计算就需要CPU参与,而冯诺依曼体系结构决定了CPU只能和内存打交道,因此为了便于CPU参与,就需要先将数据加载到内存当中。
- 所以在特定的时间内,MySQL中的数据一定是同时存在于磁盘和内存中的,当操作完内存数据后,再以特定的刷新策略将内存中的数据刷新到磁盘当中,这时MySQL和磁盘进行数据交互的基本单位就是Page。
- 为了更好的支持上述操作,MySQL服务器在启动的时候会预先申请一块内存空间来进行各种缓存,这块内存空间叫做Buffer Pool,后续磁盘中加载的数据就会保存在Buffer Pool中,刷新数据时也就是将Buffer Pool中的数据刷新到磁盘。
- 由于内核中是有内核缓冲区的,因此MySQL从磁盘读取数据时,需要先将数据从磁盘读取到内核缓冲区,再将数据从内核缓冲区读取到Buffer Pool,MySQL将数据刷新到磁盘时,同样需要先将数据从Buffer Pool刷新到内核缓冲区,再将数据从内核缓冲区刷新到磁盘。
因此所谓的操作系统和磁盘交互的基本单位是4KB,就是指内核缓冲区与磁盘之间是以4KB为单位进行交互的。而MySQL的Buffer Pool和磁盘实际并不是直接交互的,因此所谓的MySQL与磁盘交互的基本单位是16KB,指的是MySQL的Buffer Pool与内核缓冲区之间是以16KB为单位进行交互的。只不过在说的时候更关注的是MySQL和磁盘之间的关系,所以直接说的是MySQL与磁盘交互的基本单位是16KB,相当于忽略了中间的内核缓冲区。
示意图如下:
四、建立共识
现在经过上面三个部分的讲解后,我们终于得到一些结论。
- MySQL 中的数据文件,是以 page 为单位保存在磁盘当中的。
- MySQL 的 CURD 操作都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。
- 而只要涉及计算,就需要 CPU 参与,而为了便于 CPU 参与,一定要能够先将数据加载到内存当中。
- 所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时就涉及到磁盘和内存的数据交互,也就是 IO 了。而此时 IO 的基本单位就是 Page。
- 为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为 Buffer Pool 的的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行 IO 交互。
- 为了更高的效率,一定要尽可能的减少系统和磁盘 IO 的次数。
但是还没完,我们只是尝试理解MySQL与内核与磁盘是如何IO的,但是关键的索引到底是什么还是没有解决。我们接着往下看吧。
五、一个现象与结论
5.1 观察主键索引现象
创建一个用户表,表当中包含用户的id、年龄和姓名,并将用户的id设置为主键。如下:
创建完表后向表中插入一些数据,并插入数据时没有按照主键大小顺序进行插入,如下:
但最终我们查看表中数据时,却发现显示出来的数据是按照主键进行有序排列的。
这又是为什么呢?这样的疑问我们要到下一节去讲解了。
总结:
至此,我们已经完成了《MySQL 第十一讲---进阶篇 索引(上)》的学习旅程。我们一起明确了索引的基本概念及其重要性,理解了磁盘硬件(特别是机械硬盘) 的物理特性(如寻道时间、旋转延迟)如何成为数据库性能的关键瓶颈,并认识到索引作为一种精巧的软件结构,正是为了解决这个核心问题而诞生。通过建立共识,我们知道了索引的核心目标是减少磁盘 I/O。最后,通过一个典型的现象对比(有索引 vs 无索引的查询速度差异) 及其结论,我们直观地感受到了索引对查询性能带来的革命性提升。本讲为你揭开了索引世界的第一层面纱,理解了其存在的根本原因和底层逻辑。但这仅仅是开始!关于MySQL的索引结构,我们会在下一讲当中进行讲解
我们下期见!