一文搞懂Mysql索引

Mysql索引

B+树是什么?

平衡二叉树:
在这里插入图片描述

B-树:

在这里插入图片描述

B+树:

在这里插入图片描述

我们先来分析下这几种树结构的特点,为什么Mysql最终选择了B+树来实现索引

1.平衡二叉树的查询复杂度为logn(底数为2),通过自旋来保证树的平衡性,由于数据库的数据量非常大,所以很难做到把数据库的全量数据通过二叉树保存到内存中(内存空间不足,成本太高,数据易丢),可以采用数据局部保存在内存中,这会导致无法进行自旋操作。数据量很大时,二叉树的高度会很大,从而导致查询时进行的磁盘IO次数很多。

2.B-树的实现采用多叉树的方式,可以很好的降低树的高度,每个节点通过范围区间来划分子节点,B-的每个节点都会保存数据库行记录,并且叶子节点没有通过链表来连接,所以它的范围查询会比较麻烦,需要通过节点的回溯操作(这两个特性是跟B+树的最大区别)

3.B+树的实现也是采用多叉树,它的非叶子节点只保存主键索引值,叶子节点保存主键索引值和行记录,叶子节点通过链表连接,从而实现快速的范围查询。非叶子节点不保存行记录是为了进行空间压缩,使得每个非叶子节点的内存占用更小,从而使得每一页(每次从磁盘中读取一页数据,innodb每一页大小为16k)能涵盖尽可能多的数据,从而减少磁盘IO次数。

B+树代码实现


/**
 * 这是B+树非叶子节点的定义。
 *
 * 假设keywords=[3, 5, 8, 10]
 * 4个键值将数据分为5个区间:(-INF,3), [3,5), [5,8), [8,10), [10,INF)
 * 5个区间分别对应:children[0]...children[4]
 *
 * m值是事先计算得到的,计算的依据是让所有信息的大小正好等于页的大小:
 * PAGE_SIZE = (m-1)*4[keywordss大小]+m*8[children大小]
 */
public class BPlusTreeNode {
    
    
  public static int m = 5; // 5叉树
  public int[] keywords = new int[m-1]; // 键值,用来划分数据区间
  public BPlusTreeNode[] children = new BPlusTreeNode[m];//保存子节点指针
}

/**
 * 这是B+树中叶子节点的定义。
 *
 * B+树中的叶子节点跟内部节点是不一样的,
 * 叶子节点存储的是值,而非区间。
 * 这个定义里,每个叶子节点存储3个数据行的键值及地址信息。
 *
 * k值是事先计算得到的,计算的依据是让所有信息的大小正好等于页的大小:
 * PAGE_SIZE = k*4[keyw..大小]+k*8[dataAd..大小]+8[prev大小]+8[next大小]
 */
public class BPlusTreeLeafNode {
    
    
  public static int k = 3;
  public int[] keywords = new int[k]; // 数据的键值
  public long[] dataAddress = new long[k]; // 数据地址

  public BPlusTreeLeafNode prev; // 这个结点在链表中的前驱结点
  public BPlusTreeLeafNode next; // 这个结点在链表中的后继结点
}

上述程序中,我们定义了一个m叉树,m的值越大树的高度越小,那是不是m的值设置越大就越好呢?我们知道,数据库是按页读取数据,当读取一个行记录时会把该行记录所在的一页都读取出来,设置合理的m值,尽量使一个节点的大小等于一页的大小,使得查询节点数据时只需进行一次磁盘IO,减少磁盘的IO次数是数据库查询性能优化的核心。

节点分裂是什么?

通过预先设置合理的m值,使得节点的大小等于一页的大小。经过插入操作后,可能会使得某些节点的大小超出一页的大小,这会导致一个节点的查询操作需要进过多次磁盘IO,这会导致B+的查询性能渐渐变差,节点分裂就是为了解决这个问题。

实现思路:将超过大小的节点分裂成两个节点,这时可能导致父节点的子节点数目超过限定值,从而导致父节点也需要进行分裂操作,可能会产生级联分裂的现象,一直影响到根节点。

在这里插入图片描述

注:这也是为什么推荐Innodb的主键设置为递增主键,避免插入操作频繁触发节点分裂,递增主键插入时,节点数据大于一页,会创建一个新的节点而不是插入旧节点并且分裂。

节点合并是什么?

