前言
MySQL
作为存储业务数据的核心组件,随着数据量的增长,在没有其他优化操作下,查询性能可能逐渐下降。即在正常情况下,表的查询性能和数据量成反比,也就是数据越多,查询越慢
为什么会如此?
这是因为在没有索引的情况下,查询可能需要全表扫描——即通过磁盘 I/O
逐条读取数据并判断是否符合条件,也就是说,随着表中数据的增多,I/O
的次数也会越来越多。而且,由于磁盘 I/O
的效率远低于内存计算,数据量越大,查询延迟越明显
那么有没有什么办法增加检索效率呢?
最好的办法肯定是减少 I/O
次数,那么索引也就应运而生了
什么是索引?
索引是数据库中用于快速查找数据的数据结构,类似于书籍的目录。它通过保存特定列的值和指向这些值所在行的指针,来加速数据检索
索引的作用:
- 大大加快数据的检索速度
- 通过创建唯一性索引保证数据唯一性
- 加速表与表之间的连接
- 在使用分组和排序子句时减少查询中分组和排序的时间
索引的几种数据结构
从上面可以看出索引其实是一种加快数据检索的数据结构,但是索引其实存在好几种数据结构,如 哈希表
、有序数组
、B 树
、B+ 树
,但是都知道 MySQL
索引的结构是 B+ 树
,这有必要来学习一下各个数据结构,然后才知道为什么 MySQL
最终选择了 B+ 树
哈希表
哈希索引是一种基于哈希表的索引结构,它是一种需要精确匹配才生效的索引结构,这也就意味着在等值查询时,哈希索引有着天然的优势,因为它的查询时间复杂度接近 O(1)
,效率极高
原理
哈希索引通过哈希函数将索引键值转换为固定长度的哈希值,然后在哈希表中存储指向实际数据行的指针。其工作原理如下:
- 哈希函数处理:对索引列值应用哈希函数(如 MD5、CRC32 或自定义函数),生成哈希码
- 哈希表存储:将哈希码与数据行指针的映射关系存储在哈希表中
- 冲突处理:当不同键值产生相同哈希码(哈希冲突)时,使用链表法或开放寻址法解决冲突
哈希索引实现
Memory 存储引擎显式支持哈希索引,创建语法:
CREATE TABLE hash_table (
id INT,
name VARCHAR(100),
INDEX USING HASH (id)
) ENGINE=MEMORY;
特点:
- 完全存储在内存中,速度极快
- 只支持精确匹配查询(=, IN, <=>)
- 不支持范围查询、排序或部分键匹配
现在 MySQL
数据库大多时使用的引擎都是 InnoDB
,但是 InnoDB
是内部实现了"自适应哈希索引"(Adaptive Hash Index),无需用户手动管理。
特性:
- 自动为频繁访问的索引页创建哈希索引
- 完全由 InnoDB 自动管理,用户无法手动创建或控制
- 通过参数
innodb_adaptive_hash_index
启用/禁用
哈希索引缺点
- 哈希冲突:正如上面所说,哈希索引在精确查询时效率极高,时间复杂度接近
O(1)
,但这也不是完全正确的,可以想象,当随着表中数据量越来越大时,虽然创建了哈希索引,但是发生哈希冲突的概率也会越来越大,当发生大量冲突时,哈希表会退化成链表查询,导致性能下降 - 不支持范围查询:哈希索引不支持范围查询,这是因为哈希函数会破坏数据的原始关系
- 不支持排序:正如第二点,排序也是需要知道数据间的大小关系,但是哈希函数破坏了数据间的大小关系,所以也就无法排序
- 不支持模糊匹配:这个是因为哈希索引需要完整的键输入,你想一想,当为数据建立哈希索引时,是对整个键值进行计算的,此时你来的一个值并不完全(如模糊查询,
like abc%
,计算出来abc
的哈希值,和原始值(如abcde
)的哈希值肯定不同
有序数组
有序数组是数据库索引中一种简单但高效的数据结构,索引键值按照顺序存储,这使得它特别适合范围查询和等值查询
实际应用场景:
- 内存临时表的索引:当内存临时表较小时,MySQL会使用有序数组作为索引
- 某些特定优化场景:如IN列表的优化处理
特性:
- 查询性能:
- 等值查询:O(log n)时间复杂度(使用二分查找)
- 范围查询:O(log n)找到起始点后顺序扫描
- 插入性能:
- 最坏情况O(n),因为需要移动元素
- 适合静态数据或批量加载
- 空间效率:
- 连续存储,无额外指针开销
- 比B+树更紧凑
优点:
- 范围查询性能极佳
- 无指针开销,空间效率高
- 顺序访问速度快(缓存友好)
缺点:
- 插入和删除成本高
- 不适合频繁更新的场景
- 数据量增长时性能下降明显
从上面的分析来看,如果仅仅看查询效率,有序数组是非常棒的数据结构。但是,如果发生了写操作需要更新数据的时候就会很麻烦,因为你往中间插入一个记录需要操作索引时,就必须得挪动后面所有的记录,成本太高,所以该索引结构一般推荐型静态数据集来使用
B 树
想要实现有序数组那样的范围查询,但受限于其较差的插入/更新性能。那么有没有查询和插入效率都很高的数据结构呢?确实存在,那就是树结构。然而树结构种类繁多,为何MySQL最终选择了 B+树
,而不是选择其他的树结构呢?
那这就有必要谈谈各个树结构的特性以及区别了
二叉查找树
二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值
下图示例就是一个典型的二叉查找树的结构
该二叉树在比较平衡的状态下的查找时间复杂度是O(log(n))
那时间复杂度是怎么算出来的?
递归分析(分治思想):
二叉查找树的查找过程类似于 二分查找,每次比较都能排除一半的数据:
- 每次查找,问题规模减半:
- 在二叉树中,查找一个节点时,如果当前节点不是目标,则根据大小关系进入左子树或右子树
- 理想情况下,每次都能排除一半的节点,类似于 二分查找
- 递归关系式:
- 设查找的时间复杂度为 T(n) ,则
T(n)=T(n/2)+O(1)
T(n/2)
:进入左子树或右子树,问题规模减半O(1)
:当前节点的比较操作- 求解递归式:
- 展开递归:
- 最终:
但是二叉查找树存在一个问题:在某些极端情况下会退化成链表(就是数值按顺序插入时,此时树结构会退化成),这会导致查询的时间复杂度退回 O(n)
AVL 树
为了解决二叉树这种问题,也就引入了 AVL 树
,其规定了左右子树深度差绝对值不能超过 1,这样即使数值按照顺序插入也能避免树结构退化成链表
那么
AVL 树
是怎么做到平衡的呢?这就不得不提到两个概念:
左旋
和右旋
左旋:用于“右右”失衡,将失衡节点的右节点提为根
右旋:用于“左左”失衡,将失衡节点的左节点提为根
此时看起来,AVL 树
很是适合存储索引,因为它能维持平衡,不会退化。如果只是单纯从存储来看,确实如此,但是这个结构有一个很致命的问题,那就是每个节点只能存储一个索引,此时它的结构如下图:
这样的话,每次访问一个节点就要跟磁盘之间发生一次 IO。 而 数据库
InnoDB 引擎
操作磁盘的最小的单位是一页(或者叫一个磁盘块),那么意味着需要耗费巨大的时间,而且每个节点只能存储一条索引,那意味着要想在索引中找到目标数据,就要访问更多的节点,这样代表 I/O
次数的增加,也就代表会耗费更长的时间
其实上面存在的问题也很好解决,无非就是:
- 让每个节点能存储更多的数据
- 降低树的高度
此时就出现了 B 树
,也称 多路平衡查找树
B 树
B树
是一种多叉平衡查找树,有以下特点:
B树
的每个节点可以存储着多个元素,每个内节点有多个分叉,这意味着树的高度会显著降低- 节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点都储存数据
- 父节点当中的元素不会出现在子节点中
- 所有的叶子结点都位于同一层,叶节点具有相同的深度,叶节点之间没有指针连接
B 树的结构看起来很完美,但是考虑下面这些情况:
- 当我们需要范围查询时,如12~48,此时当查到12~16(不包含16)后,需要回到根节点重新遍历查(中序遍历)找其他的数据(16~48),这说明
B树
对于范围查询是不友好的 - 如果data存储的是行记录,行的大小随着列数的增多,所占空间会变大。这时,一个页中可存储的数据量就会变少,树相应就会变高,磁盘IO次数就会变大
这时候,B树
的查询效率就会显著降低,所以引入了索引的最终结构:B+ 树
B+ 树
从上面的 B+树
的结构可以看到他和 B树
最大的不同:
B树
叶子节点和非叶子节点都存储了数据(指向对应记录的指针),但是B+树
只有叶子节点存储了数据,而每一层非叶子节点储存的是下一层级的“索引”,例如:图中【14 18 27】和【36 58】
是叶子节点的索引,而【27 58】
是对【14 18 27】和【36 58】
的索引,所以整个B+树
可以理解成一个多级索引结构,只有叶子节点存储了数据,而非叶子节点是为了加快查询效率而存在的B+树
的非叶子节点中的数据存储的是其每一个子节点的最大值B+树
在进行数据查找时,最终都是要到叶子节点的,因为数据都是存储在叶子节点上,这点也是和B树
很大的不同
可以看出来,B+树
的结构天生就支持范围查询:
比如我想查找10~50的数据,对于 B树
而言,它需要通过中序遍历的方式去查找,这效率会很低,但是对于 B+树
,它只需要找到范围中的第一个key(图中是 14
),然后直接在叶子节点按顺序遍历找到范围中最后一个key(图中是 45
)
且因为 B+树
的非叶子节点只存储的key值,不存储数据,这意味相同的数据量,B+树
的高度会显著低于 B树
,这也能有效减少I/O
次数,提高访问效率