【MySQL】索引

1.基本概念

  • MySQL的服务器,本质是在内存中的,所有的数据库的的CURD操作都是在内存中进行的,索引也是如此,提高算法效率的因素有组织数据的方式和算法本身
  • 索引的作用是提高数据库的性能,但是查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。所以它的价值,在于提高一个
    海量数据的检索速度。
  • 常见索引分为:
    主键索引(primary key)
    唯一索引(unique)
    普通索引(index)
    全文索引(fulltext)--解决中子文索引问题。
  • 假如创建一张存在800万数据的员工表,用普通sql语句查询员工号为十万级别的可能要花费好几秒,select * from EMP where empno=998877;这还是在本机一个人来操作,在实际项目中,如果放在公网中,假如同时有1000个人并发查询,那很可能就死机。通过创建索引alter table EMP add index(empno);,再使用上述查询语句就可以发现效率量级大幅提高

2.硬件理解

在Linux中文件系统部分已经初步认识了磁盘,这里做一下回顾和mysql中的重点。

磁盘整体结构:

盘片结构:

  • 扇区:
    1.数据库文件,本质其实就是保存在磁盘的盘片当中。也就是上面的一个个小格子中,就是我们经常所说的扇区。当然,数据库文件很大,也很多,一定需要占据多个扇区。
    2.从上图可以看出来,在半径方向上,距离圆心越近,扇区越小,距离圆心越远,扇区越大,目前所有扇区都是默认512字节,我们也这样认为。因为保证一个扇区多大,是由比特位密度决定的。这就注定了不同扇区的比特位密度不一定相同。不过最新的磁盘技术,已经慢慢的让扇区大小不同了,不过我们现在暂时不考虑。
    3.我们在使用Linux,所看到的大部分目录或者文件,其实就是保存在硬盘当中的。(当然,有一些内存文件系统,如: proc , sys 之类,我们不考虑),通过ls /var/lib/mysql -l 命令可以查看我们目前MySQL中的文件
    4.所以,最基本的,找到一个文件的全部,本质,就是在磁盘找到所有保存文件的扇区。而我们能够定位任何一个扇区,那么便能找到所有扇区,因为查找方式是一样的。

定位扇区

1.柱面(磁道):

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

2.每个盘面都有一个磁头,那么磁头和盘面的对应关系便是1对1的。所以,我们只需要知道,磁头(Heads)、柱面(Cylinder)(等价于磁道)、扇区(Sector)对应的编号。即可在磁盘上定位所要访问的扇区。这种磁盘数据定位方式叫做 CHS 。不过实际系统软件使用的并不是 CHS (但是硬件是),而是 LBA ,一种线性地址,可以想象成虚拟地址与物理地址。系统将 LBA 地址最后会转化成为 CHS ,交给磁盘去进行数据读取。

随机访问与连续访问

  • 随机访问:
    本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需要作比较大的移动动作才能重新开始读/写数据。
  • 连续访问:
    如果当次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次IO操作,这样的多个IO操作称为连续访问。

因此尽管相邻的两次IO操作在同一时刻发出,但如果它们的请求的扇区地址相差很大的话也只能称为随机访问,而非连续访问。

磁盘是通过机械运动进行寻址的,随机访问不需要过多的定位,故效率比较高。所以提高IO的效率就要减少磁盘寻址的次数或者说是减少机械运动的时间

总结

  • 现在已经能够在硬件层面定位,任何一个基本数据块了(扇区)。但是在系统软件上,不直接按照扇区(512字节,部分4096字节),进行IO交互,原因如下:
  • 1.如果操作系统直接使用硬件提供的数据大小进行交互,那么系统的IO代码,就和硬件强相关,换言之,如果硬件发生变化,系统必须跟着变化。与硬件耦合性太强
    2.从目前来看,单次IO 512字节,还是太小了。IO单位小,意味着读取同样的数据内容,需要进行多次磁盘访问,会带来效率的降低。
    3.之前学习文件系统,就是在磁盘的基本结构下建立的,文件系统读取基本单位,就不是扇区,而是数据块。
    故,系统读取磁盘,是以块为单位的,基本单位是 4KB 。

