MySQL:一文掌握MySQL索引

目录

  • 概念
  • 优缺点
  • 索引的数据结构
    • Hash索引
    • 有序数组索引
    • 二叉搜索树
    • 平衡二叉树
    • B树
    • B+树
  • 索引的物理结构
    • MyISAM存储引擎
    • InnoDB存储引擎
  • 索引的分类
  • 页、区、段
  • change buffer 和索引
  • 回表和覆盖索引
  • 索引优化
  • 面试题
    • 索引哪些情况下会失效
    • 什么是索引下推
    • 主键选择自增和uuid的区别

概念

官方定义:索引(Index)是帮助MySQL高效获取数据的数据结构。

在数据库中,索引被定义为一种特殊的数据结构,由数据库中的一列或多列组合而成,可以用来快速查询数据表中某一特定值的记录,就像一本书的目录一样。索引是在表的字段的基础上建立的一种数据库对象。

优缺点

1、优点

1)减少磁盘的IO次数,提高查找效率,这是索引的核心优势
2)通过创建唯一索引,可以保证表中每行数据记录的唯一性
3)在使用分组和排序子句进行数据查询时,可以显著 减少查询中分组和排序的时间 ,降低了CPU的消耗。

2、缺点

1)占用磁盘空间:创建索引需要占据存储空间,如果表中数据量巨大,索引数量多,索引文件可能比数据本身的文件会更快到达硬件限制。

2)降低表数据更新速度:增删改数据场景,索引会影响更新效率,因为在增删改数据时,不仅要修改数据本身,而且要对数据关联的索引进行重建,会影响更新表的整体速度

索引并不是越多越好,我们要根据具体业务场景合理创建索引,当我们更新超大表数据时,最好的办法是先删除表中的索引,然后插入数据,插入成功后重建索引。

索引的数据结构

能作为索引的数据结构可以有很多,但是mysql中选择了B+树,那为什么使用B+树而不是二叉查找树、平衡二叉树或者B树等其他数据结构呢?下面一一看下各个数据结构的特点就知道了。

Hash索引

哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的键即 key,就可以找到其对应的值即 Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。不可避免地,多个 key 值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的有很多方法,工程上最常用的是,拉出一个链表。假设,你现在维护着一个身份证信息和姓名的表,需要根据身份证号查找对应的名字,这时对应的哈希索引的示意图如下所示:

MySQL:一文掌握MySQL索引_第1张图片

所以,哈希表这种结构适用于只有等值查询的场景。

问题:不支持范围查询,有时候会有hash冲突问题。

有序数组索引

而有序数组在等值查询和范围查询场景中的性能就都非常优秀。还是上面这个根据身份证号查名字的例子,如果我们使用有序数组来实现的话,示意图如下所示:

MySQL:一文掌握MySQL索引_第2张图片

这里我们假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。这时候如果你要查 ID_card_n2 对应的名字,用二分法就可以快速得到,这个时间复杂度是 O(log(N))。

如果只看查询效率,有序数组就是最好的选择。

问题:当需要进行更新数据时,需要将后面的数据全部后移,效率低,有序数组只适合做静态查询

二叉搜索树

二叉搜索树是二叉树的一种,根节点/父节点的左子树上的节点都比根节点/父节点小,而右子树上的节点都比根节点/父节点要大。

MySQL:一文掌握MySQL索引_第3张图片

整棵树根据ID进行排序,左子树的ID值 < 节点ID值 < 右子树的ID值,这样当我们查找 id:450 时,就可以得到对比路径

500 -> 400 -> 450 比较三次即可,时间复杂度为O(log(N)); 插入操作,也只需要查找到叶子节点根据规则,插入一个节点即可,非常方便。 但是这种结构也不是最优的,当我们节点插入顺序不合理的时候,就会出现一边倒的情况,比如下面这种情况,此时的查找就和普通的顺序查找基本一样了。

