简介

依旧一问一答式学习。原本打算学《MySQL45讲》的,但是细碎知识点太多,貌似不是很适合面试前突击,因此这次,我会学习xiaolin coding的图解MySQL

话不多说,发车!

1 索引常见面试题 3.31

🟢 第一部分:底层原理(深挖 B+ 树)

面试官提问 1: 在 Go 后端开发中,我们经常需要处理海量数据的分页查询。请从磁盘 I/O 的角度解释:为什么 MySQL 的 InnoDB 选择 B+ Tree 作为索引结构,而不是 B Tree 或者 Hash 索引?(提示:请至少提到两个 B+ 树相对于 B 树的决定性优势)。

面试官提问 2: 请解释什么是 “回表”?在高性能的 Go 微服务中,如果发现某条查询语句回表次数过多导致 P99 延时升高,你有什么优化方案?(要求:提到具体的索引术语)。


🟡 第二部分:索引策略(联合索引与匹配规则)

面试官提问 3: 假设你为订单系统建立了一个联合索引 idx_a_b_c (a, b, c),其中 a, b, c 均为 int 类型。 请判断以下四条 SQL 哪些能完全用到索引,哪些部分用到,哪些完全失效?并简述原因。

  1. SELECT * FROM t WHERE a = 1 AND c = 3;
  2. SELECT * FROM t WHERE a > 1 AND b = 2;
  3. SELECT * FROM t WHERE a >= 1 AND b = 2;
  4. SELECT * FROM t WHERE b = 2 AND c = 3;

🔴 第三部分:实战场景题(工程经验)

面试官提问 4: 在数据库设计规范中,通常建议“主键索引最好是自增的”且“尽量设置为 NOT NULL”。

  • 如果我用 UUID 做主键,在海量数据插入时会遇到什么底层性能问题?
  • 为什么索引列建议设置为 NOT NULL

面试官提问 5: 场景模拟:你在开发一个社交 App 的后台,数据库有一张“用户动作表”,包含 status(状态:0-离线,1-在线)和 last_login_time(最后登录时间)。 现在的查询需求是:SELECT * FROM user_action WHERE status = 1 ORDER BY last_login_time DESC LIMIT 10;

  • 请问你会如何设计索引来达到最优性能?
  • 如果我不加你建议的索引,执行计划(Explain)里的 Extra 字段大概率会出现什么令人头疼的字眼?

我的回答:

1.首先,B+树由于只在叶子节点存数据,普通节点只会存指针和键,这使得它每个非叶子节点的扇出非常大,可以分叉非常多,直接把树高压的非常低;B树由于每个节点全能,除了指针和键还会存完整的行数据,由于每个节点的空间是有限的,这导致它的扇出比B+树要多很多。而树高代表磁盘I/O次数,因此B+树在性能上毫无疑问胜出。还有一点,B+树的叶子节点之间用双向连接,这非常适合范围查找。至于为啥不用Hash?直接找确实很快,但是范围查找不够适用,所以还是算了。
2.回表就是:由于B+树的叶子节点不会全量存原表的数据,只能通过索引提供的对应记录的主键去主索引里面翻全量数据,导致二次查询。优化方案?把经常被查询的字段和查询的键捆绑在一起,做一个复合索引,这样查到对应的复合索引之后,索引里面就能拿到对应值,避免了回表。
3.第一条只用到了a=1,因为后面是先按照b排序后按照c排序的,直接跳到c,c的记录无序,用不了索引。第二条只用到了a>1,因为这是一次范围查找,a有序,但是这一批记录的b是无序的,因此用不了。第三条两个都能用到,会直接从a=1并b=2的记录开始查找,按abc顺序来的并且能取到a的具体值,能用索引。第四条两个都没用到,因为是先按照a排序的,对于b和c来说完全无序。
4.(1)由于uuid无序,而索引又要保持有序,这样就会产生大量的中间插入操作,会产生大量数据的移动,相比于自增的主键直接尾插,性能慢很多。而且移动数据的过程中还会产生内存碎片,降低存储效率。
(2)首先,NULL值会给优化器和索引的操作带来麻烦,做索引的时候需要特意忽略NULL值;其次,NULL虽然没有任何意义,但是行格式中至少会占用1字节来存储NULL列表,占用存储空间。
5.(1)我会设计一个(status ASC,llt DESC),也就是前者升序后者降序排的索引,这样只需要先定status,然后顺着status往下读10条就完事了,不需要自己排序。但是我有个疑问,status不是类似于性别,分布非常均匀,这时候索引会不会反而拖累了?
(2)会出现using filesort,文件排序的效率是很低的。

