口语八股:MySQL 核心原理系列(一):索引篇

📌 一、索引篇 - 高频考点

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树更好。主要有三个原因:

  1. B+树的非叶子节点不存数据,只存索引。这样一个磁盘页(MySQL里默认是16KB)能存更多的索引键,也就是说一个节点能有更多的子节点,树的高度就更矮,IO次数就更少。
  2. B+树所有数据都在叶子节点,而且叶子节点之间用双向链表连接 。这对范围查询特别友好。比如SELECT * FROM user WHERE age BETWEEN 20 AND 30,B+树只需要在叶子节点上顺着链表扫一遍就行了,效率很高。而B树的话,因为数据分散在各个节点,需要不断地在树里上下跳跃,效率就低多了。
  3. 查询效率更稳定。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';

执行过程是这样的:

  1. 先在idx_username这个非聚簇索引上查找,找到username='zhangsan'的记录,但这个索引树的叶子节点只有username和主键id
  2. 拿到主键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用到索引

实际工作中的经验:

  1. 联合索引的字段顺序很重要 。一般把选择性高的(区分度大的)字段放前面。比如(user_id, status),user_id的值基本都不重复,区分度高,就放前面;status可能就几个值(0、1、2),区分度低,放后面。
  2. 还要考虑实际的查询场景。如果80%的查询都是按照时间范围查,那就把时间字段放最前面,这样大部分查询都能用上索引。
  3. 口诀记忆:"最左匹配,遇到范围(>、<、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有无,全部白搭"
相关推荐
funnycoffee1231 小时前
word vba提取所有表格到1个新的文档中
数据库·word
xifangge20251 小时前
[报错] SpringBoot 启动报错:Port 8080 was already in use 完美解决(Windows/Mac/Linux)
java·windows·spring boot·macos·错误解决
没有bug.的程序员1 小时前
容器网络深度探究:从 CNI 插件选型内核到 K8s 网络策略安全防护实战指南
java·网络·安全·kubernetes·k8s·cni·容器网络
野犬寒鸦1 小时前
缓存与数据库一致性的解决方案:实际项目开发可用
java·服务器·数据库·后端·缓存
倔强的石头1061 小时前
KingbaseES 文档数据实践:MongoDB 兼容性评估与替换落地
数据库·mongodb·kingbase
黎雁·泠崖1 小时前
【魔法森林冒险】11/14 战斗系统(二):多波战斗与BOSS战
java·开发语言
Re.不晚2 小时前
Redis——主从复制
数据库·redis·缓存
我命由我123453 小时前
Android多进程开发 - AIDL 最简单的实现、传递数据大小限制
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
小高不会迪斯科9 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle