12637 字
63 分钟
100 道常见 MySQL 数据库题目索引篇
2026-04-16 15:27:11

27. 能简单说一下索引的分类吗?#

答:

MySQL 中索引的分类方式有很多,可以从不同维度来划分。 面试里通常从以下几个角度回答就比较完整:

  1. 按功能划分
  2. 按字段个数划分
  3. 按数据结构划分
  4. 按存储方式划分

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)没有索引时会发生什么?#

假设有一张用户表:

idnameage
1Tom20
2Jack21
3Lucy22

执行 SQL:

SELECT * FROM user WHERE name = 'Lucy';

如果 name 没有索引,MySQL 往往只能这样做:

  1. 先读第一行,看是不是 Lucy
  2. 不是,再读第二行
  3. 再读第三行
  4. 一直扫描到找到为止,或者扫完整张表

这就是全表扫描

如果表很大,比如几百万、几千万行,性能会非常差。


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)应该建在查询频繁的字段上#

索引的主要作用是提高查询效率,所以应优先考虑那些经常出现在以下位置的字段:

  • WHERE
  • JOIN ON
  • ORDER BY
  • GROUP BY

例如:

SELECT * FROM orders WHERE user_id = 1001;

如果这个查询非常频繁,那么 user_id 就适合建索引。


2)区分度高的字段更适合建索引#

所谓区分度,就是字段值的离散程度。

例如:

  • 身份证号:区分度很高
  • 手机号:区分度很高
  • 性别:区分度很低
  • 是否删除:区分度很低

区分度高的字段建立索引后,可以快速缩小结果范围。 而区分度低的字段即使有索引,可能仍然要扫描大量记录,收益有限。

所以像下面这种字段,一般不建议单独建索引:

  • gender
  • status(如果只有两三种值)
  • is_deleted

3)频繁更新的字段要谨慎建索引#

索引不仅在查询时用到,在插入、删除、更新时也要维护。

如果某个字段经常更新,而你又给它建了索引,就会带来额外维护成本,例如:

  • 更新索引页
  • 页分裂
  • 磁盘 IO 增加

例如:

  • 实时变化的状态字段
  • 高频更新的计数类字段

这类字段建索引要非常谨慎。


4)索引数量不要过多#

每个索引都要占空间,也要维护。 索引越多:

  • 磁盘占用越大
  • 插入/更新/删除越慢
  • 优化器选择成本也更高

所以索引不是越多越好,而是应该“必要且合理”。


5)联合索引优于多个单列索引#

如果经常有多条件组合查询,应该优先考虑联合索引

例如经常执行:

SELECT * FROM user WHERE name = 'Tom' AND age = 20;

那么建立:

(name, age)

通常比单独建两个索引:

  • idx_name
  • idx_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)总结#

创建索引时要注意:

  1. 建在查询频繁字段上
  2. 优先考虑高区分度字段
  3. 频繁更新字段慎建索引
  4. 索引数量不要过多
  5. 多条件查询优先联合索引
  6. 联合索引顺序要合理
  7. 长字段考虑前缀索引
  8. 主键尽量短而稳定

一句话总结:

索引要建在最能带来查询收益的地方,而不是所有字段都建。


30. 索引哪些情况下会失效?#

答:

索引失效指的是:虽然字段上有索引,但 SQL 执行时 MySQL 没有使用这个索引,或者没有充分利用这个索引。

索引失效是 SQL 优化中非常高频的面试题。 常见原因主要有以下几类。


1)对索引列使用函数#

如果在索引列上使用函数,MySQL 往往无法直接利用原始索引值。

例如:

SELECT * FROM user WHERE YEAR(create_time) = 2024;

虽然 create_time 上有索引,但 YEAR(create_time) 是计算后的结果,不是索引原值,所以通常索引失效。

优化方式

改成范围查询:

SELECT * FROM user
WHERE 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)隐式类型转换#

如果索引字段是字符串类型,但查询时没加引号,可能触发隐式类型转换,导致索引失效。

例如字段 phoneVARCHAR

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 A
JOIN B ON A.user_id = B.user_id;

