mysql之深入理解b+树原理

学习来源:

1. mysql面试题-深入理解B+树原理_哔哩哔哩_bilibili

b+树是什么?

B + 树是多路平衡查找树,非叶子节点只存索引和指针,数据全在叶子节点,而且叶子节点用双向链表串起来。它是数据库索引的底层结构,主要就是为了优化磁盘 IO,查询效率稳定。

大白话:

叶子节点存储数据,同时数据结构是双向链表,对于全表扫描就是链表的遍历。

但是对于大数据量,就需要进行优化,涉及到树。

比如:最顶层是一次io ,对应一个扇区,512B。序号(8B) + 地址(8B) = 16B, 512➗16 = 32个数据。 从最顶层遍历到响应序号,继续向下索引到对应扇区,也是32块数据, 直至到叶子节点,也是能对应找到对应32个数据,从32个数据中查找。 一次扇区的查找,对应一次io,这样就有了b+树对io次数的优化,并且b+树始终维护最顶层只有一个扇区

二级索引树与聚簇索引树

聚簇索引和二次索引是两棵独立的 B + 树

二次索引树,相当于索引表 ,叶子节点只存索引键和主键值,不存完整数据。非叶子节点存普通索引(比如name的具体值)和 子节点指针(地址)

聚簇索引树,相当于全表,叶子节点存主键值和完整数据。非叶子节点存主键值和指针

回表

二次索引跟聚簇索引查找数据的逻辑相似,只是多了一次回表操作。

什么是回表操作,拿到了二次索引树的叶子结点的主键值,拿这个主键值去聚簇索引树拿数据。按照b+树查找逻辑,拿到数据:不断二分查找,以及树的层层递进。

聚簇索引查询的完整 IO 过程

1.根节点 :在 InnoDB 中,聚簇索引的根节点通常会被常驻内存,这一步不需要磁盘 IO。

2.中间层节点:如果树高是 2-3 层,需要 1-2 次磁盘 IO 来读取中间节点。

3.叶子节点:需要 1 次磁盘 IO 来读取包含完整行数据的叶子节点。

整个过程大概需要 2-3 次磁盘 IO,但因为没有回表,所以不需要额外的 IO 开销。

有无索引的区别

有索引可以不用全表扫描,无索引需要全表扫描整块链表

  • 有索引的底层数据结构与查询逻辑

    • 不管是聚簇索引 还是二次索引 ,底层都是 B + 树 结构。
    • 聚簇索引:叶子节点存完整行数据,非叶子节点存主键值 + 子节点指针;查询时通过 B + 树的二分查找快速定位叶子节点,无需回表,IO 次数少。
    • 二次索引:叶子节点存普通索引键 + 主键值,非叶子节点存普通索引键 + 子节点指针;查询需先通过二次索引 B + 树拿到主键,再去聚簇索引 B + 树查完整数据(回表)。
    • 联合索引:特殊的 B + 树,按最左字段优先排序,遵循最左匹配原则,可叠加多条件缩小扫描范围。
  • 无索引的底层数据结构与查询逻辑

    • 无额外索引时,数据依托聚簇索引的叶子节点,以 双向链表 形式有序存储。
    • 没有 B + 树的非叶子节点做导航,只能从链表头开始顺序遍历每一行数据,无法用二分查找,大数据量下性能极差(时间复杂度 O (n))。

b+树与sql实际使用关联(一丁点扩展)

聚簇索引 vs 二次索引 → 回表问题

如果你用 ** 主键(聚簇索引)** 查询:

sql 复制代码
SELECT * FROM user WHERE id = 100;

如果你用 ** 普通索引(二次索引)** 查询:

sql 复制代码
SELECT * FROM user WHERE name = '张三';

这条语句会先在二次索引的 B + 树中找到name='张三'对应的主键值,再用主键去聚簇索引里查完整数据,这个过程就叫回表,会多一 次b+树的IO。

叶子节点的双向链表 → 范围查询高效

