Mysql高级篇学习总结6:索引的概念及理解、B+树产生过程详解、MyISAM与InnoDB的对比

1、索引的引入

1.1 为什么使用索引

前面已经学习了很多sql的语法等,现在需要了解下我们输入了一条select查询语句,mysql是怎么找到该查询语句对应的数据的?另外,又是怎么快速地找到的呢?

这里就可以引出索引的概念了:索引是存储引擎用于快速找到数据记录的一种数据结构

不知道你看清楚上面的概念了么,索引是一种数据结构

1.2 查找数据记录

接下来通过例子,来慢慢理解,索引是一种数据结构,到底是什么意思…

比如此时,我们有一个employees员工表,当我们输入select 语句以后,应该怎么找到这条数据呢?接下来来分析下。

select * from employees where employee_id=100;

在这里插入图片描述

首先先创建一个简单一点的表index_demo表,这个表里只有3个字段:c1,c2,c3,其中c1字段是主键。

CREATE TABLE index_demo(
    c1 int,
    c2 int,
    c3 char(1),
) ROW_FORMAT = Compact;

这里采用的行格式是Compact,也就是每一行数据实际保存的格式

  1. record_type:记录头信息的一项属性,表示记录的类型。0表示普通记录、1表示目录记录、2表示最小记录、3表示最大记录
  2. next_record:记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量。
  3. 各个列的值:这里只记录在index_demo中的三个列,分别是c1,c2,c3。
  4. 其他信息:除了上述3种信息意外的所有信息,包括其他隐藏列的值以及记录的额外信息。

在这里插入图片描述

扫描二维码关注公众号,回复: 14840411 查看本文章

1.3 插入数据,多页查找

创建了表之后,开始插入数据并开始查找,首先插入3条数据:

INSERT INTO index_demo VALUES 
(1, 4, 'u'),
(3, 9, 'd'),
(5, 3, 'y');

mysql加载磁盘中的数据记录是按页来加载的,其中一页的大小是16KB,因此我们加载一次,最多能加载16KB的数据。

虽然我们只插入了3条数据,假设这3条数据已经到了16KB,那么就可以填满一页了,示意图如下:
在这里插入图片描述

可以看到,此时数据根据主键(c1)的大小,在一页中串联成一个单向链表了。如果要查找某一条数据,可以在这一页中进行顺序查找。

由于这3条数据已经填满一页了,此时再插入一条数据的话,会重新分配一页的地址来存储新的数据记录。存储好之后,由于我们是根据主键来进行排序的,所以还需要看下该条记录是否需要移动,如果需要记录移动的话,那么需要根据主键大小进行记录移动,这个过程称为分页

比如此时又插入了一条数据。那么需要先将主键值为5的记录移动到一个新分配的页28,然后再将主键值为4的记录插入到页10。

INSERT INTO index_demo VALUES 
(4, 4, 'a');

在这里插入图片描述

好了,接下来随着数据库记录的增加,数据库中新分配的页也会越来越多,大致如下图:
在这里插入图片描述
1)由于每一页在磁盘中实际可能不是连续的,因此每一页之间使用的双向链表连接。
2)至于每一页里面,由于单链表查询速度慢,因此可以再维护一个数组,用来记录每条数据的地址。如果需要查询的话,通过二分查找,会更快地找到这条该页中的这条数据。

因此如果要查找(20,2,'e’)这条数据。
1)可以先找到页10 ,然后通过2分查找发现没找到;
2)然后再通过链表找到下一页28,再通过二分查找发现还是没找到;
3)然后再通过链表找到下一页9,再通过二分查找,此时可以找到,这是返回该条数据记录。

1.4 基于目录项记录的页查找

在1.3中,可以看到多页查找存在一个很明显的问题,就是数据量增加以后,一个一个地顺序查找,速度太慢。

因此可以给每一页做一个目录项,每个目录项包括以下2个部分:

  1. 页的最小的主键值,用key表示
  2. 页号,用page_no表示

因此1.3中的多页此时可以用下图表示。
此时还是查找(20,2,'e’)这条数据的话。
1)在目录项的这一页中,通过2分查找找到目录项3。因为主键20大于目录项3的最小主键12,小鱼目录项4的最小主键209。
3)此时到目录项3中的页9中,通过2分查找可以找到这条记录。

