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

MySQL索引深度解析:从原理到实践

目录

1、索引的价值

案例:

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

MySQL中的Page(数据页)和操作系统中的Page(内存页)核心区别:

3、建立共识

4、索引的理解

\G的作用

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

4.2.理解单个Page

4.3.为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?

4.4.理解多个Page

4.5.页目录

4.6.单页情况

4.7.多页情况

4.8.复盘一下

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

为何选择B+?

5.聚簇索引 VS 非聚簇索引

聚簇索引和非聚簇索引的详细对比

1. 数据存储

2. 数量限制

3. 查询效率

4. 写入性能

5. 适用场景

6、索引操作

6.1.创建主键索引

主键索引的特点:

6.2.唯一索引的创建

唯一索引的特点:

6.3.普通索引的创建

普通索引的特点:

6.4.查询索引:

6.5.删除索引

索引创建原则

1、索引的价值

索引:提高数据库的性能,索引是物美价廉的东西了。不用加内存,不用改程序,不用调sql,只要执行正确的 create index ,查询速度就可能提高成百上千倍。但是天下没有免费的午餐,查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。所以它的价值,在于提高一个海量数据的检索速度

常见索引分为:

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

案例:

先整一个海量表,在查询的时候,看看没有索引时有什么问题?

--构建一个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 doset 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; 
repeatset 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 ;
-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);

到此,已经创建出了海量数据的表了。

select * from EMP where empno=998877;

可以看到耗时4.93秒,这还是在本机一个人来操作,在实际项目中,如果放在公网中,假如同时有
1000个人并发查询,那很可能就死机。

解决方法:创建索引

alter table EMP add index(empno);

增加索引之后,检索时间就会大幅度减少,这就是索引的价值

2、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.01 sec)

也就是说,磁盘这个硬件设备的基本单位是 512 字节,而 MySQL InnoDB引擎 使用 16KB 进行IO交互。
即, MySQL 和磁盘进行数据交互的基本单位是 16KB 。这个基本数据单元,在 MySQL 这里叫做page(注意和系统的page区分)

MySQL中的Page(数据页)和操作系统中的Page(内存页)核心区别:

  • 联系:MySQL的Page通过操作系统的Page机制实现磁盘与内存的交互,两者在I/O操作、缓冲池管理和性能优化上存在间接关联。
  • 协同工作:MySQL的Page设计需要考虑操作系统的Page机制,例如,选择合适的Page大小以减少内存拷贝和I/O开销。

3、建立共识

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

4、索引的理解

建立测试表

