MySQL 索引:结构、对比与操作实践指南
MySQL系列
文章目录
- MySQL系列
- 前言
- 案例
- 一、认识MySQL与磁盘
- 1.1 MySQL与存储
- 1.2 MySQL 与磁盘交互基本单位
- 二、 MySQL 数据交互核心:BufferPool 与 IO 优化机制
- 三、索引的理解
- 3.1 测试案例
- 3.2 page
- 3.3 页目录
- 3.3 对比其他结构
- 四、聚簇索引 VS 非聚簇索引
- 五、索引操作
- 5.1 创建主键索引
- 5.2 唯一索引的创建
- 5.3 普通索引的创建
- 5.4 查询索引
- 5.5 删除索引
前言
上一篇:MySQL 函数大赏:聚合、日期、字符串等函数剖析
在MySQL数据库中,索引是一种特殊的数据结构,它与表中数据关联,就像书籍的目录与正文的关系——目录通过章节标题和页码快速定位内容,而索引则通过存储数据的关键列值及其对应物理位置,帮助数据库快速定位目标数据。
本篇文章以主流的InnoDB引擎为例,展开介绍
案例
在MySQL中操作存储大量数据的表时,我们往往会面临处理数据慢,性能低下等问题,这时只需要建立索引就可以将这种问题优化。
从操作执行时间不难看出,建立索引给我们操作带来的巨大提升。
在学习索引是如何优化MySQL的性能之前,需要先知道MySQL为什么存在性能方面的问题。
一、认识MySQL与磁盘
1.1 MySQL与存储
MySQL 给用户提供数据存储服务的,早在之前我就介绍过,MySQL使用的库、表对数据存储,在Linux下都表现为特殊结构的文件,要想对数据进行持久化保存,这些文件最终都要存储在磁盘中,所有MySQL下数据存储在磁盘这个外设当中,相比于计算机其他电子元件,磁盘效率是比较低的,在加上IO本身的特征,可以知道,如何提交效率,是 MySQL 的一个重要话题。
Linux文件系统
这篇文章中详细介绍了系统对磁盘的访问,强烈建议看一下,这里就不介绍了
1.2 MySQL 与磁盘交互基本单位
MySQL
作为一款应用软件,可以想象成一种特殊的文件系统,它有着更多的IO
需求,而IO
操作会大大影响执行效率,所以,为了提高基本的IO
效率, MySQL
进行IO
的基本单位是 16KB
。
磁盘这个硬件设备的基本单位是
512
字节,操作系统在和磁盘交互时以4KB
为单位,而MySQL InnoDB
引擎使用16KB
和内存进行IO
交互。所以可以理解为,MySQL
和磁盘进行数据交互的基本单位是 16KB 。这个基本数据单元,在MySQL
这里叫做page
(注意和系统的page区分)。
SHOW GLOBAL STATUS LIKE 'innodb_page_size';
使用这个sql语句可以查找引擎页大小
。
二、 MySQL 数据交互核心:BufferPool 与 IO 优化机制
通过前文介绍可知,MySQL 中的数据文件在磁盘上以 page
(16KB) 为单位存储。当执行 CURD
(增删改查)操作时,MySQL 需先通过计算定位目标数据的位置——这一过程依赖 CPU 参与,而 CPU 只能直接操作内存数据。因此,数据必须先从磁盘临时加载到内存中,形成“磁盘一份、内存一份”的临时状态。待内存中的数据操作完成后,再通过特定策略将更新同步回磁盘,这一过程即涉及磁盘与内存的交互(IO
),而 IO
的基本单位正是 page
。
为高效管理内存中的数据、减少频繁的磁盘 IO,MySQL 服务器在启动时会在内存中申请一块专用的大内存区域,称为 BufferPool
(缓冲池)。它的核心作用是:
- 缓存热点数据:将频繁访问的
page
临时存储在内存中,避免每次操作都直接读写磁盘(局部性原理:当你对某一块数据操作时,你的下一次操将有很大概率,会使用后面的数据)。 - 优化 IO 效率:所有数据操作先在
BufferPool
中完成,操作完成后同步到磁盘,大幅减少磁盘 IO 次数(不可以操作一行,就获取一行)。
因此,减少系统与磁盘的 IO 次数是提升 MySQL 效率的核心原则,而 BufferPool
正是实现这一目标的关键机制——它通过内存缓存降低了磁盘 IO 对性能的影响。
三、索引的理解
3.1 测试案例
建立测试表
create table if not exists user (
id int primary key, --一定要添加主键哦,只有这样才会默认生成主键索引
age int not null,
name varchar(16) not null
);
插入多条记录,注意此处数据的主键顺序
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, '欧阳锋');
可以发现MySQL会将插入的数据默认变为有序,那么这样做有什么好处呢?
排序插入是为了优化查询效率
具体形式后面介绍
3.2 page
我们目前可以简单理解一个独立表文件是由一个或者多个Page
构成的,那么这个表该如何管理这些page
呢?
不同的 Page
,在 MySQL
中,都是 16KB
,使用 prev
和 next
构成双向链表(像这种结构在学习Linux时,我们经常遇到),因为有主键的问题, MySQL
会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的。
页内部存放数据的模块,实质上是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的,正是因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的(这一点在后面感受)
通过页模式,MySQL 查询时会将一整页数据(16KB)加载到内存,以此减少硬盘 IO 次数、提升性能。
但页模式内部采用链表结构,本质上需通过逐条数据比较定位目标。若表数据量大,且目标数据位于最后一个 page 的最后一条,仍需遍历全表,导致查找速度过慢。
3.3 页目录
在课本中查找知识点时,我们会选择优先查看目录,找到具体的页,再从页中查找知识点,这样的查找效率要比从头开始找,高效的多。在这里每一个独立的page或每一行数据,都可以视为“页”,而我们要做的是,给这些“页”添加属于他们的目录。
page内部:
现在,要在一个Page内部,查找id=3记录,直接通过目录2[3],直接进行定位新的起始位置,提高了效率。现在我们可以再次正式回答上面的问题了,为何通过键值 MySQL 会自动排序?可以很方便引入目录,提高查找效率
图中是为了迎合上面的数据,在实际情况下目录间的区间是很大的,在进行目录查找时,一次查找可以pass掉很多数据。
多page情况:
单表数据不断被插入的情况下, MySQL 会在容量不足的时候,自动开辟新的Page来保存新的数据,然后通过指针的方式,将所有的Page组织起来。
上面的方法帮我们提高了表内部遍历数据的效率,但是仍需要将每个page都,加载值内存中,为了进一步减少IO
操作,我们采用页目录的方式
依照这个思路,我们还可以对目录页再次添加目录管理,现在可以得出结论:
- Page分为目录页和数据页。目录页只放各个下级Page的最小键值。
- 查找的时候,自定向下找,只需要加载部分目录页到内存,即可完成算法的整个查找过程,大大减少了IO次数
这个结构最终就是一颗B+树,整个过程中我们所要IO的次数,就是整个结构数的高度
目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。
3.3 对比其他结构
InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?
- 链表?线性遍历
- 二叉搜索树?退化问题,可能退化成为线性结构
- AVL &&红黑树?虽然是平衡或者近似平衡,但是毕竟是二叉结构,相比较多阶B+树,意味着树整体过高,大家都是自顶向下找,层高越低,意味着系统与硬盘更少的IO Page交互。
- Hash?官方的索引实现方式中, MySQL 是支持HASH的,不过 InnoDB 和 MyISAM 并不支持Hash跟进其算法特征,决定了虽然有时候也很快(O(1)),不过,在面对范围查找就明显不行。
B树?最值得比较的是 InnoDB 为何不用B树作为底层索引?
B树节点,既有数据,又有Page指针,而B+,只有叶子节点有数据,其他目录页,只有键值和Page指针B+,叶子节点,全部相连,而B没有,为何选择B+
节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。叶子节点相连,更便于进行范围查找
具体结构特征,你可以搜点图片理解
四、聚簇索引 VS 非聚簇索引
MyISAM
引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为MyISAM
表的主索引,Col1
为主键
其中,MyISAM
最大的特点是,将索引Page
和数据Page
分离,也就是叶子节点没有数据,只有对应数据的地址相较于InnoDB
索引,InnoDB
是将索引和数据放在一起的。
现在我们就可以回答,第一篇文章遗留的问题了
innodb引擎
create table itest(
id int primary key,
name varchar(11) not null
)engine=InnoDB;
MyISAM引擎
create table mtest(
id int primary key,
name varchar(11) not null
)engine=MyISAM;
MyISAM
这种用户数据与索引数据分离的索引方案,叫做非聚簇索引,InnoDB
这种用户数据与索引数据在一起索引方案,叫做聚簇索引。
MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引。对于MyISAM ,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复。
下图就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别
同时我们以上表中的 Col3 建立对应的辅助索引如下图:
MyISAM 的非主键索引中叶子节点并没有数据,而只有对应记录的key值。
所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。这种过程,就叫做回表查询为何MyISAM 针对这种辅助(普通)索引的场景,不给叶子节点也附上数据呢?原因就是太浪费空间了。
那么普通索引为什么要存在呢?
当我们以符合间进行索引时如:(姓名,qq),只知道第一个键值,需要查找第二个键值,我们就可以直接查找:
对于复合索引,匹配原则是,从做到右的,也就是说,我们只需要知道姓名,就可以得到qq,这种普通索引方式,要比主键索引更快。
五、索引操作
5.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
这些特点,在介绍主键时都说过,只是当时没有提出索引的概念
5.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,等价于主键索引
5.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); //创建一个索引名为 idx_name 的索引
create table user10(id int primary key, name varchar(20),email varchar(30));create index idx_name on user10(name);
普通索引的特点:
- 一个表中可以有多个普通索引,普通索引在实际开发中用的比较多
- 如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引
5.4 查询索引
show keys from 表名;show index from 表名;
5.5 删除索引
//删除主键索引
alter table 表名 drop primary key;//其他索引的删除
alter table 表名 drop index 索引名; drop index 索引名 on 表名
余下指令你自己测试吧