MySQL———深入浅出索引

Posted wtxuebc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL———深入浅出索引相关的知识,希望对你有一定的参考价值。

文章目录

索引模型理论

什么是索引?

​   一句话简单来说,索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。一本 500 页的书,如果你想快速找到其中的某一个知识点,在不借助目录的情况下,那我估计你可得找一会儿。同样,对于数据库的表而言,索引其实就是它的 “目录”

索引的常见模型

​   索引的出现是为了提高查询效率,实现索引的方式由好多种,所以索引模型的概念很重要!

​   换句话说,可以用于提高读写效率的数据结构!就是索引想要的!常见、也是比较简单的有三种:哈希表、有序数组和搜索树。

那这三种有什么区别?

哈希表

​    哈希表是一种以键 - 值(key - value)存储数据的结构,我们只要输入待查找的 key ,就可以找其对应的 Value。思路很简单:Value 都在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。

​   当然不可避免,多个 key 值经过哈希函数的换算,会出现一个值的情况。处理这种情况也很简单!拉出一个链表。这个链表存储 key 和 value 的信息! 这种方法需要精准的hash算法让值均匀的分布,不然容易出现极端的情况:一条单链表!

   假设现在需要维护一个身份证信息和姓名的表,根据身份证号查找名字,这是对应的哈希索引如下:

  此时若根据不同的身份证号对应的相同的名字,只需要对姓名所在的链表,用身份证号进行顺序遍历!

没错,这就是hashmap的底层实现!

 ​  需要注意的是,前面的 key (身份证号)是无序的!好处是:添加新的 User 时速度很快,只需要往后追加。但是缺点:因为是无序的,所以哈希索引做区间,查询的速度是很慢的。( 哈希出来的数组,顺序是没有实际意义的,因为key放在哪只与哈希函数有关、与索引计算有关;)

有序数组

​    有序数组在等值查询和范围查询的场景中的性能都非常优秀;还是身份证号的例子用有序数组索引来实现:

​    假设身份证号没重复,这个数组就是按照身份证号递增的顺序保存的。这时候要查找某身份证号对应的名字,用二分搜索效率高! O(log(N))

​    总结:有序数组能够解决hash函数不能满足支持快速范围查询。查找速度为log(n),但缺陷也很明显,针对插入和删除场景,需要挪动后面的整个记录,代价太高。有序数组适用于静态搜索引擎。(而且数组的长度是有限的,数组的下标只能是自然数)

搜索树

说起搜索树,我们会想到二叉搜索树(二叉搜索树的概念默认大家的知道)

还是上面身份证号的例子

整体查询事件复杂度为 O(log(N)) , 最坏的情况下是 O(N),退化成链表,因为没有平衡的限制!

​     当然,为了维持 O(log(N)) 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 O(log(N))。

但是索引不止存在内存中,还要写在磁盘上,有IO操作!

​     所以对速度也有要求,那么速度的衡量是什么呢?是树的深度!树的深度越深,每次就需要访问过多的节点,即访问数据块过多,磁盘随机读取数据块又比较耗时;

​    为了让一个查询尽量的少读磁盘,就必须让查询过程访问尽量少的数据块我们会自然想到减少树的层数,进而减少树的深度!多叉树绝对是个不错的选择!同样多的数据,分的叉越多,深度越小!

​    N叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。

​    我们心里要有个概念,数据库底层存储的核心就是基于这些数据模型的。因为数据库就是存储数据的!每碰到一个新数据库,需要先关注它的数据模型,这样才能从理论上分析出这个数据库的适用场景!

也可以了解下 跳表、LSM 树等数据结构,也被用于引擎设计中。

应用

​    在mysql中,索引是在存储引擎层实现的,因此没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样;即使多个引擎支持同一种类型的索引,其底层的实现也可能不同。其中 InnoDB 存储引擎在 MySQL 数据库中使用最为广泛,也是其现版本默认的存储引擎,下面就以 InnoDB 为例,展开分析!