B + 树叶子节点的链表结构,让范围查询非常快:

sql 复制代码
SELECT * FROM user WHERE id BETWEEN 100 AND 200;

这个查询只需要定位到id=100的叶子节点,然后沿着链表一直读到id=200即可,不用像 B 树那样反复回溯上层节点。

树的高度 → 查询效率的稳定性

因为 B + 树是平衡树,所有叶子节点都在同一层,所以无论查询哪个数据,IO 次数都是固定的(比如 MySQL 中通常是 2-3 次),这保证了 SQL 查询性能的稳定性。

sql优化

尽量用主键查询:避免回表,减少 IO 次数。

覆盖索引优化:如果只需要查询索引字段,就不会触发回表。

sql 复制代码
SELECT id, name FROM user WHERE name = '张三';

这条语句的查询字段idname都在二次索引里,不需要回表。

范围查询尽量用连续的索引键:比如用主键、自增 ID 做范围查询,能充分利用叶子节点的链表结构。

为什么连续的索引键更高效?

B + 树的叶子节点是一个有序的双向链表 ,比如自增主键 id 就是天然连续的。

连续的索引键(如自增 ID) 能让 B + 树的叶子节点在物理上也保持连续,范围查询时只需要顺序遍历链表,不需要额外的 IO 开销,效率最高。

  • 当你执行 WHERE id BETWEEN 100 AND 200 时:

    1. 用二分查找定位到 id=100 的叶子节点。
    2. 直接沿着链表的 next 指针,依次读取后续节点,直到 id>200 停止。
    3. 全程不需要回溯上层节点,也不需要额外的 IO,非常高效。
  • 但如果用非连续的索引键(比如 name)做范围查询:

    1. 同样定位到 name='L' 的叶子节点。
    2. 虽然也能沿链表遍历,但 name 的排序是按字符串字典序,物理上可能不连续,导致跨多个磁盘页,IO 次数会增加。

假设你的表有自增主键 id,并且 id 是聚簇索引:

sql 复制代码
-- 高效:用连续的主键做范围查询
SELECT * FROM user WHERE id BETWEEN 100 AND 200;

执行过程:

  1. 定位到 id=100 的叶子节点。
  2. 沿链表顺序读取到 id=200 的节点。
  3. 整个过程只需要 2-3 次 IO(定位初始节点 + 遍历链表)。

如果换成非连续的 name 字段:

sql 复制代码
-- 低效:用非连续的普通索引做范围查询
SELECT * FROM user WHERE name BETWEEN 'A' AND 'L';

执行过程:

  1. 定位到 name='A' 的叶子节点。
  2. 沿链表遍历到 name='L' 的节点。
  3. name 的物理存储不连续,可能需要跨多个磁盘页,IO 次数会显著增加。

大数据量 SQL 优化

SQL 优化的核心目标是:最小化数据扫描范围 + 降低磁盘 IO 次数 + 减少数据库计算开销

一、索引优化(优先级最高)

索引是优化的核心,直接决定数据库是否 "全表扫描",大数据量下无索引的查询几乎不可用。

核心索引原则:

|---------------------|--------------------------------------|---------------------------------------------------------------------------------------|
| 优化方向 | 具体做法 | 反例(索引失效) |
| 优先用主键 / 聚簇索引 | 主键(自增 ID)作为查询 / 排序 / 分页的核心条件 | - |
| 覆盖索引避免回表 | 索引包含查询所需所有字段(无需回表查聚簇索引) | SELECT * FROM user WHERE name='张三'(普通索引只含 name,需回表) |
| 联合索引按 "高频查询字段在前" 排序 | 高频条件字段(如 status)放联合索引首位,排序 / 范围字段放末尾 | CREATE INDEX idx_user_name_time ON user (create_time, name)(时间是范围字段,放首位会导致 name 失效) |
| 避免索引字段做运算 / 函数 | 提前计算好条件值,不在 SQL 中对索引字段操作 | WHERE DATE(create_time) = '2025-01-01'(函数导致索引失效) |
| 范围查询用连续索引键 | 自增 ID、时间戳等连续字段做范围查询(利用 B+ 树链表) | 表)WHERE name BETWEEN 'A' AND 'Z'(字符串非连续,IO 高) |

