MySQL索引原理与B+树应用详解

1. 本节目标

了解什么是索引

了解除索引使用的数据结构

掌握B+树在索引中的应用

掌握索引分类和使用

2. 简介

2.1 索引是什么?

MySQL的索引是一种数据结构,它可以帮助数据库高效地查询、更新数据表中的数据。索引通过 一定的规则排列数据表中的记录,使得对表的查询可以通过对索引的搜索来加快速度

MySQL 索引类似于书籍的目录,通过指向数据行的位置,可以快速定位和访问表中的数据,比如 汉语字典的目录(索引)页,我们可以按笔画、偏旁部首、拼音等排序的目录(索引)快速查找到需 要的字

2.2 为什么要使用索引?

显而易见,使用索引的目的只有一个,就是提升数据检索的效率,在应用程序的运行过程中,查 询操作的频率远远高于增删改的频率

3. 索引应该选择哪种数据结构

3.1 HASH

时间复杂度是 O(1) ,查询速度非常快,但是MySQL并没有选择HASH做为索引的默认数据结 构,主要原因是HASH不支持范围查找

3.2 二叉搜索树

二叉搜索树的中序遍历是一个有序数组,但有几个问题导致它不适合用作索引的数据结构

  1. 最坏情况下时间复杂度为O(N)

  2. 节点个数过多无法保证树高

AVL和红黑树,虽然是平衡或者近似平衡,但是毕竟是二叉结构

在检索数据时,每次访问某个节点的子节点时都会发生一次磁盘IO,而在整个数据库系统中,IO是 性能的瓶颈,减少IO次数可以有效的提升性能

3.3 N叉树

为了解决树高的问题,可以使用N叉树

通过观察,相同数据量的情况下,N叉树的树高可以得到有效的控制,也就意味着在相同数据量的情况 下可以减少IO的次数,从而提升效率。但是MySQL认为N叉树做为索引的数据结构还不够好

3.4 B+树

3.4.1 简介

B+树是一种经常用于数据库和文件系统等场合的平衡查找树,MySQL索引采用的数据结构,以4阶 B+树为例,如下图所示

3.4.2 B+树的特点

能够保持数据稳定有序,插入与修改有较稳定的时间复杂度

非叶子节点仅具有索引作用,不存储数据,所有叶子节点保真实数据

所有叶子节点构成一个有序链表,可以按照key排序的次序依次遍历全部数据

3.4.3 B+树与B树的对比

叶子节点中的数据是连续的,且相互链接,便于区间查找和搜索

非叶子节点的值都包含在叶子节点中

对于B+树而言,在相同树高的情况下,查找任一元素的时间复杂度都一样,性能均衡

4. MySQL中的页

4.1

.ibd 文件中最重要的结构体就是Page(页) ,页是内存与磁盘交互的最小单元,默认大小为 16KB,每次内存与磁盘的交互至少读取一页,所以在磁盘中每个页内部的地址都是连续的,之所 以这样做,是因为在使用数据的过程中,根据局部性原理,将来要使用的数据大概率与当前访问的 数据在空间上是临近的,所以一次从磁盘中读取一页的数据放入内存中,当下次查询的数据还在这 个页中时就可以从内存中直接读取,从而减少磁盘I/O提高性能

局部性原理

是指程序在执行时呈现出局部性规律,在一段时间内,整个程序的执行仅限于程序中的某一部 分。相应地,执行所访问的存储空间也局限于某个内存区域,局部性通常有两种形式:时间局部 性和空间局部性。

时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再 次访问

空间局部性(Spatial Locality):将来要用到的信息大概率与正在使用的信息在空间地址上是临 近的

每一个页中即使没有数据也会使用 16KB 的存储空间,同时与索引的B+树中的节点对应,后续在 专题七:索引中详细讲解B+树的内容,查看页的大小,可以通过系统变量 innodb_page_size 查看

java 复制代码
mysql> SHOW VARIABLES LIKE 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 | # 16KB
+------------------+-------+
1 row in set, 1 warning (0.04 sec)

在MySQL中有多种不同类型的页,最常用的就是用来存储数据和索引的"索引页",也叫做"数据 页",但不论哪种类型的页都会包含页头(File Header)和页尾(File Trailer),页的主体信息使用数 据"行"进行填充,数据页的基本结构如下图所示:

4.2 页文件头和页文件尾

页文件头和页文件尾中包含的信息,如下图所示

这里我们只关注,上一页页号和下一页页号,通过这两个属性可以把页与页之间链接起来,形成一个双向链表

4.3 页主体

页主体部分是保存真实数据的主要区域,每当创建一个新页,都会自动分配两个行,一个是页内最 小行 Infimun ,另一个是页内最大行 Supremun ,这两个行并不存储任何真实信息,而是做为 数据行链表的头和尾,第一个数据行有一个记录下一行的地址偏移量的区域 next_record 将页 内所有数据行组成了一个单向链表,此时新页的结构如下所示

当向一个新页插入数据时,将 Infimun 连接第一个数据行,最后一行真实数据行连接 Supremun ,这样数据行就构建成了一个单向链表,更多的行数据插入后,会按照主键从小到大 的顺序进行链接,如下图所示

4.4 页目录

当按主键或索引查找某条数据时,最直接简单的方法就是从头行 infimun 开始,沿着链表顺序逐个比对查找,但一个页有16KB,通常会存在数百行数据,每次都要遍历数百行,无法满足高效查 询,为了提高查询效率,InnoDB(存储引擎)采用二分查找来解决查询效率问题

具体实现方式是,在每一个页中加入一个叫做页目录 Page Directory 的结构,将页内包括头 行、尾行在内的所有行进行分组,约定头行单独为一组,其他每个组最多8条数据,同时把每个组 最后一行在页中的地址,按主键从小到大的顺序记录在页目录中在,页目录中的每一个位置称为一 个槽,每个槽都对应了一个分组,一旦分组中的数据行超过分组的上限8个时,就会分裂出一个新 的分组;

后续在查询某行时,就可以通过二分查找,先找到对应的槽,然后在槽内最多8个数据行中进行遍 历即可,从而大幅提高了查询效率,这时一个页的核心结构就完成了

4.5 数据页头

数据页头记录了当前页保存数据相关的信息,如下图所示

5. B+在MySQL索引中的应用

非叶子节点保存索引数据,叶子节点保存真实数据,如下图所示

以查找id为5的记录,完整的检索过程如下:

  1. 首先判断B+树的根节点中的索引记录,此时 5 < 7 ,应访问左孩子节点,找到索引页2

  2. 在索引页2中判断id的大小,找到与5相等的记录,命中,加载对应的数据页

以上的IO过程,加载索引页1 --> 加载索引页2 --> 加载数据页3

5.1 计算三层树高的B+树可以存放多少条记录

假设一条用户数据大小为1KB,在忽略数据页中数据页自身属性空间占用的情况下,一页可以存16 条数据

索引页一条数据的大小为,主键用BIGINT类型占8Byte,下一页地址6Byte,一共是14Byte,一个 索引页可以保存 16*1024/14 = 1170 条索引记录

如果只有三层树高的情况,综合只保存索引的根节点和二级节点的索引页以及保存真实数据的数据 页,那么一共可以保存 1170*1170*16 = 21,902,400 条记录,也就是说在两千多万条数据的 表中,可以通过三次IO就完成数据的检索

6. 索引分类

6.1 主键索引

当在一个表上定义一个主键 PRIMARY KEY 时,InnoDB使用它作为聚集索引

推荐为每个表定义一个主键。如果没有逻辑上唯一且非空的列或列集可以使用主键,则添加一个自 增列

6.2 普通索引

最基本的索引类型,没有唯一性的限制

可能为多列创建组合索引,称为复合索引或组全索引

6.3 唯一索引

当在一个表上定义一个唯一键 UNQUE 时,自动创建唯一索引

与普通索引类似,但区别在于唯一索引的列不允许有重复值

6.4 全文索引

基于文本列(CHAR、VARCHAR或TEXT列)上创建,以加快对这些列中包含的数据查询和DML操作

用于全文搜索,仅MyISAM和InnoDB引擎支持。

6.5 聚集索引

与主键索引是同义词

如果没有为表定义 PRIMARY KEY, InnoDB使用第一个 UNIQUE 和 NOT NULL 的列作为聚集索引

如果表中没有 PRIMARY KEY 或合适的 UNIQUE 索引,InnoDB会为新插入的行生成一个行号并 用6字节的 ROW_ID 字段记录, ROW_ID 单调递增,并使用 ROW_ID 做为索引

6.6 非聚集索引

聚集索引以外的索引称为非聚集索引或二级索引

二级索引中的每条记录都包含该行的主键列,以及二级索引指定的列

InnoDB使用这个主键值来搜索聚集索引中的行,这个过程称为回表查询

6.7 索引覆盖

当一个select语句使用了普通索引且查询列表中的列刚好是创建普通索引时的所有或部分列,这时 就可以直接返回数据,而不用回表查询,这样的现象称为索引覆盖

7. 使用索引

7.1 自动创建

当我们为一张表加主键约束(Primary key),外键约束(Foreign Key),唯一约束(Unique)时, MySQL会为对应的的列自动创建一个索引

如果表不指定任何约束时,MySQL会自动为每一列生成一个索引并用 ROW_ID 进行标识

7.2 手动创建

7.2.1 主键索引

java 复制代码
# 方式一,创建表时创建主键
create table t_test_pk (
 id bigint primary key auto_increment,
 name varchar(20)
);
# 方式二,创建表时单独指定主键列
create table t_test_pk1 (
 id bigint auto_increment,
 name varchar(20),
 primary key (id)
);
# 方式三,修改表中的列为主键索引
create table t_test_pk2 (
 id bigint,
 name varchar(20)
);
alter table t_test_pk2 add primary key (id) ;
alter table t_test_pk2 modify id bigint auto_increment;

7.2.2 唯一索引

java 复制代码
# 方式一,创建表时创建唯一键
create table t_test_uk (
 id bigint primary key auto_increment,
 name varchar(20) unique
);
# 方式二,创建表时单独指定唯一列
create table t_test_uk1 (
 id bigint primary key auto_increment,
 name varchar(20),
 unique (name)
);
# 方式三,修改表中的列为唯一索引
create table t_test_uk2 (
 id bigint primary key auto_increment,
 name varchar(20)
);
alter table t_test_uk2 add unique (name) ;

7.2.3 普通索引

java 复制代码
# 方式一,创建表时指定索引列
create table t_test_index (
 id bigint primary key auto_increment,
 name varchar(20) unique
 sno varchar(10),
 index(sno)
);
# 方式二,修改表中的列为普通索引
create table t_test_index1 (
 id bigint primary key auto_increment,
 name varchar(20),
 sno varchar(10)
);
alter table t_test_index1 add index (sno) ;
# 方式三,单独创建索引并指定索引名
create table t_test_index2 (
 id bigint primary key auto_increment,
 name varchar(20),
 sno varchar(10)
);
create index index_name on t_test_index2(sno);

7.3 创建复合索引

创建语法与创建普通索引相同,只不过指定多个列,列与列之间用逗号隔开

java 复制代码
# 方式一,创建表时指定索引列
create table t_test_index4 (
 id bigint primary key auto_increment,
 name varchar(20),
 sno varchar(10),
 class_id bigint,
 index (sno, class_id)
);
# 方式二,修改表中的列为复合索引
create table t_test_index5 (
 id bigint primary key auto_increment,
 name varchar(20),
 sno varchar(10),
 class_id bigint
);
alter table t_test_index5 add index (sno, class_id);
create table t_test_index6 (
 id bigint primary key auto_increment,
 name varchar(20),
 sno varchar(10),
 class_id bigint
);
create index index_name on t_test_index6 (sno, class_id);

7.4 查看索引

java 复制代码
# 方式一show keys from 表名
# 方式二show index from t_test_index6;
# 方式三desc t_test_index6;

7.5 删除索引

7.5.1 主键索引

java 复制代码
# 语法
alter table 表名 drop primary key;
# 示例,删除t_test_index6表中的主键
mysql> alter table t_test_index6 drop primary key;
ERROR 1075 (42000): Incorrect table definition; there can be only one auto
column and it must be defined as a key
# 如查提示由于自增列的错误,先删除自增属性
mysql> alter table t_test_index6 modify id bigint;
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 重新删除主键
mysql> alter table t_test_index6 drop primary key;
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0

7.5.2 其他索引

java 复制代码
# 语法
alter table 表名 drop index 索引名;
# 示例,删除t_test_index6表中名为index_name的索引
mysql> alter table t_test_index6 drop index index_name;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 查看结果
mysql> show keys from t_test_index6\G
Empty set (0.00 sec)

7.6 创建索引的注意事项

索引应该创建在高频查询的列上

索引需要占用额外的存储空间

对表进行插入、更新和删除操作时,同时也会修索引,可能会影响性能

创建过多或不合理的索引会导致性能下降,需要谨慎选择和规划索引

相关推荐
java干货1 小时前
用 MySQL SELECT SLEEP() 优雅模拟网络超时与并发死锁
网络·数据库·mysql
哈哈不让取名字1 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python
洛_尘2 小时前
MySQL 6:数据库约束
数据库·mysql
dawudayudaxue2 小时前
sqlite在安卓下使用ndk的交叉编译
android·数据库·sqlite
YIN_尹2 小时前
【MySQL】表的约束(下)
android·数据库·mysql
lkbhua莱克瓦242 小时前
Apache Maven全面解析
java·数据库·笔记·maven·apache
optimistic_chen2 小时前
【Redis系列】哨兵模式
linux·数据库·redis·分布式·哨兵
啊吧怪不啊吧2 小时前
极致性能的服务器Redis之Hash类型及相关指令介绍
大数据·数据库·redis·sql·mybatis·哈希算法
五阿哥永琪2 小时前
MySQL面试题 如何解决深分页?
数据库·mysql