联合索引在B+树上的结构介绍

本文深入探讨了MySQL中联合索引的内部结构与工作原理,通过实例解析了B+Tree如何支持多列索引,并解释了最左原则的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在学习MySQL的存储引擎和索引的知识。看了许多篇介绍MyISAM和InnoDB的索引的例子,都能理解。

像这张索引图:

\

PS:该图来自大神张洋的《MySQL索引背后的数据结构及算法原理》一文。

但许多文章讲述的都是单列索引,我很好奇联合索引对应的结构图是怎样的。

疑惑:联合索引的结构是怎样的

比方说联合索引 (col1, col2,col3),我知道在逻辑上是先按照col1进行排序再按照col2进行排序最后再按照col3进行排序。因此如果是select * from table where col1 = 1 and col3 = 3的话,只有col1的索引部分能生效。但是其物理结构上这个联合索引是怎样存在的,我想不懂。

解答:联合索引的结构

上网查阅了许多资料,总算有点眉目了。

假设这是一个多列索引(col1, col2,col3),对于叶子节点,是这样的:

\

PS:该图改自《MySQL索引背后的数据结构及算法原理》一文的配图。

也就是说,联合索引(col1, col2,col3)也是一棵B+Tree,其非叶子节点存储的是第一个关键字的索引,而叶节点存储的则是三个关键字col1、col2、col3三个关键字的数据,且按照col1、col2、col3的顺序进行排序。

配图可能不太让人满意,因为col1都是不同的,也就是说在col1就已经能确定结果了。自己又画了一个图(有点丑),col1表示的是年龄,col2表示的是姓氏,col3表示的是名字。如下图:

\

PS:对应地址指的是数据记录的地址。

如图,联合索引(年龄, 姓氏,名字),叶节点上data域存储的是三个关键字的数据。且是按照年龄、姓氏、名字的顺序排列的。

因此,如果执行的是:
select * from STUDENT where 姓氏='李' and 名字='安';
或者
select * from STUDENT where 名字='安';
那么当执行查询的时候,是无法使用这个联合索引的。因为联合索引中是先根据年龄进行排序的。如果年龄没有先确定,直接对姓氏和名字进行查询的话,就相当于乱序查询一样,因此索引无法生效。因此查询是全表查询。

如果执行的是:
select * from STUDENT where 年龄=1 and 姓氏='李';
那么当执行查询的时候,索引是能生效的,从图中很直观的看出,age=1的是第一个叶子节点的前6条记录,在age=1的前提下,姓氏=’李’的是前3条。因此最终查询出来的是这三条,从而能获取到对应记录的地址。
如果执行的是:
select * from STUDENT where 年龄=1 and 姓氏='黄' and 名字='安';
那么索引也是生效的。

而如果执行的是:
select * from STUDENT where 年龄=1 and 名字='安';
那么,索引年龄部分能生效,名字部分不能生效。也就是说索引部分生效。

因此我对联合索引结构的理解就是B+Tree是按照第一个关键字进行索引,然后在叶子节点上按照第一个关键字、第二个关键字、第三个关键字…进行排序。

最左原则

而之所以会有最左原则,是因为联合索引的B+Tree是按照第一个关键字进行索引排列的。

有助于理解联合索引的一个例子

假设数据 表T (a,b,c) rowid 为物理位置
rowid a b c
(1)   1 1 1
(2)   2 1 13
(3)   2 2 14
(4)   1 3 3
(5)   2 3 12
(6)   1 2 5
(7)   2 3 9
(8)   1 2 2
(9)   1 3 6
(10)  2 2 11
(11)  2 2 8
(12)  1 1 7
(13)  2 3 15
(14)  1 1 4
(15)  2 1 10
 
当你创建一个索引 create index xxx on t(a,b), 
则索引文件逻辑上等同于如下
a b rowid
1 1 1
1 1 12
1 1 14
1 2 6
1 2 8
1 3 4
1 3 9
2 1 2
2 1 15
2 2 3
2 2 10
2 2 11
2 3 5
2 3 7
2 3 13
 
