Mysql存储引擎索引原理详解

Posted 我的紫霞辣辣

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mysql存储引擎索引原理详解相关的知识,希望对你有一定的参考价值。

索引介绍

什么是索引?

  • 索引是存储引擎中一种数据结构,或者说数据的组织方式,又称之为键key,是存储引擎用于快速找到记录的一种数据结构
  • 为数据建立索引就好比是为书建目录,或者说是为字典创建音序表,如果要查某个字,如果不使用音序 表,则需要从几百页中逐页去查。

为何要用索引?

  • 一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,在生产环境中,我们遇到最多的、也是最容易出问题的,还是一些复杂的查询操作,因此对查询语句的优化显然 是重中之重。说起加速查询,就不得不提到索引了。
  • 索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高好几个数量级。

如何正确的看待索引?

错误的认知:

  1. 软件上线之后,运行了一段时间,发现软件很卡,想到要加索引

    出现软件上线之后才想着加索引,光把问题定位到索引身上都需要耗费很长的时间,排查成本很高。
    最好是在软件开发之初配合开发人员,定位到常用的查询字段。然后为该字段创建索引。
    
  2. 索引越多越好

    索引是用于加速查询的,降低写效率。
    如果是某一张表的.ibd文件中创建了很多棵索引树,意味着很小的一个update语句。 		
    就会导致很多棵索引树都需要发生变化,从而提高了硬盘的io。
    
  • 索引是应用程序设计和开发的一个重要方面。若索引太多,应用程序的性能可能会受到影响。而索引太 少,对查询性能又会产生影响,要找到一个平衡点,这对应用程序的性能至关重要。

理解索引的储备知识

机械磁盘一次IO的时间

机械磁盘一次io的时间 = 寻道时间 + 旋转延迟 + 传输时间

寻道时间

  • 寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms(毫秒)以下

旋转延迟

  • 旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1 秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;

传输时间

  • 传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽 略不计

    所以访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17=9ms左右
    

这9ms对于人来说可能非常短,但对于计算机来可是非常长的一段时间,长到什么程度呢? 一台500 -MIPS(Million Instructions Per Second)的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行约450万条指令,数据库动辄十万百万乃至 千万级数据,每次9毫秒的时间,显然是个灾难。

磁盘预读

考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化:当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助
一页就是一个磁盘块(block块),innodb存储引擎一页16k,即一次io读16k到内存中

索引的目的在于提高查询效率

索引的根本原理就是把硬盘的io次数降下来,索引树越矮,代表磁盘的io次数越少
为一张表中的一行行记录创建索引,就好比是为书的一页页内容创建目录
有了目录结构之后,我们以后的查询都应该通过目录去查询

本质都是:
通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件。
也就是说,有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。

索引的分类

索引模型分为很多种类

  1. b+树索引(等值查询与范围查询都快)

    二叉树->平衡二叉树->B树->B+树
    
  2. hash索引(等值查询快,范围查询慢)

    将数据打散再去查询
    
  3. full text:全文索引(只可以用在myisam引擎)

    通过关键字的匹配来进行查询,类似于like的模糊匹配 适用于大量的数据查询,但是准确度很低 
    百度在搜索文章的时候有可能使用的就是全文索引,mysql中基本上不怎么使用这种查询
    

不同的存储引擎支持的索引类型也不一样

  • InnoDB存储引擎

    支持事务,支持行级别锁定,支持 B-tree(默认)、Full-text 等索引,不支持Hash索引;
    
  • MyISAM存储引擎

    不支持事务,支持表级别锁定,支持 B-tree、Full-text 等索引,不支持 Hash 索引;
    
  • Memory存储引擎

    不支持事务,支持表级别锁定,支持 B-tree、Hash 等索引,不支持 Full-text 索引。
    

mysql5.58版本之后默认的存储引擎是innodb,innodb存储引擎默认的索引模型/结构是B+树! ! !

索引的数据结构

创建索引的两大步骤

为某个字段创建索引,即以某个字段的值为基础构建索引结构,那么如何构建呢?分为两大步骤

1. 提取每行记录中该字段的值,以该值当作key,至于key对应的value是什么?每种索引结构各不相同

2. 然后以key值为基础构建索引结构

例如 :

# 为user表的id字段创建索引,会以每条记录的id字段值为基础生成索引结构 
create index 索引名 on user(id);

使用索引 
select * from user where id = xxx; 

以后的查询条件中使用了该字段,则会命中索引结构

innodb存储引擎默认的索引结构为B+树,而B+树是由二叉树、平衡二叉树、B树再到B+树一路演变过来的

B+树的演变历程

二叉树 => 平衡二叉树 => B树 => B+树

二叉树

在这里插入图片描述
如果我们需要查找id=12的用户信息:

select * from user where id=12;

