【MySQL】 索引的底层原理与使用:B+树、数据页与 InnoDB

文章目录

  • [一、 索引是什么?](#一、 索引是什么?)
  • [二、 应该是用什么样的数据结构](#二、 应该是用什么样的数据结构)
    • [1. 哈希表](#1. 哈希表)
    • [2. 二叉搜索树](#2. 二叉搜索树)
    • [3. N叉树](#3. N叉树)
    • [4. b+树](#4. b+树)
  • [三、 MySQL中的页](#三、 MySQL中的页)
    • [3.1 为什么要使用页?](#3.1 为什么要使用页?)
    • [3.2 页头与页尾](#3.2 页头与页尾)
    • [3.3 页主体](#3.3 页主体)
    • [3.4 页目录](#3.4 页目录)
    • [3.5 数据页头](#3.5 数据页头)
  • [四、 B+树在MySQL中的应用](#四、 B+树在MySQL中的应用)
  • 五、索引分类
  • 六、创建索引
  • 七、删除主键
    • [7.1 删除主键索引](#7.1 删除主键索引)
    • [7.2 删除其他索引](#7.2 删除其他索引)
  • 八、创建索引的注意事项

一、 索引是什么?

索引是一种特殊数据结构,MySQL主流为 b+ 树,可以帮助数据库高效的查询、更新表中的结构。索引通过一定规则排列数据表中的记录,使对表的查询可以通过对索引的搜所加快速度,避免全表扫描。

类比:

  • 表 → 书籍
  • 数据 → 书籍内容
  • 索引 → 书籍目录

二、 应该是用什么样的数据结构

1. 哈希表

时间复杂度为O(1),查询速度非常快,MySQL没有选择哈希表主要是因为哈希表不支持范围查询

2. 二叉搜索树

主序遍历得到有序数组,支持范围查询。主要弊端有:

  1. 二叉搜索树在极端情况下可能退化为单边数,时间复杂度为O(n)
  2. 节点个数过多时不能保证树高。

AVL和红黑树虽然是平衡或近似平衡,但是主要是二叉结构,没访问一个节点的子节点都会发生一次磁盘IO,而在数据库系统中,IO是性能的瓶颈,减少IO次数可以大幅度提升性能。

3. N叉树

数据量相同的情况下,N叉树的树高减少,可以减少IO次数,但是效率提升并不多,b+树是更优的数据结构

4. b+树

b+树是一种常用于数据库和文件系统等场合的平衡查找树, 4阶b+树如图:

b+树特点:

  1. 叶子节点之间有相互连接的引用,可以通过一个节点找到它的兄弟节点。
    • MySQL在组织叶子节点是使用双向循环链表,比一般b+树更加高效
  2. 非叶子节点的值都包含在叶子节点中,非叶子节点只能提供对叶子节点的索引,真正的数据保存在叶子节点中
  3. 因为所有数据都在叶子节点,相同树高情况下,查找任意元素的时间复杂度都一样,性能均衡。后续开发的优化也易于实现

三、 MySQL中的页

3.1 为什么要使用页?

InnoDB存储引擎生成的表空间文件是.ibd,在ibd文件中最重要的结构体就是页(Page),页是内存与磁盘交互的最小单元,默认大小是16KB。每次内存与磁盘的交互至少读取一页,所以磁盘中每页内部的磁盘地址是连续的(可以通过顺序遍历在页内从第一条数据读到最后一条数据)。

根据局部性原理,将来要使用的数据大概率与当前数据在空间上是临近的,所以依次从磁盘中读取一页放到内存中,从而减少IO次数,提高性能。

每一页即使没有数据也会使用16KB存储空间,同时索引与b+树的节点对应

  • 非叶子节点对应索引页
  • 叶子节点对应数据页

对于Linux操作系统来说,管理文件的最小单位是4KB,也就是说内存中的页要分成4部分才能写入磁盘中。如果写到一半操作系统挂了,在落盘之前,MySQL会通过各种日志记录操作,保证重启后可以找到没有落盘的数据内容。

MySQL中有很多类型的页,最常用的是存储数据和索引的数据页索引页 。无论哪种页型都包括页头页尾页主体 ,页的主要信息是使用数据行来填充。数据页的基本结构:

3.2 页头与页尾


上一页页号下一页页号两个属性可以把页链接起来,形成双向链表

3.3 页主体

页主体是保存真实数据的主要区域。

最小行与最大行

每创建一个新的页,都会自动分配两个行,如下图所示

  • 页内最小行 Infimun:页内最小的虚拟记录
  • 页内最大行Supremun:页内最大的虚拟记录

这两个行不存储真实信息,而是把页内所有真实数据串成有序链表。类似链表的头节点和尾节点。

数据行

当新插入数据时,Infimun连接第一个真实记录,最后一条真实记录连接Supremun,页内用户记录按主键 升序,通过next_record指针连成单向链表


Infimumnext 指向页内主键最小的用户记录,主键最大的用户记录的 next 指向 Supremum。遍历页内记录从 Infimum 出发、到 Supremum 终止,不需要判空或边界特殊处理。

最小行,最大行与普通数据行的区别:

  1. 最小行,最大行与数据行的基本结构相同
  2. 最小行,最大行的heap_no 数值固定,分别为0,1,普通数据行从2开始递增
  3. 最小行,最大行的信息区只保留两个字符串,不记录具体内容。

3.4 页目录

每页有16KB大小,也就是说可能有几百条数据记录,如果每次都顺序遍历效率很低,为了提升查询效率,InnoDB做了进一步优化:在页中加入页目录,通过二分查找来解决查询效率问题。

对页内的数据进行分组,分组规则:

  1. 最小行单独为一组
  2. 每组最多8条记录
  3. 最大行必须在最后一行

页目录中有,每个槽对应一个分组。槽中记录的每个分组的最后一条数据的地址。这样查找数据时可以先在槽中大致确定分组,再到组内顺序查找。

3.5 数据页头

数据页头记录了当前页保存数据相关的信息:

四、 B+树在MySQL中的应用

  1. 非叶子节点保存索引信息(索引页),其中保存主键的值和子节点的引用
  2. 叶子节点保存真实数据(数据页)
  3. 表中有主键就会自动创建索引
  4. 数据页中的一列对应表中的一行数据
  5. 页与页之间是双向循环链表

假设查找id=5的数据:

  1. 在索引页1中, 5<7,到左孩子中查找
  2. 在索引页2中,恰好找到5,加载对应的数据页3

所有关于页的操作和访问都是在内存中进行的。 IO过程:加载索引页1->加载索引页2->加载索引页3

  • 计算三层B+树可以存放的数据(理论上):
    1. 假设一条用户数据为1KB,忽略数据页自身属性的空间,每页存放16条数据
    2. 索引页一条数据大小:主键BIGINT8Byte,下一页地址6Byte,一共14Byte。一个索引页可以保存16*1024/14=1170条索引记录
    3. 如果三层树高,粗略估计可以存放1170*1170*16=21,902,400条数据。也就是说两千多万条数据的表可以通过3次IO完成数据检索

五、索引分类

  1. 主键索引 :当表定义了主键 ,InnoDB自动使用它作为聚集索引,索引的值就是主键列的值
  2. 普通索引 :最基本的索引类型,没有唯一性限制。
    • 为了提升效率,实际开发中通常为查询频繁的列创建索引。
    • 可能为多列创建组合索引,成为复合索引或组合索引
  3. 唯一索引 :当表定义了唯一键时,自动创建唯一索引。与普通索引类似,不允许有重复值
  4. 全文索引:基于文本列(char,varchar或text)创建,以加快这些列中包含的数据查询和DML操作。
  5. 聚集索引 :类似主键索引。可以标识数据行的唯一性。
    • 如果没有主键,InnoDB会使用第一个设置了非空且唯一的列作为聚集索引
    • 如果没有主键或合适的唯一键,InnoDB会为插入的行生成生成一个行号,并用6字节的ROW_ID字段(数据行中的隐藏列)来记录,ROW_ID单调递增,并使用ROW_ID作为索引
  6. 非聚集索引 :聚集索引以外的索引称为非聚集索引或二级索引
    • 非聚集索引的叶子节点只包含 索引字段主键值
    • InnoDB使用这个主键值来搜索聚集索引 中的行,这个过程称为回表查询
  7. 索引覆盖:当一个select语句使用了普通索引且查询列表中的列刚好时创建普通索引时的所有或部分列,这是就可以直接返回数据,不需要回表查询,这样的现象称为索引覆盖。

注意:

  1. 创建索引后都会生成索引树,一个索引对应一个索引树。而索引树会占用磁盘空间,对于增删改的效率有影响。因此创建普通索引时应该慎重考虑这个索引是否需要。
  2. 假设创建组合索引时namesn之前,那么使用时也要先查name,再查sn。如果只是用sn,索引会失效。如果只能用name查询,应该为name单独创建索引。

六、创建索引

6.1 自动创建

  • 当我们创建了主键,唯一键或外键约束时,MySQL会为对应的列创建索引
  • 如果没有任何约束,MySQL会为每一列创建索引并用ROW_ID 进行标识

6.2 手动创建

主键索引

  1. 创建表时在列后面指定主键
sql 复制代码
create table t_pk1(
	id bigint primary key auto_increment,
	name varchar(20)
);
  1. 创建表时在定义属性之后单独指定主键列
sql 复制代码
create table t_pk2(
	id bigint auto_increment,
	name varchar(20),
	primary key (id)                                                    
);
  1. 定义表结构后,修改表结构来添加主键
sql 复制代码
create table t_pk3(
	id bigint,
	name varchar(20)
);

alter table t_pk3 add
primary key(id);

alter table t_pk3 modify
id bigint auto_increment;

show index(或 keys) from 表名用来查询一张表上所有已建立的索引 包括:主键索引、普通索引、唯一索引、联合索引、全文索引 等!。

唯一索引

与主键索引类似,可以在定义列时创建,定义列后指定列为唯一索引或者修改表结构指定列为唯一索引

sql 复制代码
--创建表时在列后面指定唯一键
create table t_u1(
	id bigint primary key auto_increment,
	name varchar(20) unique
);

-- 创建表时在定义属性之后单独指定唯一键列
create table t_u2(
	id bigint primary key auto_increment,
	name varchar(20),
	unique (name)                                                    
);

-- 定义表结构后,修改表结构来添加唯一键
create table t_u3(
	id bigint primary key auto_increment,
	name varchar(20)
);

alter table t_u3 add
unique (name);

普通索引

创建时机

  1. 创建表的时候,明确知道某些列会频繁查询,直接创建(当数据量较小的时候,全表扫描的效率可能比创建索引更高)
  2. 随着业务不断发展,数据量增大,在版本迭代过程中添加索引

添加方式

  1. 创建表时添加索引
sql 复制代码
create table t_index1(
	id bigint primary key auto_increment,
	name varchar(20) unique,
	sn varchar(20),
	index(sn)
);

index是创建索引的关键字


MUL表示普通索引

  1. 修改表结构
sql 复制代码
create table t_index2(
	id bigint primary key auto_increment,
	name varchar(20) unique,
	sn varchar(20)
);

alter table t_index2 add
index (sn);
  1. 单独创建索引并指定索引名
sql 复制代码
create index 索引名 on 表名 (列名)

索引名推荐使用表名_列名

sql 复制代码
create table t_index3(
	id bigint primary key auto_increment,
	name varchar(20) ,
	sn varchar(20)
);

create index idx1_t_index3_sn on t_index3(sn);

复合索引

  1. 创建表时指定索引列
sql 复制代码
create table t_index4(
	id bigint primary key auto_increment,
	name varchar(20) ,
	sn varchar(20),
	class_id bigint,
	index(sn,name)
);
  • non_unique:索引是否允许重复值
    • 0:不允许(主键 / 唯一索引)
    • 1:允许(普通索引)
  • key_name:索引名,复合索引默认是第一列的列名
  • seq_in_index:字段在联合索引中的位置(从 1 开始)
    • 单字段索引:永远是 1
    • 多字段索引:按创建顺序依次是 1,2,3...
  1. 创建表后修改表结构创建索引
sql 复制代码
create table t_index5(
	id bigint primary key auto_increment,
	name varchar(20) ,
	sn varchar(20),
	class_id bigint
);

alter table t_index5 add
index (sn,class_id);
  1. 单独创建索引并指定索引名
sql 复制代码
create table t_index6(
	id bigint primary key auto_increment,
	name varchar(20) ,
	sn varchar(20),
	class_id bigint
);
create index idx_t_index6_sn_class_id on t_index6(sn,class_id);

七、删除主键

7.1 删除主键索引

直接删除主键约束即可。

需要注意,如果主键列是自增的,需要先把这个字段修改为非自增,再删除主键约束

  • 修改字段
sql 复制代码
alter table t_index6 modify
id bigint;
  • 删除主键约束
sql 复制代码
alter table t_index6 drop
primary key;

7.2 删除其他索引

sql 复制代码
alter table 表名 drop index 索引名
sql 复制代码
alter table t_index6 drop
index idx_t_index6_sn_class_id;


t_index6表中的索引全部删除

八、创建索引的注意事项

  1. 创建在需要高频查询的列上
  2. 索引需要占用额外的存储空间
  3. 对表进行插入、删除或更新等更多操作是,同时也会修改索引,可能影响性能
  4. 创建过多或不合理的索引会导致性能下降,需要谨慎选择和规划索引。

可以通过explain select来检查某条查询语句使用了什么样的索引:

  1. 查询所有,没有条件

    • type 显示为all,表示没有使用索引
  2. 使用主键查询

    • type显示为const,表示常量级别查询。
    • possible_keys:分析sql可能用到的索引
    • key:查询过程中使用到的索引
  3. 子查询中使用索引

  • type的取值(性能依次变好):
    • all:扫描全表
    • index:扫描全表索引树
    • range:范围查找,扫描部分索引
    • ref:使用非唯一索引或非唯一索引前缀查找,不是主键或或不是唯一索引
    • const:单表中最多一个匹配行,查询起来非常迅速
    • null:不访问表或索引,直接得到结果。主要有三种情况:
      • 查询纯常量、函数,不写 from表名
      • 使用聚合函数但表为空
      • where条件永远不成立,直接短路
相关推荐
m0_624578597 小时前
Laravel Blade 中高效筛选并限制关联分类数据的实践方案
jvm·数据库·python
m0_591364738 小时前
golang如何实现coredump分析_golang coredump分析实现策略
jvm·数据库·python
玩代码的老秦8 小时前
后端php连接SQL Server数据库报错解决方案
开发语言·数据库·php
2401_831419448 小时前
golang如何实现分布式对象存储_golang分布式对象存储实现攻略
jvm·数据库·python
羑悻的小杀马特8 小时前
深入 LangChain 内存向量存储(Memory Vector Stores):架构解析与优化
数据库·架构·langchain·向量存储
bLEd RING8 小时前
MySQL数据库误删恢复_mysql 数据 误删
数据库·mysql·adb
梦梦代码精8 小时前
LikeShop 是怎么解决数据库瓶颈的?
java·数据库·低代码·php
yexuhgu8 小时前
Golang如何做贪心算法_Golang贪心算法教程【速学】
jvm·数据库·python
qq_229058018 小时前
conda中安装 rdkit版本的postgresql然后在Win11中使用虚拟环境里的rdkit
数据库·postgresql·conda