索引原理

25 阅读10分钟

前言

MySQL 作为存储业务数据的核心组件,随着数据量的增长,在没有其他优化操作下,查询性能可能逐渐下降。即在正常情况下,表的查询性能和数据量成反比,也就是数据越多,查询越慢

为什么会如此?

这是因为在没有索引的情况下,查询可能需要全表扫描——即通过磁盘 I/O 逐条读取数据并判断是否符合条件,也就是说,随着表中数据的增多,I/O 的次数也会越来越多。而且,由于磁盘 I/O 的效率远低于内存计算,数据量越大,查询延迟越明显

那么有没有什么办法增加检索效率呢?

最好的办法肯定是减少 I/O 次数,那么索引也就应运而生了

什么是索引?

索引是数据库中用于快速查找数据的数据结构,类似于书籍的目录。它通过保存特定列的值和指向这些值所在行的指针,来加速数据检索

索引的作用:

  • 大大加快数据的检索速度
  • 通过创建唯一性索引保证数据唯一性
  • 加速表与表之间的连接
  • 在使用分组和排序子句时减少查询中分组和排序的时间

索引的几种数据结构

从上面可以看出索引其实是一种加快数据检索的数据结构,但是索引其实存在好几种数据结构,如 哈希表有序数组B 树B+ 树,但是都知道 MySQL 索引的结构是 B+ 树,这有必要来学习一下各个数据结构,然后才知道为什么 MySQL 最终选择了 B+ 树

哈希表

哈希索引是一种基于哈希表的索引结构,它是一种需要精确匹配才生效的索引结构,这也就意味着在等值查询时,哈希索引有着天然的优势,因为它的查询时间复杂度接近 O(1),效率极高

原理

哈希索引通过哈希函数将索引键值转换为固定长度的哈希值,然后在哈希表中存储指向实际数据行的指针。其工作原理如下:

  1. 哈希函数处理:对索引列值应用哈希函数(如 MD5、CRC32 或自定义函数),生成哈希码
  2. 哈希表存储:将哈希码与数据行指针的映射关系存储在哈希表中
  3. 冲突处理:当不同键值产生相同哈希码(哈希冲突)时,使用链表法或开放寻址法解决冲突

哈希索引实现

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 启用/禁用