此时可以看到,1.3中是通过一页一页地查找,每次都要加载磁盘的一页数据,耗时多。而加了目录之后,只需要加载2次磁盘的页就找到了数据。

需要说明的是,加载磁盘页的耗时,要远远大于内存中的耗时,两者的量级至少在10以上。因此此时不用纠结程序中的某个算法的时间复杂度是0(n)还是O(n2)。因为如果磁盘加载页的次数多的话,耗时是远远大于在内存中执行程序的时间。
在这里插入图片描述

此时,基于目录项记录的页的数据结构如下图。
在这里插入图片描述
可以看到第1层是目录项记录,第2层是数据记录。
目录项记录只有主键的最小值和对应页的物理地址。而数据记录则真正含有这条记录的数据。他们的区分是根据前面介绍的record_type属性进行的区分:

  • 0:普通用户记录
  • 1:目录项记录
  • 2:最小记录
  • 3:最大记录

需要注意的是,数据记录由于包含数据,所以在一页中(16KB)存放的目录记录数往往是大于存在数据记录的数。

比如一条数据记录的大小是160B,那么一页磁盘可以存放100条数据记录。
由于一条目录记录只有该页最小主键值和该页物理地址,就2个值,假设一条的大小是16B,那么一页磁盘可以存放1000条记录。
因此此时2层的目录结构可以存放的数据记录数是:1000 * 100 = 10,0000条,也就是10万条记录。

因此10万条记录,我们在第一层中,通过2分查找可以快速定位到是那一页物理地址,然后在该页中再通过二分查找可以快速找到这条数据。因此10万条数据,大概需要加载2次磁盘页就可以找到了。

1.5 基于目录项记录页的目录页

按照上面的举例,如果超过10万条数据了呢?那一个目录项记录肯定就不够了。比如如果有1亿条数据,那么就按照上面的例子的话,就需要1000个目录项记录页了:
在这里插入图片描述

这时如果要查找某一条数据,那么就又需要在第一层的目录项记录页这里一个一个地查找了,每一次都要加载磁盘页,这样速度非常慢。

因此可以参考上面的方法,再增加一层:目录项记录页的目录页。如下图:
在这里插入图片描述

查找的方法和前面类似,此时又多了一层,按照上面的例子,此时可以存储的数据数为:
1000(第1层目录项页) × 1000 (第2层目录项页)* 100 = 1,0000,0000条数据,即1亿条数据。

当然如果数据量更大,还可以继续增加层数。再增加一层的话,就可以存储1000亿条数据,对于一般的业务来说,这已经非常多了,因此一般索引的层数不会超过4层

1.6 B+树

上面已经分析了,怎么创建一种数据结构来快速地找到数据库中的数据记录,这种数据结构大致如下图:
在这里插入图片描述
这种数据结构,它的名称就是B+树。

不论是存放用户记录的数据页,还是存放目录项记录的数据页,我们都把它们存放到B+树这个数据结构中,所以我们也称这些数据页为节点。从图中可以看出,实际用户记录其实都是存放在B+树的最底层的节点上,这些节点也被称为叶子节点,其余用来存放目录项的节点称为非叶子节点或者内节点,其中B+树最上边的那个节点也称为根节点。

一般情况下,我们用到的B+树都不会超过4层!
虽然上面举例过了,这里再总结一遍。
假设一条数据记录大小是160B,那么一个磁盘页(16K)最多可以存放100条数据。而目录页由于只需要存放数据记录的最小主键值和数据记录页的地址,因此一个磁盘页存放的目录项数据肯定比数据项个数多,假设能存放1000条。

  1. 如果B+树只有1层:一个磁盘页(16K)最多可以存放100条数据。
  2. 如果B+树有2层:最多能存放1000 × 100 = 10,0000(10万条数据)
  3. 如果B+树有3层:最多能存放1000 × 1000 × 100 = 1,0000,0000(1亿条数据)
  4. 如果B+树有4层:最多能存放1000 × 1000 × 1000 × 100 = 1000,0000,0000(1000亿条数据)

因此1000亿条数据,通过主键值去查找最多只需要加载4次磁盘页(3次目录项页、1次用户数据记录页)就可以找到数据,并且每一个页面内还有Page Directory(页目录),也就是可以通过二分法快速定位,不用通过链表一个一个地查询。

2、索引概述

通过第一小节,已经分析B+树在mysql中检索数据记录的整个过程,此时再来理解索引的概念和优缺点,可能就更好理解了。不然一上来就看着一大堆的文字描述,可能就会很懵。

所以需要说明,为什么要建索引?
从上面可以知道,建索引的目的是减少磁盘I/0的次数,加快查询效率。

2.1 索引概述

索引是帮助mysql高校获取数据的数据结构,因此索引是数据结构

索引是在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。

同时,存储引擎可以定义每个表的最大索引数和最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。

2.2 索引的优点

  1. 类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本,这是创建索引的主要原因。
  2. 通过创建唯一索引,可以保证数据库表中每一行数据的唯一性。
  3. 在实现数据的参考完整性方面,可以加速表和表之间的连接。换句话说,对于有依赖关系的子表和父表联合查询时,可提高查询速度。
  4. 在使用分组和排序子句进行数据查询时,可显著减少查询中分组和排序的时间,降低了CPU的消耗

2.3 索引的缺点

  1. 创建索引和维护索引要耗费时间,并且随着数据量的增加,所耗费的时间也会增加。
  2. 索引需要占用磁盘空间,除了数据表占数据空间外,每个索引还要占用一定的物理空间,存储在磁盘上。如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。
  3. 虽然索引大大提高了查询速度,但是却会降低更新表的速度。当对表中的数据进行增加、删除和修改的时候,索引也要动态地进行维护,这样就降低了数据的维护速度。

3、索引的常见概念

索引按照物理实现方式,可以分为2种:聚簇索引和非聚簇索引。也把非聚簇索引称为二级索引或者辅助索引。

3.1 聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(所有的用户记录都存储在叶子节点),也就是所谓的索引即数据,数据即索引

这种聚簇索引并不需要我们在mysql中显示地使用INDEX语句去创建,InnoDB存储引擎会自动地为我们创建聚簇索引

优点

  • 数据访问更快。因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快。
  • 聚簇索引对于主键的排序查找和范围查找速度非常快。
  • 按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,索引节省了大量的io操作

缺点

  • 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,一般都会定义一个自增的ID列作为主键
  • 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于innoDB表,我们一般定义主键为不可更新
  • 二级索引访问需要2次索引查找。第1次找到主键值,第2次根据主键值找到行数据。

限制

  • 对于mysql数据库目前只有innodb数据引擎支持聚簇索引,而myisam并不支持聚簇索引。
  • 由于数据物理存储方式只能有一种,所以每个mysql的表只能有一个聚簇索引。一般情况下就是该表的主键。
  • 如果没有定义主键,innodb会选择非空的唯一索引代替。如果没有这样的索引,innodb会隐式地定义一个主键来作为聚簇索引
  • 为了充分利用聚簇索引的聚簇的特性,所以innodb表的主键列尽量选用有序的顺序id,而不建议用无序的id,比如UUID, MD5, HASH, 字符串列作为主键无法保证数据的顺序增长。

3.1 非聚簇索引(二级索引、辅助索引)

上边介绍的聚簇索引只能在搜索条件是主键时才能发挥作用,因为B+树中的数据都是按照主键进行排序的。那如果我们想以别的列作为搜索条件怎么办呢?

可以多建几颗B+树
不同的B+树中的数据采用不同的排序规则,比如可以用上面例子中的c2列的大小作为数据页再建一颗B+树。
在这里插入图片描述
回表的概念
根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果想查找到完整用户记录的话,还是需要到聚簇索引中再查一遍,这个过程称为回表。

因为这种按照非主键列建立的B+树需要一次回表操作才可以定位到完整的用户记录,所以这种B+树也被称为二级索引(secondary index),或者辅助索引。

非聚簇索引的存在不影响数据子啊聚簇索引中的组织,所以一张表可以有多个非聚簇索引。

