当前位置: 首页 > news >正文

MySQL索引特性(重点)

目录

一、数据库索引

1、索引的重要性

没有索引可能导致的问题

2、索引的价值与代价(重点!!!)

优势

代价

总的来说:

3、索引分类

4、实战演示:索引效果对比

测试环境搭建

创建海量测试数据(8,000,000条记录)

性能测试对比

无索引情况下的查询

创建索引后的性能提升

5、总结

二、磁盘基础与MySQL存储原理

1、MySQL存储的本质

2、磁盘物理结构解析

盘片与扇区

扇区特性

3、磁盘文件系统视角

Linux中的MySQL数据存储

关键认识

4、磁盘寻址机制

物理定位:CHS模式

逻辑定位:LBA模式

5、系统IO交互单元

为什么不是扇区?

数据块:系统IO(操作系统和磁盘)的基本单位

6、磁盘访问模式

随机访问 vs 连续访问

随机访问 (Random Access)

连续访问 (Sequential Access)

重要区别

7、MySQL性能优化启示

基于磁盘特性的优化策略

实际应用考虑

三、MySQL与磁盘交互机制

1、MySQL的IO基本单位

MySQL:特殊的文件系统

IO单位对比

2、数据存储与处理共识

数据存储机制

CURD操作执行流程

内存磁盘交互模式

3、Buffer Pool:核心内存缓存

内存架构设计

工作流程示例(简单了解即可)

4、减少磁盘IO次数的核心价值

性能瓶颈分析

为什么要尽量减少IO次数?

1. 速度差异悬殊

2. 吞吐量优化

3. 系统资源高效利用

实际优化策略

页面大小选择权衡

缓存命中率优化

5、关于Buffer Pool的详细过程

6、总结

四、MySQL索引的原理

1、索引基础概念与测试环境搭建

测试表结构设计

测试数据插入

2、Page机制:为什么采用Page进行IO交互?

单条记录查询场景

页式存储的优势

性能优化原理

总结

如何理解MySQL中的Page概念?

3、单Page内部结构(Page的基本组织)

单个Page

单个Page内创建页内目录

4、多Page内部结构(多Page的基本组织、多Page管理与B+树演进)

1. 多个Page

2. Page之上创建页目录

3. 页目录之上再创建页目录

4. B+树最终结构

5. B+树中的Page结点是否需要全量加入到Buffer Pool中?

5、为什么选择B+树?

与其他数据结构的对比

B+树 vs B树核心区别

6、聚簇索引 vs 非聚簇索引

MyISAM存储引擎(主键索引结构、非聚簇索引)

MyISAM存储引擎(普通索引结构、非聚簇索引)

1. 为什么 InnoDB 必须要有主键?

2. InnoDB 如何选择聚簇索引?

3. 这对“普通索引”(二级索引)有什么影响?

聚簇索引 VS 非聚簇索引

1. 首先找到MySQL数据目录

2. 创建测试数据库和表

3. 查看文件结构

4. 预期结果(了解就行)

MySQL旧版本存储引擎文件说明:

MySQL新版本(8.0+)存储引擎文件说明:

核心变化总结:

7、总结与核心要点

核心概念回顾

设计哲学

实践指导

五、索引与键的重点关系

索引(Index)

键(Key)

1、主键 = 唯一索引 + NOT NULL约束

2、唯一键 = 唯一索引

3、普通索引 ≠ 键

简单记忆

六、数据库索引操作详解

1、主键索引

创建方式

主键索引特点

2、唯一索引

创建方式

唯一索引特点

3、普通索引

创建方式

普通索引特点

4、全文索引

创建全文索引

全文索引使用

性能对比

补充说明

5、查询索引

6、删除索引

7、索引创建原则

应该创建索引的情况

不应该创建索引的情况

其他考虑因素

8、扩展概念(了解即可)

9、最佳实践建议


一、数据库索引

1、索引的重要性

没有索引可能导致的问题

  • 查询性能低下:在海量数据中查询特定记录时,需要全表扫描,耗时极长

  • 系统资源浪费:大量的磁盘I/O操作和CPU计算资源消耗

  • 并发能力差:多个用户同时查询时容易导致系统瓶颈甚至死机

  • 用户体验恶劣:简单的查询操作也需要等待数秒甚至更长时间

2、索引的价值与代价(重点!!!)

优势

索引是提升数据库性能的高效解决方案,具有以下特点:

  • 成本低廉:无需增加硬件内存

  • 无需修改程序:对应用程序透明

  • SQL无需调整:保持原有查询语句

  • 效果显著:正确的索引可能让查询速度提升成百上千倍

代价

天下没有免费的午餐,索引的查询性能提升是以以下代价换取的:

  • 写入性能下降:INSERT操作变慢

  • 更新开销增加:UPDATE操作需要维护索引

  • 删除效率降低:DELETE操作涉及索引清理

  • 磁盘空间占用:索引需要额外的存储空间

  • IO负担加重:写操作产生大量额外的IO开销

核心价值:针对海量数据场景,极大提升检索速度。

总的来说:

  • 数据库表中存储的数据都是以记录为单位的,如果在查询数据时直接一条条遍历表中的数据记录,那么查询的时间复杂度将会是O(N)。

  • 索引的价值在于提高海量数据的检索速度,只要执行了正确的创建索引的操作,查询速度就可能提高成百上千倍。当一张表创建索引后,在数据库底层就会为表中的数据记录构建特定的数据结构,后续在查询表中数据时就能通过查询该数据结构快速定位到目标数据。

  • 索引虽然提高了数据的查询速度,但在一定程度上也会降低数据增删改的效率,因为这时在对表中的数据进行增删改操作时,除了需要进行对应的增删改操作之外,可能还需要对底层建立的数据结构进行调整维护。

3、索引分类

索引类型说明特点
主键索引 (primary key)唯一标识每条记录不允许NULL值,一张表只能有一个
唯一索引 (unique)确保列值的唯一性允许NULL值,一张表可以有多个
普通索引 (index)基本的索引类型无唯一性限制,最常用
全文索引 (fulltext)针对文本内容搜索解决中文文本检索问题

4、实战演示:索引效果对比

测试环境搭建

在我的电脑中,同上次的scott_data.sql这个SQL文件一样,从本地电脑拉取到当前云服务器上:

我们使用vim打开可以看到如下内容:

在MySQL中执行source命令,执行SQL文件中的SQL语句,也就是创建海量测试数据(8,000,000条记录):

source /root/index_data.sql;

        当前显示还在创建海量测试数据(8,000,000条记录)过程中,这个过程有点长,耐心等待(下面图片中的这些warning是MySQL的提示信息,通常不会影响主要功能的执行):

        然后我们使用top命令查看当前MySQL服务端的执行过程信息,如下:可以看到MySQL服务端对应的CPU利用率超高!!!这一刻我真的担心我的云服务器撑不住!!!

等待时直到出现新的交互mysql提示符,就说明完成了:

若没有的话,直接使用下面的SQL语句整合出一个一样的SQL文件,这样也是同样的效果,完整SQL文件内容如下:

创建海量测试数据(8,000,000条记录)
drop database if exists `bit_index`;
create database if not exists `bit_index` default character set utf8;
use `bit_index`;-- 构建一个8000000条记录的数据
-- 构建的海量表数据需要有差异性,所以使用存储过程来创建, 拷贝下面代码就可以了,暂时不用理解-- 产生随机字符串(随机字符串生成函数)
delimiter $$
create function rand_string(n INT)
returns varchar(255)
DETERMINISTIC
NO SQL
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)
DETERMINISTIC
NO SQL
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);

        上述SQL中创建了一个名为bit_index的数据库,在该数据库中创建了一个名为EMP的员工表,并向表当中插入了八百万条记录。SQL执行完毕后查看数据库就能看到一个名为bit_index的数据库。如下:

进入该数据库,在数据库中可以看到一个名为EMP的员工表。如下:

由于EMP表中有八百万条记录,因此在查看EMP表中的数据时可以带上limit子句。如下:

select * from EMP limit 5;

通过desc命令可以看到,目前EMP员工表中没有建立任何索引。如下:

性能测试对比

无索引情况下的查询

        查询EMP表中指定工号的员工信息,可以看到每次查询过程都需要花费5秒将近6秒左右的时间。如下:例如5.88 sec 表示 这条SQL查询执行了5.88秒。也就是说,这5.88秒包括:

  • SQL语句解析和优化时间

  • 数据检索时间(从磁盘读取数据)

  • 数据处理时间(排序、过滤、计算等)

  • 结果返回时间(将数据发送给客户端)

性能大概描述:

  • < 0.1秒:很快

  • 0.1-1秒:可以接受

  • 1-5秒:较慢,可能需要优化

  • > 5秒:很慢,建议检查索引和查询语句

        所以上面的速度是属于很慢的,查询数量小的时候还会,当查询的数据多的话,所用时间是无法想象的,效率非常之慢!!!

⚠️ 风险分析

  • 单用户查询已接近6秒

  • 公网环境中如有1000并发用户,很可能导致系统瘫痪

        这时当我们给员工表中的工号建立索引后,数据库底层就会为员工表中的数据记录构建特定的数据结构,需要注意的是,由于当前员工表中的数据量较大,因此建立索引时也需要花费较长时间。如下:

创建索引后的性能提升
-- 为员工编号字段创建索引
ALTER TABLE EMP ADD INDEX(empno);

耗时约34.23秒,这个建立索引的操作也是不容小觑的!!!

        然后我们查看EMP表中的详细信息:我们发现在empno的Key中多了MUL,它在 MySQL 中代表 普通索引 或 非唯一索引MUL = Multiple(允许多个重复值)、表示该字段有索引,但允许重复值、是最常见的索引类型,用于提高查询性能、与 UNI(唯一索引)相对,唯一索引不允许重复值。

索引后的查询测试

这时再查询EMP表中指定工号的员工信息,可以看到几乎检测不到查询时耗费的时间。如下:

SELECT * FROM EMP WHERE empno = 1234567;

执行结果:查询时间从约6秒大幅提升至毫秒级别

根本原因:给员工工号创建索引后再根据员工工号来查询数据,这时就能够直接通过底层建立的数据结构来快速定位到目标数据,从而提高数据的检索速度,这就是索引的价值。

5、总结

索引是数据库性能优化的关键手段,特别是在处理海量数据时:

  • 显著提升查询性能:从秒级到毫秒级的飞跃

  • 支撑高并发场景:避免多用户同时访问时的系统崩溃

  • 权衡读写性能:根据业务需求合理设计索引策略

  • 定期维护优化:监控索引使用情况,及时调整索引策略

最佳实践建议:在查询频繁但写入相对较少的列上创建索引,以达到最佳的性能提升效果。


二、磁盘基础与MySQL存储原理

1、MySQL存储的本质

        MySQL为用户提供数据存储服务,所有数据最终都保存在磁盘这个外部存储设备中。磁盘作为计算机中少有的机械设备,其效率远低于计算机中的其他电子元件。结合IO操作的特点,如何提升磁盘访问效率成为MySQL性能优化的核心课题。

2、磁盘物理结构解析

  • 一个磁盘由多个盘片叠加而成,每个盘片有两个盘面,所有盘面中半径相同的同心磁道构成一个柱面。

  • 每个盘面都有一个对应的磁头,每个磁头都有一个编号,所有的磁头都是连在同一个磁臂上的。

磁盘的整体结构如下:

部分说明:

  • 永磁铁: 机械硬盘的存储方式与磁带比较类似,磁体具有记忆的功能,永磁铁是为了保证磁性的稳定。

  • 音圈马达: 硬盘读取数据的关键部位,主要作用是将存储在磁盘上的信息转换为电信号向外传输。

  • 主轴: 保证电机稳定的转动,磁盘转动才能读出数据。

  • 空气滤波片: 过滤空气硬盘透气孔中进入的空气,保证硬盘内部清洁,同时还可以防止硬盘内部的零件氧化,确保硬盘安全使用。

  • 磁盘: 硬盘一般都是铝合金制作的,主要是用来存储文件的。

  • 磁头: 用来读取盘片上的信息。

  • 串行接口: 用来连接电脑与硬盘的接口,起到传输的作用。

盘片与扇区

  • 数据库文件本质上保存在磁盘的盘片

  • 数据存储在盘片上的扇区(小格子)中

  • 单个数据库文件通常需要占据多个扇区

扇区特性

扇区大小说明:(扇区大小由比特位密度决定)

  • 传统扇区大小:512字节(行业标准)

  • 新式扇区大小:4096字节(4K高级格式)

        一个磁盘由多个盘片叠加而成,盘片的表面涂有磁性物质,这些磁性物质就是用来记录二进制数据的,因为盘片的正反两面都可涂上磁性物质,因此一个盘片有两个盘面。

磁盘中的一个盘片如下:

物理特征

  • 半径方向:距离圆心越近,扇区越小;距离圆心越远,扇区越大

  • 现代磁盘技术已支持不同大小的扇区

补充说明:

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

  • 扇区: 每个磁道被划分成若干个扇区,每个扇区的存储容量为512字节,每个扇区都有一个编号。

  • 由于每个扇区的存储容量相同,因此最内侧磁道上的扇区数据密度最大,而最外侧磁道上的扇区数据密度最小。

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

  • 数据库文件就是保存在磁盘中的一个个扇区中的,因此找到一个文件本质就是,在磁盘上找到保存该文件的所有扇区。

3、磁盘文件系统视角

Linux中的MySQL数据存储

ls /var/lib/mysql

关键认识

  • 大多数目录和文件实际保存在硬盘中

  • 内存文件系统(如proc、sys)除外,这些不考虑

  • 定位文件(找到一个文件的全部)本质就是定位存储该文件的所有扇区

  • 而我们能够定位任何一个扇区,那么便能找到所有扇区,因为查找方式是一样的

4、磁盘寻址机制

物理定位:CHS模式

磁盘物理结构:

  • 磁头(Heads) : 每个盘面对应一个磁头。因此确定了磁头也就确定了数据在哪一个盘面。

  • 柱面(Cylinder)(磁道) : 多盘磁盘,每盘都是双面,大小完全相等。那么同半径的磁道,整体上便构成了一个柱 面

  • 扇区(Sector) : 数据存储的基本单元。每个磁道被划分成若干个扇区,因此在确定了数据在哪一个磁道的基础上,再确定扇区也就确定了数据在该磁道上的哪个扇区。

简单来说,CHS寻址方式就是先通过H确定数据所在的盘面,再通过C确定数据所在的磁道,最后通过S定位到目标扇区。

CHS定位原理

  • 通过磁头号、柱面号、扇区号唯一确定磁盘位置

  • 硬件层面实际使用的寻址方式

逻辑定位:LBA模式

  • 系统软件使用LBA(逻辑块寻址)

  • LBA是线性地址,类似于虚拟地址

  • 最终由系统转换为CHS交给磁盘执行

  • 提供硬件抽象层,使系统与硬件解耦

补充说明:

  • CHS寻址方式是磁盘定位扇区的方式,但实际CHS寻址方式对磁盘以外的设备来说没什么作用,因此系统软件在定位磁盘上的数据时采用的是LBA(Logical Block Address,逻辑区块地址)。

  • LBA是描述计算机存储设备上数据所在区块的通用机制,LBA和CHS之间可以通过计算公式进行相互转换,LBA存在的意义就是对底层逻辑器件进行虚拟化,让系统软件可以不用关心底层硬件具体的寻址方式,而实际底层硬件采用的还是CHS寻址方式。

5、系统IO交互单元

为什么不是扇区?

  • 强耦合问题:直接使用512字节交互会导致系统与硬件强绑定

  • 效率问题:单次IO太小,读取相同数据需要多次磁盘访问

  • 性能瓶颈:频繁的小IO操作显著降低系统效率

数据块:系统IO(操作系统和磁盘)的基本单位

  • 文件系统读取的基本单位不是扇区,而是数据块

  • 现代系统通常以4KB为基本IO单位(系统读取磁盘,是以块为单位的,基本单位是 4KB)

  • 也就是说,物理内存实际是被划分成一个个4KB大小的页框的,磁盘上的数据也会被划分成一个个4KB大小的页帧,因此操作系统与磁盘以4KB为单位进行IO交互,就能提高数据加载和保存的效率。

  • 块设备驱动负责扇区与数据块之间的转换

因此操作系统与磁盘以4KB作为IO交互的基本单位,一方面是为了提高IO效率,另一方面是为了实现硬件和系统的解耦。

6、磁盘访问模式

随机访问 vs 连续访问

随机访问 (Random Access)
  • 特征:本次IO扇区地址与上次不连续

  • 影响:磁头需要大幅移动才能开始读写

  • 性能:效率较低,寻址时间占比较大

连续访问 (Sequential Access)
  • 特征:本次IO扇区地址与上次连续

  • 影响:磁头可立即开始下次IO操作

  • 性能:效率较高,适合大数据量传输

重要区别

  • 连续性的判断依据:扇区地址的连续性,而非IO请求的时间连续性

  • 机械运动的限制:磁盘通过机械运动寻址,随机访问需要更多定位时间

        需要注意的是,尽管两次IO是在同一时刻发出的,但如果它们请求的扇区地址相差很大,那也只能称为随机访问,因为连续访问中的连续指的是访问的扇区地址的连续,而不是访问时间的连续,由于连续访问不需要过多的定位,因此效率比较高。

7、MySQL性能优化启示

基于磁盘特性的优化策略

  • 顺序写优先:尽量让数据连续存储,减少随机IO

  • 批量操作:利用4KB块大小,减少IO次数

  • 索引优化:合理的索引可以减少随机查找

  • 预读机制:利用局部性原理,提前加载可能需要的数据

实际应用考虑

  • 理解磁盘工作原理有助于设计更优的数据库结构

  • 选择适合的存储引擎(InnoDB的聚集索引减少随机IO)

  • 合理设置InnoDB页大小(通常16KB,匹配磁盘IO特性)

通过深入理解磁盘工作机制,可以更好地优化MySQL的存储性能和IO效率,为数据库的高效运行奠定基础。


三、MySQL与磁盘交互机制

1、MySQL的IO基本单位

MySQL:特殊的文件系统

        MySQL作为一款高性能数据库应用软件,可以视为一种特殊的文件系统。由于其面临更高的IO场景需求,MySQL采用了优化的IO策略,因此为了提高基本的IO效率,MySQL与磁盘交互的基本单位是16KB,这个基本数据单元在MySQL这里也叫做Page。

通过show命令查看系统中的全局变量,可以看到InnoDB存储引擎交互的基本单位是16KB。如下:

-- 查看InnoDB页面大小
SHOW GLOBAL STATUS LIKE 'innodb_page_size'; -- 16 * 1024 = 16384字节

说明一下: 后面都是以InnoDB存储引擎进行讲解(若没有做特殊说明的话)。

IO单位对比

层级基本IO单位大小说明
磁盘硬件(和磁盘自己交互)扇区(Sector)512字节物理存储最小单元
操作系统(和磁盘交互)块(Block)4KB文件系统IO单元
MySQL InnoDB(和磁盘交互)页(Page)16KB数据库IO单元

重要概念

  • MySQL与磁盘数据交互的基本单位是16KB

  • 在MySQL中这个基本数据单元称为Page(注意与操作系统页的区别)

  • 16KB大小是平衡IO效率和内存使用的优化选择

2、数据存储与处理共识

数据存储机制

CURD操作执行流程

  1. 计算定位:通过计算找到数据插入、修改或查询的位置

  2. CPU参与:所有计算操作都需要CPU介入处理

  3. 内存加载:数据必须先加载到内存才能被CPU处理

  4. 磁盘同步:操作完成后按特定策略刷新到磁盘

CURD这时就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。

内存磁盘交互模式

内存中装载着MySQL程序(用来增删查改),与磁盘以Page(16KB)方式交互:

3、Buffer Pool:核心内存缓存

        为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为 Buffer Pool 的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。

内存架构设计

Buffer Pool是MySQL性能的关键组件,在内存中申请的大空间,用于磁盘数据IO交互。

Buffer Pool核心作用

  • 大内存空间:服务器启动时申请的大块连续内存

  • 数据缓存:用于缓存频繁访问的数据页和索引页

  • IO缓冲:作为磁盘与内存之间的高速缓冲区

  • 写缓冲:延迟写入,合并多次修改

工作流程示例(简单了解即可)

# 伪代码:MySQL数据访问流程
def query_data(query):# 1. 检查数据是否在Buffer Pool中if data_in_buffer_pool(query):return get_from_buffer_pool(query)# 2. 如果不在,从磁盘加载16KB页到Buffer Poolpage = load_from_disk(query, page_size=16*1024)# 3. 在内存中进行数据处理result = process_in_memory(page, query)# 4. 按策略异步刷新到磁盘async_flush_to_disk_if_modified()return result

4、减少磁盘IO次数的核心价值

性能瓶颈分析

磁盘访问时间构成:

  • 寻道时间 : 磁头移动到目标磁道(机械运动,最耗时)

  • 旋转延迟 : 等待目标扇区转到磁头下(机械运动,较耗时)

  • 数据传输时间 : 实际读写数据时间(电子传输,最快)

为什么要尽量减少IO次数?

1. 速度差异悬殊

各级存储速度对比(近似值)

  • CPU L1缓存 : 1纳秒

  • CPU L2缓存 : 4纳秒

  • 主内存访问 : 100纳秒

  • SSD随机读 : 16,000纳秒 (16微秒)

  • 机械磁盘寻道 : 2,000,000纳秒 (2毫秒)

关键洞察:一次磁盘IO相当于数百万次CPU指令!!!

2. 吞吐量优化
  • 单次16KB IO vs 32次512字节IO

  • 减少磁头移动和旋转延迟的开销

  • 更好的预读效果,利用空间局部性原理

3. 系统资源高效利用

减少IO次数的多重好处:降低CPU等待时间,提高利用率、减少系统调用开销、降低磁盘控制器负载、提高整体系统并发能力

实际优化策略

页面大小选择权衡

为什么选择16KB而不是更大或更小?

  • 太小:增加IO次数,降低效率

  • 太大:内存浪费,加载不需要的数据

  • 16KB:经验证的最佳平衡点

缓存命中率优化
  • 预热机制:启动时加载常用数据到Buffer Pool

  • LRU算法:保留最有可能被访问的页面

  • 预读机制:提前加载可能需要的相邻页面

5、关于Buffer Pool的详细过程

        MySQL执行CRUD操作时,都需要先计算确定操作位置。由于冯诺依曼体系结构决定了CPU只能与内存交互,因此必须先将数据加载到内存中才能进行运算。因此,MySQL中的数据总是同时存在于磁盘和内存中。操作完成后,系统会按照特定策略将内存数据刷新回磁盘。在这个过程中,MySQL与磁盘交互的基本单位是Page。

        为支持这些操作,MySQL启动时会预先申请一块名为Buffer Pool的内存空间用于缓存。磁盘加载的数据会存储在Buffer Pool中,刷新操作也就是将Buffer Pool数据写回磁盘。实际上,数据读取过程需要经过内核缓冲区:首先从磁盘读取到内核缓冲区,再加载到Buffer Pool;写回操作同样需要先将Buffer Pool数据写入内核缓冲区,再同步到磁盘。

        操作系统与磁盘交互的基本单位是4KB,指的是内核缓冲区与磁盘之间的交互单位。而MySQL与磁盘交互的基本单位是16KB,实际是指Buffer Pool与内核缓冲区之间的交互单位。为简化表述,我们可以直接看做:MySQL与磁盘交互的基本单位是16KB,省略了中间的内核缓冲区环节。

6、总结

MySQL通过精心设计的IO机制在磁盘硬件限制下实现高性能:

  • 16KB页大小:平衡IO效率和内存使用

  • Buffer Pool:最大化内存缓存效果

  • 减少IO次数:核心优化方向,利用内存速度优势

  • 异步刷新:将随机写转换为顺序写,提升吞吐量

理解这些底层机制对于数据库性能调优、架构设计和故障排查都具有重要意义,是每个DBA和开发者的必备知识。


四、MySQL索引的原理

1、索引基础概念与测试环境搭建

测试表结构设计

创建一个用户表,表当中包含用户的id、年龄和姓名,并将用户的id设置成主键。如下:

-- 创建用户测试表
CREATE TABLE IF NOT EXISTS user (id INT PRIMARY KEY,      -- 主键,自动创建主键索引age INT NOT NULL,name VARCHAR(16) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表结构验证

SHOW CREATE TABLE user \G

测试数据插入

创建表完毕后向表中插入一些数据,并且插入数据时没有按照主键的大小顺序插入。如下:

-- 注意:未按主键顺序插入
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, '欧阳锋');

查询结果分析

但最终当我们查看表中的数据时,却发现显示出来的数据是按照主键进行有序排列的。如下:

SELECT * FROM user;

关键发现:因为我们创建表时设置了主键,即便向表中插入数据时是乱序插入的,MySQL底层也会自动按照主键对插入的数据进行排序,这是索引在起作用!!!

2、Page机制:为什么采用Page进行IO交互?

问题引入:MySQL与磁盘进行交互时为什么不是按需交互,而是以Page为基本单位进行交互的?

MySQL 查询记录时的 IO 效率分析:

单条记录查询场景

  • 当查询 id=2 的记录时,传统方式需要先加载 id=1(第一次 IO),再加载 id=2(第二次 IO)

  • 查询 id=5 时则需要 5 次 IO 操作

页式存储的优势

  • 多条记录(如这 5 条)存储在一个 16KB 的 Page 中

  • 首次查询 id=2 时,整个 Page 会被加载到 Buffer Pool(仅 1 次 IO)

  • 后续查询同 Page 内的其他记录(id=1,3,4,5)可直接在内存中完成,无需额外 IO

性能优化原理

  • 关键在于减少 IO 次数而非单次数据量大小

  • 基于局部性原理,用户下次访问的数据有很大概率仍在同一 Page 中

  • 虽然不能绝对保证,但这种优化能显著降低 IO 操作次数

总结

  • 当我们查询表中的某一条记录时,如果MySQL只从磁盘中将这一条记录加载到内存当中,那么当我们继续查询表中的其他记录时,MySQL就一定需要再次与磁盘进行IO交互。

  • 而如果当我们查询表中的某一条记录时,MySQL直接将这条记录所在的整个Page都加载到内存当中,那么当我们继续查询表中的其他记录时,MySQL很可能就不再需要与磁盘进行IO交互了,因为这条记录很可能也在被加载进来的Page当中,这时直接在内存中进行查询即可,大大减少了IO的次数。

  • 当然,我们不能保证用户下一次要访问的数据一定就在本次加载进来的Page当中,但是根据统计学原理,当一个数据正在被访问时,那么下一次有很大可能会访问其周围的数据(局部性原理),因此我们有较大概率保证用户下一次要访问的数据和本次访问的数据在同一个Page当中,如果局部性原理没有起作用,那就再把对应的Page加载到内存当中即可。

        这种设计有效解决了数据库性能中最主要的矛盾 - 频繁的 IO 操作问题。也就是说,MySQL与磁盘进行交互时以Page为基本单位,可以减少与磁盘IO交互的次数,进而提高IO的效率。

如何理解MySQL中的Page概念?

        在MySQL内部,数据以Page为基本单位进行管理和存储。由于系统需要同时处理大量Page,因此必须建立有效的Page管理机制。理解Page需要注意以下几点:

  • Page不仅仅是简单的内存块

  • 每个Page内部都包含必要的管理信息

  • 所有Page通过链表结构进行组织管理

例如基础Page结构可能包含:

struct page {struct page *next;struct page *prev;char buffer[PAGE_SIZE];
}

        在Buffer Pool中,MySQL会对所有Page进行统一建模和管理。标准Page大小为16KB,新创建的Page会被加入到全局管理链表中。这种设计确保了系统能高效地组织和访问海量数据页。

3、单Page内部结构(Page的基本组织)

单个Page

  • MySQL中要管理很多数据文件,在运行期间一定有大量的Page需要被换入换出,因此MySQL一定需要将内存中大量的Page管理起来。

  • MySQL 通过 Page 来组织和管理(先描述,在组织)数据表文件,每个表文件由一个或多个 16KB 大小的 Page 组成。

  • MySQL将内存中的每一个Page都用一个结构体描述起来,然后再将各个结构体以双链表的形式组织起来,因此一个Page结构体内部既包含数据字段,也包含属性字段。

  • 这些 Page 通过 prev 和 next 指针构成双向链表结构。此外,为了方便后续数据的插入和删除,每个Page结构体内部存储的数据记录会以单链表的形式组织起来,并且各个记录之间会按照主键进行排序。

假设上述测试表中的记录都在同一个Page当中,那么该Page的结构大致如下:

补充说明:

  • 每个Page结构体内部的数据会按照主键进行排序,目的是为了优化数据查询的效率,因为单链表在查找的时候是顺序查找的,有序就意味着在查找的过程中有机会提前结束查询过程。

  • 这也就是前面所说的,只要设置了主键,即便向表中插入的数据是乱序的,MySQL底层也会自动按照主键对插入的数据进行排序,因此查询得到的数据是按照主键进行有序排序的。可以说是由于主键的存在,MySQL 会默认按照主键对数据进行排序。从 Page 内部存储的数据记录可以看出,数据是以有序且相互关联的方式存储的。

        那么为什么数据库在插入数据时要进行排序呢?直接按插入顺序存储不是更简单吗?排序的主要目的是为了优化查询效率。虽然 Page 内部的数据存储结构是链表(具有增删快的优势),但查询和修改操作相对较慢,因此提升查询性能尤为重要。通过有序存储,查询时从头到尾的每次比较都是有效的,不会产生无效查找。在某些情况下,甚至可以提前终止查找过程,进一步提升查询效率。

目录的作用:

就比如我们在阅读《谭浩强C程序设计》时,若要查找指针章节的内容,通常有两种方式:

  1. 逐页翻阅,直到找到目标内容

  2. 通过书的目录,快速定位(例如指针章节位于234页)

相比逐页查找,目录虽然占用额外篇幅,但能显著提升检索效率。因此,目录的本质是以空间换取时间的优化策略。

所以,针对上面的单页Page,我们能否也引入目录呢?当然可以!!!如下:

单个Page内创建页内目录

  • Page结构体内部存储的数据记录是以单链表的形式组织起来的,当页内部的数据量增多时,本质在页内部进行的还是线性遍历,效率低下。

  • 这时可以在Page结构体内部引入页内目录,将Page结构体内部存储的数据记录按照主键划分为若干区域,页内目录中就存储着这若干区域的最小键值。

  • 在Page结构体内部引入页内目录后,在页内部查询数据时就可以先通过页内目录找到目标数据所在区域的起始记录,然后再从该记录开始向后遍历找到目标记录。

比如在之前的Page内部引入页内目录后的结构大致如下:

        目前,我们在页面内部引入了目录结构。例如,要查找id=4的记录,之前需要线性遍历4次才能获取结果。现在通过目录2[3]可以直接定位新的起始位置,显著提升了查询效率。基于此,我们现在可以正式回答之前的问题:为什么MySQL会自动对键值进行排序?因为有序的数据结构可以方便地引入目录索引。

补充说明:

  • 在每个Page结构体内部引入页内目录,目的是为了加速在单个Page内部数据查询的效率。由于这个页内目录也是保存在Page内部的,而单个Page的大小是固定的,因此添加页内目录后Page内部能够保存的数据记录变少了,所以在Page内部引入页内目录本质是一种空间换时间的做法,就像给书添加目录需要花费更多的纸张一样。

  • 每个Page结构体内部的数据会按照主键进行排序,其实就是为了引入页内目录,因为只有数据按照主键排序后引入页内目录才有意义,就像书中每一页都是按照页码进行排序的一样,如果一本书的页码是乱序的,那么它的目录根本就没有意义。

4、多Page内部结构(多Page的基本组织、多Page管理与B+树演进)

1. 多个Page

        从上述分析可以看出,当前页模式的核心功能是在查询某条数据时,将整页数据加载到内存以减少硬盘IO次数,从而提升性能。然而,该模式的内部实现采用了链表结构,数据之间通过前后指针连接,本质上仍需逐条比较才能找到特定数据。

        MySQL 中每一页的大小只有16KB ,单个Page大小固定,当数据量达到千万级别时,,单个Page中无法存下所有数据,必然需要多个Page来存储。这些Page之间通过双向链表连接,而每个Page内部的数据同样基于链表结构。在这种情况下查询数据时就需要查找特定记录只能进行线性遍历,先遍历Page双链表确定目标数据在哪一个Page,然后再在该Page内部找到目标数据,但是效率显然非常低下。

        在单表数据持续插入的情况下,MySQL会在当前页容量不足时自动创建新页存储数据,并通过指针将这些页组织起来。需要注意的是,图示展示的是理想结构,实际情况下新插入的数据可能不会直接存放在新页上(为保持数据有序),此处仅作演示说明。

        通过这种多页结构,我们可以遍历各个页,并在页内通过目录快速定位数据。然而,这种方案仍存在效率问题:MySQL需要遍历页与页之间的链接,这会产生大量IO操作,需要不断将下一页加载到内存进行线性查找。这使得之前设计的页内目录显得有些力不从心。