3.MySQL与磁盘交互的基本单位

  • MySQL 在 Linux 中的本质是一个系统进程,在网络架构中则是典型的应用层服务端。并且正因为 MySQL 在 Linux 中是一个后台进程(mysqld),它才能持续监听端口、处理网络请求,成为网络中的 "应用层服务端"

  • 而 MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB (后面统一使用 InnoDB 存储引擎讲解)

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

  • MySQL 的 page(数据页)数据库的 "磁盘存储积木"

  • 本质:

    MySQL 存储引擎(比如 InnoDB)为了高效管理磁盘数据,定义的最小磁盘 I/O 单元。简单说:MySQL 不会直接读写单个数据行,而是每次读写一整个 "数据页"------ 就像搬砖时不会只搬一块砖,而是搬一整箱砖,效率更高。

  • 关键细节:

    1.大小默认 16KB(InnoDB):比如你插入一条 1KB 的数据,MySQL 会把它放到一个 16KB 的数据页中,后续插入的相邻数据会优先填充这个页,直到装满再新建页;

    2.持久化存储:数据页最终存在磁盘的数据库文件中(如 InnoDB 的 .ibd 文件),即使 MySQL 停止,数据页也不会消失;

    3.仅服务于数据库:只用来存储数据库的 "核心资产"------ 表数据、索引,和系统其他应用无关。

  • Linux 系统的 page(内存页)操作系统的 "内存分配切片"

  • 本质:

    Linux 内核管理物理内存时,将内存划分为固定大小的最小内存分配单元。简单说:所有程序(包括 MySQL)要使用内存,内核不会零散分配,而是分配一整个 "内存页"------ 就像租房时不会只租 1 平米,而是租一整套(比如 100 平米),管理更简单。

  • 关键细节:

    1.大小固定(默认 4KB):由内核编译时确定,用户无法修改(除非重新编译内核);

    2.内存中的临时单元:内存页仅存在于物理内存 / 虚拟内存中,系统重启或进程退出后,内存页中的数据会丢失;

    3.服务于所有进程:不仅 MySQL 会用,Nginx、浏览器、终端等所有程序的内存占用,都会以 "内存页" 为单位分配。

  • 两者的关联:

    MySQL 的数据页和系统的内存页是「嵌套 / 映射关系」,但完全独立管理:

    1.当 MySQL 需要读取磁盘上的一个 16KB 数据页时,Linux 内核会把这个 16KB 的数据加载到内存中 ------ 此时会占用 4 个 Linux 内存页(因为 16KB ÷ 4KB / 个 = 4 个);

    2.MySQL 操作的是 "自己的 16KB 数据页"(逻辑层面),而 Linux 内核管理的是 "物理内存中的 4KB 内存页"(物理层面);

    3.MySQL 完全不知道 Linux 内存页的大小,只关心自己的 16KB 数据页;Linux 内核也不知道 MySQL 数据页的存在,只把它当作普通的 "文件数据" 加载到内存页中。

建立共识

  • MySQL 中的数据文件,是以page为单位保存在磁盘当中的。

  • MySQL 的 CURD 操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。

  • 而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中。所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page(mysql数据页),因为局部性原理所以不是用多少加载多少,使用page效率更高。

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

  • Linux中mysql是一个后台进程,也有自己的文件缓存区,通过文件描述符标识,通过写操作将buffer pool中数据写入文件缓存区,存在fsync系统调用将文件缓存区中数据刷新磁盘中去

  • 为了更高的效率,一定要尽可能的减少系统和磁盘IO的次数

4.索引的理解

  • 场景:当向一个具有主键的表中,乱序插入数据,发信数据自动会排序

理解page

  • MySQL 中要管理很多数据表文件,一定存在多个page,而要管理好这些文件,就需要 先描述,在组织 ,我们目前可以简单理解成一个个独立文件是有一个或者多个Page构成的。在buffer pool内部,对mysql中的page进行了一个建模

  • page不仅仅是一个内存块,内部也要写入对应的管理信息

    不同的 Page ,在 MySQL 中,都是 16KB ,使用 prev 和 next 构成双向链表

    因为有主键的问题, MySQL 会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的。

  • 当我们向一个具有主键的表中,乱序插入数据,发现数据会自动排序,这是mysql自动做的,目的是可以方便的引入目录。

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

页目录

  • 当我们看书的时候想要看某个章节时,可以通过目录快速定位,否则只能一页页翻,所以书中的目录是多花了纸张来记录,但提高了查找效率,目录是一种空间换时间的做法
  • 单页目录