create table if not exists user (id int primary key,   --一定要添加主键哦,只有这样才会默认生成主键索引age int not null,name varchar(16) not null
);
mysql> show create table user \G
*************************** 1. row ***************************Table: user
Create Table: CREATE TABLE `user` (
`id` int(11) NOT NULL,
`age` int(11) NOT NULL,
`name` varchar(16) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8  --默认就是InnoDB存储引擎
1 row in set (0.00 sec)

\G的作用

HOW CREATE TABLE user \G 是一个用于查看表结构的命令。\G 是一个MySQL客户端的特殊修饰符,它会让查询结果以垂直格式(每列单独一行)显示,而不是默认的表格格式。这在处理包含多列或长文本的查询结果时特别有用。

插入多条记录

--插入多条记录,注意,我们并没有按照主键的大小顺序插入哦
mysql> insert into user (id, age, name) values(3, 18, '杨过');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(4, 16, '小龙女');
Query OK, 1 row affected (0.00 sec)
mysql> insert into user (id, age, name) values(2, 26, '黄蓉');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(5, 36, '郭靖');
Query OK, 1 row affected (0.00 sec)
mysql> insert into user (id, age, name) values(1, 56, '欧阳锋');
Query OK, 1 row affected (0.00 sec)

查看插入结果:

mysql> select * from user;    --发现竟然默认是有序的!是谁干的呢?排序有什么好处呢?
+----+-----+-----------+
| id | age | name   |
+----+-----+-----------+
|  1 |  56 | 欧阳锋  |
|  2 |  26 | 黄蓉   |
|  3 |  18 | 杨过   |
|  4 |  16 | 小龙女  |
|  5 |  36 | 郭靖   |
+----+-----+-----------+
5 rows in set (0.00 sec)

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

为何MySQL和磁盘进行IO交互的时候,要采用Page的方案进行交互呢?用多少,加载多少不香吗?
 
如上面的5条记录,如果MySQL要查找id=2的记录,第一次加载id=1,第二次加载id=2,一次一条记录,那么就需要2次IO。如果要找id=5,那么就需要5次IO。
但,如果这5条(或者更多)都被保存在一个Page中(16KB,能保存很多记录),那么第一次IO查找id=2的时候,整个Page会被加载到MySQL的Buffer Pool中,这里完成了一次IO。但是往后如果在查找id=1,3,4,5等,完全不需要进行IO了,而是直接在内存中进行了。所以,就在单Page里面,大大减少了IO的次数。
你怎么保证,用户一定下次找的数据,就在这个Page里面?我们不能严格保证,但是有很大概率,因为有局部性原理
往往IO效率低下的最主要矛盾不是IO单次数据量的大小,而是IO的次数

4.2.理解单个Page

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

不同的 Page ,在 MySQL 中,都是 16KB ,使用 prev 和 next 构成双向链表
因为有主键的问题, MySQL 会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的。

4.3.为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?

插入数据时排序的目的,就是优化查询的效率。
页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的。
正式因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是可以提前结束查找过程的。

4.4.理解多个Page

  • 通过上面的分析,我们知道,上面页模式中,只有一个功能,就是在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。但是,我们也可以看到,现在的页模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。
  • 如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的。那么,查找特定一条记录,也一定是线性查找。这效率也太低了

4.5.页目录

我们在看《谭浩强C程序设计》这本书的时候,如果我们要看<指针章节>,找到该章节有两种做法

  • 从头逐页的向后翻,直到找到目标内容通过书提供的目录,发现指针章节在234页(假设),那么我们便直接翻到234页。同时,查找目录的方案,可以顺序找,不过因为目录肯定少,所以可以快速提高定位
  • 本质上,书中的目录,是多花了纸张的,但是却提高了效率
  • 所以,目录,是一种“空间换时间的做法”

4.6.单页情况

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

那么当前,在一个Page内部,我们引入了目录。比如,我们要查找id=4记录,之前必须线性遍历4次,才能拿到结果。现在直接通过目录2[3],直接进行定位新的起始位置,提高了效率。现在我们可以再次正式回答上面的问题了,为何通过键值 MySQL 会自动排序?
可以很方便引入目录

4.7.多页情况

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

在单表数据不断被插入的情况下, MySQL 会在容量不足的时候,自动开辟新的Page来保存新的数据,然后通过指针的方式,将所有的Page组织起来。
需要注意,上面的图,是理想结构,大家也知道,目前要保证整体有序,那么新插入的数据,不一定会在新Page上面,这里仅仅做演示。

这样,我们就可以通过多个Page遍历,Page内部通过目录来快速定位数据。可是,貌似这样也有效率问题,在Page之间,也是需要 MySQL 遍历的,遍历意味着依旧需要进行大量的IO,将下一个Page加载到内存,进行线性检测。这样就显得我们之前的Page内部的目录,有点杯水车薪了

那么如何解决呢?解决方案,其实就是我们之前的思路,给Page也带上目录
使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值。
和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行。
其中,每个目录项的构成是:键值+指针。图中没有画全。

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

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

这货就是传说中的B+树啊!没错,至此,我们已经给我们的表user构建完了主键索引。

随便找一个id=?我们发现,现在查找的Page数一定减少了,也就意味着IO次数减少了,那么效率也就提高了

4.8.复盘一下

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

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

  • 链表?线性遍历
  • 二叉搜索树?退化问题,可能退化成为线性结构
  • AVL &&红黑树?虽然是平衡或者近似平衡,但是毕竟是二叉结构,相比较多阶B+,意味着树整体过高,大家都是自顶向下找,层高越低,意味着系统与硬盘更少的IO Page交互。虽然你很秀,但是有更秀的。
  • Hash?官方的索引实现方式中, MySQL 是支持HASH的,不过 InnoDB 和 MyISAM 并不支持.Hash跟进其算法特征,决定了虽然有时候也很快(O(1)),不过,在面对范围查找就明显不行,另外还有其他差别,有兴趣可以查一下。

B树?最值得比较的是 InnoDB 为何不用B树作为底层索引?

B树

B+树

  • B树节点,既有数据,又有Page指针,而B+,只有叶子节点有数据,其他目录页,只有键值和Page指针
  • B+叶子节点,全部相连,而B没有

为何选择B+?

  • 节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。
  • 叶子节点相连,更便于进行范围查找

5.聚簇索引 VS 非聚簇索引

MyISAM 存储引擎-主键索引
MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。

下图为 MyISAM表的主索引, Col1 为主键。

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

创建MyISAM索引的方法:

-终端A
mysql> create database myisam_test;  --创建数据库
Query OK, 1 row affected (0.00 sec)
mysql> use myisam_test;
Database changed
mysql> create table mtest(-> id int primary key,-> name varchar(11) not null-> )engine=MyISAM;        --使用engine=MyISAM

其中, InnoDB 这种用户数据与索引数据在一起索引方案,叫做聚簇索引

聚簇索引和非聚簇索引的详细对比

1. 数据存储
  • 聚簇索引
    • 数据行按照聚簇索引的键值物理排序存储。
    • 叶子节点直接存储完整的数据行(InnoDB中,主键是聚簇索引的默认键)。
    • 类似“图书馆的分类书架”:书(数据)按照分类(索引)顺序排列,直接放在书架上。
  • 非聚簇索引
    • 索引结构与数据行分开存储。
    • 叶子节点存储的是指向数据行的指针(如主键值或物理地址)。
    • 类似“图书馆的目录卡片”:卡片(索引)记录书名和书架位置(指针),需要到书架上找书(数据)。
2. 数量限制
  • 聚簇索引
    • 一个表只能有一个聚簇索引(因为数据只能按一种方式物理排序)。
    • 在InnoDB中,如果没有显式定义主键,会选择一个唯一非空索引作为聚簇索引;如果没有这样的索引,会隐式生成一个6字节的ROWID作为聚簇索引。
  • 非聚簇索引
    • 一个表可以有多个非聚簇索引(因为它们只是指向数据的指针)。
    • 类似“图书馆的多个目录”:可以按作者、出版社等分类,每个分类都是独立的目录。
3. 查询效率
  • 聚簇索引
    • 范围查询(如 WHERE id BETWEEN 1 AND 100)效率高,因为数据是物理连续的。
    • 按索引顺序的访问(如 ORDER BY id)无需排序。
  • 非聚簇索引
    • 精确查询(如 WHERE username = 'Alice')效率高,但范围查询可能需要回表。
    • 回表:如果查询的列不在索引中,需要先通过索引找到主键值,再通过主键值查询数据行。
4. 写入性能
  • 聚簇索引
    • 插入、更新和删除操作可能较慢,因为需要维护物理顺序。
    • 例如,插入数据时可能需要移动其他数据行以保持顺序。
  • 非聚簇索引
    • 写入操作通常较快,因为只需更新索引结构,不涉及数据行的物理移动。
    • 例如,插入数据时只需在索引中添加一个条目。
5. 适用场景
  • 聚簇索引
    • 适合频繁按主键或索引顺序查询的场景(如时间序列数据、订单表按时间排序)。
    • 适合范围查询较多的场景(如 WHERE date BETWEEN '2023-01-01' AND '2023-12-31')。
  • 非聚簇索引
    • 适合需要快速定位数据行的场景(如精确查询、多条件查询)。
    • 适合覆盖索引(Covering Index)的场景(即索引包含查询所需的所有列,无需回表)。

6、索引操作

6.1.创建主键索引

第一种方式

-- 在创建表的时候,直接在字段名后指定 primary key
create table user1(id int primary key, name varchar(30));

第二种方式

-- 在创建表的最后,指定某列或某几列为主键索引
create table user2(id int, name varchar(30), primary key(id));

第三种方式

create table user3(id int, name varchar(30));
-- 创建表以后再添加主键
alter table user3 add primary key(id);

主键索引的特点:

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

6.2.唯一索引的创建

第一种方式

-- 在表定义时,在某列后直接指定unique唯一属性。
create table user4(id int primary key, name varchar(30) unique);

第二种方式

-- 创建表时,在表的后面指定某列或某几列为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);

