简介
依旧一问一答式学习。原本打算学《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 哪些能完全用到索引,哪些部分用到,哪些完全失效?并简述原因。
SELECT * FROM t WHERE a = 1 AND c = 3;SELECT * FROM t WHERE a > 1 AND b = 2;SELECT * FROM t WHERE a >= 1 AND b = 2;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)”和“双向链表”这两个核心点。
标准答案分析:
- 磁盘 I/O: InnoDB 的页(Page)默认大小是 16KB。由于 B+ 树非叶子节点不存数据,只存键值和指针,这使得单个节点能容纳更多的索引项。这意味着树的扇出更高,千万级数据通常 3-4 层就能覆盖,磁盘 I/O 次数极少。
- 范围查询: B+ 树叶子节点之间通过双向链表连接且有序。在 Go 业务中常见的
WHERE age > 20这种查询,只需定位到第一个 20,然后沿着链表向后遍历即可。B 树则需要频繁的中序遍历,效率低下。 - Hash 局限性: 无法支持排序、范围查询,且在大量哈希冲突时性能退化。
🟢 第二部分:索引策略(回表与覆盖索引)
你的表现: 满分。准确识别了优化手段。
标准答案分析:
- 回表(Look-up): 简单说就是“根据二级索引找到主键,再根据主键去聚簇索引找整行数据”。
优化: 你的方案就是覆盖索引(Covering Index)。
面试官加分项: 在 Go 中,如果你使用的是
SELECT *,极易导致回表。在写GORM代码时,建议通过.Select("field1", "field2")只取索引包含的字段。
🟡 第三部分:逻辑判断(最左匹配原则)
你的表现: 接近满分。Q3 的理解非常到位(这是很多人的盲点)。
标准答案核对:
a = 1 AND c = 3:部分用到。只用了a,c没用上(a 相同的情况下 b 才有序,b 断了,c 自然用不到)。a > 1 AND b = 2:部分用到。a用到了范围查询,但范围查询后的b失效。a >= 1 AND b = 2:全部用到。MySQL 优化器能处理>=。当a = 1时,b是有序的,可以直接定位。b = 2 AND c = 3:完全失效。没带“老大哥”a,索引这棵树根本没法爬。
🔴 第四部分:底层陷阱(UUID 与 NULL)
你的表现: 优秀。你提到了“内存碎片”,更专业的术语叫页分裂(Page Split)。
标准答案分析:
UUID 的痛点:
- 随机性: 导致聚簇索引在插入时不是“追加”,而是随机插入页中间,导致频繁的页分裂,产生空间空洞。
- 体积大: UUID 字符长,会导致二级索引(存主键)也变得臃肿,浪费内存和磁盘。
NOT NULL 的必要性:
- 存储空间:
NULL值需要额外的NULL 标志位列表(1 字节),增加行格式开销。 - 索引复杂度:
NULL会改变统计信息,使count()等操作逻辑变复杂,优化器更难选择最优路径。
- 存储空间:
🔴 第五部分:实战场景题(社交 App)
你的表现: 亮眼。你的质疑非常有深度,这涉及到索引选择性(Index Selectivity)。
标准答案分析:
- 索引设计:
(status, last_login_time DESC)。 - Extra 字眼:
Using filesort。 深度解答你的疑问: * 如果
status = 1的用户占总体的 90%,优化器确实可能放弃索引改用全表扫描,因为它觉得回表 90% 的成本比全表扫描还高。- 但在“在线用户”通常只占一小部分(比如 5%)的场景下,这个联合索引就是神技,它利用了索引的有序性,直接跳过了排序步骤。