在一个Page内部,我们引入了目录。比如,我们要查找id=4记录,之前必须线性遍历4次,才能拿到结果。现在直接通过目录2[3],直接进行定位新的起始位置,提高了效率。

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

    在单表数据不断被插入的情况下, MySQL 会在容量不足的时候,自动开辟新的Page来保存新的数据,然后通过指针的方式,将所有的Page组织起来。
  • 问题?
    这样,我们就可以通过多个Page遍历,Page内部通过目录来快速定位数据。可是,这样也有效率问题,在Page之间,也是需要 MySQL 遍历的,遍历意味着依旧需要进行大量的IO,将下一个Page加载到内存,进行线性检测。这样就显得我们之前的Page内部的目录,有点杯水车薪了。解决方案,其实就是我们之前的思路,给Page也带上目录。

    新增page来专门存放原page中最小数据的键值作为目录,每个目录项的构成是键值+指针,避免了查找原page中某个数据时对page间的遍历,这种目录管理的级别是页,而页内目录管理的级别是行,可以把这些page称为目录页,目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址
  • 问题?
    虽然顶层的目录页少了,但是每次检索数据时还是要从头开始遍历,所以还可以加目录页,直到最上层只有一个目录页,此时的结构就是b+树,到此为止才对表完成了主键索引,再进行特定数据查找时,不需要一个个遍历page,只需要在page中判断索引值并通过指针找到对应的page即可,减少了IO次数,效率随之提高
  • 查找的时候,自定向下找,只需要加载部分目录页到内存,不需要将整颗b+树都加载到内存中,即可完成算法的整个查找过程,大大减少了IO次数
  • InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?
    链表?线性遍历
    二叉搜索树?退化问题,可能退化成为线性结构(插入有序数据)
    AVL &&红黑树?虽然是平衡或者近似平衡,但是毕竟是二叉结构,节点仅存2个子节点,磁盘page空间利用率极低相比较多阶B+,意味着树整体过高,大家都是自顶向下找,层高越低,意味着系统与硬盘更少的IO Page交互。虽然你很秀,但是有更秀的。
    Hash?官方的索引实现方式中, MySQL 是支持HASH的,不过 InnoDB 和 MyISAM 并不支持.Hash跟进其算法特征,决定了虽然有时候也很快(O(1)),不过,在面对范围查找就明显不行,并且无法利用索引排序,Hash 结果无序

b树与b+树

  • b树:
  • b+树:
  • 区别:
    B树节点,既有数据,又有Page指针,而B+,只有叶子节点有数据,其他目录页,只有键值和Page指针B+叶子节点,全部相连,而B没有
  • 选择b+的原因:
    节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。叶子节点相连,更便于进行范围查找

5.聚簇索引与非局促索引

  • 非聚簇索引
    MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为 MyISAM表的主索引, Col1 为主键。

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


    MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引。
    对于 MyISAM ,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复。和主键索引没有差别
  • 聚簇索引
    InnoDB 这种用户数据与索引数据在一起索引方案,叫做聚簇索引


    相较于MyISAM,只生成了两个后缀文件,对应表结构数据、主键索引和用户数据
  • InnoDB 除了主键索引,用户也会建立辅助(普通)索引

    InnoDB 的非主键索引中叶子节点并没有数据,而只有对应记录的key值。
    所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。这种过程,就叫做回表查询
    为何 InnoDB 针对这种辅助(普通)索引的场景,不给叶子节点也附上数据呢?原因就是太浪费空间了,没必要让同一份数据出现两次。
  • 注意:
    1.即使无手动索引,InnoDB 也有默认 B + 树(聚簇索引),本质是没有可用的 "二级索引 B + 树",只能遍历聚簇索引 B + 树的所有叶子节点(全表扫描)
    2.无手动索引时非聚簇索引 B + 树不存在,只有聚簇索引是 "隐式强制创建"(保障数据存储的基础结构),而非聚簇索引的核心定位是 "优化特定查询的辅助索引"

6.索引操作

创建主键索引

三种方式:


特点:

1.一个表中,最多有一个主键索引,当然可以是复合主键

2.主键索引的效率高(主键不可重复)

3.创建主键索引的列,它的值不能为null,且不能重复

4.主键索引的列基本上是int

唯一索引的创建

-- 在表定义时,在某列后直接指定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);

唯一索引的特点:

1.一个表中,可以有多个唯一索引

2.查询效率高

3.如果在某一列建立唯一索引,必须保证这列不能有重复数据

4.如果一个唯一索引上指定not null,等价于主键索引

普通索引的创建

--在表的定义最后,指定某列为索引

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);

普通索引的特点:

一个表中可以有多个普通索引,普通索引在实际开发中用的比较多

如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引