哈希索引缺点

  1. 哈希冲突:正如上面所说,哈希索引在精确查询时效率极高,时间复杂度接近 O(1),但这也不是完全正确的,可以想象,当随着表中数据量越来越大时,虽然创建了哈希索引,但是发生哈希冲突的概率也会越来越大,当发生大量冲突时,哈希表会退化成链表查询,导致性能下降
  2. 不支持范围查询:哈希索引不支持范围查询,这是因为哈希函数会破坏数据的原始关系
  3. 不支持排序:正如第二点,排序也是需要知道数据间的大小关系,但是哈希函数破坏了数据间的大小关系,所以也就无法排序
  4. 不支持模糊匹配:这个是因为哈希索引需要完整的键输入,你想一想,当为数据建立哈希索引时,是对整个键值进行计算的,此时你来的一个值并不完全(如模糊查询,like abc%,计算出来 abc 的哈希值,和原始值(如 abcde)的哈希值肯定不同

有序数组

有序数组是数据库索引中一种简单但高效的数据结构,索引键值按照顺序存储,这使得它特别适合范围查询和等值查询

实际应用场景

  1. 内存临时表的索引:当内存临时表较小时,MySQL会使用有序数组作为索引
  2. 某些特定优化场景:如IN列表的优化处理

特性

  • 查询性能:
    • 等值查询:O(log n)时间复杂度(使用二分查找)
    • 范围查询:O(log n)找到起始点后顺序扫描
  • 插入性能:
    • 最坏情况O(n),因为需要移动元素
    • 适合静态数据或批量加载
  • 空间效率:
    • 连续存储,无额外指针开销
    • 比B+树更紧凑

优点

  • 范围查询性能极佳
  • 无指针开销,空间效率高
  • 顺序访问速度快(缓存友好)

缺点

  • 插入和删除成本高
  • 不适合频繁更新的场景
  • 数据量增长时性能下降明显

从上面的分析来看,如果仅仅看查询效率,有序数组是非常棒的数据结构。但是,如果发生了写操作需要更新数据的时候就会很麻烦,因为你往中间插入一个记录需要操作索引时,就必须得挪动后面所有的记录,成本太高,所以该索引结构一般推荐型静态数据集来使用

B 树

想要实现有序数组那样的范围查询,但受限于其较差的插入/更新性能。那么有没有查询和插入效率都很高的数据结构呢?确实存在,那就是树结构。然而树结构种类繁多,为何MySQL最终选择了 B+树,而不是选择其他的树结构呢?

那这就有必要谈谈各个树结构的特性以及区别了

二叉查找树

二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值

下图示例就是一个典型的二叉查找树的结构

image.png

该二叉树在比较平衡的状态下的查找时间复杂度是O(log(n))

那时间复杂度是怎么算出来的?

递归分析(分治思想)

二叉查找树的查找过程类似于 二分查找,每次比较都能排除一半的数据:

  1. 每次查找,问题规模减半
    • 在二叉树中,查找一个节点时,如果当前节点不是目标,则根据大小关系进入左子树或右子树
    • 理想情况下,每次都能排除一半的节点,类似于 二分查找
  2. 递归关系式
    • 设查找的时间复杂度为 T(n) ,则 T(n)=T(n/2)+O(1)
      • T(n/2):进入左子树或右子树,问题规模减半
      • O(1):当前节点的比较操作
  3. 求解递归式
    • 展开递归: T(n)=T(n/2)+1=T(n/4)+1+1=T(n/8)+1+1+1=...=T(1)+k(其中 n/2k=1k=log2nT(n)=T(n/2)+1 = T(n/4)+1+1=T(n/8)+1+1+1 =...= T(1)+k(其中 n/2^k=1⇒k=\log_2n)
    • 最终:T(n)=O(logn) T(n)=O(logn)

但是二叉查找树存在一个问题:在某些极端情况下会退化成链表(就是数值按顺序插入时,此时树结构会退化成),这会导致查询的时间复杂度退回 O(n)

AVL 树

为了解决二叉树这种问题,也就引入了 AVL 树,其规定了左右子树深度差绝对值不能超过 1,这样即使数值按照顺序插入也能避免树结构退化成链表

那么 AVL 树 是怎么做到平衡的呢?

这就不得不提到两个概念:左旋右旋

左旋:用于“右右”失衡,将失衡节点的右节点提为根 image.png

右旋:用于“左左”失衡,将失衡节点的左节点提为根 image.png

此时看起来,AVL 树很是适合存储索引,因为它能维持平衡,不会退化。如果只是单纯从存储来看,确实如此,但是这个结构有一个很致命的问题,那就是每个节点只能存储一个索引,此时它的结构如下图: image.png 这样的话,每次访问一个节点就要跟磁盘之间发生一次 IO。 而 数据库 InnoDB 引擎 操作磁盘的最小的单位是一页(或者叫一个磁盘块),那么意味着需要耗费巨大的时间,而且每个节点只能存储一条索引,那意味着要想在索引中找到目标数据,就要访问更多的节点,这样代表 I/O 次数的增加,也就代表会耗费更长的时间

其实上面存在的问题也很好解决,无非就是:

  • 让每个节点能存储更多的数据
  • 降低树的高度

此时就出现了 B 树 ,也称 多路平衡查找树

B 树

B树 是一种多叉平衡查找树,有以下特点:

  • B树 的每个节点可以存储着多个元素,每个内节点有多个分叉,这意味着树的高度会显著降低
  • 节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点都储存数据
  • 父节点当中的元素不会出现在子节点中
  • 所有的叶子结点都位于同一层,叶节点具有相同的深度,叶节点之间没有指针连接

image.png

B 树的结构看起来很完美,但是考虑下面这些情况:

  • 当我们需要范围查询时,如12~48,此时当查到12~16(不包含16)后,需要回到根节点重新遍历查(中序遍历)找其他的数据(16~48),这说明 B树 对于范围查询是不友好的
  • 如果data存储的是行记录,行的大小随着列数的增多,所占空间会变大。这时,一个页中可存储的数据量就会变少,树相应就会变高,磁盘IO次数就会变大

这时候,B树 的查询效率就会显著降低,所以引入了索引的最终结构:B+ 树

B+ 树

image.png

从上面的 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次数,提高访问效率