MySQL深度剖析-InnoDB索引与B+树

1. 什么是B+树?

B + 树是一种自平衡的多叉树,它是 B 树的一种变体。与 B 树不同,B + 树的所有数据都存储在叶子节点,非叶子节点仅存储索引,且叶子节点之间通过双向链表相连。这种结构使得 B + 树在范围查询和排序操作上具有更高的效率。

B + 树的特点包括:

  • 平衡:随着数据的插入和删除,B + 树会自动调整结构,保持平衡,确保查询时间稳定。
  • 有序:叶子节点中的数据按顺序排列,便于范围查询和排序。
  • 高扇出:每个节点可以有多个子节点,减少树的高度,提高查询效率。
  • 缓存友好:适合现代计算机的缓存机制,减少磁盘 I/O。

B+树核心特性解析:

  • 多路平衡树结构:每个节点可存储多个键值(典型为1200+)
  • 层级控制专家:3-4层即可存储千万级数据
  • 叶子链表连接:所有数据节点形成双向链表
  • 非叶仅存索引:中间节点仅保存导航键值
  • 绝对平衡特性:所有叶子节点处于同一深度

2. 为什么使用B+树作为索引的数据结构?

B+树与常见数据结构的对比:

结构类型 查询复杂度 插入复杂度 范围查询 磁盘友好度
哈希表 O(1) O(1) 不支持
二叉搜索树 O(logN) O(logN) 支持
AVL树 O(logN) O(logN) 支持
B树 O(logN) O(logN) 支持
B+树 O(logN) O(logN) 极优

MySQL 选择 B + 树作为索引的数据结构,主要原因如下:

  • 范围查询高效:由于叶子节点有序且相连,B + 树可以快速定位某个区间内的数据,适合范围查询。
  • 减少磁盘 I/O:B + 树的高扇出特性使得树的高度降低,减少了磁盘 I/O 次数,提高查询性能。
  • 数据存储集中:所有数据都存储在叶子节点,使得数据存储更集中,便于管理和维护。
  • 插入和删除高效:B + 树的自平衡特性保证了插入和删除操作的效率,不会因为数据的变化而导致性能大幅下降。

3. 贯穿本文索引的数据例子

为了更好的理解,之后的聚簇索引、二级索引、联合索引的图例等都用到的是下面的例子:

  1. 建表:
sql 复制代码
CREATE TABLE `anarkh_slave`.`index_demo` (
  `c1` INT NOT NULL,
  `c2` INT NULL,
  `c3` CHAR(1) NULL,
  PRIMARY KEY (`c1`)) ROW_FORMAT = COMPACT;
  1. 插入数据
sql 复制代码
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('1', '4', 'u');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('3', '9', 'd');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('4', '4', 'a');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('5', '3', 'y');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('8', '7', 'a');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('10', '4', 'o');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('12', '7', 'd');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('20', '2', 'e');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('100', '9', 'x');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('209', '5', 'b');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('220', '6', 'i');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('300', '8', 'a');
INSERT INTO `anarkh_slave`.`index_demo` (`c1`, `c2`, `c3`) VALUES ('320', '5', 'm');

准备好了数据,我们还需要回顾一些知识:

  1. 首先是行格式,我们新建的index_demo表是使用COMPACT行格式来存储记录的,为了方便理解,将index_demo表的行格式简化为下图所示:
  1. 接下来还需要回顾一下记录在页中是如何放置的。下图为记录放到页里面的示意图:

准备好了这些,就可以继续探讨索引了。

4. 聚簇索引

4.1 概念

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。在 InnoDB 存储引擎中,聚簇索引的数据行和相邻的键值紧密存储在一起,通常以主键作为聚簇索引。

通常来说,把具有以下两个特点的B+树称为聚簇索引:

  1. 使用记录主键值的大小进行记录和页的排序,这包括3方面的含义:
    1. 页(包括叶子节点和内节点)内的记录按照主键的大小顺序排成一个单向链表,页内的记录被划分成若干组,每个组中主键值最大的记录在页内的偏移量会被当作槽依次放在页目录中(当然Supremum记录比任何用户记录都大),我们可以在页目录中通过二分法快速定位到主键列等于某个值的记录。
    2. 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。
    3. 存放目录项记录的页分为不同的层级,在同一层级中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。
  2. B+树的叶子节点存储的是完整的用户记录。所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。

也就是说页与页之间是双向链表,页内记录是通过单向链表进行连接。

歪个题,说一下单向链表和双向链表的优缺点:

双向链表

  • 优点
    • 双向遍历:从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点,可进可退,在需要频繁双向查找的场景中优势明显,比如实现浏览器的历史记录功能,用户可以方便地进行前进和后退操作125。
    • 查找效率高:对于某些需要查找前一个节点的操作,双向链表无需像单向链表那样从头开始遍历,能直接通过指针找到前一个节点,提高了查找效率。在已知当前节点的情况下,查找其前驱节点的时间复杂度为 O (1)356。
    • 删除操作灵活:删除节点时,可以快速定位前后节点,进行相应的指针调整。若已知要删除的节点,直接修改其前后节点的指针即可完成删除,无需像单向链表那样需要保存前一个节点的指针57。
    • 数据安全性高:指针机制使得数据更难丢失,因为即使丢失了尾节点,仍然可以通过尾节点的后指针访问到链表5。
  • 缺点
    • 空间开销大:每个节点除了存储数据和一个指向下一节点的指针外,还需要额外存储一个指向前一节点的指针,因此占用的存储空间比单向链表多124。
    • 操作复杂度高:插入和删除节点时,需要同时修改前一个节点和后一个节点的指针,操作相对复杂,实现代码量也相对较多56。
    • 内存对齐问题:由于节点中有两个指针,在内存对齐方面可能会面临一些挑战,需要更多地考虑内存管理和分配问题5。

单向链表

  • 优点
    • 结构简单:每个节点只包含数据和指向下一个节点的指针,实现和操作较为容易,理解和编写相关代码相对轻松567。
    • 节省空间:不需要额外的指针来存储前一个节点的地址,相比双向链表,每个节点占用的存储空间更少,在存储大量数据时,能节省一定的内存空间124。
    • 插入删除简单:在进行插入和删除操作时,只需修改相邻节点的指针,不需要移动其他大量元素。例如在头部插入一个新节点,只需让新节点的指针指向原来的头节点,然后更新头指针即可,时间复杂度为 O (1)56。
  • 缺点
    • 单向遍历:只能从头到尾单向遍历,无法直接访问前一个节点,若需要反向遍历,需要从头开始重新遍历整个链表,效率较低136。
    • 查找效率低:查找节点时,需要从头部开始逐个节点进行比较,直到找到目标节点,平均时间复杂度为 O (n),在链表长度较大时,查找速度较慢3。
    • 删除操作限制:删除节点时,如果不知道要删除节点的前一个节点,就需要从头遍历链表来找到前一个节点,才能进行删除操作,增加了操作的复杂性和时间成本45。

4.2 本质特征

  1. 主键索引即数据:<font style="color:rgb(64, 64, 64);">叶节点=数据页</font>
  2. 物理有序存储:相邻主键的数据行物理相邻
  3. 自动创建规则:没有主键时隐式创建6字节ROWID
  4. 索引覆盖扫描:可以直接使用叶节点中的主键值

4.3 图例解析

从根节点开始查找数据,如果用户记录的主键值在[1,320)之间,则到页30中查找更详细的目录项记录,如果主键值不小于320,就到32页中查找。下面节点依次类推。

5. 二级索引

5.1 概念

把以非主键列的大小为排序规则而建立的B+树,且需要执行回表操作才可以定位到完整用户记录的B+树称为二级索引,也叫辅助索引。

5.2 结构特征

  1. 叶节点存储了索引键值和主键值
  2. 非页节点存储了索引键值和指针(页号)
  3. 需要回表操作(通过携带主键信息到聚簇索引中重新定位完整的用户记录的过程称为回表操作)

5.3 图例解析

  • B+树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键这两个列的值。
  • 目录项记录中不再是主键+页号的搭配,而变成了c2列+页号的搭配。

6. 联合索引

6.1 概念

联合索引是指在多个列上创建的索引。

6.2 核心特征

  • 多列组合索引:最多支持16列(建议不超过5列)
  • 有序排列规则:先按第一列排序,第二列在第一列相同的情况下排序
  • 索引复用特性:可以支持多种查询条件组合

注意事项

**最左前缀原则:**查询条件必须按照索引列的顺序依次出现,否则索引可能不会被使用。例如,如果索引是name和age,那么查询WHERE name = 'Alice'会使用索引,但WHERE age = 25则不会。

**覆盖索引:**如果查询的所有列都在索引中,那么可以直接从索引中获取数据,而不需要回表。例如,SELECT name, age FROM users WHERE name = 'Alice'。

6.3 联合索引设计四象限

  1. 高频查询组:将WHERE中最常出现的列放在左边
  2. 基数梯度组:高区分度列优先(城市 > 性别)
  3. 排序需求组:包含ORDER BY/GROUP BY的列
  4. 覆盖索引组:包含SELECT需要的所有列

6.4 图例解析