事务也是在存储引擎层实现的,因为不是所有的存储引擎都支持事务!

InnoDB 的索引模型

   在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表成为索引组织表。

索引组织表的理解:

​    在InnoDB存储引擎中,表都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table IOT)。

​ 在InnoDB存储引擎中,每张表都有个主键(Primary key),如果在创建表时没有地定义主键,则InnoDB存储引擎会选择表中符合条件的列去创建主键。

条件:

  1. 首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列即为主键。

  2. 如果不符合上述条件,InnoDB存储引擎自动创建一个6字节大小的指针。

​    当表中存在多个非空的唯一索引的时候,InnoDB存储引擎会根据建表时所创建的第一个非空唯一索引作为主键。

InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 数中的。

关于 InnoDB 的表结构:

在 InnoDB 中,每一张表其实就是多个 B+ 树,即一个主键索引树和多个非主键索引树。

(每一个索引在 InnoDB 里边对应一颗 B+ 树。)

如果不使用索引进行查询,则从主索引 B+ 树的叶子节点进行遍历。

概念总是含糊的,举个例子吧!

假设,我们有一个主键列为ID的表,表中有字段 k ,并且在 k 上有索引。

这个表的建表语句是:

mysql> create table T(
id int primary key,
k int not null,
name varchar(16),
index (k))engine=InnoDB;

​ 表中R1 ~ R5 的 ( ID,K ) 值分别是 ( 100,1 )、( 200,2 )、( 300,3 )、( 500,5 ) 和 ( 600,6 ),因为有两个索引,因此存在两个B+树,示意图如下:

根据叶子节点的内容,索引类型分为:

主键索引(叶子节点存放的该行的数据);在 InnoDB 里边,主键索引也被称为 聚簇索引

非主键索引(叶子节点存放的是主键的值);非主键索引也被称为二级索引

那么,有一个问题:

基于主键索引和普通索引的查询有什么区别?

  • 如果语句是select * from T where ID=500,即主键查询,则只需要搜索 ID 这颗 B+ 树;
  • 如果语句是select * from T where k=5,即需要先搜索 k 索引树,得到 ID 的值为 500,再 ID 索引树搜索一次。这个过程成为回表。

主键索引 是唯一且有序的,非主键索引,是需要找到结果ID,回表,获取最后的结果的。

也就是说,**基于非主键索引的查询需要多扫描一棵树。**因此!我们再生活中应该尽量使用主键查询!

索引维护

B+ 树为了维护索引有序性,在插入新值得时候需要做必要得维护,来确保索引有序。

这就涉及到 B+ 为了满足节点数据有序做的维护操作;

逻辑(自增)主键 和 业务主键

逻辑主键(surrogate key):无意义的字段,即自增长字段,即identity。这其中还有一个选择GUID(Globally Unique Identifier)。 也叫代理主键。

业务主键(natrual key):有意义的字段,比如身份证 ID。也叫自然主键

逻辑主键和业务主键的使用场景与区别:

自行思考

​    自增主键是指自增列上定义得主键,在建表语句中一般时这么定义的:NOT NULL PRIMARY KEY AUTO_INCREMENT。

   插入新纪录的时候可以不指定 ID 的值,系统会获得当前 ID 最大值加 1 作为下一条记录的 ID 值;

   也就是说,自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是“追加操作”,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。

这里关于页分裂提一嘴:

​ mysql 在底层又是以数据页为单位来存储数据的,一个数据页大小默认为 16k,当然你也可以自定义大小,也就是说如果一个数据页存满了,mysql 就会去申请一个新的数据页来存储数据。

​ 如果主键自增主键的话,mysql 在写满一个数据页的时候,直接申请另一个新数据页接着写就可以了 !

​ 如果主键不是自增主键的话,为了确保索引有序,mysql 就需要将每次插入的数据都放到合适的位置上。当往一个快满或已满的数据页中插入数据时,新插入的数据会将数据页写满,mysql 就需要申请新的数据页,并且把上个数据页中的部分数据挪到新的数据页上。这就造成了页分裂,这个大量移动数据的过程是会严重影响插入效率的。

