MySQL 索引的底层数据结构详解
大纲
索引的介绍
索引概述
MySQL 官方对索引的定义为:索引(Index)是帮助 MySQL 高效获取数据的数据结构。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。简而言之,索引的本质是数据结构。
索引结构
MySQL 的索引是在存储引擎层实现的,不同的存储引擎支持不同的索引结构,主要包含以下几种:
MySQL 不同的存储引擎对于索引结构的支持情况如下:
提示
在 MySQL 中,通常所说的索引,如果没有特别指明,都是指 B+ 树数据结构的索引。
索引的数据结构
二叉树
假如说 MySQL 的索引结构采用二叉树的数据结构,比较理想的结构如下:
如果主键(数据)是顺序插入的,则会形成一个单向链表,结构如下:
所以,如果选择二叉树作为索引结构,会存在以下缺点:
- 按顺序插入时,会形成一个链表,查询性能大大降低。
- 大数据量情况下,层级较深,检索速度慢。
由于红黑树(如上所示)也是一颗二叉树,因此也存在 “大数据量情况下,层级较深,检索速度慢” 的问题。所以,在 MySQL 的索引结构中,并没有选择二叉树或者红黑树,而选择的是 B+ 树。那什么是 B+ 树呢?在详解 B+ 树 之前,先来介绍一下 B- 树。
B- 树
定义:
- B- 树是一种多叉路平衡查找树,相对于二叉树,B- 树每个节点可以有多个分支,即多叉。
- 以一颗最大度数(max-degree)为 5 (5 阶) 的 B- 树为例,那这个 B- 树每个节点最多存储 4 个 Key,5 个指针。
- 树的度数指的是一个节点的子节点个数。
特点:
- 5 阶的 B- 树,每一个节点最多存储 4 个 Key,对应 5 个指针。
- 一旦节点存储的 Key 数量到达 5,就会裂变,中间元素向上分裂。
- 在 B- 树中,非叶子节点和叶子节点都会存放数据。
B+ 树
B+ 树是 B- 树的变种,以一颗最大度数(max-degree)为 4(4 阶)的 B+ 树为例,来看一下其结构示意图:
从上图可以看到,两部分:
- 绿色框框起来的部分,是索引部分,仅仅起到索引数据的作用,不存储数据。
- 红色框框起来的部分,是数据存储部分,在其叶子节点中要存储具体的数据。
B+ 树与 B- 树的区别:
- B+ 树的叶子节点会形成一个单向链表。
- B+ 树的所有数据都只会存放在叶子节点。
- B+ 树的非叶子节点仅仅起到索引数据作用,具体的数据都是存放在叶子节点。
B+ 树相比 B- 树的优势:
- 单一节点存储更多的元素,使得查询的 I/O 次数更少。
- 所有查询都要查找到叶子节点,查询性能稳定。
- 所有叶子节点形成有序链表,便于范围查询。
上述所看到的结构是标准的 B+ 树的数据结构,接下来再看看 MySQL 中优化之后的 B+ 树。MySQL 索引数据结构对经典的 B+ 树进行了优化,在原 B+ 树的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+ 树,提高区间访问的性能,利于排序。
为什么 InnoDB 选择 B+ 树作为索引结构?
- 相对于二叉树,B+ 树的层级更少,搜索效率更高。
- 相对 Hash 索引,B+ 树支持范围查找以及排序操作。
- 对于 B- 树,无论是叶子节点还是非叶子节点都会保存数据,这样会导致一页中存储的键值减少,指针跟着减少;要同样保存大量数据,只能增加树的高度,导致性能降低。
- B+ 树中间节点没有卫星数据(索引元素所指向的数据记录),只有索引。这就意味着同样的大小的磁盘页可以容纳更多节点元素,在相同的数据量下,B+ 树更加 “矮胖”,I/O 操作更少。
- 因为卫星数据的不同,导致查询过程也不同;B- 树的查找只需找到匹配元素即可,最好情况下查找到根节点,最坏情况下查找到叶子结点,所说性能很不稳定,而 B+ 树每次必须查找到叶子结点,性能稳定。
提示
MySQL 索引使用的是 B+ 树,因为索引是用来加快查询速度的,而 B+ 树会对数据进行排序,所以它可以提高查询速度。另外,在一个 B+ 树的节点中可以存储多个元素,从而可以使得 B+ 树的高度不会太高。在 MySQL 中一个 InnoDB ⻚就是一个 B+ 树节点,一个 InnoDB ⻚默认 16KB, 所以一般情况下一颗两层的 B+ 树可以存 2000 万行左右的数据。通过利用 B+ 树的叶子节点存储所有数据,并进行排序,而且叶子节点之间通过链表连接,这样可以很好地支持范围查询等 SQL 语句。
Hash
MySQL 中除了支持 B+ 树索引,还支持一种 Hash 索引类型。
- 结构:
- 哈希索引就是采用一定的 Hash 算法,将键值换算成新的 Hash 值,映射到对应的槽位上,然后存储在 Hash 表中。
如果两个(或多个)键值,映射到一个相同的槽位上,那就会产生哈希冲突(也称为哈希碰撞),可以通过链表来解决。
- 特点:
- Hash 索引只支持对等比较(如 =,in),不支持范围查询(如 between、>、<、…)。
- 无法利用索引完成排序操作。
- 查询效率高,通常(不存在哈希冲突的情况)只需要一次检索就可以了,效率通常要高于 B+ 树索引。
存储引擎的支持
在 MySQL 中,支持 Hash 索引的是 Memory 存储引擎。而 InnoDB 中具有自适应 Hash 功能,Hash 索引是 InnoDB 存储引擎根据 B+ 树索引在指定条件下自动构建的。
联合索引的数据结构
联合索引的定义
联合索引又叫组合索引(复合索引),例如下表:
1 | CREATE TABLE `test` ( |
如下索引就是一个联合索引:
1 | `idx_name_age_position` (`name`,`age`,`position`) USING BTREE |
联合索引的结构
联合索引的底层数据结构如下:
比较相等时,先比较第一列的值,如果相等,再继续比较第二列,以此类推。了解联合索引的存储结构后,就知道索引最左前缀匹配原则是怎么回事了。在使用联合索引时,对于索引列的定义顺序将会影响到最终查询时索引的使用情况。例如联合索引 (name, age, position)
,MySQL 会从最左边的列优先匹配,如果最左边的带头大哥 name
字段没有使用到,在未使用覆盖索引的情况下,就只能全表扫描(因为索引失效了)。简而言之,使用联合索引时,MySQL 会优先使用联合索引的第一列进行匹配,此后才会匹配下一列;如果不指定第一列匹配的值,也就无法得知下一步查询哪个节点。
聚簇索引与非聚簇索引
概念介绍
在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为两种:聚簇索引(主键索引、聚集索引)、非聚簇索引(非主键索引、辅助索引、二级索引)。
聚簇索引(Clustered Index):
- 定义:索引和数据存储在一起。数据表中的记录按索引的键值顺序存储在磁盘上。每个表只能有一个聚簇索引。
- 特点:
- 数据存储顺序:数据物理上按索引键的顺序存储。
- 主键:通常主键会自动成为聚簇索引,但也可以指定其他列为聚簇索引。
- 索引结构:InnoDB 的聚簇索引是按照主键顺序构建 B+ 树结构的。
- 检索速度:对索引键的查询速度较快,因为数据和索引在一起。
- 插入和更新:插入和更新可能会比较慢,因为需要保持数据的物理顺序。
- 适用场景:适用于经常需要范围查询的场景,比如查找某一范围内的数据。
非聚簇索引(Non-Clustered Index):
- 定义:索引和数据分开存储。非聚簇索引存储的是索引键值和指向数据记录的指针。每个表可以有多个非聚簇索引。
- 特点:
- 数据存储顺序:数据的物理存储顺序与索引无关。
- 索引存储:索引存储的是键值和指向数据的指针。
- 检索速度:对索引键的检索速度较快,但访问实际数据时需要通过指针进行二次查找。
- 插入和更新:插入和更新相对较快,因为不需要保持数据的物理顺序。
- 适用场景:适用于查找特定值或者少量记录的场景。
聚簇索引的选取规则:
- 如果存在主键(Primary Key),主键索引就是聚簇索引。
- 如果不存在主键,将使用第一个唯一(Unique)索引作为聚簇索引。
- 如果表没有主键,或没有合适的唯一索引,则 InnoDB 会自动创建一个隐藏的 row-id 作为聚簇索引。
存储引擎的支持
- InnoDB 使用的是聚簇索引,树的叶子节点上的 data 就是数据本身。
- MyISAM 使用的是非聚簇索引,树的叶子节点上的 data 不是数据本身,而是数据存放的地址。
使用案例
当执行如下的 SQL 语句时(name 字段建立了索引),具体的查找过程是什么样子的?
具体查找过程如下:
- (1) 由于是根据 name 字段进行查询,所以先根据 name=’Arm’ 条件到 name 字段的二级索引中进行匹配查找,但是在二级索引中只能查找到 Arm 对应的主键值 10。
- (2) 由于查询返回的数据是主键值,所以此时,还需要根据主键值 10,到聚集索引中查找对应的数据记录。
- (3) 最终拿到这一行的数据,直接返回即可。
思考题目一
以下两条 SQL 语句(id 字段为主键,name 字段建立了索引),哪个执行效率更高?为什么?
- A. select * from user where id = 10;
- B. select * from user where name = ‘Arm’;
A 语句的执行效率要高于 B 语句。因为 A 语句直接走聚簇索引,可以直接返回数据。而 B 语句需要先查询 name 字段的非聚簇索引(二级索引),然后再查询聚簇索引,也就是需要进行回表查询,所以效率不高。
什么是回表查询
先根据普通索引(非聚簇索引)查询到主键值,再根据主键值在聚簇索引中获取行记录,这就是回表查询。值得一提的是,回表查询,相对于只扫描一遍聚簇索引的性能,要低一些。
使用覆盖索引来避免回表:
什么是覆盖索引:指索引包含了查询中涉及的所有列(包括需要筛选、排序和返回的列),这样就可以直接从索引中获取所有需要的数据,而不需要访问数据表本身。覆盖索引是一种避免回表查询的优化策略,只需要在一棵索引树上就能获取 SQL 所需的所有列数据,无需回表,速度更快。
具体的实现方式:将被查询的字段都建立普通索引或者联合索引,这样的话就可以直接返回索引中的数据,不需要再通过聚簇索引去定位行记录,避免了回表操作的执行。
思考题目二
InnoDB 主键索引的 B+ 树最多能存储多少条记录?
1 | 假设:一行数据大小为 1k,一页中就可以存储 16 行这样的数据(页的大小 16K)。 |
索引的创建
语法介绍
- 创建索引
1 | CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name, ... ) ; |
- 查看索引
1 | SHOW INDEX FROM table_name ; |
- 删除索引
1 | DROP INDEX index_name ON table_name ; |
使用案例
1 | -- A. name 字段为姓名字段,该字段的值可能会重复,为该字段创建索引。 |
面试题
高频面试题一
面试题:为什么建议 InnoDB 表必须创建主键,并且推荐使用整型的自增主键?
(1) 如果在创建表时不设置主键,InnoDB 会自动从第一列开始筛选一列数据不重复的列(唯一列)做为主键,如果找不到这样的列,就会自动创建一个隐藏的列(row-id)做为主键,这会大大增加 MySQL 的工作量,所以建议在创建 InnoDB 表时一定要设置主键。
(2) 使用整型的字段做为主键,一方面在数据比较时不需要进行转换,另一方面存储也比较节省空间。那为什么要强调主键自增呢?如果主键 ID 是无序的,那么很有可能新插入的值会导致当前节点分裂,此时 MySQL 不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过 OPTIMIZE TABLE 来重建表并优化填充页面。反之,如果每次插入有序,那就会在当前页后面连续写入,写不下就会重新分配一个节点,内存都是连续的,这样效率自然也就最高了。
高频面试题二
面试题:为什么非聚簇索引结构的叶子节点存储的是主键值?
非聚簇索引的叶子节点存储主键值而非全部数据,主要也是为了一致性和节省空间。如果非聚簇索引储存的也是数据,那么每次插入 MySQL 都不得不更新每棵索引树,这样就加剧了新增、更改数据时的性能损耗,并且这样一来空间利用率也不高,必然产生了大量冗余数据。
高频面试题三
面试题:为什么 InnoDB 选择 B+ 树作为索引结构?
- 相对于二叉树,B+ 树的层级更少,搜索效率更高。
- 相对 Hash 索引,B+ 树支持范围查找以及排序操作。
- 对于 B- 树,无论是叶子节点还是非叶子节点都会保存数据,这样会导致一页中存储的键值减少,指针跟着减少;要同样保存大量数据,只能增加树的高度,导致性能降低。
- B+ 树中间节点没有卫星数据(索引元素所指向的数据记录),只有索引。这就意味着同样的大小的磁盘页可以容纳更多节点元素,在相同的数据量下,B+ 树更加 “矮胖”,I/O 操作更少。
- 因为卫星数据的不同,导致查询过程也不同;B- 树的查找只需找到匹配元素即可,最好情况下查找到根节点,最坏情况下查找到叶子结点,所说性能很不稳定,而 B+ 树每次必须查找到叶子结点,性能稳定。
高频面试题四
面试题:B 树与 B+ 树的区别是什么?
B 树和 B+ 树是两种不同的树形数据结构,主要用于数据库索引和文件系统。
B 树:
- (1) 节点结构:每个节点包含键值和指向子节点的指针。一个节点可以存储多个键值和多个指针。
- (2) 数据存储:所有节点(包括叶子节点和非叶子节点)都可以存储数据记录。
- (3) 查找路径:在查找过程中,可以在任意节点上找到目标键值。
- (4) 叶子节点:叶子节点没有链表结构,不需要相互连接。
- (5) 保证有序:键值在节点内是按顺序排列的,而且节点之间的子节点指针也会根据键值的顺序进行排列。
B+ 树:
- (1) 节点结构:非叶子节点只存储键值和指向子节点的指针,不存储数据记录。
- (2) 数据存储:所有数据记录都存储在叶子节点上,非叶子节点仅用于索引。
- (3) 查找路径:在查找过程中,所有查找最终都会落在叶子节点上,因为数据记录只存储在叶子节点。
- (4) 叶子节点:所有叶子节点形成一个有序的链表,便于范围查询和顺序访问。
- (5) 保证有序:键值在非叶子节点内是按顺序排列的,而且数据记录在叶子节点内也是按顺序排列的。
B 树与 B+ 树的区别如下:
(1) 叶子节点的结构:
- B 树:叶子节点之间没有链表结构。
- B+ 树:叶子节点通过链表连接,便于顺序访问。
(2) 数据存储位置:
- B 树:数据记录存储在所有节点上。
- B+ 树:数据记录只存储在叶子节点上,非叶子节点仅用于索引。
(3) 查询效率:
- B 树:查询时可能在非叶子节点找到数据,搜索路径较短。
- B+ 树:所有查询都必须到达叶子节点,但叶子节点之间的有序链表结构提高了范围查询和顺序访问的效率。
(4) 范围查询:
- B 树:范围查询不如 B+ 树高效,叶子节点之间没有链表结构。
- B+ 树:范围查询更高效,因为叶子节点按顺序链表连接。
综上所述,B 树和 B+ 树各有优劣。B 树在查找单个键值时可能效率更高,而 B+ 树在范围查询和顺序访问方面表现更佳,因此在数据库系统中,B+ 树更为常用。