mysql的索引结构
MySQL 索引底层结构对比图
========================================================================================
【 InnoDB 引擎 结构 】 —— 索引即数据(主键索引树就是数据本身)
========================================================================================
[主键 B+树 索引] (聚簇索引)
[ 根节点: Page 1 ]
/ | \
[ ID=1 ] [ ID=5 ] [ ID=10 ]
/ | \
[叶子节点: Page 2] <=====> [叶子节点: Page 3] <--- (双向链表相连,极利于范围查询)
+--------------+ +--------------+
| ID=1 | 张三 | | ID=5 | 李四 | <--- (叶子节点直接存放了【整行完整数据】)
| ID=2 | 王五 | | ID=6 | 赵六 |
+--------------+ +--------------+
[ age 字段 B+树 索引] (二级索引 / 辅助索引)
[ 根节点: Page 4 ]
/ \
[ age=18 ] [ age=30 ]
/ \
[叶子节点: Page 5] <========> [叶子节点: Page 6]
+------------------+ +------------------+
| age=18 | 主键ID=5| | age=30 | 主键ID=1|
+--------|---------+ +------------------+
|
+======= (回表查询) =======> 拿着 ID=5 重新回到【主键 B+树】中
从根节点开始向下查找,最终获取到“李四”整行数据
========================================================================================
【 MyISAM 引擎 结构 】 —— 索引与数据彻底分离
========================================================================================
[.MYI 索引文件] (无论是主键还是二级索引,结构都一样)
[ 根节点: Page 1 ]
/ \
[ ID=1 ] [ ID=5 ]
/ \
[叶子节点: Page 2] <========> [叶子节点: Page 3]
+----------------------+ +----------------------+
| ID=1 | 磁盘物理地址A | | ID=5 | 磁盘物理地址B | <-- (只存键值和内存指针)
+------|---------------+ +------|---------------+
| |
| |
=========|=====================================|========================================
v (通过物理地址直接寻址) v (通过物理地址直接寻址)
[.MYD 数据文件] (纯粹的数据行列表)
+--------------------------------------------+
| 地址A: [ ID=1 | 张三 | age=30 ] |
| 地址B: [ ID=5 | 李四 | age=18 ] | <--- (所有的实际数据全在这里按顺序排列)
+--------------------------------------------+
InnoDB 的非叶子节点 vs 叶子节点
========================================================================================
【 非叶子节点 内部结构 】 —— 作用:纯粹的“路标”,只用来指引方向
========================================================================================
[ 根节点 / 中间节点 (Page 1) ]
+-------------------------------------------------------------------------+
| 索引键值 (ID=1) | 索引键值 (ID=5) | 索引键值 (ID=10) | ... (可以存上千个) |
|------------------|------------------|------------------|----------------|
| 指向Page 2的指针 | 指向Page 3的指针 | 指向Page 4的指针 | ... |
+-------------------------------------------------------------------------+
| |
| +----------------------------+
v v
========================================================================================
【 叶子节点 内部结构 】 —— 作用:数据的“终点站”,存放真正的业务数据
========================================================================================
[ 叶子节点 (Page 2) ] [ 叶子节点 (Page 3) ]
+-----------------------------------+ +-----------------------------------+
| ID=1 | 张三 | 28岁 | 上海市... | <=========> | ID=5 | 李四 | 32岁 | 北京市... |
| ID=2 | 王五 | 19岁 | 深圳市... | (双向链表) | ID=6 | 赵六 | 24岁 | 广州市... |
+-----------------------------------+ +-----------------------------------+
非叶子节点的“区间指路”原理
非叶子节点里记录的键值(如 ID=1、ID=5),其实代表的是每一个子节点(页)所能接收的“最小主键值”。
========================================================================================
【 非叶子节点 (Page 1) 的逻辑区间 】
========================================================================================
如果你要找的 ID 满足:
* [大于等于 1,且小于 5] ====> 请去 Page 2 找(包含 ID = 1, 2, 3, 4)
* [大于等于 5,且小于 10] ====> 请去 Page 3 找(包含 ID = 5, 6, 7, 8, 9)
* [大于等于 10] ====> 请去 Page 4 找(包含 ID = 10, 11, 12...)
========================================================================================
【 查找轨迹模拟 】
========================================================================================
当你执行 SELECT * FROM table WHERE id = 2 时:
1. 数据库查看非叶子节点(Page 1)。
2. 发现 2 落在 [1 到 5) 的区间内。
3. 于是,它顺着 ID=1 下方的指针,走向了下一层的【叶子节点 Page 2】。
4. 在 Page 2 内部,成功找到了 ID=2 的张三。
所以在非叶子节点上,数据库不会为 id=1、id=2、id=3 分别建一排指针。非叶子节点只需要记录这个数据页的开头(最小值)ID=1,这就足够涵盖 id=2 了。id=1 和 id=2 在非叶子节点阶段走的是同一条路,直到进入了最底层的叶子节点(Page 2)后,它们才会作为两条独立的数据行分开存储。
联合索引的非叶子节点区间
联合索引(又叫复合索引)在非叶子节点上的设计非常巧妙。它的核心逻辑是:在非叶子节点中,同时存入多个列的值,并严格按照从左到右的顺序进行排序(即“最左匹配原则”的由来)。
例如: 我们建立了一个联合索引:KEY idx_name_age (name, age)。此时,非叶子节点里的“路标”就不再是单个数字,而是一个个数据元组 (name, age)。
========================================================================================
【 联合索引非叶子节点 (Page 1) 】 —— 严格按 name 排序;name 相同时按 age 排序
========================================================================================
| 索引键值对: (Apple, 20) | 索引键值对: (Bob, 18) | 索引键值对: (Bob, 25) |
|-------------------------|-------------------------|-------------------------|
| 指向下一层: Page 2 | 指向下一层: Page 3 | 指向下一层: Page 4 |
+-----------------------------------------------------------------------------+
| | |
| | |
v v v
========================================================================================
【 叶子节点层(数据终点)】 —— 内部同样严格按此顺序排列,且带有主键 ID(用于回表)
========================================================================================
[ 叶子节点: Page 2 ] [ 叶子节点: Page 3 ] [ 叶子节点: Page 4 ]
+--------------------+ +--------------------+ +--------------------+
| (Apple, 20) -> ID=5| | (Bob, 18) -> ID=2 | | (Bob, 25) -> ID=9 |
| (Apple, 25) -> ID=1| <===> | (Bob, 23) -> ID=7 | <===> | (Charlie,10)-> ID=4|
+--------------------+ +--------------------+ +--------------------+
它是如何进行“区间指路”的?
非叶子节点的指路逻辑与单列索引一样,依然是“看谁比我大,看谁比我小”。如果我们要查找 WHERE name = ‘Bob’ AND age = 23,数据库的寻找轨迹如下:看第一个键值对 (Apple, 20):由于字母 B 的排序在 A 后面,说明我们要找的 Bob 比 Apple 大,继续往后看。看第二个键值对 (Bob, 18):名字对上了,都是 Bob。开始比对第二个字段:我们要找的年龄是 23,而路标是 18。因为 23 > 18,说明目标在 (Bob, 18) 后面。看第三个键值对 (Bob, 25):名字一样是 Bob。比对年龄:我们要找的 23 比路标 25 小。得出结论:目标落在了 [(Bob, 18), (Bob, 25)) 这个区间内!精准定位:顺着 (Bob, 18) 下方的指针,直接切入 Page 3 叶子节点,瞬间找到了 (Bob, 23) -> ID=7。
为什么会有“最左匹配原则”?
场景 A:WHERE name = ‘Bob’(正常走索引)非叶子节点是按照 name 排序的(Apple -> Bob -> Charlie)。数据库可以直接利用二分法在非叶子节点定位到 Bob 的区间,能走索引。场景 B:WHERE age = 23(索引失效,全表扫描)如果跳过 name 只查 age,你看一眼非叶子节点里的 age:20, 18, 25。它们是无序的!因为 age 只有在 name 相同的前提下才有序。既然非叶子节点上的 age 全是乱序的,数据库就根本无法在非叶子节点做区间判断,无法指路,因此索引直接失效。场景 C:WHERE name = ‘Bob’ AND age > 18(部分走索引)name = ‘Bob’ 可以利用非叶子节点定位到 Page 3。在 Page 3 叶子节点内部,由于 age 有序,可以快速找到大于 18 的所有数据。但是,如果条件变成 WHERE name > ‘Apple’ AND age = 23,因为 name 变成了一个范围(Bob 和 Charlie),在多个不同的 name 之间,age 是无法一起排序的,所以 age 的索引在此时就会失效。
拓展
MySQL 索引的物理尺寸限制(基于 InnoDB)
在 InnoDB 中,所有的数据和索引都是以页(Page)为单位存放在磁盘上的,默认一页的大小是 16KB。
为什么 B+树只有 3-4 层就能存千万数据?假设一个主键为 BIGINT(8 字节),指针为 6 字节,一个非叶子节点的键值对大约占 14 字节。一个 16KB 的非叶子节点页面,大约可以容纳 16384 ÷ 14 ≈ 1170 个指针。如果树高为 3 层:第一层(根节点):1 个节点,指向 1170 个第二层节点。第二层:1170 个节点,指向 1170 × 1170 ≈ 1,368,900 个叶子节点。第三层(叶子节点):假设每行数据 1KB,一个叶子节点页存 16 行。总容量:1,368,900 × 16 ≈ 21,900,000(约 2190 万行数据)。也就是说,在 2000 万数据量下,InnoDB 只需要 3 次磁盘 I/O 就能定位到任意一行数据。
在 MySQL(尤其是 InnoDB 引擎)中,你每创建普通索引、唯一索引或联合索引,底层的存储引擎都会在磁盘上多建一棵全新的 B+ 树。
- 点赞
- 收藏
- 关注作者
评论(0)