MySQL:一文掌握MySQL索引_第4张图片

因为每个节点有两个子节点,数据量的增大必然导致高度的快速增加,对那种逐渐增大的数据查询相当于链表查询,效率低下,显然这个不适合作为大量数据存储的基础结构。

问题:数据库的索引不止存在于内存中,还要写在磁盘上,每次查询都要去访问数据块,进行一次磁盘I/O,当存储大数据量时,树的高度很高,查询还是慢;有些情况可能退化为链表。

平衡二叉树

平衡二叉树是是二叉搜索树的一种拓展,要求左右两颗子树树高差值不能超过1。 因此当新插入的数据破坏了树的平衡(左右两颗子树树高差值大于1),就要通过内部算法LL操作,LR操作等,对树的节点进行重新排列进而达到新的平衡,这里具体的实现细节我们就不再深究了。

平衡二叉树解决上面二叉树可能退化为链表的问题,但是没有解决在大数据量时,树的高度很高的问题。

B树

为了降低树的高度,减少访问磁盘的次数,人们又提出了新的数据结构,B树,也叫B-树,还叫多路平衡查找树,通过名字就能够看出来,B树和平衡二叉树,就是树的分叉数不一样而已,其他的都是一样的。

B树特点:

  1. B树的节点中存储着多个元素,每个内节点有多个分叉。
  2. 节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点都储存数据。
  3. 父节点当中的元素不会出现在子节点中。
  4. 所有的叶子结点都位于同一层,叶节点具有相同的深度,叶节点之间没有指针连接。

B树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,高度也会很低,子节点的个数一般称为阶。B树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。

问题:

  1. 范围查询非常缓慢。B树不支持范围查询的快速查找,你想想这么一个情况如果我们想要查找10和35之间的数据,查找到15之后,需要回到根节点重新遍历查找,需要从根节点进行多次遍历,查询效率有待提高。
  2. 因为所以节点都存储数据,所有当单行数据比较大时,所占空间会变大。这时,一个页中可存储的数据量就会变少,树相应就会变高,磁盘IO次数就会变大。

B+树

B+树,是B树的升级版,改进点在于,B树的非叶子节点和叶子节点都会存储数据,但是B+树只有叶子节点才会存储数据,非叶子节点至存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。

因为B+Tree只在叶子节点存储数据,所以B+Tree的单个节点的数据量更小,只存储索引,可以放更多的索引,在相同的磁盘I/O次数下,就能查询更多的节点。另外,B+Tree叶子节点采用的是双指针连接,适合MySQL中常见的基于范围的顺序查找。

一个节点可以放16kb,1170个元素,1170117016=2千多万,高度为3,即使是2千万数据,B+树的高度为3,这样我们查找数据的效率会比较高,高度是由非叶子节点能放多少个索引元素决定的。

没有有特别说明,默认都是使用B+树结构组织的索引。

索引的物理结构

索引的物理存储结构,在不同的存储引擎中有着不同的实现方式,这里我们重点介绍两个存储引擎的存储方式,也是最常用的两个存储引擎,一个是MyISAM存储引擎,一个是InnoDB存储引擎。

MyISAM存储引擎

MyISAM存储引擎对于一个表的存储是分为三个文件存储在硬盘上,对于一个user表,分别是

    • user.frm : 表结构文件
    • user.MYD : 数据文件
    • user.MYI : 索引文件

可以发现,MyISAM存储引擎,将一个表的索引和数据分开存在了不同的文件中。那么具体的情况是什么样的呢?

  1. 主键索引

如下图,ID列为主键索引。

MySQL:一文掌握MySQL索引_第5张图片

可以看到非叶子节点存储键值,叶子节点存储键值以及指向该主键所对应的数据的地址指针,这也是为什么MyISAM存储引擎,将索引和数据存储在不同文件的原因。每次索引查询,当MyISAM查询,找到对应的叶子节点时,根据该对应的地址指针,再去数据文件找到对应的数据。

  1. 非主键索引

