27. 能简单说一下索引的分类吗?
答:
MySQL 中索引的分类方式有很多,可以从不同维度来划分。 面试里通常从以下几个角度回答就比较完整:
- 按功能划分
- 按字段个数划分
- 按数据结构划分
- 按存储方式划分
1)按功能划分
这是最常见的分类方式。
(1)主键索引(Primary Key)
主键索引是表的主键对应的索引。
特点:
- 不允许重复
- 不允许为
NULL - 一张表只能有一个主键索引
在 InnoDB 中,主键索引同时也是聚簇索引。
例如:
CREATE TABLE user ( id BIGINT PRIMARY KEY, name VARCHAR(50));这里 id 上就有主键索引。
(2)唯一索引(Unique Index)
唯一索引要求索引列值不能重复,但通常允许 NULL(具体行为要结合版本和约束语义理解)。
例如:
CREATE UNIQUE INDEX idx_email ON user(email);适用于:
- 邮箱
- 手机号
- 身份证号
- 用户名
(3)普通索引(Normal Index)
最普通的索引,没有唯一性限制。 主要目的是提升查询性能。
例如:
CREATE INDEX idx_name ON user(name);2)按字段个数划分
(1)单列索引
索引只包含一个字段。
例如:
CREATE INDEX idx_age ON user(age);(2)联合索引(组合索引)
索引由多个字段共同组成。
例如:
CREATE INDEX idx_name_age ON user(name, age);联合索引非常重要,它不仅可以提高多条件查询效率,还涉及最左匹配原则。
3)按数据结构划分
(1)B+Tree 索引
这是 MySQL 最常见、最重要的索引结构。 InnoDB 默认使用 B+Tree 作为索引底层结构。
适合:
- 等值查询
- 范围查询
- 排序
- 分组
(2)Hash 索引
哈希索引适合等值查询,不适合范围查询和排序。 MySQL 的 MEMORY 引擎支持哈希索引。 InnoDB 也有自适应哈希索引,但不是用户手工创建的普通 Hash 索引。
4)按存储方式划分
(1)聚簇索引
索引和数据存储在同一棵树中,叶子节点存整行数据。 InnoDB 的主键索引就是聚簇索引。
(2)非聚簇索引(辅助索引 / 二级索引)
索引和数据分离,或者说索引叶子节点不直接存整行数据。 在 InnoDB 中,二级索引叶子节点存的是主键值,需要回表。
5)补充一些常见特殊索引
全文索引(Fulltext Index)
适用于文本搜索,例如文章内容关键字搜索。 不过现代项目里更常用 ES 等搜索引擎替代。
前缀索引
对长字符串字段只索引前面一部分内容,例如:
CREATE INDEX idx_email ON user(email(10));适合长字段节省索引空间。
6)总结
如果面试官问“索引分类”,推荐按下面方式回答:
- 按功能分:主键索引、唯一索引、普通索引
- 按字段数分:单列索引、联合索引
- 按结构分:B+Tree、Hash
- 按存储方式分:聚簇索引、非聚簇索引
一句话总结:
MySQL 中最常用的索引,本质上是基于 B+Tree 的主键索引、唯一索引、普通索引和联合索引。
28. 为什么使用索引会加快查询?
答:
索引的核心作用是:帮助数据库更快地定位数据。 如果没有索引,MySQL 在很多情况下只能从头到尾扫描整张表; 有了索引之后,可以像查字典目录一样快速定位到目标记录。
1)没有索引时会发生什么?
假设有一张用户表:
| id | name | age |
|---|---|---|
| 1 | Tom | 20 |
| 2 | Jack | 21 |
| 3 | Lucy | 22 |
| … | … | … |
执行 SQL:
SELECT * FROM user WHERE name = 'Lucy';如果 name 没有索引,MySQL 往往只能这样做:
- 先读第一行,看是不是
Lucy - 不是,再读第二行
- 再读第三行
- 一直扫描到找到为止,或者扫完整张表
这就是全表扫描。
如果表很大,比如几百万、几千万行,性能会非常差。
2)有索引时会发生什么?
如果对 name 建了索引:
CREATE INDEX idx_name ON user(name);MySQL 就可以先在索引结构中查找 Lucy 的位置,再快速定位到对应数据。
这样就不需要扫描整张表了。
这就像:
- 没有索引:一本书从第一页翻到最后一页找内容
- 有索引:先查目录,再翻到具体页码
3)索引为什么快?本质原因是什么?
本质有两个原因:
(1)减少扫描的数据量
索引能帮助数据库缩小查找范围,不必遍历整张表。
(2)索引本身是高效的数据结构
MySQL 的索引通常基于 B+Tree。 B+Tree 的特点是:
- 树高很低
- 每次查找只需要很少几次磁盘 IO
- 范围查询也很高效
所以索引不是“简单标记一下”,而是通过高效的数据结构提升查找性能。
4)索引不仅能优化等值查询
很多人以为索引只能优化:
WHERE id = 1其实索引还能优化:
范围查询
WHERE age BETWEEN 20 AND 30排序
ORDER BY age分组
GROUP BY age联表
JOIN ... ON a.id = b.a_id只要索引设计合理,都可能提升性能。
5)为什么索引特别适合大表?
如果一张表只有几十条、几百条数据,扫表成本并不高,索引优势不明显。 但如果表很大,比如:
- 百万级
- 千万级
- 上亿级
没有索引时,扫描成本会非常高。 而索引的查找复杂度会低很多,所以数据量越大,索引价值越明显。
6)索引也不是万能的
虽然索引能加快查询,但不是所有场景都一定快。
例如:
- 查询条件区分度太低
- 返回结果集太大
- 索引失效
- 写入非常频繁
这时候索引收益可能就不明显,甚至会拖慢写性能。
7)一句话总结
索引之所以能加快查询,是因为它让数据库不必扫描整张表,而是通过高效的数据结构快速定位到目标数据。
29. 创建索引有哪些注意点?
答:
索引虽然能显著提升查询性能,但并不是建得越多越好。 创建索引要遵循“有收益、可维护、不过度”的原则。
面试里回答这个问题,建议从“哪些字段适合建索引、哪些字段不适合建索引、联合索引怎么设计”三个方向回答。
1)应该建在查询频繁的字段上
索引的主要作用是提高查询效率,所以应优先考虑那些经常出现在以下位置的字段:
WHEREJOIN ONORDER BYGROUP BY
例如:
SELECT * FROM orders WHERE user_id = 1001;如果这个查询非常频繁,那么 user_id 就适合建索引。
2)区分度高的字段更适合建索引
所谓区分度,就是字段值的离散程度。
例如:
- 身份证号:区分度很高
- 手机号:区分度很高
- 性别:区分度很低
- 是否删除:区分度很低
区分度高的字段建立索引后,可以快速缩小结果范围。 而区分度低的字段即使有索引,可能仍然要扫描大量记录,收益有限。
所以像下面这种字段,一般不建议单独建索引:
genderstatus(如果只有两三种值)is_deleted
3)频繁更新的字段要谨慎建索引
索引不仅在查询时用到,在插入、删除、更新时也要维护。
如果某个字段经常更新,而你又给它建了索引,就会带来额外维护成本,例如:
- 更新索引页
- 页分裂
- 磁盘 IO 增加
例如:
- 实时变化的状态字段
- 高频更新的计数类字段
这类字段建索引要非常谨慎。
4)索引数量不要过多
每个索引都要占空间,也要维护。 索引越多:
- 磁盘占用越大
- 插入/更新/删除越慢
- 优化器选择成本也更高
所以索引不是越多越好,而是应该“必要且合理”。
5)联合索引优于多个单列索引
如果经常有多条件组合查询,应该优先考虑联合索引。
例如经常执行:
SELECT * FROM user WHERE name = 'Tom' AND age = 20;那么建立:
(name, age)通常比单独建两个索引:
idx_nameidx_age
效果更好。
原因:
- 联合索引可以更高效定位数据
- 还能支持最左匹配
- 更可能用于排序/覆盖索引
6)联合索引字段顺序要合理
联合索引不是字段简单拼起来就行,字段顺序非常重要。
一般设计原则:
- 把查询过滤性更强、区分度更高的字段放前面
- 同时考虑最左匹配原则
- 还要结合排序、分组场景
例如经常:
WHERE city = ? AND name = ?那 (city, name) 往往比 (name, city) 更合适,前提是 city 更常作为查询入口。
7)长字段可以考虑前缀索引
如果字段很长,例如:
- 邮箱
- URL
- 长字符串标识
直接建完整索引会很占空间,这时可以考虑前缀索引。
例如:
CREATE INDEX idx_email ON user(email(10));这样能节省空间、提升索引维护效率。
但前缀索引也有局限:
- 不能很好支持覆盖索引
- 对排序分组能力有限
- 需要评估前缀长度是否有足够区分度
8)尽量选择稳定、短小的字段做主键
虽然这是“主键设计”问题,但和索引非常相关。
在 InnoDB 中,主键索引是聚簇索引,二级索引叶子节点都存主键值。 所以主键如果很长,会导致:
- 主键树变大
- 二级索引也变大
- 整体性能变差
因此一般建议主键:
- 短
- 稳定
- 不频繁修改
- 最好是自增主键或趋势递增主键
9)不要对所有字段一股脑建索引
有些人看到 SQL 慢,就给所有条件字段都建索引,这样很危险。 应该先分析:
- 哪个查询最重要
- 哪个索引收益最大
- 是否存在重复索引
- 是否已有联合索引覆盖
否则容易出现:
- 冗余索引
- 维护成本上升
- 反而拖慢写入
10)创建索引前要结合执行计划分析
索引不是拍脑袋建的,最好结合:
EXPLAIN来看当前 SQL 是否真的没走索引、走错索引,或者是否有更优的联合索引设计。
11)总结
创建索引时要注意:
- 建在查询频繁字段上
- 优先考虑高区分度字段
- 频繁更新字段慎建索引
- 索引数量不要过多
- 多条件查询优先联合索引
- 联合索引顺序要合理
- 长字段考虑前缀索引
- 主键尽量短而稳定
一句话总结:
索引要建在最能带来查询收益的地方,而不是所有字段都建。
30. 索引哪些情况下会失效?
答:
索引失效指的是:虽然字段上有索引,但 SQL 执行时 MySQL 没有使用这个索引,或者没有充分利用这个索引。
索引失效是 SQL 优化中非常高频的面试题。 常见原因主要有以下几类。
1)对索引列使用函数
如果在索引列上使用函数,MySQL 往往无法直接利用原始索引值。
例如:
SELECT * FROM user WHERE YEAR(create_time) = 2024;虽然 create_time 上有索引,但 YEAR(create_time) 是计算后的结果,不是索引原值,所以通常索引失效。
优化方式
改成范围查询:
SELECT * FROM userWHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';2)对索引列进行运算
例如:
SELECT * FROM user WHERE age + 1 = 20;这里对 age 做了计算,索引通常无法直接使用。
应改写成:
SELECT * FROM user WHERE age = 19;3)隐式类型转换
如果索引字段是字符串类型,但查询时没加引号,可能触发隐式类型转换,导致索引失效。
例如字段 phone 是 VARCHAR:
SELECT * FROM user WHERE phone = 13800138000;这里 MySQL 可能会把 phone 转成数字比较,索引失效。
正确写法:
SELECT * FROM user WHERE phone = '13800138000';4)LIKE 以 % 开头
例如:
SELECT * FROM user WHERE name LIKE '%Tom';或:
SELECT * FROM user WHERE name LIKE '%Tom%';这种前缀不确定的模糊查询,通常无法利用 B+Tree 索引。
但如果是:
SELECT * FROM user WHERE name LIKE 'Tom%';通常可以使用索引。
5)联合索引不满足最左匹配原则
例如有联合索引:
(a, b, c)如果查询条件是:
WHERE b = 2 AND c = 3因为没有从最左列 a 开始匹配,所以索引往往不能充分利用,甚至失效。
最左匹配原则是联合索引使用中的核心规则。
6)使用 OR 不当
例如:
SELECT * FROM user WHERE name = 'Tom' OR age = 20;如果 name 有索引,但 age 没有索引,MySQL 可能为了统一执行策略,直接选择全表扫描,导致索引失效。
解决思路包括:
- 两边都建索引
- 拆成
UNION ALL - 具体看执行计划
7)使用 !=、<>、NOT IN
这类条件有时会导致索引效果变差甚至失效。
例如:
SELECT * FROM user WHERE status != 1;因为“不等于”通常匹配范围太大,优化器可能认为走索引不划算,转而全表扫描。
8)使用 IS NULL / IS NOT NULL
这个要区分场景,不能一概而论。 有些情况下索引可以用,有些情况下优化器可能放弃索引。
例如:
- 如果
NULL值很多 - 或者扫描比例太高
那优化器可能会认为全表扫描更快。
所以面试里可以说:
IS NULL/IS NOT NULL在某些场景下可能导致索引无法被有效使用,需要结合执行计划判断。
9)优化器认为全表扫描更快
即使语法上“可以走索引”,优化器也可能主动不走。
例如:
- 表数据量很小
- 条件筛选性差
- 需要回表太多
- 索引区分度低
这时候 MySQL 可能直接选择 ALL 全表扫描。
所以“索引没用上”不一定是写法错,也可能是优化器的成本选择。
10)左连接 / 右连接字段类型不一致
例如 join 字段一个是 INT,另一个是 VARCHAR,会发生隐式转换,可能导致索引失效。
例如:
SELECT *FROM AJOIN B ON A.user_id = B.user_id;如果 A.user_id 是 int,B.user_id 是 varchar,就可能影响索引使用。
11)总结
常见索引失效场景包括:
- 索引列上使用函数
- 索引列上做运算
- 隐式类型转换
LIKE '%xxx'- 联合索引不满足最左匹配
OR使用不当!=/<>/NOT IN- 某些
IS NULL场景 - 优化器主动选择全表扫描
- join 字段类型不一致
一句话总结:
索引失效的本质,通常是 MySQL 无法按索引原有顺序定位数据,或者优化器判断走索引不划算。
31. 索引不适合哪些场景?
答:
虽然索引能提升查询性能,但并不是任何场景都适合建索引。 有些情况下建索引收益很低,甚至会适得其反。
1)数据量很小的表
如果一张表只有几十行、几百行,MySQL 全表扫描的成本其实非常低。 此时建索引的收益不明显,反而会增加额外维护成本。
例如:
- 配置表
- 枚举字典表
- 系统初始化小表
这类表一般没必要刻意加太多索引。
2)区分度很低的字段
例如:
- 性别:男 / 女
- 是否删除:0 / 1
- 状态:待支付 / 已支付 / 已取消
这些字段取值种类少,区分度低。 即使建了索引,查询时往往仍需要扫描大量记录,优化器可能直接放弃使用索引。
所以像这种字段,一般不适合单独建索引。
3)频繁更新的字段
索引不仅要支持查询,还要在写操作时维护。 如果某个字段经常被更新,而它又建立了索引,就会导致:
- 索引页频繁调整
- IO 增加
- 写性能下降
例如:
- 实时状态字段
- 热点计数类字段
- 高频变化的更新时间字段(需具体评估)
所以高频变更字段通常不适合轻易建索引。
4)频繁插入 / 删除的大写入场景
如果一张表以写为主,比如:
- 日志流水表
- 实时埋点表
- 高频消息记录表
而查询需求又不复杂,那么过多索引会明显影响写入性能。
因为每次插入一条记录,都要维护所有相关索引。
这类场景需要权衡:
- 查询性能
- 写入吞吐量
5)大字段 / 超长字段
例如:
- 超长 URL
- 大段文本
- 富文本内容
直接对这种字段建完整索引:
- 占空间大
- 维护成本高
- 效率未必高
通常更适合:
- 前缀索引
- 全文索引
- 搜索引擎
而不是简单建普通 B+Tree 索引。
6)很少用于查询条件的字段
如果某个字段几乎从不出现在:
WHEREJOINORDER BYGROUP BY
那给它建索引通常没什么意义。
比如纯展示型字段:
- 备注
- 描述信息
- 不参与筛选的文本字段
7)返回结果集很大的查询
即使字段有索引,如果查询结果会返回表中大部分数据,索引收益也可能很低。
例如:
SELECT * FROM orders WHERE status = 1;如果 status = 1 占了 80% 数据,走索引反而可能因为大量回表,比全表扫描还慢。
这种场景下,优化器也可能主动放弃索引。
8)没有明确查询需求支撑的“预防式建索引”
有些人喜欢“先把可能用到的字段都建索引”,这是很危险的。 没有真实业务查询支撑的索引,大多只是:
- 占空间
- 降低写性能
- 增加维护复杂度
索引最好基于真实 SQL 来设计,而不是拍脑袋预建。
9)总结
索引通常不适合以下场景:
- 小表
- 区分度低字段
- 高频更新字段
- 高频写入表中过多索引
- 超长字段直接建普通索引
- 不参与查询的字段
- 返回结果集非常大的查询
- 没有真实需求支撑的字段
一句话总结:
索引适合“高频查询、筛选性强”的字段,不适合“低区分、高频变更、低使用价值”的字段。
32. 索引是不是建得越多越好?
答:
不是。 索引绝对不是越多越好,而是要适量、精准、有效。
这个问题本质上考察你是否理解: 索引是一种“以空间换时间”的手段,但它本身也有成本。
1)索引会占用额外存储空间
索引本质上也是数据结构,也要存到磁盘上。 一个表索引越多:
- 磁盘占用越大
- 内存缓存压力越大
- 备份恢复成本也更高
尤其是大表、长字段索引、联合索引多的时候,这种空间成本非常明显。
2)索引会降低写入性能
每次执行:
INSERTUPDATEDELETE
不仅要改数据本身,还要维护相关索引。
例如插入一条记录时:
- 主键索引要插入
- 每个普通索引也要插入
- 还可能触发页分裂、页合并等操作
所以索引越多,写性能通常越差。
3)过多索引会增加优化器选择成本
MySQL 优化器在执行 SQL 时,需要从多个索引中选择最优方案。 索引太多时:
- 优化器评估成本会增加
- 可能选择出次优索引
- SQL 执行计划更复杂
虽然这个成本通常没有写入维护成本那么大,但在复杂查询中也会有影响。
4)容易产生冗余索引、重复索引
索引多了以后,很容易出现:
重复索引
例如同时存在:
idx_name- 又建了一个
idx_name2
冗余索引
例如已有联合索引:
(name, age)又单独建了:
(name)因为联合索引已经能支持最左匹配 (name),后者往往是冗余的。
冗余索引不仅浪费空间,还增加维护成本。
5)索引多不代表查询一定更快
如果索引设计不合理,例如:
- 区分度低
- 联合索引顺序不对
- 查询根本用不到
- 索引经常失效
那这些索引不仅没帮助,还拖累整体性能。
所以关键不是“索引数量”,而是:
- 是否真正命中业务查询
- 是否设计合理
6)什么样的索引才是好索引?
好的索引应该满足:
- 有明确查询场景支撑
- 能显著减少扫描行数
- 能帮助排序 / 分组 / join
- 写入维护成本可接受
- 没有重复和冗余
也就是说,索引要追求的是“高质量”,不是“多数量”。
7)正确的索引观念
正确做法通常是:
- 先看真实慢 SQL
- 再分析执行计划
- 然后设计最有价值的索引
- 最后持续清理无用索引
而不是先把一堆字段都建上索引。
8)总结
索引不是越多越好,因为:
- 会占用额外磁盘空间
- 会降低插入、更新、删除性能
- 会增加优化器选择成本
- 会产生重复/冗余索引
- 不合理索引甚至没有收益
一句话总结:
索引要“少而精”,只保留真正能提升核心查询性能的索引。
33. MySQL 索引用的什么数据结构?了解吗?
答:
MySQL 中最常用、最核心的索引数据结构是 B+Tree。 尤其是在 InnoDB 存储引擎中,主键索引和普通索引默认都基于 B+Tree 实现。
不过要注意,MySQL 不只有一种索引结构,不同引擎或场景下还可能涉及:
- B+Tree
- Hash
- Fulltext(全文索引底层实现不同)
- R-Tree(空间索引)
但面试里最核心的答案一定是:InnoDB 默认使用 B+Tree。
1)什么是 B+Tree?
B+Tree 是一种多路平衡搜索树,是数据库和文件系统里非常常见的数据结构。 它相比普通二叉树、平衡树,更适合磁盘存储场景。
它的核心特点有:
- 非叶子节点只存键值,不存真实数据
- 所有真实数据都存放在叶子节点
- 叶子节点之间通过链表连接
- 树的高度很低,适合减少磁盘 IO
2)为什么数据库索引适合 B+Tree?
数据库索引的目标是:
- 快速查找单条数据
- 支持范围查询
- 支持排序
- 尽量减少磁盘 IO
B+Tree 正好能满足这些要求。
原因一:树高低
一个节点能存很多 key,所以整棵树高度很低。 通常几层高度就能支持千万级数据查找。
原因二:磁盘 IO 少
数据库每次从磁盘读数据,通常按“页”读取。 B+Tree 一个节点通常对应一个页,一次 IO 能加载很多 key。 树越矮,查找时访问页数越少。
原因三:范围查询强
叶子节点之间是有序链表,所以做:
BETWEEN><ORDER BYGROUP BY时非常高效。
3)InnoDB 中索引具体怎么存?
主键索引
InnoDB 的主键索引是聚簇索引。 叶子节点存的是整行数据。
二级索引
普通索引、唯一索引、联合索引等,通常是二级索引。 叶子节点存的是:
- 索引列值
- 对应主键值
所以通过二级索引查整行数据时,往往需要回表。
4)除了 B+Tree 还有哪些索引结构?
Hash 索引
哈希索引特点:
- 等值查询快
- 不支持范围查询
- 不支持排序
- 不支持最左匹配
MySQL 的 MEMORY 引擎支持哈希索引。 InnoDB 也有自适应哈希索引,但不是用户手工创建的普通哈希索引。
Fulltext
全文索引底层实现与 B+Tree 不同,用于全文检索。
5)总结
MySQL 中最核心的索引结构是:
B+Tree
尤其是 InnoDB 中:
- 主键索引用 B+Tree
- 二级索引用 B+Tree
一句话总结:
MySQL 默认索引之所以使用 B+Tree,是因为它同时适合等值查询、范围查询和磁盘 IO 优化。
34. 那一棵 B+ 树能存储多少条数据呢?
答:
这个问题本质上是在考察你对 B+Tree “为什么查得快”的理解。 结论先说:
一棵高度只有 3 层左右的 B+Tree,通常就足以支撑千万级甚至上亿级数据。
也就是说,MySQL 之所以查得快,很重要的原因就是: B+Tree 很矮,查找一次数据通常只需要几次磁盘 IO。
1)为什么能存很多数据?
因为 B+Tree 的非叶子节点只存:
- 键值
- 指针
不存整行数据,所以一个节点里能放很多 key。 而每个 key 又能指向一个子节点,于是整棵树的“分叉数”非常高。
树分叉越多,高度增长越慢,能容纳的数据量就越大。
2)一个简单估算
假设:
- InnoDB 页大小默认是 16KB
- 主键类型是
BIGINT,占 8 字节 - 指针占 6 字节
- 一个索引项大概占
14 字节
那么一个非叶子节点大概能存:
16384 / 14 ≈ 1170也就是说,一个非叶子节点大约能有 1170 个分支。
3)树高为 2 层时能存多少?
如果一棵 B+Tree 高度为 2:
- 根节点有约 1170 个子节点
- 每个子节点又指向一个叶子页
假设每个叶子页能放若干行数据,那么已经能存储非常多记录。
4)树高为 3 层时能存多少?
继续估算:
- 第一层:1 个根节点
- 第二层:1170 个中间节点
- 第三层:1170 × 1170 个叶子页
如果每个叶子页能存约 1000 行记录(这里取决于行大小,实际不固定),那么总量会非常大。
粗略估算可以到千万级甚至更高。 这也是为什么很多资料会说:
InnoDB 的 B+Tree 高度一般在 2 ~ 4 层,就足够支持海量数据。
5)为什么树高度低这么重要?
因为查找数据时,每访问一层节点,通常就意味着一次磁盘页读取。 如果树高度只有 3,那么最坏情况下只需要 3 次左右页访问就能定位数据。
相比全表扫描,这个效率高很多。
也就是说:
- 数据量 1000 万
- 查一条记录
- 可能只需要 3 次左右 IO
这就是 B+Tree 适合数据库索引的核心原因之一。
6)实际中受什么影响?
一棵 B+Tree 能存多少数据,不是固定值,会受很多因素影响:
- 页大小(InnoDB 默认 16KB)
- 索引字段大小
- 主键大小
- 叶子节点存的是整行还是主键值
- 数据行大小
- 是否有页分裂、碎片等
所以面试里不用死背某个精确数字,更重要的是讲清楚原理:
- 分叉数大
- 树很矮
- 所以能存很多数据
7)总结
一棵 B+Tree 能存很多数据,原因是:
- 一个节点能存很多键值
- 分支数非常高
- 树高度很低
- 查找只需很少几次 IO
一句话总结:
B+Tree 之所以适合数据库索引,是因为它用很低的树高支撑了海量数据存储和高效查询。
35. 为什么要用 B+Tree,而不用普通二叉树?
答:
数据库索引最关心的是:磁盘 IO 次数。 而普通二叉树并不适合磁盘场景,所以数据库不会用它来做主流索引结构。
1)普通二叉树可能退化成链表
普通二叉查找树如果插入顺序不合理,例如递增插入:
1 -> 2 -> 3 -> 4 -> 5树可能会退化成一条链表。
这样查找复杂度会从理想的 O(logN) 退化成 O(N),性能非常差。
这显然不适合作为数据库索引。
2)即使平衡二叉树也不适合数据库
有人会继续问: 那为什么不用平衡二叉树(AVL、红黑树)?
原因是: 二叉树每个节点只能有两个子节点,分叉太少,树会很高。
树一高,意味着查找一条数据要访问更多节点。 而数据库节点通常对应磁盘页,一层就可能对应一次磁盘 IO。
如果树高很高,IO 次数就会明显增加,性能差很多。
3)B+Tree 的分叉数远大于二叉树
B+Tree 是多路平衡树,一个节点可以有很多个子节点,而不是只有 2 个。
这意味着:
- 同样的数据量
- B+Tree 高度远低于二叉树
树越矮:
- 访问节点越少
- 磁盘 IO 越少
- 查询越快
这才是数据库最看重的点。
4)B+Tree 更适合页存储
数据库每次从磁盘读取数据,不是一条一条读,而是按页读。 InnoDB 默认页大小 16KB。
B+Tree 的每个节点可以设计成刚好适配一个页,里面装很多键值和指针。 这样一次磁盘 IO,就能把大量索引信息加载到内存。
而普通二叉树每个节点存的内容很少,会浪费大量页空间,不适合磁盘页模型。
5)B+Tree 更适合范围查询
普通二叉树主要适合单点查询。 而数据库常见操作还包括:
BETWEEN><ORDER BYGROUP BY
B+Tree 的叶子节点之间有链表连接,天然适合范围扫描。 普通二叉树要做范围查询则不够高效。
6)总结
不用普通二叉树,主要原因有:
- 普通二叉树可能退化
- 平衡二叉树分支太少,树太高
- 磁盘 IO 次数太多
- 不适合数据库页存储
- 范围查询不如 B+Tree 高效
一句话总结:
数据库不用普通二叉树做索引,是因为它树太高、IO 太多,不适合磁盘场景。
36. 为什么用 B+Tree,而不用 B 树呢?
答:
B 树和 B+Tree 都是多路平衡树,二者都比普通二叉树更适合做数据库索引。 但 MySQL 最终选择 B+Tree,原因是 B+Tree 更适合数据库的查询模式和磁盘访问特性。
1)B 树和 B+Tree 的主要区别
B 树
- 非叶子节点也存数据
- 查找数据可能在中间节点就结束
- 每个节点存储的数据较多,能容纳的索引项相对更少
B+Tree
- 非叶子节点只存键值和指针
- 所有真实数据都在叶子节点
- 叶子节点之间通过链表连接
2)B+Tree 单节点能存更多 key
因为 B+Tree 的非叶子节点不存整行数据,只存:
- key
- 指针
所以同样一个页大小下,B+Tree 的一个节点能放更多索引项。 这意味着:
- 分叉更多
- 树更矮
- 查找时 IO 更少
而 B 树因为非叶子节点还要存数据,所以单节点能放的 key 更少,树会更高一些。
3)B+Tree 查询更稳定
B 树中:
- 有些数据可能在中间节点找到
- 有些数据要走到叶子节点
所以不同 key 查询时,访问路径长度不一定一样。
而 B+Tree 中:
- 所有数据都在叶子节点
- 每次查询基本都走到叶子节点
这样查询路径更稳定,性能更可预测。
4)B+Tree 更适合范围查询
这是非常关键的一点。
B+Tree 的叶子节点之间有链表连接,所以做范围查询时,只要先定位起点,然后顺着链表往后扫就行。
例如:
WHERE id BETWEEN 100 AND 200或者:
ORDER BY id这种操作 B+Tree 非常高效。
而 B 树由于数据分布在各层节点,范围查询时不如 B+Tree 顺滑高效。
5)B+Tree 更适合排序和扫描
数据库中除了单点查询,还有很多:
- 全表扫描
- 范围扫描
- 排序
- 分组
B+Tree 的叶子节点天然有序并通过链表串起来,非常适合这类场景。 而 B 树则没有这种优势。
6)总结
MySQL 选择 B+Tree 而不用 B 树,主要因为:
- 非叶子节点不存数据,单节点能容纳更多 key
- 树更矮,磁盘 IO 更少
- 查询路径更稳定
- 范围查询更高效
- 排序、扫描能力更强
一句话总结:
B+Tree 比 B 树更适合数据库,因为它更节省 IO,更擅长范围查询和顺序扫描。
37. Hash 索引和 B+Tree 索引区别是什么?
答:
Hash 索引和 B+Tree 索引是两种完全不同的索引结构。 它们最核心的区别在于:
- Hash 索引适合等值查询
- B+Tree 索引既适合等值查询,也适合范围查询、排序、分组
1)底层原理不同
Hash 索引
把索引值通过哈希函数计算成哈希值,再定位到对应桶位置。 它本质上是“值 -> 哈希地址”的映射。
B+Tree 索引
把索引值按有序方式组织成多路平衡树结构。
所以:
- Hash 是无序结构
- B+Tree 是有序结构
2)等值查询性能
Hash 索引
在理想情况下,等值查询非常快,时间复杂度接近 O(1)。
例如:
WHERE id = 100这类精确匹配很适合 Hash。
B+Tree 索引
等值查询通常是 O(logN),理论上比 Hash 略慢,但在数据库场景下依然非常高效。
3)范围查询能力
这是两者最大的区别之一。
Hash 索引
不支持范围查询
例如:
WHERE id > 100WHERE id BETWEEN 100 AND 200Hash 索引无法利用“有序性”,所以基本帮不上忙。
B+Tree 索引
非常适合范围查询,因为索引天然有序,叶子节点还能顺序遍历。
4)排序能力
Hash 索引
不支持排序优化。
例如:
ORDER BY idHash 索引无法利用。
B+Tree 索引
支持按索引顺序扫描,所以能用于:
ORDER BYGROUP BY
5)联合索引能力
Hash 索引
不支持最左匹配原则。
B+Tree 索引
支持联合索引和最左前缀匹配原则。
例如索引 (a,b,c) 可以支持:
aa,ba,b,c
6)模糊查询能力
Hash 索引
不支持 LIKE 'abc%' 这类前缀查询。
B+Tree 索引
支持有前缀的模糊查询,例如:
WHERE name LIKE 'Tom%'但不支持 %Tom 这种前导模糊高效查找。
7)MySQL 中谁更常用?
在 MySQL 中,最常用的是 B+Tree 索引。
- InnoDB 默认索引是 B+Tree
- MyISAM 默认索引也是 B+Tree
Hash 索引常见于:
- MEMORY 引擎
- InnoDB 自适应哈希索引(自动,不是手工普通索引)
所以实际开发中,大多数时候你面对的都是 B+Tree 索引。
8)总结
Hash 索引和 B+Tree 索引主要区别:
| 对比项 | Hash | B+Tree |
|---|---|---|
| 等值查询 | 快 | 快 |
| 范围查询 | 不支持 | 支持 |
| 排序 | 不支持 | 支持 |
| 联合索引 | 不支持最左匹配 | 支持 |
| 模糊查询 | 不支持 | 支持前缀匹配 |
| 有序性 | 无序 | 有序 |
一句话总结:
Hash 索引擅长精确匹配,B+Tree 索引更全面,更适合数据库的综合查询场景。
38. 聚簇索引与非聚簇索引的区别?
答:
聚簇索引和非聚簇索引的核心区别在于:
数据和索引是不是存放在一起。
这个问题在 MySQL 里通常结合 InnoDB 来回答,因为 InnoDB 是典型的聚簇索引实现。
1)什么是聚簇索引?
聚簇索引不是一种“新索引类型”,而是一种数据存储方式。
在聚簇索引中:
- 索引和数据存储在同一棵 B+Tree 中
- 叶子节点直接存整行数据
在 InnoDB 中:
- 主键索引就是聚簇索引
也就是说,表数据本身就是按主键索引这棵树组织的。
2)什么是非聚簇索引?
非聚簇索引中:
- 索引结构和数据行分开存储
- 叶子节点不直接存整行数据
在 InnoDB 中,普通索引、唯一索引、联合索引通常都是二级索引。 它们的叶子节点存的是:
- 索引列值
- 主键值
然后再通过主键去主键索引查整行,这就产生了“回表”。
在 MyISAM 中,索引叶子节点通常存的是数据文件地址,也属于非聚簇索引思路。
3)两者最大区别
聚簇索引
- 数据和索引在一起
- 叶子节点就是数据
- 一张表只能有一个聚簇索引
非聚簇索引
- 数据和索引不在一起
- 叶子节点不是整行数据
- 一张表可以有多个非聚簇索引
4)为什么一张表只能有一个聚簇索引?
因为“聚簇”本质上是数据的物理组织方式。 数据行在磁盘上的排列方式只能有一种,所以一张表只能按一种键值顺序来组织数据。
在 InnoDB 中,这个顺序通常就是主键顺序。
5)聚簇索引的优点
(1)主键查询非常快
因为直接在主键索引树的叶子节点就能拿到整行数据。
(2)范围查询性能好
按主键范围查询时,叶子节点天然有序。
(3)减少一次额外查找
不像二级索引那样还要回表。
6)聚簇索引的缺点
(1)主键太大会导致所有二级索引都变大
因为 InnoDB 二级索引叶子节点存的是主键值。
(2)主键频繁修改代价高
因为数据本身是按主键组织的,改主键会引发大量移动。
(3)无序主键容易造成页分裂
比如 UUID 做主键,插入分布随机,会带来页分裂和碎片问题。
7)非聚簇索引的特点
优点:
- 可以建多个
- 灵活支持不同查询条件
缺点:
- 查询整行时可能要回表
- 多一次索引树查找
8)总结
聚簇索引和非聚簇索引的区别可以总结为:
| 对比项 | 聚簇索引 | 非聚簇索引 |
|---|---|---|
| 数据是否和索引一起存 | 是 | 否 |
| 叶子节点内容 | 整行数据 | 主键值或地址 |
| 数量 | 一张表只有一个 | 一张表可以多个 |
| 查询整行效率 | 高 | 可能要回表 |
一句话总结:
聚簇索引是“索引即数据”,非聚簇索引是“索引指向数据”。
39. 回表了解吗?
答:
回表是 InnoDB 中一个非常重要的概念,指的是:
通过二级索引查到主键值后,再去主键索引中把整行数据查出来的过程。
简单说就是: 先查辅助索引,再回主键索引树查数据。
1)为什么会发生回表?
因为在 InnoDB 中:
- 主键索引(聚簇索引)叶子节点存的是整行数据
- 二级索引叶子节点存的不是整行数据,而是主键值
所以,如果你使用的是普通索引查询,而查询结果又需要整行数据,那么只靠二级索引是不够的。
这时 MySQL 会:
- 先从二级索引找到对应主键
- 再根据主键去聚簇索引中查完整记录
这第二步就叫回表。
2)举个例子
假设有表:
CREATE TABLE user ( id BIGINT PRIMARY KEY, name VARCHAR(50), age INT, INDEX idx_name(name));执行:
SELECT * FROM user WHERE name = 'Tom';过程大致是:
- 先走
idx_name - 在
idx_name中找到name='Tom'对应的主键 id,例如id=100 - 再根据
id=100去主键索引里查整行数据
因为你查的是 *,需要整行,所以一定要回主键索引去拿完整记录。
3)什么时候会回表?
通常在以下场景会回表:
- 使用的是二级索引
- 查询字段不完全包含在该索引中
- 需要拿整行数据或额外字段
例如:
SELECT age FROM user WHERE name = 'Tom';如果索引只有 (name),而 age 不在索引里,也要回表。
4)什么时候不会回表?
如果查询所需字段全部都包含在索引中,那么就不需要回表。 这种情况叫做:覆盖索引。
例如有联合索引:
(name, age)执行:
SELECT name, age FROM user WHERE name = 'Tom';因为需要的字段都在索引中,可以直接从索引返回结果,不必回表。
5)为什么回表会影响性能?
因为回表意味着多一次索引树查找。
如果查询结果很多,比如:
SELECT * FROM user WHERE city = '深圳';返回了几万条数据,那么每条记录都可能要回表一次,代价很高。
这会带来:
- 更多随机 IO
- 更多页访问
- 更高 CPU / Buffer Pool 压力
所以在大结果集场景下,回表成本会非常明显。
6)怎么减少回表?
常见优化手段有:
(1)使用覆盖索引
让查询字段都包含在索引中。
(2)优化查询字段
不要 select *,只查需要的字段。
(3)设计合理联合索引
让 where 条件和 select 字段尽量落在同一个索引里。
7)总结
回表就是:
通过二级索引查到主键后,再根据主键去聚簇索引中取完整数据。
一句话总结:
回表的本质,是“普通索引不存整行,只能先找主键再找数据”。
40. 覆盖索引了解吗?
答:
覆盖索引是 SQL 优化中非常重要的概念。 它指的是:
查询所需要的字段,全部都能从索引中直接获取,而不需要再回表。
也就是说,查询结果被“索引本身覆盖”了,所以叫覆盖索引。
1)为什么覆盖索引重要?
因为在 InnoDB 中,普通二级索引叶子节点通常只存:
- 索引列值
- 主键值
如果查询字段不在索引里,就要回表。 而回表会增加额外 IO 和索引树查找成本。
如果使用覆盖索引,就能省掉这一步,查询效率通常会明显更高。
2)举个例子
假设表结构如下:
CREATE TABLE user ( id BIGINT PRIMARY KEY, name VARCHAR(50), age INT, city VARCHAR(50), INDEX idx_city_name(city, name));执行 SQL:
SELECT name FROM user WHERE city = '深圳';因为联合索引 (city, name) 中已经包含了:
city(查询条件)name(返回字段)
所以这条 SQL 可以直接在索引里完成,不需要回表。
这就是覆盖索引。
3)再看一个不能覆盖的例子
SELECT age FROM user WHERE city = '深圳';这里虽然 city 在索引里,但 age 不在索引里。
所以执行时会:
- 用
idx_city_name找到符合条件的记录 - 取到主键值
- 回表去主键索引里查
age
这就不是覆盖索引。
4)覆盖索引的好处
(1)减少回表
最直接的收益就是避免回表。
(2)减少随机 IO
回表本质上通常是额外的随机访问,覆盖索引能减少这种开销。
(3)提高查询效率
特别是结果集较大时,覆盖索引优势很明显。
5)什么场景适合设计覆盖索引?
比较适合的场景:
- 高频查询
- 返回字段较少
- 条件字段和查询字段相对固定
- 读性能要求高
例如:
- 列表页查询
- 后台管理分页字段
- 高频统计查询
6)是不是覆盖索引一定最好?
也不是。 为了覆盖索引,有时候会把很多字段都塞进联合索引,这会导致:
- 索引过大
- 维护成本上升
- 写入变慢
所以设计覆盖索引要平衡:
- 查询收益
- 索引大小
- 写入成本
7)怎么判断是否使用了覆盖索引?
可以通过 EXPLAIN 看 Extra 字段。
如果看到:
Using index通常说明用了覆盖索引。
注意:
Using index一般是好信号- 它表示查询字段都在索引中
8)总结
覆盖索引就是:
查询需要的所有字段都在索引里,可以直接从索引返回结果,不需要回表。
一句话总结:
覆盖索引的价值在于“少一次回表,多一份性能提升”。
41. 什么是最左前缀原则 / 最左匹配原则?
答:
最左前缀原则(也叫最左匹配原则)是联合索引使用时最核心的规则之一。
它的意思是:
联合索引在使用时,必须从最左边的列开始连续匹配,才能有效利用索引。
1)什么是联合索引?
比如建立一个联合索引:
CREATE INDEX idx_abc ON user(a, b, c);这个索引不是简单地给 a、b、c 各建一个独立索引,
而是按 (a, b, c) 的组合顺序构建一棵 B+Tree。
可以理解为,它相当于支持以下前缀形式:
(a)(a, b)(a, b, c)
但不天然支持:
(b)(c)(b, c)
2)为什么叫“最左”?
因为联合索引是从左到右排序构建的。
在 (a, b, c) 这个索引里:
- 先按
a排序 a相同再按b排序a、b都相同再按c排序
所以如果查询时连最左边的 a 都没有,数据库就无法确定从哪段索引开始查。
3)举几个例子
假设有索引:
(a, b, c)可以有效使用索引的情况
WHERE a = 1WHERE a = 1 AND b = 2WHERE a = 1 AND b = 2 AND c = 3WHERE a = 1 AND b > 2不能充分使用索引的情况
WHERE b = 2WHERE c = 3WHERE b = 2 AND c = 3因为都没有从 a 开始匹配。
4)遇到范围查询会怎样?
例如:
WHERE a = 1 AND b > 2 AND c = 3索引通常可以用到:
ab
但 b > 2 是范围条件,后面的 c 往往就不能继续用于索引定位了(具体还要看执行计划和 ICP 等优化)。
所以最左匹配还有一个延伸点:
联合索引遇到范围查询后,后续列通常无法继续用于索引查找。
5)为什么联合索引不是任意顺序都行?
因为 B+Tree 中索引键是按固定顺序组织的。
例如 (name, age) 索引:
- 先按
name排序 - 同名时再按
age
所以如果你只查 age = 20,数据库不知道应该去树的哪一部分开始找,因为树第一层比较维度是 name。
6)最左前缀原则有什么价值?
它指导我们怎么设计联合索引:
- (1)高频查询条件放前面
- (2)最常作为过滤入口的字段放前面
- (3)排序字段、分组字段顺序也要一起考虑
否则虽然建了联合索引,但查询可能用不好。
7)总结
最左前缀原则的核心是:
联合索引必须从最左边开始连续匹配,才能高效利用索引。
一句话总结:
联合索引不是“有这些字段就行”,而是“字段顺序非常重要”。
42. 什么是索引下推优化(ICP)?
答:
索引下推优化,全称是:
Index Condition Pushdown,简称 ICP
这是 MySQL 5.6 引入的一项查询优化能力。 它的核心思想是:
把原本需要在 Server 层判断的部分条件,下推到存储引擎层提前过滤,从而减少回表次数。
1)先理解没有 ICP 时会怎样
假设有一张表,联合索引如下:
(name, age)SQL:
SELECT * FROM user WHERE name LIKE '张%' AND age = 10;在没有 ICP 时,执行大致可能是这样:
- 存储引擎根据
name LIKE '张%'找到一批索引记录 - 根据这些索引记录逐个回表,取出整行数据
- 回到 Server 层再判断
age = 10是否成立
问题在于:
- 很多记录其实
age != 10 - 这些记录本不该回表
- 但没有 ICP 时,它们还是被回表了
这就造成了不必要的 IO。
2)有了 ICP 之后会怎样?
有了索引下推后,执行过程优化为:
- 存储引擎先根据
name LIKE '张%'找到候选索引记录 - 在索引层就继续判断
age = 10 - 只有同时满足条件的记录,才回表取整行数据
也就是说:
- 过滤条件更早执行了
- 无效记录更早被排除
- 回表次数减少了
3)为什么叫“下推”?
因为原本在 MySQL Server 层完成的条件判断,被“推”到了更底层的存储引擎去做。
所以叫:
- 条件下推
- 索引条件下推
4)ICP 适用于什么场景?
通常出现在:
- 使用二级索引
where条件里包含索引列- 但不是所有条件都能完全用于索引定位
- 又需要回表时
ICP 的作用就是: 尽量在回表之前,多过滤一批无效记录。
5)ICP 的好处
(1)减少回表次数
这是最直接的收益。
(2)减少存储引擎返回给 Server 层的数据量
降低 Server 层判断压力。
(3)提升查询效率
尤其在:
- 数据量大
- 候选记录很多
- 回表代价高
的场景下效果明显。
6)如何判断是否用了 ICP?
可以通过 EXPLAIN 的 Extra 字段查看。
如果出现:
Using index condition通常就表示用了索引下推。
注意:
Using index condition≠ 覆盖索引- 它只是表示 where 条件的一部分下推到了存储引擎层
7)ICP 和覆盖索引的区别
很多人会混淆这两个概念。
覆盖索引
查询字段全部在索引中,不用回表
ICP
依然可能要回表,只是回表前先多过滤一些无效记录
所以:
- 覆盖索引是“完全不回表”
- ICP 是“少回一些表”
8)总结
索引下推优化(ICP)的核心是:
把部分 where 条件下推到存储引擎层,在回表前先过滤数据,从而减少回表次数。
一句话总结:
ICP 不是让你不回表,而是让你“少回无意义的表”。