news 2026/6/4 16:47:27

mysql索引当中的B+树,聚簇/二级索引,最左匹配,失效场景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
mysql索引当中的B+树,聚簇/二级索引,最左匹配,失效场景

一、B+ 树:索引的底层数据结构

1.1 为什么是 B+ 树?

在深入 B+ 树之前,先理解为什么 MySQL 选择它而不是其他数据结构:

数据结构磁盘 I/O 次数适用场景问题
数组O(log n) 二分查找静态数据插入删除需要移动数据
二叉搜索树O(log n) 但可能退化为 O(n)内存中树高度不可控,磁盘 I/O 多
AVL/红黑树O(log n) 但高度约 log₂(n)内存中2000万数据高度约25层
B 树O(log_m n),m 为阶数磁盘存储非叶子节点也存数据
B+ 树O(log_m n),更低的高度磁盘存储所有数据在叶子节点

关键洞察:磁盘 I/O 是数据库的瓶颈。B+ 树通过高扇出(每个节点存储更多键)将树高控制在 3-4 层。

1.2 B+ 树的核心特征

sql

\-\- 示例:一个 B+ 树索引结构 \-\- 阶数 m = 5(每个节点最多 5 个指针,4 个键值) \[50, 100, 150\] \-\- 根节点 / | \ \ / | \ \ \[10,20,30,40\] \[60,70,80,90\] \[120,130,140\] \[160,170,180,190\] / | | | \ ... ... ... ... 叶子节点 ↓ ↓ ↓ ↓ ↓ \[数据\] \[数据\] \[数据\] \[数据\] \[数据\] \-\- 数据都在叶子

B+ 树的核心特征

特征说明优势
所有数据在叶子节点内部节点只存键值和指针内部节点能存更多键 → 树更矮
叶子节点形成链表叶子节点有指向下一个叶子节点的指针范围查询高效
叶子节点存储数据根据数据存储方式分为聚簇/二级索引详见后文
节点大小 = 页大小MySQL 默认 16KB一次 I/O 读取一页

1.3 B+ 树的高度计算

python

\# 计算 B+ 树的高度(以 InnoDB 为例) import math \# 参数 page_size = 16 * 1024 \# 16KB key_size = 8 \# BIGINT 类型键值 8 字节 pointer_size = 6 \# 指针大小 6 字节 row_size = 200 \# 估算每行数据 200 字节 \# 每个非叶子节点可存储的键数量 slot_size = key_size + pointer_size \# 14 字节 keys\_per\_node = page_size // slot_size \# 16384 // 14 ≈ 1170 \# 每个叶子节点可存储的数据行数 rows\_per\_leaf = page_size // row_size \# 16384 // 200 ≈ 81 \# 计算 3 层 B+ 树能存储的行数 \# 层1(根):1170 个指针 \# 层2:1170 * 1170 = 1,368,900 个指针 \# 层3(叶子):1,368,900 * 81 ≈ 110,880,900 行 print(f"2层B+树可存储约 {1170 * 81:,} 行") \# 约 94,770 行 print(f"3层B+树可存储约 {1170 * 1170 * 81:,} 行") \# 约 1.1 亿行

结论:B+ 树通常只有 2-4 层,意味着查找任何数据只需要 2-4 次磁盘 I/O。

1.4 B+ 树的查找过程

sql

\-\- 示例表 CREATE TABLE user ( id INT PRIMARY KEY, name VARCHAR(50), age INT, INDEX idx_age (age) ); \-\- 执行查询 SELECT * FROM user WHERE id = 25;

查找过程(聚簇索引)

text