7. 数据页中的槽与记录中的next_record

  • InnoDB页结构核心组件
    • 槽(Slot):页目录中的二分查找锚点
    • next_record:行记录间的单向链表指针
    • 最大最小记录:页的边界标记
  • 记录分组与槽的形成:一个数据页里的确包含多条记录,这些记录会按照规则进行分组。每组的最后一条记录通常是该组内主键值(或索引键值)最大的记录,将每组最后一条记录的地址偏移量按顺序存放在页目录中,这些偏移量就是槽。槽起到了对页内记录进行索引的作用,能帮助快速定位记录所在的组。
  • **next_record**** 属性**:next_record 是每条记录都具备的属性,它以相对偏移量的形式,指向数据页内的下一条记录,从而将数据页中的记录连接成一个单向链表。
  • 查找流程 :在数据页内查找特定记录时,首先会使用二分查找法在页目录中查找槽,以此确定目标记录所在的组。然后在该组内,通过 next_record 属性遍历记录链表,直至找到目标记录。
  • 记录排序规则:记录一般按照主键值(如果是聚集索引)或索引键值(如果是非聚集索引)从小到大的顺序进行排序。在分组时,也是基于这个排序结果进行划分的。
  • 槽的二分查找:在页目录中使用二分查找槽时,比较的是槽所指向记录的键值。通过不断缩小查找范围,快速定位到可能包含目标记录的组。
  • 虚拟记录与链表遍历 :数据页中存在两条虚拟记录,即最小记录和最大记录。最小记录位于数据页的起始位置,最大记录位于数据页的末尾位置,它们也参与到记录链表的组织中。在组内遍历记录时,从该组的第一条记录开始,依据 next_record 属性依次访问下一条记录,直到遍历完该组的所有记录或者找到目标记录。
  • 示例辅助理解

假设一个数据页存储了学生记录,按照学生的 ID 从小到大排序并分组。页目录中有多个槽,分别指向每组的最后一条记录。当要查找 ID 为 10 的学生记录时,首先在页目录中通过二分查找槽,确定 ID 为 10 的记录可能所在的组。然后在该组内,从第一条记录开始,利用 next_record 属性依次遍历记录,直到找到 ID 为 10 的记录。

8. B+树索引黄金法则

  1. **页分裂代价:**插入无序数据可能导致50%页分裂
  2. **填充因子控制:**默认预留1/16空间用于更新
  3. **索引选择性:**基数/总行数 > 0.2 推荐建索引
  4. **覆盖索引优化:**避免回表的终极方案
  5. **最左前缀原则:**联合索引的左优先匹配
  6. **索引下推技术:**5.6+版本的条件过滤优化
  7. **MRR优化:**随机IO转顺序IO的缓冲机制
  8. **索引合并:**OR条件的优化策略
  9. **隐式转换陷阱:**VARCHAR字段用数字查询
  10. **函数计算禁区:**WHERE YEAR(create_time)=2023
  11. **前缀索引权衡:**节省空间但影响排序
  12. **唯一索引代价:**检查唯一性带来的性能损耗
  13. 列顺序法则:等值查询列在前,范围查询列在后
  14. 长度控制法则:单列索引长度总和不超过3072字节
  15. 索引合并预警:出现index_merge可能暗示需要联合索引
  16. 前缀索引陷阱:联合索引中的前缀索引会阻断后续列使用

9. 索引优化实战策略

9.1 创建策略

  • 优先选择WHERE/JOIN/ORDER字段
  • 联合索引列顺序:区分度高的列在前
  • 控制单表索引数量(建议不超过5个)

9.2 注意事项

  • 索引字段选择:选择选择性高(即字段值的重复度低)的字段作为索引,能提高索引的效率。
  • 前缀索引:对于较长的字段,可以使用前缀索引,减少索引占用的空间,但要注意前缀长度的选择,避免降低索引的选择性。
  • 索引维护:定期维护索引,如重建索引,以保持索引的性能。

9.3 使用禁忌

sql 复制代码
-- 反例:模糊查询失效
SELECT * FROM products WHERE name LIKE '%手机%';

-- 正例:使用覆盖索引
SELECT id FROM products WHERE name LIKE '小米%';
相关推荐
库库林_沙琪马37 分钟前
Redis 持久化:从零到掌握
数据库·redis·缓存
牵牛老人2 小时前
Qt中使用QPdfWriter类结合QPainter类绘制并输出PDF文件
数据库·qt·pdf
尼尔森系3 小时前
排序与算法:希尔排序
c语言·算法·排序算法
卡西里弗斯奥4 小时前
【达梦数据库】dblink连接[SqlServer/Mysql]报错处理
数据库·mysql·sqlserver·达梦
AC使者4 小时前
A. C05.L08.贪心算法入门
算法·贪心算法
冠位观测者4 小时前
【Leetcode 每日一题】624. 数组列表中的最大距离
数据结构·算法·leetcode
sushang~4 小时前
leetcode203.移除链表元素
数据结构·链表
温柔小胖4 小时前
sql注入之python脚本进行时间盲注和布尔盲注
数据库·sql·网络安全
yadanuof4 小时前
leetcode hot100 滑动窗口&子串
算法·leetcode
可爱de艺艺4 小时前
Go入门之函数
算法