当select * from T where a=1 and b=3 的时候,
数据库系统可以直接从索引文件中直接二分法找到A=1的记录,
然后再B=3的记录。
但如果你 where b=3 则需要遍历这个索引表的全部!

这样的解释,参考过别人的文章,自己也有思考过,但并不能确定实际上的结构就是这种。但是,也算是学到了许多东西,像是明白了为什么会有最左原则的存在。

<think>我们正在讨论MySQL覆盖索引的B+树实现原理和结构。覆盖索引是指一个索引包含了查询所需的所有字段,因此无需回表即可完成查询。在B+树中,这种索引是如何存储和组织的呢? 根据引用[4]:对于非主键索引联合索引),在维护B+树时,会根据联合索引的字段依次排序。例如,索引(name, address, age)会先按name排序,name相同则按address排序,再相同则按age排序,最后按主键排序。同时,非主键索引的B+树节点只存储索引字段和主键字段。 覆盖索引的B+结构: 1. 节点结构:B+树的每个节点包含多个键值对(索引字段值)和指向子节点的指针(非叶子节点)或指向数据行的指针(叶子节点)。但在覆盖索引中,叶子节点除了存储索引键值,还会存储查询所需的其他字段(即覆盖字段)以及主键值。 2. 存储内容:在覆盖索引的情况下,如果索引包含所有查询需要的列,那么叶子节点就会额外存储这些列的值,而不仅仅是主键。这样,当查询命中索引时,可以直接从叶子节点读取所有数据,无需回表。 但是,这里有一个关键点:在InnoDB中,非主键索引(二级索引)的叶子节点存储的是主键值,而不是数据行的物理地址。因此,如果查询需要除索引列和主键外的其他列,就必须回表。覆盖索引要避免回表,就必须在索引中包含所有查询需要的列。 因此,覆盖索引的B+结构实际上与普通二级索引的B+结构相同,区别在于覆盖索引的键值包含了查询所需的所有列,这样在叶子节点中就已经包含了所有需要的数据(包括主键和覆盖的列)。 具体结构: - 非叶子节点:存储索引键值(例如,联合索引的多个字段组合)和指向下一级节点的指针。 - 叶子节点:存储索引键值、主键值以及覆盖的列(如果覆盖索引包含了这些列)。注意,在InnoDB中,二级索引的叶子节点存储的是主键值,而覆盖索引需要包含的额外列也会存储在叶子节点中。 例如,有一个表: CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100), age INT, address VARCHAR(100), phone VARCHAR(20) ); 创建一个覆盖索引:INDEX idx_cover (age, name) INCLUDE (phone) -- 注意:MySQL 5.7及之前不支持INCLUDE,但MySQL 8.0支持在索引中增加非键列(通过INCLUDE)或者直接使用联合索引。 在MySQL 8.0之前,我们通常使用联合索引来创建覆盖索引,例如: CREATE INDEX idx_cover ON users(age, name, phone); 这样,索引idx_cover的B+结构如下: - 非叶子节点:存储(age, name, phone)的组合值(实际存储的是每个索引字段的值,以及指向下一层的指针)。 - 叶子节点:存储(age, name, phone)的值,以及该行对应的主键id。 当执行查询: SELECT phone FROM users WHERE age=25 AND name='John'; 这个查询只需要访问索引idx_cover的B+树,因为phone已经包含在索引中。查询过程: 1. 从根节点开始,根据age和name定位到叶子节点。 2. 在叶子节点中,找到age=25且name='John'的条目,该条目中已经存储了phone的值,直接取出即可。 注意:在叶子节点中,除了存储索引列(age, name, phone)的值,还会存储主键id(因为这是二级索引)。但在这个查询中,我们并不需要id,所以不需要回表。 覆盖索引的B+树与普通二级索引的B+结构相同,只是覆盖索引的键值包含了更多的列,从而能够满足特定查询的所有列需求。 总结: 覆盖索引的B+树实现原理: 1. 索引键值按照定义顺序(联合索引的顺序)存储在B+树的节点中。 2. 叶子节点存储索引键值、主键值以及覆盖索引中包含的其他列的值(如果有)。 3. 查询时,如果索引覆盖了查询所需的所有列,则直接返回叶子节点中的数据,避免回表操作。 引用: [^4] 说明了非主键索引的B+树维护方式,即按照索引字段顺序排序,并存储索引字段和主键字段。但要注意,覆盖索引需要存储额外的列,因此索引中会包含这些列的值(在叶子节点中)。</think>### MySQL 覆盖索引的 B+ 树实现原理及结构详解 覆盖索引的核心在于其 B+结构**直接存储查询所需的所有数据**,避免回表操作。以下是详细实现原理: #### 一、B+ 树基础结构 1. **节点组成** - **非叶子节点**:存储索引键值 + 子节点指针 ```mermaid graph LR A[非叶子节点] --> |指针| B[子节点1] A --> |指针| C[子节点2] A --> |键值: age| D[排序键] ``` - **叶子节点**:存储索引键值 + **实际数据列或主键**(关键区别!) ```mermaid graph LR E[叶子节点] --> |键值: age| F[数据列] E --> |键值: gender| G[数据列] E --> |主键| H[id] ``` 2. **与普通索引的差异 | **特性** | 普通二级索引 | 覆盖索引 | |------------------|----------------------------|----------------------------| | 叶子节点存储内容 | 仅主键值 | 索引+ 查询列 + 主键值 | | 查询流程 | 需回表查数据行 | 直接从索引取数据 | | 磁盘I/O | 索引+ 数据页 | 仅索引页 | #### 二、覆盖索引的 B+ 树实现原理 1. **数据存储优化** - 叶子节点额外存储查询所需的非索引列(如 `phone`) - 示例结构索引 `(age, gender)` 覆盖查询 `SELECT phone`): ```mermaid graph BT Root[根节点] --> |age<25| Node1 Root --> |age≥25| Node2 Node1[叶子节点] --> |age=20<br>gender=M<br>phone=123| Data1 Node1 --> |age=22<br>gender=F<br>phone=456| Data2 ``` 2. **查询流程**(以 `SELECT phone FROM users WHERE age=20 AND gender='M'` 为例): ```mermaid flowchart TB A[查询] --> B[在B+树定位 age=20] B --> C[在叶子节点筛选 gender='M'] C --> D[直接读取 phone 值] D --> E[返回结果 无需回表] ``` 3. **排序机制** 按索引定义顺序逐级排序(如索引 `(age, gender, phone)`): - 先按 `age` 排序 - `age` 相同时按 `gender` 排序 - 两者相同再按 `phone` 排序 (引用自[^4]) #### 三、性能优势原理 1. **I/O 优化** - 索引大小通常仅为数据行的 30%-50%(引用自[^1]) - 单次查询可减少 50% 以上磁盘访问 2. **顺序访问优势** B+ 树叶子节点形成链表结构,对范围查询高效(如 `WHERE age BETWEEN 20 AND 30`) (引用自[^3]) 3. **缓存利用率** 更小的索引尺寸使更多节点可缓存在 InnoDB Buffer Pool 中 #### 四、设计注意事项 1. **列顺序策略** - 高频查询条件放索引左侧 - 覆盖列置于索引末尾 ```sql -- 优化示例:覆盖查询 SELECT phone CREATE INDEX idx_cover ON users(age, gender, phone); ``` 2. **空间权衡** - 每增加一列,索引体积增长约 20-30% - 适用于低频更新的列(引用自[^1]) 3. **主键处理** 即使使用覆盖索引,叶子节点**仍存储主键值**(用于唯一标识) > **关键结论**:覆盖索引本质是通过**扩展 B+ 树叶子节点的数据存储**,将随机 I/O 转化为顺序 I/O,从而提升查询效率[^1][^4]。 --- ### 相关问题 1. 如何验证覆盖索引是否减少了回表操作? 2. 在哪些场景下覆盖索引可能导致性能下降? 3. MySQL 8.0 的「降序索引」如何影响覆盖索引设计? 4. 如何计算覆盖索引的空间占用成本?
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值