而有业务逻辑的字段做主键,往往不容易保证有序插入,这样写数据成本相对高。

​     除了考虑性能外,还可以从存储空间的角度来看。“假设你的表中有一个唯一字段,比如字符串类型的身份证号,那么应该用身份证号做主键还是用自增字段做主键?

​     注意回想:每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,每个二级索引的叶子节点占用约20字节,如果用整数做主键,只需要4个字节,如果长整型,则需要8字节。

显然,主键长度越小,普通索引鞥带叶子节点就越小,普通索引占用的空间也就越小。

总结一下

所以:业务字段不一定是递增的,有可能会造成主键索引的页分裂,导致性能不稳定。

二级索引存储的值是主键,如果使用业务字段,占用大小不好控制,如果业务字段过长可能会导致二级索引占用空间过大,利用率不高。

综上:从性能和存储空间方面综合考量,自增主键往往是更合理的选择。

当然,也有适合用业务字段做主键得到场景:比如,有些需求是这样的:

  1. 只有一个索引;
  2. 该索引必须是唯一索引。

没错,这就是典型的 KV 场景。由于没有其他索引,就不用考虑其他索引的叶子节点大小问题了。

这时候,要考虑 ”尽量使用主键查询“ 原则,直接将这个索引设置为主键,避免每次查询搜索两棵树。

覆盖索引

先看一个例子:

在下面这个表中,如果我执行 select * from T where k between 3 and 5,需要执行几次树的操作,会扫描多少行?

表的初始语句:

mysql> create table T (
ID int primary key,
k int NOT NULL DEFAULT 0, 
s varchar(16) NOT NULL DEFAULT '',
index k(k))engine=InnoDB;
insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');

现在,我们一起来看看这条 SQL 查询语句的执行流程:

  • 在 k 索引树上找到 k=3 的记录,取得 ID = 300;

  • 再到 ID 索引树查到 ID=300 对应的 R3;

  • 在 k 索引树取下一个值 k=5,取得 ID=500;

  • 再回到 ID 索引树查到 ID=500 对应的 R4;

  • 在 k 索引树取下一个值 k=6,不满足条件,循环结束。

​ 在这个过程中,回到主键索引树搜索的过程,我们称为回表。可以看到,这个查询过程读了 k 索引树的 3 条记录(步骤 1、3 和 5),回表了两次(步骤 2 和 4)。

​    在这个例子中,查询结果只有主键索引上有,所以不得不回表;那有没有可能经过优化之后不回表呢?

什么是覆盖索引?

​    解释一: 就是select的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。

​    解释二: **索引是高效找到行的一个方法,当能通过检索索引就可以读取想要的数据,那就不需要再到数据表中读取行了。如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫做覆盖索引。 **

就是说!如果用非主键索引查询,非主键索引已经覆盖了我们的查询要求!

再来看下一个问题:

在一个市民信息表上,是否有必要将身份证号和名字建立联合索引?

​    身份证号是一个市民的唯一标识。在身份证号字段上建立索引就够了。而再建立一个联合索引肯定是会浪费空间的,但是如果要根据市民的身份证号查他的名字,这个联合主键就很有意义。它可以再高频请求上用到覆盖索引,不再需要回表查整行记录,这个时候再建立冗余索引来支持覆盖索引就需要利弊权衡

(注意这里的身份证号码,只是用它建立索引,它不是主键,因此要根据身份证id查名字建立联合索引很有必要!)

最左前缀原则

​    在我们涉及表的过程中,如果为每一个查询都设计一个索引,那索引岂不是太多了。虽然我们要查询的属性在业务中出现的概率很低,但是全表查询又比较麻烦!

莫慌!B+树这种索引结构,可以利用索引的 “ 最左前缀 ”,来定位记录。

先举个栗子:

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

索引项是按照索引定义里边出现得字段顺序排序得。**