目前这个多Page的问题:千万级数据的线性查找

  • 总记录数:10,000,000条

  • 每Page记录数:约400条(假设)

  • 总Page数:25,000页

  • 最坏情况:需要25,000次IO!

        解决方案延续了我们之前的思路:为页也建立目录。具体实现是使用目录项指向特定页,每个目录项记录所指向页中存储的最小键值。与页内目录管理行数据不同,这种目录管理的是页级别。每个目录项由键值和指针组成(图中未完整展示)。如下:

2. Page之上创建页目录

  • 虽然在单个Page内部能够通过页内目录来快速定位数据,但在遍历Page双链表寻找目标Page时本质进行的还是线性遍历。

  • 这时可以给各个Page结构体也建立页目录,页目录中的每个目录项都指向一个Page,而这个目录项存放的就是其指向的Page中存放的最小数据的键值。

  • 在给各个Page结构体建立页目录后,在查询数据时就可以先通过遍历页目录找到目标数据所在的Page,然后再在该Page内部找到目标数据。

补充说明:

  • 这里的页目录与之前的页内目录的区别在于,页目录管理的是一个个的Page,而页内目录管理的是一条条的记录。此外,页内目录与其管理的多条记录是保存在同一个Page中的,而页目录是重新申请的一个Page结构体来保存的。

  • 随着数据量不断增大,Page变得越来越多,这时一个页目录无法管理所有的Page,这时就需要更多个的页目录。这些页目录也是一个个的Page结构体,只不过这些Page结构体中存放的不是数据记录,而是各个Page的目录信息。但是在MySQL看来,无论Page当中存储的是什么数据,都应该被管理起来,因此这些Page页目录也需要用双链表连接起来。

        使用目录页来管理页目录时,目录页中存储的是指向页中最小的数据值。通过比较这些数据值,就能确定需要访问哪个页,并利用指针找到下一个页。目录页本质上也是页,只是普通页存储的是用户数据,而目录页存储的是普通页的地址信息。进行数据检索时,可能有人会问:虽然顶层目录页数量减少了,但仍需要遍历查找,该如何处理?其实这个问题可以通过增加多级目录页来解决。如下:

3. 页目录之上再创建页目录

  • 就算给各个Page结构体也建立了页目录,但随着数据量不断增大,页目录的数量也会越来越多,这时在遍历页目录寻找目标Page时本质进行的还是线性遍历。

  • 类似的,我们可以不断在页目录之上再创建页目录,最终就一定能够得到一个入口页目录,这时在查询数据时就可以从入口页目录开始不断查询页目录,最终找到目标数据所在的Page,然后再在该Page内部找到目标数据。

最终构建出来的结构如下:

补充说明:

  • 最终构建出来的实际就是一棵B+树,这棵B+树就是InnoDB的索引结构,其中每一层Page的作用就是加速它的下一层的查找效率。

  • 如果我们创建表时设置了主键,那么MySQL在底层就会自动将这张表中的的数据以B+树的形式组织起来,保存在Buffer Pool当中,当我们查询数据时就可以通过查询这棵B+树来提高查询效率。

  • MySQL中可能同时有大量的表正在被处理,因此Buffer Pool中可能会存在多个索引结构,也就是同时存在多个B+树结构,当我们查询表时访问的就是这张表对应的B+树结构。

4. B+树最终结构

  • 结构根节点 : 存储目录页的指针范围

  • 目录页 : 存储数据页的指针范围

  • 数据页 : 存储实际用户记录

  • 叶子节点 : 通过指针连接成链表

        这就是著名的B+树结构。至此,我们已经成功为user表构建了主键索引。当我们查询特定id时,可以发现需要访问的Page数量明显减少,这意味着IO操作次数降低,查询效率自然得到提升。

        让我们简单总结一下:Page分为目录页和数据页两种类型。其中目录页仅存储下级Page的最小键值。查询时采用自上而下的查找方式,只需将部分目录页加载到内存中,就能完成整个查找流程,从而显著减少IO操作次数。

5. B+树中的Page结点是否需要全量加入到Buffer Pool中?

  • 当对MySQL中的某张表进行增删查改操作时,不需要将其对应B+树的所有结点全量加入到Buffer Pool中,甚至在刚开始时只需要将B+树的根结点加入到Buffer Pool中。

  • 当后续访问表中的数据时,再将该数据对应路径上的结点加入到Buffer Pool中即可,对于其他不需要的结点根本不用加入到Buffer Pool中,这一点和操作系统中的页表是很像的。

  • 此外,在刷新数据时也不需要将B+树中所有的结点都进行刷新,在Page结构体中有一个标记位用来标记当前Page是否被修改过,如果被修改过则说明这是一个脏数据,在刷新数据时只有脏数据才需要被刷新到磁盘上。

  • 由于B+树中的结点都是16KB大小的Page,因此无论是刷新数据到磁盘函数从磁盘加载数据到Buffer Pool,都是以Page为单位进行的,这也就是所谓的MySQL与磁盘交互的基本单位是Page。

如果把这棵B+树逆时针旋转90度,就会发现这其实就是操作系统中的页表结构,本质操作系统中的页表也是B+树结构。如下:

例如在32位系统中,页表将虚拟地址转换为物理地址的过程如下:

  1. 首先提取虚拟地址的前10位,通过页目录定位到对应的页表

  2. 接着提取后续10位,在已定位的页表中查找物理页框的基地址

  3. 最后使用剩余的12位作为偏移量,从页框基地址开始计算,最终获得物理地址

补充说明:

  • 12位地址空间可表示2^12种偏移量(即4KB),因此物理内存页框大小固定为4KB。这也是操作系统与磁盘以4KB为基本交互单位的原因。

  • 页表采用类B+树结构,只需将访问路径上的节点加载到内存,无需全部驻留。这种按需加载机制使得页表内存占用可控,也是二级页表架构可行的关键所在。

5、为什么选择B+树?

与其他数据结构的对比

InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?

数据结构优点缺点适用性
链表插入删除快查询O(n)不适合
二叉搜索树查询O(log n)可能退化成链表不适合
AVL/红黑树平衡性好树高较高,IO次数多不适合
Hash等值查询O(1)范围查询效率低有限场景
B树所有节点存数据节点容量小,树更高不如B+树
B+树✅ 节点容量大
✅ 树矮IO少
✅ 范围查询优
✅ 顺序访问快
内部节点不存数据✅ 最佳选择

补充:

  • AVL/红黑树:虽然保证了二叉树是绝对或近似平衡的,不会退化成线性结构,但AVL树和红黑树都是二叉树结构,这就意味着树的层高会比较高,而查询数据时都是从根结点开始向下进行查找的,这也就意味着在查询过程中需要遍历更多结点,如果这些结点还没有被加载到Buffer Pool中,这时就需要进行更多次的IO操作,所以最终没有选择其作为索引结构。

  • 哈希表:在官方索引实现方式中,MySQL 确实支持 HASH 索引,但 InnoDB 和 MyISAM 存储引擎并不支持。由于 HASH 算法的固有特性,虽然它能在某些情况下实现快速查询(时间复杂度为 O(1)),但在处理范围查找时性能明显不足。

下面是几个常见的存储引擎,与其所支持的索引类型:(这里的BTREE是指B+树)

存储引擎支持的索引类型
InnoDBBTREE
MyISAMBTREE
MEMORY/HEAPHASH、BTREE
NDBHASH、BTREE

如果大家有兴趣的话可以使用这个网站来模拟一下相关的数据结构,它可以以图形化界面动态演示数据结构的增删查改:Data Structure Visualization

B+树 vs B树核心区别

B树的具体数据结构例图:

B+树的具体数据结构例图:

为什么选择B+树而非普通B树作为索引结构?首先讲解B+树的优势,然后讲解主要的两个关键原因:

B+树优势

  • 更矮的树:内部节点只存key,单个节点存储更多指针

  • 更少的IO:树高降低,减少磁盘访问次数

  • 范围查询:叶子节点链表支持高效范围扫描

  • 全盘扫描:遍历叶子节点即可获取全部数据

存储效率问题:

  • 普通B树的每个节点都同时存储索引和数据信息

  • 由于Page大小固定,包含数据会导致节点存储的索引信息减少

  • 这使得树结构变得更高更瘦,查询时需要更多的磁盘IO操作

查询效率问题:

  • 普通B树的叶子节点之间没有链接

  • B+树的叶子节点通过指针相连

  • 进行范围查询时,只需定位首条记录即可顺序遍历后续数据

  • 这种设计显著提升了范围查询的效率

6、聚簇索引 vs 非聚簇索引

MyISAM存储引擎(主键索引结构、非聚簇索引)

  • 之前推导的主键索引结构是InnoDB存储引擎的主键索引结构,而MyISAM存储引擎同样采用B+树作为索引的基本数据结构。

  • 与InnoDB存储引擎的B+树不同的是,MyISAM存储引擎的B+树的叶节点的data域存放的不是数据记录,而是数据记录对应的地址。

  • 其中, MyISAM 最大的特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址。 相较于 InnoDB 索引, InnoDB 是将索引和数据放在一起的。

比如下图为MyISAM存储引擎的主键索引结构,其中Col1为主键。如下:

MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引。

对于 MyISAM,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复。下面就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别!!!

MyISAM存储引擎(普通索引结构、非聚簇索引)

  • MyISAM存储引擎的普通索引采用的也是B+树结构,与主键索引唯一不同的地方就是普通索引的B+树中的键值可以重复。

  • 因此一张表可能会同时存在多个B+树结构,但由于MyISAM存储引擎的B+树叶子结点中,存储的是对应的数据记录的地址,因此有效数据只会存储一份(可以在存的时候重复,但是会自动去重)。

比如下图为MyISAM存储引擎的普通索引结构,其中Col2为索引列。如下:

InnoDB存储引擎(聚簇索引、普通索引结构)

        下面讨论的这种索引结构,若没有主键,就是在 InnoDB 中通常就被称为 普通索引 或 二级索引若在 InnoDB 存储引擎中,一旦为表定义了主键,那么这个主键所使用的索引结构就是“主键索引”,并且它一定是“聚簇索引”。

  • InnoDB存储引擎的普通索引采用的也是B+树结构,但普通索引的B+树中的键值可以重复,并且B+树的叶子结点中存储的不是数据记录,而是对应数据记录的主键值。

  • 当根据普通索引查询数据时,会先查找普通索引对应的B+树找到目标记录的主键值,然后再查找主键索引对应的B+树找到目标记录,这个过程就叫做回表查询。

比如下图为InnoDB存储引擎的普通索引结构,其中Col3为索引列。如下:

说明一下:

  • 可以看到, InnoDB 的非主键索引中叶子节点并没有数据,而只有对应记录的key值,也就是说,InnoDB存储引擎的普通索引的B+树叶子结点中没有保存整条数据记录,是为了节省空间,因为同一张表可能会创建多个普通索引,每个普通索引的B+树中都保存一份数据会造成数据冗余,所以通过回表查询主键索引对应的B+来获取整个数据记录,该做法本质一种以时间换取空间的做法。

  • 当根据普通索引查询数据时,其实也不一定需要进行回表查询,因为有可能我们要查询的就是这条记录对应的主键值,因此查询完普通索引对应B+树后即可完成查询。

  • 采用InnoDB存储引擎建立的每张表都会有一个主键,就算用户没有设置,InnoDB也会自动帮你创建一个不可见的主键,因为完整数据记录只会存储在主键索引对应的B+树中的,因此采用InnoDB存储引擎建立的表必须有主键。

        这段话的意思是:在 InnoDB 中,即使你创建的是一个“没有显式定义主键的表”(即你所说的“无主键表”),InnoDB 引擎也会在内部自动为你生成一个隐藏的主键。

  • 从用户/开发者的视角看:这是一张“没有主键的表”。

  • 从 InnoDB 存储引擎的底层实现视角看不存在真正“无主键”的表,引擎会自动处理,保证每张表都有一个“主键”来组织数据。

1. 为什么 InnoDB 必须要有主键?

这源于 InnoDB 的核心设计:聚簇索引

  • 聚簇索引:在 InnoDB 中,表数据本身的物理存储顺序就是按照某个索引的结构来组织的。这个索引就叫做“聚簇索引”。换句话说,聚簇索引的叶子节点直接存储了完整的行数据

  • 因为表数据只能以一种物理顺序存放,所以每张 InnoDB 表必须有且仅有一个聚簇索引

而 主键,就是聚簇索引的默认候选人

2. InnoDB 如何选择聚簇索引?

InnoDB 会按照以下优先级为表创建聚簇索引:

  • 首选:如果表定义了 PRIMARY KEY,那么主键就被用作聚簇索引。

  • 次选:如果没有显式定义主键,InnoDB 会选择第一个所有列都是 NOT NULL 的 UNIQUE 索引作为聚簇索引。

  • 保底:如果以上两种情况都不满足(即你创建了一个既无主键又无合适的唯一索引的表),InnoDB 会在后台自动生成一个名为 GEN_CLUST_INDEX 的隐藏主键。这个主键是一个 6 字节的自增 rowid,对用户不可见。

3. 这对“普通索引”(二级索引)有什么影响?
  • 普通索引(也称二级索引)的叶子节点,存储的不是完整的行数据,而是该索引列的值和对应的主键值

  • 当通过普通索引查询数据时,引擎会先通过普通索引找到对应的主键值,然后再用这个主键值回到聚簇索引中去查找完整的行数据。这个过程叫做 回表

关键点来了:这个“主键值”具体是什么?

  • 如果表有显式定义的主键,比如是 id 列,那么普通索引的叶子节点存储的就是 id 的值。

  • 如果表没有显式主键,用的是 InnoDB 自动生成的隐藏主键,那么普通索引的叶子节点存储的就是那个隐藏的 rowid。

聚簇索引 VS 非聚簇索引

  • 聚簇索引: 像InnoDB存储引擎这种,将数据记录与索引结构放在一起的索引方案,叫做聚簇索引。

  • 非聚簇索引: 像MyISAM存储引擎这种,将数据记录与索引结构分离的索引方案,叫做非聚簇索引。

1. 首先找到MySQL数据目录
-- 在MySQL中执行
SHOW VARIABLES LIKE 'datadir';

通常会显示类似:/var/lib/mysql/,如下:

2. 创建测试数据库和表
-- 创建测试数据库
CREATE DATABASE storage_test;
USE storage_test;-- 创建InnoDB表
CREATE TABLE innodb_table (id INT PRIMARY KEY,name VARCHAR(50)
) ENGINE=InnoDB;-- 创建MyISAM表  
CREATE TABLE myisam_table (id INT PRIMARY KEY,name VARCHAR(50)
) ENGINE=MyISAM;

3. 查看文件结构

在Ubuntu终端中执行:

# 进入MySQL数据目录
cd /var/lib/mysql# 查看storage_test数据库的文件
sudo ls -la storage_test/

4. 预期结果(了解就行)

InnoDB表文件:

  • innodb_table.frm - 表结构定义文件

  • innodb_table.ibd - 表数据和索引文件

MyISAM表文件:

  • myisam_table.frm - 表结构定义文件

  • myisam_table.MYD - 表数据文件

  • myisam_table.MYI - 表索引文件

一般来说,输出结果跟预期结果是一样的;但是像我上面的一样的原因是使用的MySQL版本较新(可能是8.0+),文件结构有变化。

分析当前我的输出结果:

InnoDB表:

  • innodb_table.ibd - 表空间文件(包含表结构和数据)

  • 没有 .frm 文件(新版本已移除)

MyISAM表:

  • myisam_table.MYD - 数据文件

  • myisam_table.MYI - 索引文件

  • myisam_table_542.sdi - 新版本增加的序列化字典信息文件

新版本MySQL的文件变化:

1. 移除了 .frm 文件

MySQL 8.0+ 不再使用 .frm 文件存储表结构,改为:

  • InnoDB:表结构存储在系统表空间或 .ibd 文件中

  • MyISAM:表结构存储在 .sdi 文件中

2. 新增了 .sdi 文件

  • SDI = Serialized Dictionary Information

  • 包含表的元数据信息(列定义、索引等)

  • 每个MyISAM表都有一个对应的 .sdi 文件

验证结论:虽然文件数量与预期不同,但仍然能看出差异:

InnoDB:主要文件是 .ibd        MyISAM:主要文件是 .MYD + .MYI + .sdi

