📌 一、索引篇 - 高频考点
1.1 为什么要使用索引?
❌ 错误回答示范:
"索引可以加快查询速度。"
✅ 正确回答思路:
面试官您好,我从三个方面来回答这个问题:
首先从实际问题出发 ,假设我们有一张用户表,里面有500万条数据。如果没有索引,执行一条SELECT * FROM user WHERE username = 'zhangsan'这样的查询,MySQL就得从第一行开始,一直扫描到最后一行,这叫全表扫描。500万数据扫一遍,那性能肯定是非常差的。
然后说索引的本质,索引其实就像一本书的目录。比如你要在一本500页的书里找"MySQL索引"这个知识点,你会怎么办?肯定是先看目录,找到在第230页,然后直接翻到那一页对吧?索引就是这个道理,它通过B+树这种数据结构,把数据按照某种规则排好序,查询的时候就能快速定位到数据所在的位置。
最后补充实际效果,在我之前的项目中,有张订单表数据量大概200万左右,一开始没建索引,查询订单列表的接口耗时要2-3秒。后来在订单号、用户ID、创建时间这些常用查询字段上建了索引,查询时间直接降到了几十毫秒。所以索引对性能提升是非常明显的。
💡 加分项: 你还可以补充说"当然索引也不是万能的,它会占用额外的存储空间,而且在进行INSERT、UPDATE、DELETE操作时也会有额外开销,因为索引需要维护。所以要根据实际业务场景来合理设计索引。"
1.2 MySQL索引底层为什么用B+树,而不用B树或者红黑树?
✅ 正确回答思路:
这个问题很好,我分三个层次来回答:
第一,先说为什么不用红黑树。红黑树虽然查询效率也不错,时间复杂度是O(log n),但它有个致命问题:树的高度太高了。您想啊,如果有100万条数据,红黑树的高度可能要20层甚至更多。MySQL的数据是存在磁盘上的,每访问一个节点就要进行一次磁盘IO,20次IO那性能肯定受不了。而B+树是多叉树,同样100万数据,高度可能只有3-4层,IO次数大大减少。
第二,说为什么B+树比B树更好。主要有三个原因:
- B+树的非叶子节点不存数据,只存索引。这样一个磁盘页(MySQL里默认是16KB)能存更多的索引键,也就是说一个节点能有更多的子节点,树的高度就更矮,IO次数就更少。
- B+树所有数据都在叶子节点,而且叶子节点之间用双向链表连接 。这对范围查询特别友好。比如
SELECT * FROM user WHERE age BETWEEN 20 AND 30,B+树只需要在叶子节点上顺着链表扫一遍就行了,效率很高。而B树的话,因为数据分散在各个节点,需要不断地在树里上下跳跃,效率就低多了。 - 查询效率更稳定。B+树所有查询都要到叶子节点,所以无论查什么数据,磁盘IO次数都是一样的,都是树的高度。而B树因为数据分散在各层,查询不同数据的IO次数可能不同,性能就不稳定。
第三,结合实际数据,以InnoDB为例,一个16KB的数据页,如果索引是bigint类型(8字节),加上6字节的指针,一个节点大概能存16KB / 14B ≈ 1170个索引。如果是3层B+树,就能存1170 × 1170 × 16 ≈ 2000万条数据,而磁盘IO只需要3次。这效率就非常高了。
💡 加分项: "所以B+树在MySQL这种需要大量磁盘IO的场景下,是最适合的数据结构。"
1.3 聚簇索引和非聚簇索引(二级索引)的区别?什么是回表?
✅ 正确回答思路:
好的,我先解释下这两种索引,然后说什么是回表:
聚簇索引(也叫主键索引、一级索引):
- 在InnoDB引擎中,聚簇索引的叶子节点直接存储的是完整的行数据
- 一张表只能有一个聚簇索引,一般就是主键索引
- 数据本身就是按照聚簇索引的顺序来物理存储的
非聚簇索引(也叫二级索引、辅助索引):
- 叶子节点存的不是完整数据,只存索引列的值和对应的主键值
- 一张表可以有多个非聚簇索引
- 比如我们在username字段上建的索引就是非聚簇索引
然后说什么是回表,我举个例子您就明白了:
假设我们有个用户表:
sql
CREATE TABLE user (
id bigint PRIMARY KEY, -- 主键,聚簇索引
username varchar(50),
age int,
email varchar(100),
INDEX idx_username(username) -- 非聚簇索引
);
当我们执行:
sql
SELECT * FROM user WHERE username = 'zhangsan';
执行过程是这样的:
- 先在
idx_username这个非聚簇索引上查找,找到username='zhangsan'的记录,但这个索引树的叶子节点只有username和主键id - 拿到主键id后,还得再到主键索引(聚簇索引)上再查一次,才能拿到完整的行数据(age、email等其他字段)
这个再次根据主键查询的过程,就叫回表。显然回表会增加IO操作,影响性能。
💡 如何避免回表? "可以使用覆盖索引。比如改成SELECT id, username FROM user WHERE username = 'zhangsan',因为username索引的叶子节点本身就有id和username,不需要再查主键索引了,就避免了回表,这就是覆盖索引的应用。"
1.4 什么是最左前缀原则?联合索引怎么用?
✅ 正确回答思路:
最左前缀原则是联合索引使用的核心规则,我用例子来说明:
假设我们建了一个联合索引:
sql
INDEX idx_abc(a, b, c)
最左前缀原则就是:查询条件必须从最左边的列开始,并且是连续的,才能用到索引。
我列举几种情况:
✅ 能用到索引的:
sql
-- 情况1: 使用a,走索引,只用了a列
WHERE a = 1
-- 情况2: 使用a和b,走索引,用了a、b两列
WHERE a = 1 AND b = 2
-- 情况3: 使用a、b、c,走索引,三列全用上
WHERE a = 1 AND b = 2 AND c = 3
-- 情况4: where顺序无所谓,MySQL优化器会优化,走索引
WHERE b = 2 AND a = 1 AND c = 3 -- MySQL会优化成a、b、c的顺序
❌ 不能完全用到索引或不走索引:
sql
-- 情况5: 跳过了a,从b开始,不走索引
WHERE b = 2 AND c = 3
-- 情况6: 只用了a和c,中间断了,只能用到a列的索引
WHERE a = 1 AND c = 3
-- 情况7: a是范围查询,后面的b、c用不上索引
WHERE a > 1 AND b = 2 AND c = 3 -- 只有a用到索引
实际工作中的经验:
- 联合索引的字段顺序很重要 。一般把选择性高的(区分度大的)字段放前面。比如
(user_id, status),user_id的值基本都不重复,区分度高,就放前面;status可能就几个值(0、1、2),区分度低,放后面。 - 还要考虑实际的查询场景。如果80%的查询都是按照时间范围查,那就把时间字段放最前面,这样大部分查询都能用上索引。
- 口诀记忆:"最左匹配,遇到范围(>、<、BETWEEN)就停止匹配后面的列"
💡 加分项: "在实际项目中,我会用EXPLAIN命令来查看SQL的执行计划,看key列显示用了哪个索引,key_len能看出用了联合索引的几个字段,这样能准确判断索引是否生效。"
1.5 哪些情况会导致索引失效?
✅ 正确回答思路:
这个问题在实际工作中非常重要,我总结了最常见的几种情况:
1. 在索引列上使用函数或表达式
sql
-- ❌ 索引失效
SELECT * FROM user WHERE YEAR(create_time) = 2024;
SELECT * FROM user WHERE id + 1 = 100;
-- ✅ 应该这样写
SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
SELECT * FROM user WHERE id = 99;
原因是:索引是按列的原始值建立的,一旦对列做了计算或函数处理,MySQL就不知道该怎么用索引了。
2. 类型隐式转换
sql
-- 假设username是varchar类型
-- ❌ 索引失效,因为发生了隐式类型转换
SELECT * FROM user WHERE username = 123;
-- ✅ 应该这样
SELECT * FROM user WHERE username = '123';
这个坑我之前就踩过!MySQL会把username转成数字类型再比较,相当于对索引列用了函数,索引就失效了。
3. LIKE以%开头
sql
-- ❌ 索引失效
SELECT * FROM user WHERE username LIKE '%zhang%';
SELECT * FROM user WHERE username LIKE '%zhang';
-- ✅ 这样可以走索引
SELECT * FROM user WHERE username LIKE 'zhang%';
这个很好理解,如果是'zhang%',索引树可以快速定位到所有zhang开头的记录。但如果是'%zhang',那得扫描所有记录才知道哪些包含zhang。
4. OR条件有一个字段没索引,整个查询都不走索引
sql
-- 假设username有索引,age没索引
-- ❌ 整个查询都不走索引
SELECT * FROM user WHERE username = 'zhangsan' OR age = 20;
-- ✅ 改用UNION或者给age也加索引
SELECT * FROM user WHERE username = 'zhangsan'
UNION
SELECT * FROM user WHERE age = 20;
5. 不等于(!=、<>)、NOT IN可能导致索引失效
sql
-- 有可能不走索引,要看数据分布
SELECT * FROM user WHERE status != 1;
SELECT * FROM user WHERE id NOT IN (1,2,3);
这种情况MySQL优化器会判断,如果不等于的数据占大多数,可能觉得全表扫描更快,就不走索引了。
6. IS NULL、IS NOT NULL情况比较复杂
- 如果表里NULL值很少,
IS NULL可能走索引 - 如果表里非NULL值很少,
IS NOT NULL可能走索引 - 具体要看MySQL优化器的判断
实际经验总结: 在我日常开发中,遇到慢查询,第一步就是用EXPLAIN看执行计划,如果type是ALL(全表扫描),key是NULL(没用索引),就要检查是不是踩了上面这些坑。然后针对性地优化SQL或者调整索引。
💡 记忆口诀:
- "函数计算,索引白建"
- "类型转换,索引不用"
- "开头模糊,全表遍历"
- "OR有无,全部白搭"