小结:

  1. 聚簇索引的叶子节点存储的就是我们的用户数据记录,非聚簇索引的叶子节点存储的是数据位置。非聚簇索引不会影响数据表的物理存储顺序。
  2. 一个表只能有一个聚簇索引,因为只能有一种排序存储的方式,但可以有多个非聚簇索引,也就是多个索引目录提供数据检索。
  3. 使用聚簇索引的时候,数据的查询效率高,但如果对数据进行插入,删除,更新等操作,效率会比非聚簇索引低。

3.3 联合索引

联合索引可以理解为非聚簇索引中的一种,只是它是同时为多个列建立索引。

比如使用上面的介绍的c2和c3列建立索引:
在这里插入图片描述

4、InnoDB的B+树索引的注意事项

4.1 根页面位置万年不动

一个B+树索引的根节点自诞生之日起,便不会再移动。也就是每当为一张表建立B+树索引时,就会创建一个根节点页面,该页面一开始存放的是用户记录,当该页满了之后,就会发生页分裂,这样用户数据就到了第2层,那么根节点页面就变为了目录项记录页了。

更通俗一点的解释就是,上面介绍的B+树,它是由上慢慢向下创建的。

4.2 内节点中目录项记录的唯一性

如果非叶子节点,也就是内节点目录项记录是完全一致的,比如下图。
那么新来一条数据:0,1,‘c’,就不知道该插入到那一页了。
在这里插入图片描述

此时就需要保证在B+树的同一层内节点的目录项记录除页号这个字段以外是唯一的,那么此时可以加上主键值,这样内节点的目录项记录就一定是唯一的了:

  • 索引列的值
  • 主键值
  • 页号

在这里插入图片描述

4.3 一个页面最少存储2条记录

InnoDB的一个数据页至少存放2条记录,不然上面介绍的B+树结构的方案就没有任何意义了。

5、MyISAM中的索引方案

5.1 MyISAM索引的原理

MyISAM引擎使用B+树作为索引结构,但是它的叶子节点的data域存放的是数据记录的地址

InnoDB中索引即数据(.idb),也就是聚簇索引的那颗B+树的叶子节点中包含了完整的用户数据记录。
MyISAM虽然也使用树形结构,但是却将索引和数据分开存储

  1. MyISAM将表中的记录按照插入顺序单独存储在一个文件中,称之为数据文件(.MYD)。由于在插入数据的时候并没有刻意按照主键大小排序,所以并不能在这些数据上使用二分法进行查找。
  2. MyISAM会把索引信息存储到一个称为索引文件(.MYI)中。MyISAM会单独为表的主键创建一个索引,只不过在索引的叶子节点中存储的不是完整的用户记录,而是主键值+用户数据记录地址

如下图就是一个以col1为主键的索引文件的存储格式。
在这里插入图片描述
如下图就是一个以col2建立的二级索引。
在这里插入图片描述

5.2 MyISAM与InnoDB的对比

MyISAM的索引方式都是非聚簇的。而InnoDB除了有非聚簇之外,还包含一个聚簇索引。

  1. InnoDB的数据文件本身就是索引文件(.idb)。而MyISAM的索引文件(.MYI)和数据文件(.MYD)是分离的,索引文件仅保存数据记录的地址
  2. InnoDB如果根据主键值对聚簇索引进行查找,只需要一次就能找到用户数据记录。但是MyISAM的索引文件由于存储的是用户数据记录的地址,所以一定会有一次回表操作
  3. InnoDB的非聚簇索引存储的是数据记录的主键值,然后需要通过回表操作,通过主键值再来找到数据记录。而MyISAM索引记录的就是用户记录的地址,所以MyISAM的回表操作肯定比InnoDB要快
  4. InnoDB要求表必须要有主键,如果没有显示指定,则会自动选择一个可以非空且唯一标识数据记录的列作为主键,如果没找到的话,会自动生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。而MyISAM可以没有。

小结:
了解不同存储引擎的索引实现方式,对于正确使用和优化索引都非常有帮助。
举例1:知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键。因为所有二级索引都引用主键索引,过长的主键会令二级索引变得过大。
举例2:在InnoDB中,用非单调的字段作为主键不是一个好主意。非单调的主键会造成在插入新纪录时,数据文件为了维持B+树的特性而频繁地分裂调整,十分低效,使用自增字段作为主键则是一个很好的选择

猜你喜欢

转载自blog.csdn.net/xueping_wu/article/details/125351669