MySIAM的非主键索引的存储结构我们可以发现,是和主键索引结构完全一样的,没有什么区别,叶子节点的数据存储的都是行记录的磁盘地址。只是主键索引的键值是唯一的,而非主键索引的键值可以重复。

查询数据时,由于非主键索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在非主键索引树中检索数据。

InnoDB存储引擎

InnoDB存储引擎对于一个表的存储是分为两个文件存储在硬盘上,对于一个user表,分别是

    • user.frm : 表结构文件
    • user.ibd : 索引和数据文件

可以发现,InnoDB存储引擎,将一个表的索引数据分开存在了相同的文件中。那么具体的情况是什么样的呢?

  1. 主键索引

再InnoDB中,主键索引也就是聚簇索引。每个InnoDB表都有一个聚簇索引 ,聚簇索引使用B+树构建,叶子节点存储的数据是整行记录。一般情况下,聚簇索引等同于主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID字段来构建聚簇索引。InnoDB创建索引的具体规则如下:

  1. 在表上定义主键PRIMARY KEY,InnoDB将主键索引用作聚簇索引。
  2. 如果表没有定义主键,InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引。
  3. 如果以上两个都没有,InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增。

MySQL:一文掌握MySQL索引_第6张图片

InnoDB存储引擎中,非叶子节点存储的是键值,也就是主键。而叶子节点,则存储的是键值和对应的该行数据!在InnoDB存储引擎的主键索引中,是将数据和键值放在一起的,这也是为什么索引和数据存在一个文件中的原因。

  1. 非主键索引

除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。

以表user的age列为例,age索引的索引结果如下图:

MySQL:一文掌握MySQL索引_第7张图片

我们能够发现,非叶子节点一样的只是存储键值,到了叶子节点后,存储的是键值以及对应的主键的值. 所以,也正是基于这个原因,InnoDB存储引擎,在普通索引上查找到数据后,还要根据得到的主键值,到主键索引上再进行一次查找,才能找到对应的数据。

例如查询age=19的数据,第一步先从辅助索引查找主键,找到主键为47,然后根据主键47查找行数据,示意图如下:

MySQL:一文掌握MySQL索引_第8张图片

上面这种根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为回表查询。

  1. 联合索引

联合索引又叫复合索引,例如下表:

CREATE TABLE `test` (`id` bigint NOT NULL AUTO_INCREMENT,`name` varchar(24) NOT NULL,`age` int NOT NULL,`position` varchar(32) NOT NULL,`address` varchar(128) NOT NULL,`birthday` date NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

如下索引就是一个联合索引。

`idx_name_age_position` (`name`,`age`,`position`) USING BTREE

联合索引底层数据结构长什么样?

MySQL:一文掌握MySQL索引_第9张图片

比较相等时,先比较第一列的值,如果相等,再继续比较第二列,以此类推(这也就是最左前缀匹配原则)。

索引的分类

  1. 按照基本使用分类
  • 主键索引: InnoDB 主键是默认的索引,数据列不允许重复,不允许为 NULL,⼀个表只能有⼀个主键。
  • 唯⼀索引: 数据列不允许重复,允许为 NULL 值,⼀个表允许多个列创建唯⼀索引。
  • 普通索引: 基本的索引类型,没有唯⼀性的限制,允许为 NULL 值。
  • 全文索引:只能在文本类型CHAR,VARCHAR,TEXT类型字段上创建全文索引。字段长度比较大时,如果创建普通索引,在进行like模糊查询时效率比较低,这时可以创建全文索引。
  • 前缀索引:在文本类型如CHAR,VARCHAR,TEXT类列上创建索引时,可以指定索引列的长度,但是数值类型不能指定。
  • 空间索引:MySQL在5.7之后的版本支持了空间索引,而且支持OpenGIS几何数据模型。MySQL在空间索引这方面遵循OpenGIS几何数据模型规则。
  1. 按照索引字段个数
  • 单列索引:对单独的列 设置一个索引
  • 联合索引:对 多个字段 一起建立索引
  1. 按照物理存储结构
  • 聚簇索引:叶子节点上存储了完整的数据记录(完整的数据记录是指存储了单条记录中所有列的值),通过主键找到了索引也就找到了数据。
  • 非聚簇索引:也叫二级索引或辅助索引,将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行。

页、区、段

页是InnoDB存储数据记录的基本单位,也是数据库IO操作的最小单位,页的大小默认是16KB,一个页中存放了多条数据记录。

InnoDB将数据划分为若干个页存放到磁盘上,并且以作为磁盘和内存之间交互的基本单位,也就是说一次IO操作最少会从磁盘中读取16KB的内容到内存中,一次最少会把内存中的16KB内容刷新到磁盘中。从使用角度来说,在InnoDB引擎环境下,不论是读取一行还是多行记录,都是将这些数据记录所在的页进行加载。

区是比页大一级的存储结构,在InnoDB中,一个区内会分配64个连续的页,因此一个区的大小是64*16KB=1MB。

段是由至少一个区组成的,在段中不要求区与区之间连续。

段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。 当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。

change buffer 和索引

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。

需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。

显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。

整个 change buffer 和 索引有什么关系呢?

如果是唯一索引,在插入的时候,需要对插入的数据进行唯一性判定,就必须将数据从硬盘上读入内存,因此,唯一索引无法应用 change buffer。而对于普通索引,则可以应用 change buffer ,减少 磁盘I/O,这在数据量很大的时候,对性能的影响还是很明显的。因此,在能够使用普通索引的情况下,尽量使用普通索引,不要使用唯一索引

回表和覆盖索引

前面我们知道,在InnoDB存储引擎中,如果是非主键索引,需要先在非主键索引上查询主键,再去主键索引上查询整行数据,它⽐基于主键索引的查询多扫描了⼀棵索引树,这个过程就叫回表

而如果执行的是 select ID from T where k between 3 and 5 这时只需要查 ID 的值,且 ID 是主键索引,即索引和值在一起,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 ID已经“覆盖了”我们的查询需求,我们称为覆盖索引

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

索引优化

  1. 关于建立索引

哪些需要建

1、主键自动建立唯一非空索引

2、频繁作为查询条件的字段

3、查询中与其他表建立关联,外键关系建立索引

4、单值/复合索引选择,在高并发下倾向复合索引

5、查询中排序的字段

6、查询中统计或分组的字段

哪些不建议建索引

1、表记录太少

2、经常增删改的表和更新的字段

3、某列数据种类太少,如性别、一些分类等

4、where条件里用不到的不建立索引

  1. 尽量不使用!=,not in ,not exists

因为这些操作无法使用索引可能会导致全表扫描 ,!=的结果集可能会很大,走索引也和全表扫描差不多,这样MySQL优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。

  1. 尽量不使用is null,is not null

然这个字段是索引字段,但是也可能不会走索引,对于null的字段,在索引树中会集中起来处理,统一在左端或者右端。

  1. like不要以%开头

like以通配符开头(’%Lei’)MySQL索引失效会变成全表扫描操作,%号在前就意味着前面还有很多其他的字符串,跳过这些字符串在整个索引树里面就不是有序的了,定位不到,没办法用索引;%号在后等于是用了%号前面的字符串,这些字符串在整个索引树里面是有序的,所以能走索引。

  1. 尽量使用覆盖索引

减少 select * 语句。

查询的时候指明具体的字段,尽量被联合索引覆盖掉,如果要查的是全部列的话,数据量又特别大,可以考虑使用搜索引擎。

  1. 少用or或in

因为用它查询时,MySQL不一定使用索引,MySQL内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。

  1. 使用最左前缀原则

如果索引了多列,要遵守最左前缀法则,指的是查询从索引的最左前列开始并且不跳过索引中的列。

最左前缀原则是针对联合索引的,它的底层是一个B+树,但键值数是大于1的,而构建一个B+树就只能根据一个键值来进行,所以数据库依据联合索引最左的字段来构建B+树,是按照联合索引从左到右排好序的,如果跳过前面的,这样后面的就不是有序的了,查询就需要全表扫描。

  1. 不在索引列上做操作

计算、函数等操作可能会导致索引失效而转向全表扫描,比如SQL中截取name左边的三位,这样就不是有序了,走不了索引。

  1. 不能使用索引中范围条件右边的列

在联合索引中,如果前面索引列使用了范围,那么后面的索引列就走不了索引,这是因为在索引树中前面的是范围,后面的就不一定是有序的。

  1. order by 优化

order by满足两种情况会使用Using index,order by语句使用索引最左前列;使用where子句与order by子句条件列组合满足索引最左前列,如果order by的条件不在索引列上,就会产生Using filesort。

另外,where与order by冲突时优先where。因为大多数情况基于索引进行where筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可能会小很多。

面试题

索引哪些情况下会失效

查询条件包含 or,可能导致索引失效

如果字段类型是字符串,where 时⼀定⽤引号括起来,否则会因为隐式类型转换,索引失效

like 通配符可能导致索引失效。

联合索引,查询时的条件列不是联合索引中的第⼀个列,索引失效。

在索引列上使⽤ mysql 的内置函数,索引失效。

对索引列运算(如,+、-、*、/),索引失效。

索引字段上使⽤(!= 或者 < >,not in)时,可能会导致索引失效。

索引字段上使⽤ is null, is not null,可能导致索引失效。

左连接查询或者右连接查询查询关联的字段编码格式不⼀样,可能导致索引失效。

MySQL 优化器估计使⽤全表扫描要⽐使⽤索引快,则不使⽤索引

什么是索引下推

索引条件下推优化 (Index Condition Pushdown (ICP) ) 是 MySQL5.6 添加的,⽤于优化数据查询。

  • 不使⽤索引条件下推优化时存储引擎通过索引检索到数据,然后返回给 MySQL Server,MySQL Server 进⾏过滤条件的判断。

  • 当使⽤索引条件下推优化时,如果存在某些被索引的列的判断条件时,MySQL Server 将这⼀部分判断条件下推给存储引擎,然后由存储引擎通过判断索引是否符合 MySQL Server 传递的条件,只有当索引符合条件时才会将数据检索出来返回给 MySQL 服务器。

例如⼀张表,建了⼀个联合索引(name, age),查询语句:` select * from t_user where name like ‘张%’

and age=10;` ,由于 name 使⽤了范围查询,根据最左匹配原则:

  • 不使⽤ ICP,引擎层查找到 name like ‘张%’ 的数据,再由 Server 层去过滤 age=10 这个条件,这样⼀来,就回表了两次,浪费了联合索引的另外⼀个字段 age 。
  • ,使⽤了索引下推优化,把 where 的条件放到了引擎层执⾏,直接根据 name like ‘张%’ and age=10 的条件进⾏过滤,减少了回表的次数。

主键选择自增和uuid的区别

InnoDB存储引擎的最小存储单位是 页,一页有16KB,但插入的索引或者数据等在一个页中满了之后,要调整节点顺序,进行分页分为两个页面,这个过程是比较消耗性能的,而且页面的内存利用率也会下降。自增主键的插入数据模式,正符合了递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。

因此一般情况下,我们推荐使用自增主键,但UUID主键也不是完全没有优点,UUID主键在数据非常庞大,而进行分库分表的时候,或者合并表的时候,就很方便,很容易做到一致性。

你可能感兴趣的:(MySQL,mysql,数据库,后端)