利用我们创建的二叉查找树索引,查找流程如下:

1. 将根节点作为当前节点,把12与当前节点的键值10比较,12大于10,接下来我们把当前节点>的右子节点作为当前节点。 
2. 继续把12和当前节点的键值13比较,发现12小于13,把当前节点的左子节点作为当前节点。 
3. 把12和当前节点的键值12对比,12等于12,满足条件,我们从当前节点中取出data,即 id=12,name=xm。

平衡二叉树

基于上图的二叉树,我们确实可以快速地找到数据。
但是让我们回到二叉查找树地特点上,只论二叉查找树,它的特点只是:

任何节点的左子节点的键值都小于当前节点的键值,右子节点的键值都大于当前节点的键值。

所以,依据二叉查找树的特点,二叉树可以是这样构造的,如下所示:
在这里插入图片描述

平衡二叉树又称AVL树,在满足二叉查找树特性的基础上,要求每个节点的左右子树的高度不能超过1。
下面是平衡二叉树和非平衡二叉树的对比:
在这里插入图片描述

B树

那么直接用平衡二叉树这种数据结构来构建索引有什么问题?

1. 首先,因为内存的易失性。一般情况下,我们都会选择将user表中的数据和索引存储在磁盘这种外围设备中。
但是和内存相比,从磁盘中读取数据的速度会慢上百倍千倍甚至万倍,所以,我们应当尽量减 少从磁盘中读取数据的次数。

2. 另外,从磁盘中读取数据时,都是按照磁盘块来读取的,并不是一条一条的读。 
如果我们能把尽量多的数据放进磁盘块中,那一次磁盘读取操作就会读取更多数据,那我们查找数据的时间也会大幅度降低。

3. 所以,如果我们单纯用平衡二叉树这种数据结构作为索引的数据结构,即每个磁盘块只放一个节点,每个节点中只存放一组键值对。	
此时如果数据量过大,二叉树的节点则会非常多,树的高度也随即变高。我们查找数据的也会进行很多次磁盘IO,查找数据的效率也会变得极低!

在这里插入图片描述

综上,如果我们能够在平衡二叉的树的基础上,把更多的节点放入一个磁盘块中,那么平衡二叉树的弊端也就解决了。即构建一个单节点可以存储多个键值对的平衡树,这就是B树。 B树(Balance Tree)即为平衡树的意思,下图即是一颗B树。
在这里插入图片描述
注意:

  1. B树的构造是有一些规定的,但这不是本文的关注点,有兴趣的同学可以令行了解。
  2. B树也是平衡的,当增加或删除数据而导致B树不平衡时,也是需要进行节点调整的

B树的弊端:

  • B树只擅长做等值查询,而对于范围查询(范围查询的本质就是n次等值查询),或者说排序操作,B树也帮不了我们。

    select * from user where id=3; 		-- 擅长 
    
    select * from user where id>3; 		-- 不擅长
    

    如上图所示,如果我们需要查询id大于3的值,那么B树索引结构,会把id为[4,5,6 . . .]大于3的值全部遍历出来,从页1开始进行等值查询。

B+树

B+树是对B树的进一步优化。
在这里插入图片描述
B+树的四大特点:

  1. 非叶子节点只存放key值,只有叶子节点才存放key以及对应的value ===> 非叶子节点能存放的key的个数变多,衍生出的指针越多,树会变得更矮更胖,io效率进一步提升。

  2. 叶子节点彼此之间有双向链表 ===> 范围查询速度快。

    select * from user where id > 3;
    
    先找到id=4所在的叶子节点,然后根据叶子节点(id=5、id=6...)即可。
    不需要再回到根节点查找。
    
  3. 叶子节点内的key值是单向链表,叶子节点与叶子节点之间是双向链表,即全部排好序了 ===> 排序速度快。

  4. B+树的阶数是等于键的数量的,例如上图,我们的B+树中每个节点可以存储3个键,3层B+树存可以存储 3×3×3=9个数据。所以如果我们的B+树一个节点可以存储1000个键值,那么3层B+树可以存储 1000×1000×1000=10亿个数据。而一般根节点是常驻内存的,所以一般我们查找10亿数据,只需要2次 磁盘IO。

MyISAM中的B+树索引实现与innodb中的略有不同。在MyISAM中,B+树索引的叶子节点并不存储数据,而是存储数据的文件地址。

B+树索引结构分类