我们在删除某个数据的时候,也要对应地更新索引节点。这个处理思路有点类似跳表中删除数据的处理思路。频繁的数据删除,就会导致某些节点中,子节点的个数变得非常少,长此以往,如果每个节点的子节点都比较少,势必会影响索引的效率。我们可以设置一个阈值。在 B+ 树中,这个阈值等于 m/2。如果某个节点的子节点个数小于 m/2,我们就将它跟相邻的兄弟节点合并。不过,合并之后节点的子节点个数有可能会超过 m。针对这种情况,我们可以借助插入数据时候的处理方法,再分裂节点。

在这里插入图片描述

总结

  • 数据库索引用于加速查询
  • 虽然哈希索引是O(1),树索引是O(log(n)),但SQL有很多“有序”需求,故数据库使用树型索引
  • InnoDB不支持哈希索引
  • 数据预读的思路是:磁盘读写并不是按需读取,而是按页预读,一次会读一页的数据,每次加载更多的数据,以便未来减少磁盘IO
  • 局部性原理:软件设计要尽量遵循“数据读取集中”与“使用到一个数据,大概率会使用其附近的数据”,这样磁盘预读能充分提高磁盘IO
  • 数据库的索引最常用B+树:

(1)很适合磁盘存储,能够充分利用局部性原理,磁盘预读;

(2)很低的树高度,能够存储大量数据;

(3)索引本身占用的内存很小;

(4)能够很好的支持单点查询,范围查询,有序性查询;

数据库索引

聚簇索引与普通索引的区别?

一,MyISAM的索引

MyISAM的索引与行记录是分开存储的,叫做非聚簇索引。

主键索引与普通索引没有本质的差异:

(1)有连续聚集的区域单独存储行记录

(2)主键索引的叶子节点存储主键,并且存储相对应的行记录指针

(3)普通索引的叶子节点存储索引列,与之对应的行记录指针

主键索引与普通索引是两棵独立的索引B+树,通过索引列查找时,先定位到B+树的叶子节点,再通过指针定位到行记录。

在这里插入图片描述

二、Innodb的索引

Innodb的主键索引与行记录是存储在一起的,因此称为聚簇索引

  • 没有单独区域存储行记录
  • 主键索引的叶子节点,存储主键,与对应行记录(而不是指针)

因为这个特性,InnoDB的表必须要有聚集索引:

(1)如果表定义了PK,则PK就是聚集索引;

(2)如果表没有定义PK,则第一个非空unique列是聚集索引;

(3)否则,InnoDB会创建一个隐藏的row-id作为聚集索引;

聚集索引,也只能够有一个,因为数据行在物理磁盘上只能有一份聚集存储。

InnoDB的普通索引可以有多个,它与聚集索引是不同的:

  • 普通索引的叶子节点,存储主键(也不是指针)

对于InnoDB表,这里的启示是:

(1)不建议使用较长的列做主键,例如char(64),因为所有的普通索引都会存储主键,会导致普通索引过于庞大;

(2)建议使用趋势递增的key做主键,由于数据行与索引一体,这样不至于插入记录时,有大量索引分裂,行记录移动;

在这里插入图片描述

总结

(1)MyISAM的索引与数据分开存储。

(2)MyISAM的叶子节点保存行记录指针,主键索引与普通索引的区别不大。

(3)Innodb的聚簇索引叶子节点存储行记录和主键,普通索引存储索引列和主键值。

(4)Innodb有且只能有一个聚簇索引,普通索引可以有多个。

(5)Innodb适合使用趋势递增整数作为PK,而不适合使用较长的列作为PK。

覆盖索引、前缀索引、索引下推是什么?

覆盖索引

通过前文分析可知,通过普通索引进行查询时,先在普通索引查找到对应的主键,再通过主键在聚簇索引上找到对应的行记录,每次查询都会有个回表操作。我们来看下怎么通过覆盖索引来优化这个问题。

我们创建一张用户信息表

CREATE TABLE `tuser` (
  `id` int(11) NOT NULL,
  `id_card` varchar(32) DEFAULT NULL,
  `name` varchar(32) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `ismale` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `id_card` (`id_card`),
  KEY `name_age` (`name`,`age`)
) ENGINE=InnoDB

查询需求:

根据身份证号(id_card)来查询用户名称