如果 A.user_id 是 int,B.user_id 是 varchar,就可能影响索引使用。


11)总结#

常见索引失效场景包括:

  1. 索引列上使用函数
  2. 索引列上做运算
  3. 隐式类型转换
  4. LIKE '%xxx'
  5. 联合索引不满足最左匹配
  6. OR 使用不当
  7. != / <> / NOT IN
  8. 某些 IS NULL 场景
  9. 优化器主动选择全表扫描
  10. join 字段类型不一致

一句话总结:

索引失效的本质,通常是 MySQL 无法按索引原有顺序定位数据,或者优化器判断走索引不划算。


31. 索引不适合哪些场景?#

答:

虽然索引能提升查询性能,但并不是任何场景都适合建索引。 有些情况下建索引收益很低,甚至会适得其反。


1)数据量很小的表#

如果一张表只有几十行、几百行,MySQL 全表扫描的成本其实非常低。 此时建索引的收益不明显,反而会增加额外维护成本。

例如:

  • 配置表
  • 枚举字典表
  • 系统初始化小表

这类表一般没必要刻意加太多索引。


2)区分度很低的字段#

例如:

  • 性别:男 / 女
  • 是否删除:0 / 1
  • 状态:待支付 / 已支付 / 已取消

这些字段取值种类少,区分度低。 即使建了索引,查询时往往仍需要扫描大量记录,优化器可能直接放弃使用索引。

所以像这种字段,一般不适合单独建索引。


3)频繁更新的字段#

索引不仅要支持查询,还要在写操作时维护。 如果某个字段经常被更新,而它又建立了索引,就会导致:

  • 索引页频繁调整
  • IO 增加
  • 写性能下降

例如:

  • 实时状态字段
  • 热点计数类字段
  • 高频变化的更新时间字段(需具体评估)

所以高频变更字段通常不适合轻易建索引。


4)频繁插入 / 删除的大写入场景#

如果一张表以写为主,比如:

  • 日志流水表
  • 实时埋点表
  • 高频消息记录表

而查询需求又不复杂,那么过多索引会明显影响写入性能。

因为每次插入一条记录,都要维护所有相关索引。

这类场景需要权衡:

  • 查询性能
  • 写入吞吐量

5)大字段 / 超长字段#

例如:

  • 超长 URL
  • 大段文本
  • 富文本内容

直接对这种字段建完整索引:

  • 占空间大
  • 维护成本高
  • 效率未必高

通常更适合:

  • 前缀索引
  • 全文索引
  • 搜索引擎

而不是简单建普通 B+Tree 索引。


6)很少用于查询条件的字段#

如果某个字段几乎从不出现在:

  • WHERE
  • JOIN
  • ORDER BY
  • GROUP BY

那给它建索引通常没什么意义。

比如纯展示型字段:

  • 备注
  • 描述信息
  • 不参与筛选的文本字段

7)返回结果集很大的查询#

即使字段有索引,如果查询结果会返回表中大部分数据,索引收益也可能很低。

例如:

SELECT * FROM orders WHERE status = 1;

如果 status = 1 占了 80% 数据,走索引反而可能因为大量回表,比全表扫描还慢。

这种场景下,优化器也可能主动放弃索引。


8)没有明确查询需求支撑的“预防式建索引”#

有些人喜欢“先把可能用到的字段都建索引”,这是很危险的。 没有真实业务查询支撑的索引,大多只是:

  • 占空间
  • 降低写性能
  • 增加维护复杂度

索引最好基于真实 SQL 来设计,而不是拍脑袋预建。


9)总结#

索引通常不适合以下场景:

  1. 小表
  2. 区分度低字段
  3. 高频更新字段
  4. 高频写入表中过多索引
  5. 超长字段直接建普通索引
  6. 不参与查询的字段
  7. 返回结果集非常大的查询
  8. 没有真实需求支撑的字段

一句话总结:

索引适合“高频查询、筛选性强”的字段,不适合“低区分、高频变更、低使用价值”的字段。


32. 索引是不是建得越多越好?#

答:

不是。 索引绝对不是越多越好,而是要适量、精准、有效