1. 加载根节点到内存(磁盘I/O #1)
根节点包含:[10, 20, 30, 40, …] 的键值
判断 25 在 20 和 30 之间 → 走第二个指针
2. 加载第二层节点(磁盘I/O #2)
该节点包含:[21, 22, 23, 24, 25, 26, …]
找到 25 对应的指针
3. 加载叶子节点(磁盘I/O #3)
读取包含 id=25 的完整行数据
返回结果


二、聚簇索引 vs 二级索引

2.1 核心区别

特性聚簇索引二级索引
数据存储位置叶子节点存储完整行数据叶子节点存储主键值
每表数量只有 1 个可以有多个
默认索引PRIMARY KEY 自动创建普通 INDEX / UNIQUE
查询效率直接找到数据(1次回表)找到主键后需要回表
占用空间较大(存储完整行)较小(只存键+主键)

2.2 聚簇索引结构

sql CREATE TABLE student ( id INT PRIMARY KEY, \-\- 聚簇索引 name VARCHAR(50), class VARCHAR(20), score INT );

聚簇索引的 B+ 树结构

text

内部节点(只存键值): \[100, 200, 300, ...\] / | \ / | \ 叶子节点(存完整行数据): ┌─────────────────────────────────────────────────────┐ │ 100 | name='Alice' | class='A' | score=95 │ ├─────────────────────────────────────────────────────┤ │ 101 | name='Bob' | class='A' | score=87 │ ├─────────────────────────────────────────────────────┤ │ 102 | name='Carol' | class='B' | score=92 │ ├─────────────────────────────────────────────────────┤ │ ... → 下一个叶子节点 │ └─────────────────────────────────────────────────────┘

查询过程

sql

SELECT * FROM student WHERE id = 101;
-- 直接命中叶子节点,一次 I/O 拿到所有数据

2.3 二级索引结构

sql

-- 创建二级索引
CREATE INDEX idx_name ON student(name);

二级索引的 B+ 树结构

text

内部节点:索引键值 + 指针
[‘Bob’, ‘David’, ‘Frank’, …]
/ |
/ |
叶子节点:索引键值 + 主键值
┌─────────────────────────────────────────────────────┐
│ ‘Alice’ | 100 │
├─────────────────────────────────────────────────────┤
│ ‘Bob’ | 101 │
├─────────────────────────────────────────────────────┤
│ ‘Carol’ | 102 │
└─────────────────────────────────────────────────────┘

查询过程(需要回表)

sql

SELECT * FROM student WHERE name = ‘Bob’;
-- 步骤1:在二级索引 idx_name 中查找 ‘Bob’
-- 找到主键值 101(磁盘I/O #1)
-- 步骤2:回表 - 用主键 101 到聚簇索引中查找完整数据
-- 找到完整行(磁盘I/O #2)

2.4 覆盖索引(避免回表)

sql

-- 如果查询只需要索引中的字段,无需回表
SELECT name FROM student WHERE name = ‘Bob’;
-- ✅ 直接在 idx_name 的叶子节点就能拿到 name,无需回表
SELECT id, name FROM student WHERE name = ‘Bob’;
-- ✅ id 是主键,也存储在 idx_name 的叶子节点
SELECT * FROM student WHERE name = ‘Bob’;
-- ❌ 需要回表,因为 * 包含 class 和 score

覆盖索引示例

sql

-- 创建覆盖索引(包含所有查询字段)
CREATE INDEX idx_name_score ON student(name, score);
-- 下面的查询只需要这个索引,无需回表
SELECT name, score FROM student WHERE name = ‘Bob’;
SELECT id, name, score FROM student WHERE name = ‘Bob’;


三、最左匹配原则

3.1 核心原理

最左匹配原则:MySQL 使用联合索引时,会从左到右依次匹配查询条件,遇到范围查询(>、<、between、like)后停止匹配

sql

-- 创建联合索引
CREATE INDEX idx_a_b_c ON table_name (a, b, c);
-- 索引的排序结构:
-- 先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序

3.2 索引排序可视化

sql

-- 表数据
INSERT INTO test VALUES
(1, 1, 1), (1, 1, 2), (1, 2, 1), (1, 2, 2),
(2, 1, 1), (2, 1, 2), (2, 2, 1), (2, 2, 2);
-- 联合索引 (a, b, c) 的排序结果:
(1,1,1) → (1,1,2) → (1,2,1) → (1,2,2) → (2,1,1) → (2,1,2) → (2,2,1) → (2,2,2)

3.3 哪些查询走索引?

WHERE 条件是否走索引原因
a = 1✅ 走索引匹配第一列
a = 1 AND b = 2✅ 走索引匹配前两列
a = 1 AND b = 2 AND c = 3✅ 走索引匹配全部三列
b = 2 AND c = 3❌ 不走索引第一列缺失,无法定位
a = 1 AND c = 3⚠️ 部分索引a 过滤后,c 无法用索引(中间缺 b)
a > 1 AND b = 2⚠️ 部分索引a 是范围查询,b 无法用索引
a = 1 AND b > 2 AND c = 3⚠️ 部分索引b 是范围查询,c 无法用索引

3.4 详细分析

sql

-- 场景1:完美匹配 ✅
SELECT * FROM test WHERE a = 1 AND b = 2 AND c = 3;
-- 索引定位:(1,2,3) 精确位置
-- 场景2:左侧缺失 ❌
SELECT * FROM test WHERE b = 2 AND c = 3;
-- 无法使用索引,因为不知道 a 的值,无法定位起始位置
-- 场景3:中间缺失 ⚠️
SELECT * FROM test WHERE a = 1 AND c = 3;
-- 过程:
-- 1. 索引先按 a=1 定位到范围
-- 2. 但这个范围内,b 有多种值,c 不是有序的
-- 3. 只能用于过滤 a,c 需要回表后再过滤
-- 场景4:范围查询后的列失效 ⚠️
SELECT * FROM test WHERE a = 1 AND b > 2 AND c = 3;
-- 过程:
-- 1. a=1 定位
-- 2. b>2 范围查找,找到所有 b>2 的记录
-- 3. 在这个范围内,c 是无序的,无法用索引
-- 场景5:使用 ORDER BY(注意排序方向)
SELECT * FROM test WHERE a = 1 ORDER BY b, c;
-- ✅ 索引已经按 (a,b,c) 排序,直接取数据,无需 filesort
SELECT * FROM test WHERE a = 1 ORDER BY b DESC, c ASC;
-- ❌ 排序方向不一致,需要 filesort

3.5 最佳实践

sql

-- 1. 等值查询在前,范围查询在后
-- 推荐:
CREATE INDEX idx_status_time ON orders (status, created_at);
SELECT * FROM orders WHERE status = ‘paid’ AND created_at > ‘2024-01-01’;
-- 不推荐:
CREATE INDEX idx_time_status ON orders (created_at, status);
-- 原因:时间范围查询后,status 索引失效
-- 2. 区分度高的列在前
-- 假设 gender 只有 ‘M’/‘F’,user_id 唯一
-- 推荐:
CREATE INDEX idx_user_gender ON orders (user_id, gender);
-- 不推荐:
CREATE INDEX idx_gender_user ON orders (gender, user_id);
-- 3. 索引下推(ICP - Index Condition Pushdown)
-- MySQL 5.6+ 支持,可以在索引层面过滤,减少回表


四、索引失效场景(完整清单)

4.1 失效场景速查表

失效场景示例原因
函数操作WHERE YEAR(date) = 2024索引存储的是原值,不是函数结果
类型转换WHERE phone = 13800138000(phone是varchar)隐式转换,函数操作
计算操作WHERE age + 1 = 25对索引列计算
LIKE ‘%abc’WHERE name LIKE '%Bob'通配符在前,无法匹配B+树排序
OR 条件WHERE a = 1 OR b = 2OR 两边的列都需要索引
NOT 条件WHERE a != 1/WHERE NOT (a=1)范围太大,优化器认为全表扫描更快
IS NULL / IS NOT NULLWHERE a IS NULL取决于 NULL 值比例
使用 != 或 <>WHERE a <> 1同 NOT 条件
联合索引未用最左列WHERE b = 1 AND c = 2无法定位起始位置
范围查询后的列WHERE a = 1 AND b > 2 AND c = 3范围后列无序

4.2 详细示例

sql

\-\- 表结构 CREATE TABLE user ( id INT PRIMARY KEY, name VARCHAR(50), age INT, email VARCHAR(100), phone VARCHAR(20), create_date DATE, INDEX idx_name (name), INDEX idx_age (age), INDEX idx_phone (phone), INDEX idx_date (create_date), INDEX idx\_name\_age (name, age) ); \-\- ❌ 1\. 对索引列使用函数 EXPLAIN SELECT * FROM user WHERE LOWER(name) = 'bob'; \-\- 解决:存储时统一小写,或使用虚拟列 \-\- ❌ 2\. 隐式类型转换 EXPLAIN SELECT * FROM user WHERE phone = 13800138000; \-\- phone 是 VARCHAR \-\- 实际执行:WHERE CAST(phone AS SIGNED) = 13800138000 \-\- ❌ 3\. 对索引列进行计算 EXPLAIN SELECT * FROM user WHERE age + 1 = 25; \-\- 改写为:WHERE age = 24 \-\- ❌ 4\. LIKE 前缀模糊匹配 EXPLAIN SELECT * FROM user WHERE name LIKE '%Bob%'; \-\- ✅ 可以使用:WHERE name LIKE 'Bob%' \-\- ❌ 5\. OR 条件(两边都需要独立索引) EXPLAIN SELECT * FROM user WHERE name = 'Bob' OR age = 25; \-\- 解决:UNION 或使用 IN (如果可能) \-\- ✅ OR 的替代方案 SELECT * FROM user WHERE name = 'Bob' UNION SELECT * FROM user WHERE age = 25; \-\- ❌ 6\. NOT 条件 EXPLAIN SELECT * FROM user WHERE name != 'Bob'; \-\- ❌ 7\. 联合索引未使用最左列 EXPLAIN SELECT * FROM user WHERE age = 25; \-\- idx\_name\_age 无效 \-\- 虽然 age 是索引第二列,但无法使用 \-\- ❌ 8\. 范围查询后的列 EXPLAIN SELECT * FROM user WHERE name = 'Bob' AND age > 25; \-\- 这个例子中 age 是范围,但如果后面还有列,后面列会失效 \-\- ⚠️ 9\. 数据分布不均(优化器选择) \-\- 如果表中 99% 的数据 age > 10,MySQL 可能选择全表扫描 EXPLAIN SELECT * FROM user WHERE age > 10;

4.3 特殊情况:看似失效实则有效

sql

\-\- 1\. IS NULL 在某些情况下有效 EXPLAIN SELECT * FROM user WHERE name IS NULL; \-\- 如果 NULL 值很少,可能走索引 \-\- 2\. 使用索引列排序且无 WHERE EXPLAIN SELECT * FROM user ORDER BY name; \-\- 可以走索引,但可能不如 filesort 快 \-\- 3\. IN 查询可以走索引 EXPLAIN SELECT * FROM user WHERE name IN ('Bob', 'Alice', 'Tom'); \-\- IN 相当于多个等值查询 \-\- 4\. BETWEEN 对等值查询有效 EXPLAIN SELECT * FROM user WHERE age BETWEEN 20 AND 30; \-\- 范围查询,但后面的列会失效

4.4 实战:优化 SQL 示例

sql

\-\- 原 SQL:各种问题 SELECT * FROM orders WHERE YEAR(create_time) = 2024 AND status != 'cancelled' AND amount + 10 > 100 AND user_phone = 13800138000 \-\- phone 是 VARCHAR ORDER BY create_time DESC; \-\- 优化后 CREATE INDEX idx\_create\_time_status ON orders(create_time, status); CREATE INDEX idx\_user\_phone ON orders(user_phone); SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01' AND status IN ('pending', 'paid', 'shipped') \-\- 排除 cancelled AND amount > 90 \-\- 移除了计算 AND user_phone = '13800138000' \-\- 字符串 ORDER BY create_time DESC;

五、总结与最佳实践

5.1 核心要点速记

text

B+ 树特征:
├── 所有数据在叶子节点
├── 叶子节点形成双向链表
└── 高度通常 2-4 层
聚簇索引:
├── 每表唯一,叶子存完整行
└── 主键自动创建
二级索引:
├── 每表多个,叶子存主键
└── 查询需要回表
最左匹配:
├── 联合索引从左到右匹配
├── 等值查询先于范围查询
└── 遇到范围查询后失效
失效场景:
├── 函数/计算/类型转换
├── LIKE ‘%xx’
├── OR 两边都要索引
└── 联合索引缺左列

5.2 设计建议

场景建议
主键选择使用自增 BIGINT,避免 UUID(插入随机,页分裂严重)
索引数量单表不超过 5-6 个,维护成本高
选择性索引列区分度越高越好,性别类不适合单独建索引
覆盖索引高频查询字段建覆盖索引,避免回表
顺序等值查询列在前,范围查询列在后

5.3 验证方法

sql

-- 使用 EXPLAIN 分析查询
EXPLAIN SELECT * FROM user WHERE name = ‘Bob’;
-- 关键字段:
-- type: const > ref > range > index > ALL(越左越好)
-- key: 实际使用的索引名
-- rows: 扫描行数,越小越好
-- Extra: Using index(覆盖索引)、Using filesort(需要排序)
-- 使用 FORCE INDEX 测试
SELECT * FROM user FORCE INDEX(idx_name) WHERE name = ‘Bob’;
-- 查看索引使用情况
SHOW INDEX FROM user;

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/4 16:44:04

2026年中国建筑照明优质企业TOP3盘点:头部总部照明服务商选品指南

根据中国照明电器协会2026年发布的《城市照明行业发展白皮书》数据显示&#xff0c;2025年国内建筑泛光照明工程市场规模突破3200亿元&#xff0c;同比增长18.7%&#xff0c;其中头部科技企业总部、城市地标类项目的招标门槛持续提升&#xff0c;68%的项目要求服务商具备“设计…

作者头像 李华
网站建设 2026/6/4 16:40:03

基于小程序的社区养老服务平台的设计与实现毕业设计源码

博主介绍&#xff1a;✌ 专注于Java,python,✌关注✌私信我✌具体的问题&#xff0c;我会尽力帮助你。一、研究目的本研究旨在构建一个基于小程序的社区养老服务平台&#xff0c;以应对我国快速老龄化进程所带来的社会服务需求激增问题。随着人口预期寿命延长及生育率持续下降&…

作者头像 李华
网站建设 2026/6/4 16:39:18

人在家却要连公司内网Kafka?我用cpolar把消息队列穿透到公网,5分钟搞定

前言 远程办公的时候突然要验证一个 Kafka 消息生产消费的逻辑&#xff0c;但 Kafka 集群跑在公司内网测试环境&#xff0c;没有公网 IP、防火墙也不开放端口——这种情况对于需要调试后端服务的开发者来说应该不陌生。传统的解法要么等 CI/CD 部署、要么找运维开防火墙、要么…

作者头像 李华
网站建设 2026/6/4 16:39:16

基于树莓派与SANE打造独立扫描仪:低成本实现文档数字化

1. 项目概述与核心价值 手头有一台闲置的佳能PIXMA MG2500系列一体机&#xff0c;它的扫描功能不错&#xff0c;但每次想扫个文件都得开电脑、装驱动、等软件启动&#xff0c;实在麻烦。作为一个喜欢折腾硬件的玩家&#xff0c;我一直在想&#xff0c;能不能把它变成一个像复印…

作者头像 李华