sql 复制代码
-- 场景:查询2025年1月的有效用户(status=0),返回id/name/phone
-- 低效:普通索引只含create_time,需回表,且未包含status
CREATE INDEX idx_user_create_time ON user (create_time);

-- 高效:联合覆盖索引(包含查询/条件/返回字段,无回表)
CREATE INDEX idx_user_status_time ON user (status, create_time) INCLUDE (id, name, phone);

-- 优化后的查询(走覆盖索引,无回表,IO最少)
SELECT id, name, phone FROM user 
WHERE status=0 AND create_time BETWEEN '2025-01-01' AND '2025-01-31';

联合索引按 "高频查询字段在前" 排序的原理

说白了,就是 先对最左边进行排序,没有第一个的前提,就没有第二个排序的说法。

1.联合索引的排序规则

联合索引的 B + 树是层级排序的:

  • 首先按照最左侧的第一个字段进行全局排序
  • 只有在第一个字段值相同的前提下,才会对第二个字段进行排序
  • 如果前两个字段值都相同,才会对第三个字段排序,以此类推

2. 为什么高频字段要放在最前面

因为联合索引遵循最左匹配原则,只有从最左的字段开始匹配,才能触发索引的完整效用:

  • 如果高频查询条件是 status=0,把 status 放在索引最前面,即使查询只带这个条件,也能触发索引
  • 如果把低频字段放在前面,高频查询可能因为不匹配最左字段而无法使用索引,导致全表扫描

3. 实战中的设计建议

  • 等值字段优先 :高频的等值条件(如 status=0)放在最前面
  • 范围字段置后 :范围条件(如 create_time > '2025-01-01')放在最后,避免后续索引列失效
  • 示例 :联合索引 idx_user_status_time (status, create_time)
    • 支持查询:WHERE status=0WHERE status=0 AND create_time > '2025-01-01'
    • 不支持查询:WHERE create_time > '2025-01-01'(跳过了最左的 status 字段)
为什么要使用联合索引
  • 进一步缩小扫描范围 仅靠 status=0 可能返回几十万条数据,联合索引可以叠加第二个条件(如 create_time),直接过滤出几千条目标数据,避免全表扫描。

  • 减少回表 IO如果把查询需要的字段都包含在联合索引里,就可以直接从索引返回结果,不用再去聚簇索引里查完整数据,减少磁盘 IO 次数。

  • 用一个索引覆盖多个场景一个联合索引可以同时支持 "单条件查询" 和 "多条件组合查询",比创建多个单字段索引更节省空间和维护成本。

。。。。。。下次再更新。

相关推荐
踢足球09292 小时前
寒假打卡:2026-01-26
数据库
angushine2 小时前
鲲鹏ARM服务MySQL镜像方式部署主从集群
android·mysql·adb
漂洋过海的鱼儿2 小时前
Qt--元对象系统
开发语言·数据库·qt
沧澜sincerely2 小时前
分组数据【GROUP BY 与 HAVING的使用】
数据库·sql·group by·having
知识分享小能手2 小时前
Oracle 19c入门学习教程,从入门到精通,Oracle数据库控制 —— 事务与并发控制详解(14)
数据库·学习·oracle
2301_811232982 小时前
使用Flask快速搭建轻量级Web应用
jvm·数据库·python
予枫的编程笔记2 小时前
【Redis实战进阶篇】高并发下数据安全与性能平衡?Redis准存储三大核心场景实战指南
数据库·redis·缓存·高并发优化·电商实战·redis准存储·redis pipeline
jiunian_cn2 小时前
【Redis】Redis基本全局命令
数据库·redis·缓存
m0_561359672 小时前
高级爬虫技巧:处理JavaScript渲染(Selenium)
jvm·数据库·python