🟢 第一部分:底层原理(深挖 B+ 树)

你的表现: 满分。准确抓住了“扇出(Fan-out)”和“双向链表”这两个核心点。

标准答案分析:

  1. 磁盘 I/O: InnoDB 的页(Page)默认大小是 16KB。由于 B+ 树非叶子节点不存数据,只存键值和指针,这使得单个节点能容纳更多的索引项。这意味着树的扇出更高,千万级数据通常 3-4 层就能覆盖,磁盘 I/O 次数极少。
  2. 范围查询: B+ 树叶子节点之间通过双向链表连接且有序。在 Go 业务中常见的 WHERE age > 20 这种查询,只需定位到第一个 20,然后沿着链表向后遍历即可。B 树则需要频繁的中序遍历,效率低下。
  3. Hash 局限性: 无法支持排序、范围查询,且在大量哈希冲突时性能退化。

🟢 第二部分:索引策略(回表与覆盖索引)

你的表现: 满分。准确识别了优化手段。

标准答案分析:

  • 回表(Look-up): 简单说就是“根据二级索引找到主键,再根据主键去聚簇索引找整行数据”。
  • 优化: 你的方案就是覆盖索引(Covering Index)

    面试官加分项: 在 Go 中,如果你使用的是 SELECT *,极易导致回表。在写 GORM 代码时,建议通过 .Select("field1", "field2") 只取索引包含的字段。

🟡 第三部分:逻辑判断(最左匹配原则)

你的表现: 接近满分。Q3 的理解非常到位(这是很多人的盲点)。

标准答案核对:

  1. a = 1 AND c = 3部分用到。只用了 ac 没用上(a 相同的情况下 b 才有序,b 断了,c 自然用不到)。
  2. a > 1 AND b = 2部分用到a 用到了范围查询,但范围查询后的 b 失效。
  3. a >= 1 AND b = 2全部用到。MySQL 优化器能处理 >=。当 a = 1 时,b 是有序的,可以直接定位。
  4. b = 2 AND c = 3完全失效。没带“老大哥” a,索引这棵树根本没法爬。

🔴 第四部分:底层陷阱(UUID 与 NULL)

你的表现: 优秀。你提到了“内存碎片”,更专业的术语叫页分裂(Page Split)

标准答案分析:

  • UUID 的痛点:

    1. 随机性: 导致聚簇索引在插入时不是“追加”,而是随机插入页中间,导致频繁的页分裂,产生空间空洞。
    2. 体积大: UUID 字符长,会导致二级索引(存主键)也变得臃肿,浪费内存和磁盘。
  • NOT NULL 的必要性:

    1. 存储空间: NULL 值需要额外的 NULL 标志位 列表(1 字节),增加行格式开销。
    2. 索引复杂度: NULL 会改变统计信息,使 count() 等操作逻辑变复杂,优化器更难选择最优路径。

🔴 第五部分:实战场景题(社交 App)

你的表现: 亮眼。你的质疑非常有深度,这涉及到索引选择性(Index Selectivity)

标准答案分析:

  1. 索引设计: (status, last_login_time DESC)
  2. Extra 字眼: Using filesort
  3. 深度解答你的疑问: * 如果 status = 1 的用户占总体的 90%,优化器确实可能放弃索引改用全表扫描,因为它觉得回表 90% 的成本比全表扫描还高。

    • 但在“在线用户”通常只占一小部分(比如 5%)的场景下,这个联合索引就是神技,它利用了索引的有序性,直接跳过了排序步骤。
最后修改:2026 年 03 月 31 日
如果觉得我的文章对你有用,请随意赞赏