唯一索引的特点:

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

6.3.普通索引的创建

第一种方式

create table user8(id int primary key,
name varchar(20),
email varchar(30),
index(name) --在表的定义最后,指定某列为索引
);

第二种方式

create table user9(id int primary key, name varchar(20), email
varchar(30));
alter table user9 add index(name); --创建完表以后指定某列为普通索引

第三种方式

create table user10(id int primary key, name varchar(20), email
varchar(30));
-- 创建一个索引名为 idx_name 的索引
create index idx_name on user10(name);

普通索引的特点:

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

6.4.查询索引:

第一种方法: show keys from 表名

mysql> show keys from goods\G
*********** 1. row ***********Table: goods  <= 表名
Non_unique: 0    <= 0表示唯一索引Key_name: PRIMARY <= 主键索引
Seq_in_index: 1
Column_name: goods_id <= 索引在哪列Collation: A
Cardinality: 0Sub_part: NULLPacked: NULLNull:
Index_type: BTREE  <= 以二叉树形式的索引Comment:
1 row in set (0.00 sec)

第二种方法: show index from 表名;
第三种方法(信息比较简略): desc 表名;

6.5.删除索引

  • 第一种方法-删除主键索引: alter table 表名 drop primary key;
  • 第二种方法-其他索引的删除: alter table 表名 drop index 索引名; 索引名就是show keysfrom 表名中的 Key_name 字段mysql> alter table user10 drop index idx_name;
  • 第三种方法方法: drop index 索引名 on 表名mysql> drop index name on user8