这说明成功验证了不同存储引擎的文件差异!新版本MySQL的文件组织方式有所变化,但核心原理相同。

MySQL旧版本存储引擎文件说明

  • 采用InnoDB和MyISAM存储引擎创建表时都会生成xxx.frm文件,该文件中存储的是表结构相关的信息。

  • 采用InnoDB存储引擎创建表时会生成一个xxx.ibd文件,该文件中存储的是索引和数据相关的信息,这就是所谓的聚簇索引,索引和数据是存储在同一个文件中的。

  • 采用MyISAM存储引擎创建表时会生成一个xxx.MYD文件和一个xxx.MYI文件,其中xxx.MYD文件中存储的是数据相关的信息,而xxx.MYI文件中存储的是索引相关的信息,这就是所谓的非聚簇索引,索引和数据是分开存储的。

MySQL新版本(8.0+)存储引擎文件说明:

采用InnoDB存储引擎创建表时

  • 会生成一个 xxx.ibd 文件,该文件中同时存储表结构、索引和数据信息

  • 这就是所谓的聚簇索引,表结构、索引和数据都存储在同一个文件中

  • 不再生成单独的 .frm 文件

采用MyISAM存储引擎创建表时

  • 会生成三个文件:

    • xxx.MYD 文件:存储数据相关信息

    • xxx.MYI 文件:存储索引相关信息

    • xxx_xxx.sdi 文件:存储表结构信息(序列化字典信息)

  • 这就是所谓的非聚簇索引,表结构、索引和数据是分开存储的

  • 不再生成单独的 .frm 文件,表结构信息移至 .sdi 文件

核心变化总结:

  • 移除了 .frm 文件:表结构不再存储在 .frm 文件中

  • InnoDB:表结构合并到 .ibd 文件中

  • MyISAM:表结构移至新的 .sdi 文件中

  • SDI文件:新的表结构存储格式,采用JSON格式存储元数据

7、总结与核心要点

核心概念回顾

  • 磁盘基础:理解扇区、磁道、柱面等物理结构

  • IO优化:Page机制减少IO次数是关键

  • 索引演进:从单Page到多Page再到B+树的完整演进

  • 存储引擎差异:InnoDB聚簇索引 vs MyISAM非聚簇索引

设计哲学

  • 空间换时间:目录页、索引结构都是空间换时间的体现

  • 局部性原理:预读和缓存基于程序访问的局部性特征

  • 平衡艺术:在查询、插入、更新、删除之间寻找平衡点

实践指导

  • 主键选择:自增主键有利于顺序插入,减少页分裂

  • 索引设计:理解聚簇索引特性,避免过度回表

  • 查询优化:利用索引有序性,优化排序和范围查询

  • 存储引擎选择:根据业务特点选择适合的存储引擎

通过深入理解MySQL索引的工作原理,我们能够更好地进行数据库设计、查询优化和性能调优,构建高效稳定的数据存储系统。


五、索引与键的重点关系

索引(Index)

  • 是一种数据结构,用于加快数据检索速度

  • 相当于书的"目录"

  • 可以是可见的,也可以是隐式的

键(Key)

  • 是一种约束,用于保证数据的完整性和唯一性

  • 是业务逻辑的概念

核心关系:所有键都是索引,但不是所有索引都是键!!!

1、主键 = 唯一索引 + NOT NULL约束

特点:自动创建唯一索引、不允许NULL值、一个表只能有一个

2、唯一键 = 唯一索引

特点:自动创建唯一索引、允许NULL值(但非NULL值必须唯一)、一个表可以有多个

3、普通索引 ≠ 键

特点:只是性能优化工具、没有数据约束功能、允许重复值

一句话总结:键是"有约束的索引" ,它既加速查询,又保证数据规则。

类型是否是索引是否有约束是否允许重复
主键✅ 是✅ 有❌ 不允许
唯一键✅ 是✅ 有❌ 不允许
普通索引✅ 是❌ 无✅ 允许
外键⚠️ 可能是✅ 有✅ 允许

简单记忆

  • 所有键(主键、唯一键)都会创建索引

  • 但不是所有索引都是键

  • 索引主要为了性能,键主要为了数据完整性


六、数据库索引操作详解

1、主键索引

创建方式

第一种方式 - 在创建表时直接在字段后指定主键

创建表时,直接在对应的字段名后指定primary key。如下:

CREATE TABLE user1(id INT PRIMARY KEY, name VARCHAR(30)
);

第二种方式 - 在创建表的最后指定主键

在创建表的最后,指定某列或某几列为主键索引。如下:

CREATE TABLE user2(id INT, name VARCHAR(30), PRIMARY KEY(id)
);

第三种方式 - 创建表后添加主键

创建表后,使用alter命令给指定字段添加主键索引。如下:

CREATE TABLE user3(id INT, name VARCHAR(30));
ALTER TABLE user3 ADD PRIMARY KEY(id);

主键索引特点

  • 一个表中最多有一个主键索引(可以是复合主键,也就是一个主键可以由多个列同时承担)

  • 主键索引查询效率极高

  • 主键列的值不能为NULL且不能重复

  • 主键索引列通常使用INT类型

  • 若使用InnoDB引擎,主键索引会自动创建聚簇索引

2、唯一索引

创建方式

第一种方式 - 在表定义时直接指定UNIQUE属性

在创建表时,直接在对应的字段名后指定unique。如下:

CREATE TABLE user4(id INT PRIMARY KEY, name VARCHAR(30) UNIQUE
);

第二种方式 - 在表定义最后指定唯一索引

在创建表的最后,指定某列或某几列为唯一索引。如下:

CREATE TABLE user5(id INT PRIMARY KEY, name VARCHAR(30), UNIQUE(name)
);

第三种方式 - 创建表后添加唯一索引

CREATE TABLE user6(id INT PRIMARY KEY, name VARCHAR(30));
ALTER TABLE user6 ADD UNIQUE(name);

唯一索引特点

  • 一个表中可以有多个唯一索引,一个唯一键可以由多个列同时承担。

  • 查询效率较高

  • 唯一索引列不能有重复数据,但可以为NULL(除非指定NOT NULL)

  • 如果唯一索引指定NOT NULL,功能上等价于主键索引

3、普通索引

创建方式

第一种方式 - 在表定义时创建

在创建表的最后,指定某列或某几列为普通索引。如下:

CREATE TABLE user8(id INT PRIMARY KEY,name VARCHAR(20),email VARCHAR(30),INDEX(name)  -- 指定name列为普通索引
);

第二种方式 - 使用ALTER TABLE添加

创建表后,使用alter命令给指定字段添加普通索引。如下:

CREATE TABLE user9(id INT PRIMARY KEY, name VARCHAR(20), email VARCHAR(30));
ALTER TABLE user9 ADD INDEX(name);

第三种方式 - 使用CREATE INDEX创建

创建表后,使用create命令给指定字段创建普通索引,并指定索引名。如下:

CREATE TABLE user10(id INT PRIMARY KEY, name VARCHAR(20), email VARCHAR(30));
CREATE INDEX idx_name ON user10(name);  -- 创建名为idx_name的索引

普通索引特点

  • 一个表中可以有多个普通索引,一个普通索引可以由多个列同时承担。

  • 普通索引在实际开发中使用最频繁

  • 适用于有重复值的列(创建普通索引的列,其列值可以为NULL,也可以重复)

  • 可以提高查询性能,但效果低于唯一索引

4、全文索引

创建全文索引

全文索引主要用于对文本内容进行全文检索,需要注意:

  • 表的存储引擎必须是MyISAM

  • 默认支持英文,不支持中文(中文需使用sphinx/coreseek)

        全文索引比较常见的案例就是对文章中的词进行搜索,比如下面创建一个文章表,表当中包含文章的id、文章名称、文章内容,并在创建表的最后通过fulltext给title和body列创建全文索引。如下:

CREATE TABLE articles (id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,title VARCHAR(200),body TEXT,FULLTEXT (title,body)
) ENGINE=MyISAM;desc articles;

下面向表当中插入一些测试数据。如下:

-- 插入测试数据
INSERT INTO articles (title,body) VALUES
('MySQL Tutorial','DBMS stands for DataBase ...'),
('How To Use MySQL Well','After you went through a ...'),
('Optimizing MySQL','In this tutorial we will show ...'),
('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),
('MySQL vs. YourSQL','In the following database comparison ...'),
('MySQL Security','When configured properly, MySQL ...');select * from articles;

全文索引使用

错误用法 - 无法使用全文索引:

如果要查询哪些文章中包含database关键字,我们可以通过模糊匹配进行查找。注意,LIKE 匹配默认不区分大小写。如下:

SELECT * FROM articles WHERE body LIKE '%database%';

正确用法 - 使用MATCH...AGAINST语法:(了解即可)

如果要通过全文索引来查询,需要使用match against进行搜索。如下:

SELECT * FROM articles 
WHERE MATCH (title,body) AGAINST ('database');

与LIKE的对比(了解即可)

步骤LIKE '%database%'MATCH AGAINST 'database'
1无分词,直接字符串匹配分词,提取关键词
2全表扫描,逐行比较使用全文索引快速定位
3无相关性计算TF-IDF算法计算相关度
4按物理顺序返回按相关度排序返回

性能对比

        但实际这种查找方式并没有用到全文索引,在SQL语句前面加上explain,可以看到key对应的值为NULL,表示这条SQL在执行过程中没有用到任何索引。如下:

使用EXPLAIN分析查询计划:

-- LIKE查询无法使用索引
EXPLAIN SELECT * FROM articles WHERE body LIKE '%database%';
-- 使用\G格式化方便查询
EXPLAIN SELECT * FROM articles WHERE body LIKE '%database%' \G

下面恰好则验证了与LIKE的对比表中提到的MATCH AGAINST 'database'使用全文索引快速定位的特性:

-- 全文索引查询能够利用索引
EXPLAIN SELECT * FROM articles 
WHERE MATCH (title,body) AGAINST ('database');
-- 使用\G格式化方便查询
EXPLAIN SELECT * FROM articles 
WHERE MATCH (title,body) AGAINST ('database') \G

在这条SQL语句前面加上explain,可以看到key对应的值为title,表示这条SQL在执行过程中用到了索引名为title的索引。如下:

补充说明

  • MyISAM存储引擎是支持全文索引的,而InnoDB存储引擎是在5.6以后才开始支持全文索引的。

  • 同时使用title和body建立全文索引时,相当于建立了一个复合索引,默认会选择fulltext中的第一个列名作为这个复合索引的索引名,所以这里explain中key对应的索引名为title。

  • 由于是title和body共同建立的全文索引,所以如果文章当中没有出现关键字,但文章名称中出现了关键字则也会被筛选出来(当前示例没有体现出来)。

5、查询索引

第一种方法 - 显示详细信息:

SHOW KEYS FROM 表名;
-- 或
SHOW KEYS FROM 表名\G  -- 垂直显示(适用于命令行)

使用show keys from 表名SQL查询,比如查询articles表中的索引信息。如下:

相关字段讲解:

  • Table: 表示创建索引的表的名称。

  • Non_unique: 表示该索引是否是唯一索引,如果是则为0,如果不是则为1。

  • Key_name: 表示索引的名称。

  • Seq_in_index: 表示该列在索引中的位置,如果索引是单列的,则该列的值为1,如果索引是复合索引,则该列的值为每列在索引定义中的顺序。

  • Column_name: 表示定义索引的列字段。

  • Collation: 表示列以何种顺序存储在索引中,“A”表示升序,NULL表示无分类。

  • Cardinality: 索引中唯一值数目的估计值。基数根据被存储为整数的统计数据计数,所以即使对于小型表,该值也没有必要是精确的。基数越大,当进行联合时,MySQL使用该索引的机会就越大。

  • Sub_part: 表示列中被编入索引的字符的数量,若列只是部分被编入索引,则该列的值为被编入索引的字符的数目,若整列被编入索引,则该列的值为NULL。

  • Packed: 指示关键字如何被压缩。若没有被压缩,则值为NULL。

  • Null: 用于显示索引列中是否包含NULL,若包含则为YES,若不包含则为NO。

  • Index_type: 显示索引使用的类型和方法(BTREE、FULLTEXT、HASH、RTREE)。

  • Comment: 显示评注。

第二种方法 - 显示索引信息:

SHOW INDEX FROM 表名;

使用show index from 表名SQL查询,比如查询articles表中的索引信息。如下:

第三种方法 - 简略信息:

DESC 表名;

使用desc 表名SQL查询(信息比较简略),比如查询articles表中的索引信息。如下:

6、删除索引

删除主键索引

ALTER TABLE 表名 DROP PRIMARY KEY;

使用alter table 表名 drop primary keySQL即可删除主键索引。如下:

ALTER TABLE user10 DROP PRIMARY KEY;

删除其他索引

ALTER TABLE 表名 DROP INDEX 索引名;
-- 或
DROP INDEX 索引名 ON 表名;

示例

使用alter table 表名 drop index 索引名SQL即可删除指定的非主键索引。如下:

ALTER TABLE user10 DROP INDEX idx_name;

此外,也可以使用drop index 索引名 on 表名SQL也可以删除指定的非主键索引。如下:

DROP INDEX name ON user8;

补充说明:一个表只有一个主键索引,所以在删除主键索引的时候不用指明索引名,而一个表中可能有多个非主键索引,所以在删除非主键索引时需要指明索引名。

7、索引创建原则

记住创建索引的目的就是为了提高查询的效率!!!

应该创建索引的情况

  • 频繁作为查询条件的字段

  • 经常需要排序、分组的字段

  • 用于表连接的字段

不应该创建索引的情况

  • 唯一性太差的字段(如性别、状态标志)

  • 更新非常频繁的字段

  • 不会出现在WHERE子句中的字段

  • 数据量太小的表(通常小于1000行)

其他考虑因素

  • 选择区分度高的列创建索引

  • 考虑索引的维护成本

  • 避免创建过多索引,影响写性能

8、扩展概念(了解即可)

  • 复合索引 - 在多列上创建的索引:

    CREATE INDEX idx_name_email ON users(name, email);
  • 最左匹配原则 - 复合索引按照从左到右的顺序匹配

  • 索引覆盖 - 查询只需要通过索引就能获取所需数据,无需回表

  • 索引下推 - MySQL 5.6+ 特性,在索引遍历过程中完成部分过滤

9、最佳实践建议

  • 为经常查询的列创建合适的索引

  • 避免在频繁更新的列上创建过多索引

  • 定期分析慢查询,优化索引策略

  • 使用EXPLAIN分析查询执行计划

  • 注意索引的选择性和数据分布

通过合理使用索引,可以显著提高数据库查询性能,但需要根据实际业务需求和数据特点进行权衡和优化。

http://www.dtcms.com/a/428067.html

相关文章:

  • 【有序数组去重】2022-11-25
  • 【11408学习记录】考研数学线性代数精讲:矩阵方程求解与秩的深度解析
  • 专业沈阳网站制作大数据公司排名
  • 做受视频网站现在流行什么语言建设网站
  • TDengine 时序函数 STATEDURATION 用户手册
  • Java-Spring入门指南(十二)SpringAop的三种实现方式
  • 网站在线统计代码cms开发框架
  • CometD 长轮询协议及在Salesforce中的应用
  • 企术建站网站收录查询主要由哪几个网站
  • 中小型网站建设与网络搭建教育机构加盟
  • 重庆职业能力建设投稿网站网站正能量免费推广软件晚上
  • LeetCode 114. 二叉树展开为链表
  • 网站性能优化电子商务网站建设与管理期末答案
  • 橙色网站设计手机社区网站模板
  • ns3 配置 Ubuntu × CLion
  • 大模型——长文拆解上下文工程落地策略与实践
  • 网站免费建站pixiv appdw如何在网站做弹窗
  • 分身宝 1.0.8 | 无限多开系统级分身~更稳定安全支持同时登录多个社交软件、游戏账号,互不干扰,操作简单便捷,一键切换
  • 网站服务器租用价格 贴吧磁力神器
  • 山东seo推广网站建设个人网站推广方案
  • 商务网站建设工程师是做网站找俊义 合优
  • 简要介绍IDM(Internet Download Manager)的功能及其在下载管理领域的地位
  • 杭州网站开发设计购物网站开发计划书
  • Javascript常量介绍
  • 从 Vercel 构建失败谈 Git 大小写敏感性问题:一个容易被忽视的跨平台陷阱
  • 门户网站有哪些品牌推广理论
  • wordpress 电商网站政务网站建设发言材料
  • 自己做的网站提示不安全企业做网站可以带中国吗
  • thumbnail(资源管理器 缩略图)
  • Java 25 新特性解析与代码示例