方案一:id_card建立普通索引,查询时先通过普通索引查找到对应的主键,再通过主键在聚集索引中查找到对应的行记录。(会有一次回表操作)

方案二:id_card与name建立联合索引,通过id_card直接在普通索引中查找到对应的name,直接返回。(没有回表操作)

方案二就是通过联合索引来实现了覆盖索引,避免了每次查询都要查找聚簇索引。

最左前缀原则

在建立联合索引时,我们应该如何来安排索引内的字段顺序?

我们用(name,age)这个联合索引来分析

在这里插入图片描述

查询需求1:根据姓名来查询用户信息,查询条件where name ='张三’或者where name like '张%'都能通过上述联合索引快速查找到记录。

查询需求2:通过age来查询用户信息,查询条件where age=30,这时索引失效,会进行全表扫描。

通过索引来满足上述两个查询需求的方案有:

  1. 创建一个(name,age)联合索引和age的普通索引。
  2. 创建一个(age,name)联合索引和name的普通索引。

这时我们要考虑的原则是空间。name字段要比age字段所占的空间更大,所以方案1会是一个更好的方案。

总结:

1.第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。

2.优先选择占用空间更小的方案来创建索引。

索引下推

我们还是以用户表的联合索引(name, age)为例。如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是10岁的所有男孩”。那么,SQL语句是这么写的:

mysql> select * from tuser where name like '张%' and age=10 and ismale=1;

借助前缀索引规则,通过“张”定位到第一条前缀为“张”的记录,然后判断其他条件是否满足。

在MySQL 5.6之前,只能从ID3开始一个个回表。到主键索引上找出数据行,再对比字段值。

而MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

无索引下推执行流程:

在这里插入图片描述

索引下推执行流程:

在这里插入图片描述

如何给字符串字段添加索引?

上文分析索引结构时我们总结出:建议使用趋势递增的整数字段来创建索引,但是现实业务中我们可能需要创建字符串索引。

我们拿用户中心来分析:

mysql> create table user(
ID bigint unsigned primary key,
email varchar(64), 
idno varchar(128),
... 
)engine=innodb; 

通过邮箱登录时,需要频繁地通过邮箱来查询用户信息:select * from user where email = ‘[email protected]’,我们要如何来优化这个查询需求呢?我们通常的做法是给email创建唯一索引,这是一个很好的方案吗?

方案一:为email添加普通索引

alter table user add index index1(email);

优点:

1.无需对字符串值进行额外的处理。

缺点:

1.字段值较长时,索引结构会占用较多的空间。

方案二:为email添加前缀索引

alter table User add index index2(email(6));

优点:

1.存储在索引结构中的数据是字符串的局部子串,节约了索引所占空间大小。

缺点:

1.需要对字符串的值进行截取操作。

2.前缀索引会使覆盖索引失效。

注:这种方案的核心思想是当字符串的子串具有足够的唯一性时,可以使用子串来创建索引。

方案三:将字符串字段转换成整数字段,在整数字段上创建索引

alter table t add id_card_crc int unsigned, add index(id_card_crc);

通过hash算法(crc32)将字符串转成int值,基于hash值来创建普通索引。

优点:

1.通过整数值来创建索引,可以很好的减少索引所占空间大小,并且每个节点(一页16k)所能容纳的数据更多从而减少磁盘IO。

缺点:

1.需要通过一定的hash算法来把字符串转换成整数值,这个计算操作会耗费一定的cpu资源。

注:要保证计算出来的整数值具有很好的辨识度。

方案三:将字符串字段转换成整数字段,在整数字段上创建索引

alter table t add id_card_crc int unsigned, add index(id_card_crc);

通过hash算法(crc32)将字符串转成int值,基于hash值来创建普通索引。

优点:

1.通过整数值来创建索引,可以很好的减少索引所占空间大小,并且每个节点(一页16k)所能容纳的数据更多从而减少磁盘IO。

缺点:

1.需要通过一定的hash算法来把字符串转换成整数值,这个计算操作会耗费一定的cpu资源。

注:要保证计算出来的整数值具有很好的辨识度。

参考资料:

[1] MySQL技术内幕 InnoDB存储引擎 第2版

[2]数据结构与算法之美

[3]MySQL实战45讲

[4]架构师之路

猜你喜欢

转载自blog.csdn.net/qa76774730/article/details/108494374