学习来源:
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 = '张三';
这条语句的查询字段id和name都在二次索引里,不需要回表。
范围查询尽量用连续的索引键:比如用主键、自增 ID 做范围查询,能充分利用叶子节点的链表结构。
为什么连续的索引键更高效?
B + 树的叶子节点是一个有序的双向链表 ,比如自增主键 id 就是天然连续的。
连续的索引键(如自增 ID) 能让 B + 树的叶子节点在物理上也保持连续,范围查询时只需要顺序遍历链表,不需要额外的 IO 开销,效率最高。
当你执行
WHERE id BETWEEN 100 AND 200时:
- 用二分查找定位到
id=100的叶子节点。- 直接沿着链表的
next指针,依次读取后续节点,直到id>200停止。- 全程不需要回溯上层节点,也不需要额外的 IO,非常高效。
但如果用非连续的索引键(比如
name)做范围查询:
- 同样定位到
name='L'的叶子节点。- 虽然也能沿链表遍历,但
name的排序是按字符串字典序,物理上可能不连续,导致跨多个磁盘页,IO 次数会增加。
假设你的表有自增主键 id,并且 id 是聚簇索引:
sql
-- 高效:用连续的主键做范围查询
SELECT * FROM user WHERE id BETWEEN 100 AND 200;
执行过程:
- 定位到
id=100的叶子节点。- 沿链表顺序读取到
id=200的节点。- 整个过程只需要 2-3 次 IO(定位初始节点 + 遍历链表)。
如果换成非连续的 name 字段:
sql
-- 低效:用非连续的普通索引做范围查询
SELECT * FROM user WHERE name BETWEEN 'A' AND 'L';
执行过程:
- 定位到
name='A'的叶子节点。- 沿链表遍历到
name='L'的节点。- 但
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=0、WHERE status=0 AND create_time > '2025-01-01'- 不支持查询:
WHERE create_time > '2025-01-01'(跳过了最左的status字段)
为什么要使用联合索引
进一步缩小扫描范围 仅靠
status=0可能返回几十万条数据,联合索引可以叠加第二个条件(如create_time),直接过滤出几千条目标数据,避免全表扫描。减少回表 IO如果把查询需要的字段都包含在联合索引里,就可以直接从索引返回结果,不用再去聚簇索引里查完整数据,减少磁盘 IO 次数。
用一个索引覆盖多个场景一个联合索引可以同时支持 "单条件查询" 和 "多条件组合查询",比创建多个单字段索引更节省空间和维护成本。
。。。。。。下次再更新。