MySQL索引特性
1.索引概念
(1)没有索引,可能会有什么问题?
让我们深入探讨索引的优势:
索引是数据库性能优化的利器,能以极低成本实现显著提升。无需增加内存、修改程序或调整SQL语句,仅需执行正确的create index操作,查询速度就能实现数百甚至上千倍的飞跃。然而,任何优势都有其代价——查询效率的提升伴随着插入、更新和删除操作性能的下降,这些写操作会带来大量额外的IO开销。因此,索引的核心价值在于大幅提升海量数据的检索效率。
常见索引分为: 主键索引(primary key)
唯一索引(unique)
普通索引(index)
全文索引(fulltext)--解决中子文索引问题。
eg:
先整一个海量表,在查询的时候,看看没有索引时有什么问题?
1.创建海量表
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);
将上述SQL保存到文件,然后在MySQL中通过source命令依次执行这些SQL语句即可,如下:
构建一个8000000条记录的数据,index_demon数据库。
根据上图我们可以看出当前表是没有任何索引的!!!!
(2)查询员工编号为123456的员工
可以看到耗时0.17秒,这还是在本机一个人来操作,在实际项目中,如果放在公网中,假如同时有 1000个人并发查询,那很可能就死机。
(3)解决方法,创建索引
(4)换一个员工编号,测试看看查询时间
如图所示,查询时间几乎可以忽略不计。这是因为在员工工号字段创建索引后,系统可以直接通过底层建立的数据结构快速定位目标数据,从而显著提升检索效率,这正是索引的核心价值所在。
2.认识磁盘
(1)MySQL与存储
MySQL 作为数据存储服务,其核心功能是将数据持久化保存在磁盘设备中。由于磁盘属于机械部件,其读写速度远低于计算机的其他电子元件,再加上 I/O 操作本身的特性,因此如何提升 MySQL 的性能效率成为其关键优化方向。
(2)磁盘
在磁盘中我们可以看到很多盘片(下图为磁盘中的一个盘片)
磁道:磁盘表面由多个同心圆组成,这些同心圆被称为磁道。每条磁道都有唯一的编号,其中最外圈的磁道编号为0。
扇区:每个磁道被进一步划分为若干扇区。每个扇区的存储容量固定为512字节,并且具有独立的编号标识。
近三十年来,扇区大小一直是512字节,但最近几年正在迁移到更大、更高效的4096字节扇区,通常称为4K扇区。
(3)扇区
数据库文件本质上是存储在磁盘盘片上的数据集合。这些数据以扇区为单位进行存储,每个扇区相当于磁盘上的一个小存储单元。由于数据库文件通常体积较大,它们往往需要占用多个连续的扇区空间。
我们在使用Linux,所看到的大部分目录或者文件,其实就是保存在硬盘当中的。所以,最基本的,找到一个文件的全部,本质,就是在磁盘找到所有保存文件的扇区。 而我们能够定位任何一个扇区,那么便能找到所有扇区,因为查找方式是一样的。
定位扇区
柱面(磁道)是指多盘片磁盘中,所有盘面上相同半径的磁道组成的立体结构。每个盘面都配有独立的磁头,形成1:1的对应关系。
磁盘定位采用CHS(柱面-磁头-扇区)编号系统,通过这三个参数即可准确定位目标扇区。不过现代系统实际使用的是LBA(逻辑块地址),这是一种线性编址方式,类似于虚拟地址与物理地址的映射关系。系统会将LBA地址最终转换为CHS格式供磁盘读取。我们暂时无需深入理解转换细节,只需建立这个基本概念框架即可。
(4)我们现在已经能够在硬件层面定位,任何一个基本数据块了(扇区)。那么在系统软件上,就直接按照扇区 (512字节,部分4096字节),进行IO交互吗?
答案当然不是。
如果操作系统直接基于硬件的原生数据单元进行交互,那么系统的IO代码就会与底层硬件高度耦合。这就意味着一旦硬件规格发生变化,系统也必须进行相应调整。
从实际应用来看,512字节的单次IO操作确实过于局限。较小的IO单位会导致读取相同数据量时需要执行更多次磁盘访问,从而显著降低整体效率。
在文件系统的设计中,我们已观察到这种优化思路:文件系统运行在磁盘基础架构之上,但其读取的基本单位并非扇区,而是更大的数据块。
故,系统读取磁盘,是以块为单位的,基本单位是 4KB 。
(5)磁盘随机访问(Random Access)与连续访问(Sequential Access)
随机访问:当前IO操作的扇区地址与上一次不连续,导致磁头需要大幅移动才能重新定位,从而进行数据读写。
连续访问:当当前I/O请求的扇区地址与上一个I/O操作结束的扇区地址连续时,磁头可以立即开始新的I/O操作,这种连续的I/O操作序列称为连续访问。
因此,即使相邻的两次IO操作同时发出,若请求的扇区地址差异较大,这仍属于随机访问而非连续访问。
由于磁盘通过机械运动实现寻址,连续访问无需频繁定位,因而具有更高的效率。
3.MySQL与磁盘交互基本单位
MySQL作为一款数据库软件,可被视为一种高效的文件系统。由于其面临更高的IO需求,为提升基础IO效率,MySQL采用16KB作为基本IO操作单位。
执行SHOW
命令可查看系统全局变量,其中显示InnoDB存储引擎的基本交互单位为16KB。具体如下:
换句话说,磁盘硬件的基本读写单位是512字节,而MySQL InnoDB引擎与磁盘进行数据交互时采用16KB作为基本单元。在MySQL中,这个16KB的数据单元被称为page(注意与操作系统的page概念区分)。
4.建立共识
MySQL 数据以页(page)为单位存储在磁盘中。
所有增删改查(CURD)操作都需要计算出对应的插入位置,或定位待修改/查询的数据。计算过程需要 CPU 参与,而 CPU 处理数据的前提是先将数据加载到内存中。
因此,在执行操作时,数据会同时存在于磁盘和内存中。完成内存数据处理后,系统会根据特定的刷新策略将变更同步到磁盘。这一过程涉及内存与磁盘的数据交换,即 I/O 操作,其基本单位也是页。
为了优化上述流程,MySQL 服务器在运行时会申请一块称为 Buffer Pool 的大内存空间,专门用于数据缓存。Buffer Pool 本质上是一块预留的内存区域,用于高效处理与磁盘的 I/O 交互。
为了提升整体效率,关键在于尽量减少系统与磁盘之间的 I/O 操作次数。
5.索引的理解
(1)建立测试表(此处将id设为主键,默认InnoDB存储引擎)
(2)插入多条记录
请注意,我们并未按照主键顺序来插入这些记录。
(3)查看插入结果
真是意外发现!原来默认是有序排列的。不知道这个巧妙的设计出自谁手?这样排序有什么特别的优势呢?
根本原因是创建表时设置了主键,即便数据以乱序方式插入,MySQL底层仍会自动按照主键进行排序。
(4)中断一下---为何IO交互要是 Page
为何MySQL和磁盘进行IO交互的时候,要采用Page的方案进行交互呢?用多少,加载多少不好吗?
假设MySQL需要查询id=2的记录,如果采用单条记录加载的方式:首次加载id=1需要1次IO,第二次加载id=2还要1次IO,总共需要2次IO。若要查询id=5,则需要5次IO。
但如果将这些记录都存储在同一个Page中(16KB的容量可以保存大量记录),情况就完全不同:首次查询id=2时,整个Page会被加载到MySQL的Buffer Pool,仅需1次IO。后续查询id=1、3、4、5等记录时,由于数据已在内存中,无需再执行IO操作。因此,使用单Page存储能显著减少IO次数。
你可能会问:如何确保下次查询的数据就在这个Page中?虽然无法严格保证,但基于局部性原理,这种可能性很高。实际上,影响IO效率的关键因素往往不是单次数据量的大小,而是IO操作的次数。
(5)理解单个Page
MySQL 中要管理很多数据表文件,而要管理好这些文件,就需要成一个个独立文件是有一个或者多个Page构成的。 先描述,在组织,我们目前可以简单理解。
在 MySQL 中,每个 Page 的大小固定为 16KB,并通过 prev 和 next 指针组成双向链表结构。由于主键的存在,MySQL 默认会按照主键值对数据进行排序。从 Page 内部的数据记录可以看出,这些数据不仅有序排列,而且彼此之间保持着紧密的关联关系。
为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗? 插入数据时排序的目的,就是优化查询的效率。 页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询 的效率是必须的。 正式因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是 可以提前结束查找过程的。
为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?
插入数据时排序的目的,就是优化查询的效率。
页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的。
正式因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是可以提前结束查找过程的。
(5)理解多个Page
通过上述分析可以看出,当前页模式的主要功能是在查询数据时将整页数据加载到内存,以此减少硬盘IO次数来提升性能。然而,该模式内部实际上采用了链表结构,通过前驱和后继指针连接数据,本质上仍需逐条比较才能获取特定数据。
当数据量达到1000万条时,必然需要多个页来存储。这些页之间通过双向链表相互连接,且每个页内部的数据同样采用链表结构。这种情况下,查找特定记录只能进行线性遍历,效率极其低下。
(6)页目录
在阅读《谭浩强C程序设计》时,查找特定章节通常有两种方法:
- 从头开始逐页翻阅,直到找到目标内容
- 通过书籍目录定位(假设指针章节在234页),直接翻到对应页码。目录查找可采用顺序查找,但由于目录内容较少,能显著提升定位效率
从本质上说,书籍目录虽然会占用额外的纸张空间,但能大幅提高查找效率。因此,目录是一种典型的"以空间换时间"的设计理念。
(7) 单页情况
针对上面的单页Page,我们能否也引入目录呢?当然可以!!!
目前,我们在 Page 内部引入了目录机制。例如,要查找 id=4 的记录,原先需要线性遍历 4 次才能获取结果。现在通过目录索引(如 2[3])可以直接定位到新的起始位置,显著提升了查询效率。
(8)多页情况
MySQL 中每个页的大小固定为16KB。随着数据量增长,单个页无法存储全部数据,因此需要通过多页来存储数据。
当单表数据持续插入时,MySQL会在当前页容量不足时自动分配新页存储数据,并通过指针将所有页链接起来。
(请注意,上图展示的是理想结构。在实际操作中,为了保持整体有序性,新插入的数据可能不会出现在新Page上,此处仅作演示用途。)
这样,我们可以通过多个Page来遍历数据,每个Page内部通过目录实现快速定位。但这种方式仍存在效率问题:在Page之间切换时,MySQL仍需进行遍历操作,这意味着需要频繁执行IO操作,将下一个Page加载到内存中进行线性检测。如此一来,之前设计的Page内部目录就显得作用有限了。
那么如何解决呢?解决方案,其实就是我们之前的思路,给Page也带上目录。
通过目录项可以指向特定页面,该目录项存储的是目标页面中最小的键值。
与页内目录不同,这种目录管理的是页面级别,而页内目录管理的是行级别。
每个目录项由两部分组成:键值和指针(图中未完整展示)。
目录页用于管理页目录结构,其中存储的数据对应于所指向页中的最小数据项。通过比较这些数据,可以快速定位目标页,再借助指针跳转至下一个页。
其实目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。
检索数据时,我们该从哪里入手呢?虽然顶层目录页减少了,但仍需遍历。不过别担心,我们可以通过增加目录页来解决这个问题。
这就是著名的B+树结构!没错,至此我们已经成功为user表构建了完整的主键索引。
通过任意ID查询时,所需扫描的页数明显减少,从而降低了IO操作次数,显著提升了查询效率。
总结:Page分为目录页和数据页两类。目录页仅存储下级Page的最小键值。
查找时采用自上而下的方式,只需将部分目录页加载到内存,就能完成整个查找过程,显著减少了IO操作次数。
(9)InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?
链表:仅支持线性遍历,效率较低
二叉搜索树:存在退化风险,可能退化为线性结构
AVL树与红黑树:虽然是平衡或近似平衡结构,但作为二叉树,相比多阶B+树而言,树高较高。由于查询是自顶向下进行的,树高越低意味着与磁盘的IO Page交互次数越少
哈希表:MySQL官方索引实现支持哈希,但InnoDB和MyISAM引擎不支持。哈希算法虽然查询速度快(O(1)),但不适合范围查找,还存在其他局限性(可进一步查阅相关资料)
(10)B+ vs B
目前这两棵树的显著区别在于:
数据存储方式:
- B树节点同时包含数据和Page指针
- B+树只有叶子节点存储数据,其他目录页仅包含键值和Page指针
节点连接方式:
- B+树的叶子节点通过指针全部相连
- B树没有这种连接结构
为何选择B+:
节点不存储实际数据,因此单个节点能够容纳更多键值。这种设计让树结构更加紧凑,减少树的高度,从而降低IO操作频率。同时,叶子节点通过指针相连,大幅提升了范围查询的效率。
(11)聚簇索引 VS 非聚簇索引
MyISAM 存储引擎-主键索引
MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为 MyISAM 表的主索引, Col1 为主键。
MyISAM 的核心特性在于将索引页(Index Page)与数据页(Data Page)完全分离。其叶子节点仅存储指向实际数据的地址信息,而非数据本身。
与 InnoDB 索引不同,InnoDB 将索引和数据存储在同一位置。
其中, MyISAM 这种用户数据与索引数据分离的索引方案,叫做非聚簇索引
其中, InnoDB 这种用户数据与索引数据在一起索引方案,叫做聚簇索引
--表结构数据--
该表除了默认创建的主键索引外,用户还可以基于其他列创建辅助索引(或称普通索引)。
MyISAM存储引擎中,辅助索引(普通索引)与主键索引在结构上是相同的,唯一的区别在于主键索引要求值唯一,而普通索引允许重复值。
下图就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别
同样, InnoDB 除了主键索引,用户也会建立辅助(普通)索引,我们以上表中的 Col3 建立对应的辅助索引如下图:
可以看到, InnoDB 的非主键索引中叶子节点并没有数据,而只有对应记录的key值。
所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。这种过程,就叫做回表查询
为何I nnoDB 针对这种辅助(普通)索引的场景,不给叶子节点也附上数据呢?原因就是太浪费空间了。
6.索引操作
6.1创建主键索引
第一种方式:(在创建表的时候,直接在字段名后指定 primary key)
第二种方式:(在创建表的最后,指定某列或某几列为主键索引)
第三种方式:(创建表以后再添加主键)
主键索引的核心特性:
- 唯一性约束:每个表最多只能有一个主键索引(可以是单列或多列组合的主键)
- 高效性能:主键索引具有极高的查询效率(基于其唯一不可重复的特性)
- 数据约束:主键列必须满足非空且唯一的值要求
- 常规实践:主键列通常采用int类型作为最佳实践
6.2唯一键索引的创建
第一种方式:(在表定义时,在某列后直接指定unique唯一属性。)
第二种方式:(创建表时,在表的后面指定某列或某几列为unique)
第三种方式:(创建表以后再添加唯一键)
唯一索引的特点:
一个表可以包含多个唯一索引
查询性能较高
为某列创建唯一索引时,需确保该列数据不重复
若在唯一索引上设置NOT NULL约束,其作用等同于主键索引
6.3普通索引的创建
第一种方式:(在表的定义最后,指定某列为索引)
第二种方式:(创建完表以后指定某列为普通索引)
第三种方式:(创建一个索引名为 idx_name 的索引)
普通索引的特点:
一张表可以创建多个普通索引,这种索引在实际开发中应用较为广泛。
当需要为某列建立索引,而该列包含重复值时,使用普通索引是最合适的选择。
6.4全文索引的创建
MySQL 的全文索引功能专为处理文本字段和大容量文本检索而设计。需要注意的是,该功能仅适用于 MyISAM 存储引擎的表,且默认仅支持英文检索。如需实现中文全文检索,可采用 Sphinx 的中文版解决方案(Coreseek)。
eg:(创建文本字段表)
(1)查询有没有database数据
如果使用如下查询方式,虽然查询出数据,但是没有使用到全文索引
可以用explain工具看一下,是否使用到索引
上图:key为null表示没有用到索引。
(2)如何使用全文索引呢?
通过explain来分析这个sql语句
由上述key:title可知,使用到了索引
6.5查询索引
第一种方法:
show keys from 表名
第二种方法:
show index from 表名;
第三种方法(信息比较简略):
desc 表名;
6.6删除索引
第一种方法-删除主键索引:
alter table 表名 drop primary key;
第二种方法-其他索引的删除:
alter table 表名 drop index 索引名;索引名就是show keys
from 表名中的 Key_name 字段
第三种方法方法:
drop index 索引名 on 表名
6.7索引创建原则
比较频繁作为查询条件的字段应该创建索引
唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件
更新非常频繁的字段不适合作创建索引
不会出现在where子句中的字段不该创建索引