索引创建原则

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

相关文章:

  • Maven Profile高级策略与冲突解决
  • 修复ubuntu server笔记本合盖导致的无线网卡故障
  • 电子学会的二级考试复习资料
  • 基于微信小程序的漫展系统的设计与实现
  • 【从0到1搞懂大模型】chatGPT 中的对齐优化(RLHF)讲解与实战(9)
  • 北京航空航天大学保研上机真题
  • 相机内参 opencv
  • Linux架构篇、第五章_03gitlab的搭建
  • Linux中的文件系统和软硬连接
  • 豆瓣电视剧数据工程实践:从爬虫到智能存储的技术演进(含完整代码)
  • 【linux】mount命令
  • 【TDengine源码阅读】taosMemoryDbgInit函数
  • Vue 3 (2) 模块化开发入门教程(ESM方式)
  • 深入解析MongoDB WiredTiger存储引擎:原理、优势与最佳实践
  • 【计算机网络】基于UDP进行socket编程——实现服务端与客户端业务
  • 线性代数之张量计算,支撑AI算法的数学原理
  • 历年复旦大学保研上机真题
  • MADS-box编码基因Tunicate1通过增加玉米果穗上方的叶片数量正向调控玉米产量。
  • 【c++11】智能指针 -- 摆脱内存困扰,现代编程的智能选择
  • 2025.5.19-2025.5.25学习周报
  • 网站设计方案案例分析/网站建设推广服务
  • 网站建设美工招聘/商丘优化公司
  • 微信里的网站怎么做/seo推广经验
  • 中国神鹰网站建设/app推广拉新一手渠道
  • 山东建设厅官方网站临沂/网上营销型网站
  • 怎样搭建一个个人网站/泉州百度搜索推广