这个问题本质上考察你是否理解: 索引是一种“以空间换时间”的手段,但它本身也有成本。


1)索引会占用额外存储空间#

索引本质上也是数据结构,也要存到磁盘上。 一个表索引越多:

  • 磁盘占用越大
  • 内存缓存压力越大
  • 备份恢复成本也更高

尤其是大表、长字段索引、联合索引多的时候,这种空间成本非常明显。


2)索引会降低写入性能#

每次执行:

  • INSERT
  • UPDATE
  • DELETE

不仅要改数据本身,还要维护相关索引。

例如插入一条记录时:

  • 主键索引要插入
  • 每个普通索引也要插入
  • 还可能触发页分裂、页合并等操作

所以索引越多,写性能通常越差。


3)过多索引会增加优化器选择成本#

MySQL 优化器在执行 SQL 时,需要从多个索引中选择最优方案。 索引太多时:

  • 优化器评估成本会增加
  • 可能选择出次优索引
  • SQL 执行计划更复杂

虽然这个成本通常没有写入维护成本那么大,但在复杂查询中也会有影响。


4)容易产生冗余索引、重复索引#

索引多了以后,很容易出现:

重复索引#

例如同时存在:

  • idx_name
  • 又建了一个 idx_name2

冗余索引#

例如已有联合索引:

(name, age)

又单独建了:

(name)

因为联合索引已经能支持最左匹配 (name),后者往往是冗余的。

冗余索引不仅浪费空间,还增加维护成本。


5)索引多不代表查询一定更快#

如果索引设计不合理,例如:

  • 区分度低
  • 联合索引顺序不对
  • 查询根本用不到
  • 索引经常失效

那这些索引不仅没帮助,还拖累整体性能。

所以关键不是“索引数量”,而是:

  • 是否真正命中业务查询
  • 是否设计合理

6)什么样的索引才是好索引?#

好的索引应该满足:

  1. 有明确查询场景支撑
  2. 能显著减少扫描行数
  3. 能帮助排序 / 分组 / join
  4. 写入维护成本可接受
  5. 没有重复和冗余

也就是说,索引要追求的是“高质量”,不是“多数量”。


7)正确的索引观念#

正确做法通常是:

  1. 先看真实慢 SQL
  2. 再分析执行计划
  3. 然后设计最有价值的索引
  4. 最后持续清理无用索引

而不是先把一堆字段都建上索引。


8)总结#

索引不是越多越好,因为:

  1. 会占用额外磁盘空间
  2. 会降低插入、更新、删除性能
  3. 会增加优化器选择成本
  4. 会产生重复/冗余索引
  5. 不合理索引甚至没有收益

一句话总结:

索引要“少而精”,只保留真正能提升核心查询性能的索引。


33. MySQL 索引用的什么数据结构?了解吗?#

答:

MySQL 中最常用、最核心的索引数据结构是 B+Tree。 尤其是在 InnoDB 存储引擎中,主键索引和普通索引默认都基于 B+Tree 实现。

不过要注意,MySQL 不只有一种索引结构,不同引擎或场景下还可能涉及:

  • B+Tree
  • Hash
  • Fulltext(全文索引底层实现不同)
  • R-Tree(空间索引)

但面试里最核心的答案一定是:InnoDB 默认使用 B+Tree


1)什么是 B+Tree?#

B+Tree 是一种多路平衡搜索树,是数据库和文件系统里非常常见的数据结构。 它相比普通二叉树、平衡树,更适合磁盘存储场景。

它的核心特点有:

  1. 非叶子节点只存键值,不存真实数据
  2. 所有真实数据都存放在叶子节点
  3. 叶子节点之间通过链表连接
  4. 树的高度很低,适合减少磁盘 IO

2)为什么数据库索引适合 B+Tree?#

数据库索引的目标是:

  • 快速查找单条数据
  • 支持范围查询
  • 支持排序
  • 尽量减少磁盘 IO

B+Tree 正好能满足这些要求。

原因一:树高低#

一个节点能存很多 key,所以整棵树高度很低。 通常几层高度就能支持千万级数据查找。

原因二:磁盘 IO 少#