全文索引

  • 前面总结的三种索引统称为传统索引,基于精确的字段值快速定位行记录,核心是 "值匹配"
    适用查询:等值查询(=)、范围查询(>/</BETWEEN)、排序 / 分组
    不支持:模糊的文本语义查询(如 "包含某关键词""某短语的相关内容")
  • 全文索引:
    目标:针对文本内容实现语义级的关键词检索,核心是 "内容匹配"。
    适用查询:全文关键词查询(MATCH...AGAINST)
    不支持:等值或范围的字段值匹配,无法用于排序 / 分组。
  • 当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文索引机制,但是有要求,要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版(coreseek)
  • 查询有没有包含database的数据
    使用普通查找的结果如下,并没有用到全文索引

    可以通过explain工具来查看是否用到索引
  • 使用全文索引

查询索引

第一种方法: show keys from 表名

第二种方法: show index from 表名;(作用和keys相同)

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

删除索引

第一种方法-删除主键索引: alter table 表名

drop primary key;(由于主键唯一,所以可以直接删除不用指定主键名)

第二种方法-其他索引的删除: alter table 表名 drop index 索引名;(索引名就是show keys from 表名中的Key_name 字段,包括普通索引和唯一索引)

mysql> alter table user10 drop index idx_name;

第三种方法方法: drop index 索引名 on 表名

mysql> drop index name on user8

索引创建原则

1.比较频繁作为查询条件的字段应该创建索引

2.唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件

3.更新非常频繁的字段不适合作创建索引

4.不会出现在where子句中的字段不该创建索引

索引最左匹配原则

  • 定义:
    索引最左匹配原则是 MySQL 使用联合索引(多列索引) 时的核心检索规则:MySQL 会优先匹配联合索引中最左侧的列,并依次向右匹配,一旦遇到无法匹配的条件(如范围查询、跳过某列),则停止向右匹配索引,未匹配的列无法利用索引加速。
  • 以name和addr为复合唯一索引为例,测试
    场景1:以左列进行匹配符合条件

通过explain来查看是否使用了索引,重点关注

1.type:索引匹配级别(ref/range 表示索引生效,ALL/index 表示索引未高效匹配);

2.key:实际使用的索引(应为索引名才表示索引命中);

场景2:跳过左列,直接以右列匹配不行

type: index:这是 "索引遍历" 的核心标识,代表 MySQL 没有通过非叶子节点缩小范围,而是遍历了整个索引树的叶子节点;

场景 3:左列范围查询 + 右列等值→ 右列索引失效

场景 4:跳过左列,右列 + 其他列→ 索引完全失效(全表扫描)

索引覆盖

  • 索引覆盖是指:查询语句需要的所有列(SELECT、WHERE、ORDER BY 等涉及的列),都能从索引的 B + 树节点中直接获取,无需回表查询聚簇索引的整行数据。
  • 通俗理解
    把索引比作 "图书的目录":
    非索引覆盖:查 "第 5 章的内容",目录只标了 "第 5 章在第 100 页",需翻到 100 页(回表)才能看内容;
    索引覆盖:目录直接把 "第 5 章的所有文字" 都印在目录页上,无需翻正文页(回表),直接从目录(索引)就能拿到所有需要的信息。
  • 核心价值
    避免 "回表操作"(二级索引→聚簇索引的磁盘 IO),大幅减少磁盘访问次数,是优化查询性能的核心手段之一;InnoDB 中,主键会默认包含在所有二级索引的叶子节点中
相关推荐
jnrjian1 小时前
Hash index initrans 的修改及 partition的增
数据库·oracle
一 乐2 小时前
美食推荐|基于springboot+vue的美食分享系统设计与实现(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·美食
星环处相逢2 小时前
MySQL MHA 全解析与实战部署指南
数据库·mysql
qq_12498707532 小时前
基于springboot+vue+mysql的校园博客系统(源码+论文+部署+安装)
java·vue.js·spring boot·mysql·毕业设计
一只专注api接口开发的技术猿2 小时前
构建电商数据中台:基于淘宝 API 关键词搜索接口的设计与实现
大数据·开发语言·数据库
未来之窗软件服务2 小时前
服务器运维(十八)国产化数据库服务漏洞安全——东方仙盟炼气期
运维·服务器·数据库·服务器运维
MM_MS2 小时前
SQL Server数据库和Visual Studio (C#)联合编程
开发语言·数据库·sqlserver·c#·visual studio
韩立学长3 小时前
基于Springboot民族文化与旅游网站j9x74dt2(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·旅游
runfarther3 小时前
mysql_mcp_server部署及应用案例
linux·mysql·centos·mcp