​    当你的逻辑要求是查到所有名字是 " 张三 "的人时,可以快速的定位到 ID4,然后遍历得到所有需要的结果。

​    如果要查的是所有名字第一个是 “ 张 ” 的人,你的 SQL 语句的条件是 “ where name like '张 %‘。这时,你也能用上这个索引,查找到第一符合条件的记录是 ID3,然后向后遍历,直到不满意为止!

​    所以说!不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 N 个字段**

有一个问题:在建立联合索引的时候,如何安排索引内的字段顺序?

​    这里我们的评估标准是 ” 索引的服用能力 “。因为支持最左前缀,当已经有了(a,b)这个联合索引后,我们不需要单独在 a 上建立索引了。因此!第一原则:如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。

回到开始!

​    我们可以用(身份证id,姓名)这个联合索引,因为支持最左前缀,此时我们就少维护了一个 ” 身份证id “ 的索引,可以用此联合索引支持 ” 根据身份证号查询地址 “ 的需求。

如果!

​    如果既有联合查询,又有基于 a、b (a,b)各自的查询呢?这时候不得不维护另外一个索引,自然就是(a,b)和(b)这两个索引。(因为支持最左前缀,不同单独在 a 上建索引)。

所以!第二个考虑原则就是空间了。如果 a 字段比 b 大,那么就可以创建一个 ( a, b )的联合索引和一个 ( b ) 的单字段索引。

索引下推

还是上面的栗子,其中用最左前缀在索引中定位了记录,那不满足索引前缀的部分会怎么样呢

   回到市民的联合索引 ( name,age) ;现有一个需求,要检索出 ” 名字第一个字是张,而且年龄是 10 岁的所有男孩 “;先写其 SQL 语句。

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

   根据索引前缀:只能用 ” 张 “,找到第一个满足条件记录的 ID3.当然,这总比全表扫面要好!

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

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

​    就是说!5.6 以前查到只要是 name 第一个字是张,管 age 满足与否,先查主键回表再说!而索引下推就是在找到满足 name 第一个字是张的时候,还会会其 age 进行筛选,选择合适的索引,并查其主键进行回表。

其过程如下图:

无索引下推执行流程

有索引下推执行流程

中间一个箭头就表示回表一次,显然!有索引下推流程的回表次数少!

小总结一下索引覆盖、最左前缀原则、索引下推

索引覆盖:如果能通过检索索引就能得到要查询的值 ( 这个索引覆盖了要查询的值),大可不必再去数据表中去读取数据了,减少了回表的操作。

最左前缀原则:对于那些不经常查询的列,对其建立索引显得太浪费(索引也需要维护),全表查询又太慢,这时候可以,这时候就可以用 ”最左前缀” 来查询,通过索引的最左边的 N 个字段进行定位,由于一个属性的最左边 N 个字段比较模糊,经常和联合索引一块使用,这里会牵扯到联合索引的字段顺序的一个优化!

索引下推:索引下推是在非主键查询的时候使用的!因为主键是唯一的,所以在非主键索引确定的情况下,不需要下推;

​    因此,索引下推一般和最左前缀一块使用,满足最左前缀的时候,最左前缀可以用在索引中定位记录。而不满足最左前缀的部分就可以用索引下推;在不使用索引下推的时候,定位到满足前缀的第一个节点时,不管其他部分(不满足最左前缀的部分)满足与否,都会根据主键的值回表查询;

   而使用索引下推后,会先检查其他部分满足与否,不满足的就不会进行回表那步操作

参考文献:林晓斌《MySQL实战 45 讲》

以上是关于MySQL———深入浅出索引的主要内容,如果未能解决你的问题,请参考以下文章

浅入浅出 MySQL 索引

MySQL45讲唯一索引和普通索引的选择

MYSQL索引的深入学习

Day873.普通索引&唯一索引的选择 -MySQL实战

Day873.普通索引&唯一索引的选择 -MySQL实战

从原理到优化,深入浅出数据库索引