数据库每次从磁盘读数据,通常按“页”读取。 B+Tree 一个节点通常对应一个页,一次 IO 能加载很多 key。 树越矮,查找时访问页数越少。

原因三:范围查询强#

叶子节点之间是有序链表,所以做:

BETWEEN
>
<
ORDER BY
GROUP 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 能存多少数据,不是固定值,会受很多因素影响:

  1. 页大小(InnoDB 默认 16KB)
  2. 索引字段大小
  3. 主键大小
  4. 叶子节点存的是整行还是主键值
  5. 数据行大小
  6. 是否有页分裂、碎片等

所以面试里不用死背某个精确数字,更重要的是讲清楚原理:

  • 分叉数大
  • 树很矮
  • 所以能存很多数据

7)总结#

一棵 B+Tree 能存很多数据,原因是:

  1. 一个节点能存很多键值
  2. 分支数非常高
  3. 树高度很低
  4. 查找只需很少几次 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 BY
  • GROUP BY

B+Tree 的叶子节点之间有链表连接,天然适合范围扫描。 普通二叉树要做范围查询则不够高效。


6)总结#

不用普通二叉树,主要原因有:

  1. 普通二叉树可能退化
  2. 平衡二叉树分支太少,树太高
  3. 磁盘 IO 次数太多
  4. 不适合数据库页存储
  5. 范围查询不如 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 树,主要因为:

  1. 非叶子节点不存数据,单节点能容纳更多 key
  2. 树更矮,磁盘 IO 更少
  3. 查询路径更稳定
  4. 范围查询更高效
  5. 排序、扫描能力更强

一句话总结:

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 > 100
WHERE id BETWEEN 100 AND 200

Hash 索引无法利用“有序性”,所以基本帮不上忙。

B+Tree 索引

非常适合范围查询,因为索引天然有序,叶子节点还能顺序遍历。


4)排序能力#

Hash 索引

不支持排序优化。

例如:

ORDER BY id

Hash 索引无法利用。

B+Tree 索引

支持按索引顺序扫描,所以能用于:

  • ORDER BY
  • GROUP BY

5)联合索引能力#

Hash 索引

不支持最左匹配原则。

B+Tree 索引

支持联合索引和最左前缀匹配原则。 例如索引 (a,b,c) 可以支持:

  • a
  • a,b
  • a,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 索引主要区别:

对比项HashB+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 会:

  1. 先从二级索引找到对应主键
  2. 再根据主键去聚簇索引中查完整记录

这第二步就叫回表


2)举个例子#

假设有表:

CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
age INT,
INDEX idx_name(name)
);

执行:

SELECT * FROM user WHERE name = 'Tom';

过程大致是:

  1. 先走 idx_name
  2. idx_name 中找到 name='Tom' 对应的主键 id,例如 id=100
  3. 再根据 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 不在索引里。 所以执行时会:

  1. idx_city_name 找到符合条件的记录
  2. 取到主键值
  3. 回表去主键索引里查 age

这就不是覆盖索引。


4)覆盖索引的好处#

(1)减少回表#

最直接的收益就是避免回表。

(2)减少随机 IO#

回表本质上通常是额外的随机访问,覆盖索引能减少这种开销。

(3)提高查询效率#

特别是结果集较大时,覆盖索引优势很明显。


5)什么场景适合设计覆盖索引?#

比较适合的场景:

  • 高频查询
  • 返回字段较少
  • 条件字段和查询字段相对固定
  • 读性能要求高

例如:

  • 列表页查询
  • 后台管理分页字段
  • 高频统计查询

6)是不是覆盖索引一定最好?#

也不是。 为了覆盖索引,有时候会把很多字段都塞进联合索引,这会导致:

  • 索引过大
  • 维护成本上升
  • 写入变慢

所以设计覆盖索引要平衡:

  • 查询收益
  • 索引大小
  • 写入成本

7)怎么判断是否使用了覆盖索引?#

可以通过 EXPLAINExtra 字段。 如果看到:

Using index

通常说明用了覆盖索引。

注意:

  • Using index 一般是好信号
  • 它表示查询字段都在索引中

8)总结#

覆盖索引就是:

查询需要的所有字段都在索引里,可以直接从索引返回结果,不需要回表。

一句话总结:

覆盖索引的价值在于“少一次回表,多一份性能提升”。


41. 什么是最左前缀原则 / 最左匹配原则?#

答:

最左前缀原则(也叫最左匹配原则)是联合索引使用时最核心的规则之一。

它的意思是:

联合索引在使用时,必须从最左边的列开始连续匹配,才能有效利用索引。


1)什么是联合索引?#

比如建立一个联合索引:

CREATE INDEX idx_abc ON user(a, b, c);

这个索引不是简单地给 abc 各建一个独立索引, 而是按 (a, b, c) 的组合顺序构建一棵 B+Tree。

可以理解为,它相当于支持以下前缀形式:

  • (a)
  • (a, b)
  • (a, b, c)

但不天然支持:

  • (b)
  • (c)
  • (b, c)

2)为什么叫“最左”?#

因为联合索引是从左到右排序构建的。 在 (a, b, c) 这个索引里:

  1. 先按 a 排序
  2. a 相同再按 b 排序
  3. ab 都相同再按 c 排序

所以如果查询时连最左边的 a 都没有,数据库就无法确定从哪段索引开始查。


3)举几个例子#

假设有索引:

(a, b, c)

可以有效使用索引的情况#

WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND b > 2

不能充分使用索引的情况#

WHERE b = 2
WHERE c = 3
WHERE b = 2 AND c = 3

因为都没有从 a 开始匹配。


4)遇到范围查询会怎样?#

例如:

WHERE a = 1 AND b > 2 AND c = 3

索引通常可以用到:

  • a
  • b

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 时,执行大致可能是这样:

  1. 存储引擎根据 name LIKE '张%' 找到一批索引记录
  2. 根据这些索引记录逐个回表,取出整行数据
  3. 回到 Server 层再判断 age = 10 是否成立

问题在于:

  • 很多记录其实 age != 10
  • 这些记录本不该回表
  • 但没有 ICP 时,它们还是被回表了

这就造成了不必要的 IO。


2)有了 ICP 之后会怎样?#

有了索引下推后,执行过程优化为:

  1. 存储引擎先根据 name LIKE '张%' 找到候选索引记录
  2. 在索引层就继续判断 age = 10
  3. 只有同时满足条件的记录,才回表取整行数据

也就是说:

  • 过滤条件更早执行了
  • 无效记录更早被排除
  • 回表次数减少了

3)为什么叫“下推”?#

因为原本在 MySQL Server 层完成的条件判断,被“推”到了更底层的存储引擎去做。

所以叫:

  • 条件下推
  • 索引条件下推

4)ICP 适用于什么场景?#

通常出现在:

  • 使用二级索引
  • where 条件里包含索引列
  • 但不是所有条件都能完全用于索引定位
  • 又需要回表时

ICP 的作用就是: 尽量在回表之前,多过滤一批无效记录。


5)ICP 的好处#

(1)减少回表次数#

这是最直接的收益。

(2)减少存储引擎返回给 Server 层的数据量#

降低 Server 层判断压力。

(3)提升查询效率#

尤其在:

  • 数据量大
  • 候选记录很多
  • 回表代价高

的场景下效果明显。


6)如何判断是否用了 ICP?#

可以通过 EXPLAINExtra 字段查看。 如果出现:

Using index condition

通常就表示用了索引下推。

注意:

  • Using index condition ≠ 覆盖索引
  • 它只是表示 where 条件的一部分下推到了存储引擎层

7)ICP 和覆盖索引的区别#

很多人会混淆这两个概念。

覆盖索引

查询字段全部在索引中,不用回表

ICP

依然可能要回表,只是回表前先多过滤一些无效记录

所以:

  • 覆盖索引是“完全不回表”
  • ICP 是“少回一些表”

8)总结#

索引下推优化(ICP)的核心是:

把部分 where 条件下推到存储引擎层,在回表前先过滤数据,从而减少回表次数。

一句话总结:

ICP 不是让你不回表,而是让你“少回无意义的表”。

Comment seems to stuck. Try to refresh?✨