B+树主要分为两种索引结构:聚集索引和非聚集索引

  1. 聚集索引(又称聚簇索引、主键索引,一张表必须有且只有一个):以innodb作为存储引擎的表, 表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。这是因为innodb是把数据存放在B+树中的,而B+树的键用的就是主键,在B+树的叶子节点中,存储了表中所有的数据。这种以主键作为B+树索引的键值而构建的B+树索引,我们称之为聚集索引。

    # 命中主键索引查询
    select * from user where id=2;
    
  2. 非聚集索引(又称非聚簇索引、辅助索引,一张表可以创建多个辅助索引):以主键以外的列值作为键值构建的B+树索引,我们称之为非聚集索引。非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。

    # 创建辅助索引
    create index xxx on user(name);
    
    # 命中辅助索引查询
    select * from user where name="nana";
    

明白了聚集索引和非聚集索引的定义,我们应该明白这样一句话:数据即索引,索引即数据。
一张innodb存储引擎表中必须有且只有一个聚集索引,但是可以有多个辅助索引。

覆盖索引与回表操作

回表操作
命中了辅助索引,然后要找的字段值不存在与辅助索引的叶子节点上,则需要根据拿到的主键值再去聚集索引中查询一遍,然后再聚集索引的叶子节点找到你想要的内容,这就叫回表操作。
例如:

# 创建辅助索引
create index xxx on user(name);

# 下述语句,命中了辅助索引,但是select需要查询出的除了辅助索引叶子节点有的name字段值外,还想要age字段的值,那么需要进行回表操作
select name,age from user where name="nana";

覆盖索引
命中了某棵索引树,然后在其叶子节点就找到了你想要的值,即不需要回表操作,就是覆盖了索引。
例如:

create index xxx on user(name); 
  
# 下述语句,覆盖了索引 
select name from user where name="nana";

使用主键字段当作条件,百分百覆盖了索引,效率极高,推荐使用

# 如果id字段是主键,那么下述语句也覆盖了索引 
select * from user where id=3;

MySQL索引管理

B+树常见的索引分类(innodb存储引擎默认)

聚集索引:即主键索引,primary key

用途:
1. 加速查找
2. 约束(不为空、不能重复)

唯一索引:uniqe

用途:
1. 加速查找
2. 约束(不能重复) 

普通索引(即非聚簇索引):index

用途:
1. 加速查找

联合索引:

primary key(id,name): 联合主键索引 
unique(id,name): 联合唯一索引 
index(id,name): 联合普通索引
# 建表时指定索引
mysql> create table t1(
    -> id int primary key auto_increment,
    -> class_name varchar(16) unique,
    -> name varchar(16),
    -> age int
    -> );
Query OK, 0 rows affected (0.00 sec)

# 添加非聚簇索引指定为name字段
mysql> create index xxx on t1(name);
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

# 查看表结构
mysql> desc t1;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | int(11)     | NO   | PRI | NULL    | auto_increment |
| class_name | varchar(16) | YES  | UNI | NULL    |                |
| name       | varchar(16) | YES  | MUL | NULL    |                |
| age        | int(11)     | YES  |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

# 删除非聚簇索引
mysql> drop index xxx on t1;
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0
# 创建一张没有指定任何索引的表
# mysql会默认创建一个非空且唯一的当作隐式主键
mysql> create table t2(
    -> id int,
    -> class_name varchar(16),
    -> name varchar(16),
    -> age int
    -> );
Query OK, 0 rows affected (0.00 sec)

# 添加主键索引
mysql> alter table t2 add primary key t1(id);
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

# 删除主键索引
mysql> alter table t2 drop primary key;
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0


# 创建唯一索引,不知道索引名,mysql默认会将索引名创建成表名
mysql> alter table t2 add unique key t2(class_name);
Query OK, 0 rows affected (0.00 sec)
Records: 0  Duplicates: 0  Warnings: 0

# 查看表的详细信息
mysql> show create table t2;
+-------+----------------------------------+
| Table | Create Table                     |                                                                                                                                                                                                  
+-------+----------------------------------+
| t2    | CREATE TABLE `t2` (
  `id` int(11) DEFAULT NULL,
  `class_name` varchar(16) DEFAULT NULL,
  `name` varchar(16) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  UNIQUE KEY `t2` (`class_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
+-------+-----------------------------------+
1 row in set (0.00 sec)

# 删除唯一索引
# 第一个t2是表名,第二个t2是mysql默认创建的索引名
mysql> alter table t2 drop index t2;
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0


# # 添加非聚簇索引指定为name字段
mysql> create index xxx on t1(name);
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

# 删除非聚簇索引
mysql> drop index xxx on t1;
Query OK, 0 rows affected (0.00 sec)
Records: 0  Duplicates: 0  Warnings: 0

以上是关于Mysql存储引擎索引原理详解的主要内容,如果未能解决你的问题,请参考以下文章

MySQL索引详解

mysql索引数据结构详解---mysql详解

不会吧不会吧,难道还有人不了解MySQL索引底层原理?

MySql存储引擎和索引原理

MySQL索引详解

图文动画详解原理系列1.MySQL 索引原理详解