MySQL索引的10个面试高频问题

摘要:从面试官的经典问题出发,深度剖析索引的分类、聚簇索引与非聚簇索引的区别、联合索引的最左前缀原则、B+树的特性、以及索引优化的实战技巧。通过真实案例、图解和性能测试,系统性回答"为什么主键用自增ID"、"什么字段适合做主键"、"B+树的叶子节点是单向还是双向"等高频问题,给出面试标准答案。


💥 面试现场

哈吉米接到了心仪公司的面试通知,二面是技术深度面。

面试官:"聊聊MySQL索引吧,先说说索引是什么?有什么好处?"
哈吉米:"索引是帮助MySQL快速查找数据的数据结构,就像书的目录……"
面试官:"嗯,那索引有哪些分类?"
哈吉米:"呃……主键索引、唯一索引、普通索引……"
面试官:"聚簇索引和非聚簇索引的区别知道吗?"
哈吉米:"……"(冷汗)
面试官:"那B+树的叶子节点是单向链表还是双向链表?"
哈吉米:"……"(崩溃)

晚上,哈吉米找南北绿豆和阿西噶阿西复盘。

哈吉米:"我被问蒙了,索引的问题这么多!"
南北绿豆:"来,我把面试高频问题都给你梳理一遍!"


🎯 问题1:索引的分类是什么?

面试标准答案

索引可以从多个维度分类:

按数据结构分类

类型说明引擎
B+树索引最常用,InnoDB默认InnoDB、MyISAM
哈希索引等值查询快,不支持范围查询Memory
全文索引支持全文搜索InnoDB(5.6+)、MyISAM
空间索引GIS地理位置MyISAM

按物理存储分类

类型说明
聚簇索引(Clustered Index)数据和索引存在一起,叶子节点就是数据
非聚簇索引(Secondary Index)索引和数据分开,叶子节点存主键值

重点:InnoDB的主键索引是聚簇索引,其他索引都是非聚簇索引。


按列数分类

类型说明示例
单列索引只包含一个列INDEX idx_name(name)
联合索引(复合索引)包含多个列INDEX idx_name_age(name, age)

按功能分类

类型说明特点
主键索引PRIMARY KEY唯一、非NULL、聚簇索引
唯一索引UNIQUE唯一、可NULL
普通索引INDEX可重复
全文索引FULLTEXT支持全文搜索

🎯 问题2:聚簇索引和非聚簇索引的区别?

面试标准答案

聚簇索引(主键索引)

定义:索引和数据存储在一起,叶子节点就是完整的数据行。

结构

主键索引(聚簇索引):
          [50]
         /    \
    [20,35]    [65,80]
    /  |  \     /  |  \
   ↓   ↓   ↓   ↓   ↓   ↓
叶子节点(存储完整数据行):
[10, alice, 25, ...]
[25, bob, 30, ...]
[40, charlie, 28, ...]
...

特点

  • ✅ 查询快(一次查询就能拿到完整数据)
  • ✅ 范围查询快(叶子节点是链表,顺序扫描)
  • ❌ 一张表只能有一个聚簇索引(主键)

非聚簇索引(二级索引)

定义:索引和数据分开,叶子节点存储的是主键值。

结构

二级索引 idx_name(非聚簇索引):
          [lucy]
         /      \
    [alice]     [tom]
    /  |  \     /  |  \
   ↓   ↓   ↓   ↓   ↓   ↓
叶子节点(只存主键值):
[alice, 10]  ← 主键值
[bob, 25]
[charlie, 40]
...

查询流程:
1. 在二级索引找到主键值(如102. 拿着主键值回到主键索引查询(回表)
3. 拿到完整数据

特点

  • ✅ 可以有多个二级索引
  • ❌ 需要回表(两次查询)

回表的代价

-- 查询SQL
SELECT * FROM user WHERE name = 'alice';

-- 执行流程
1. 在idx_name索引找到name='alice'的主键值:id=10
2. 拿着id=10回到主键索引查询完整数据
3. 返回结果

总IO次数:2次(索引查询 + 回表)

时序图

sequenceDiagram
    participant Query as 查询
    participant SecondaryIndex as 二级索引(idx_name)
    participant PrimaryIndex as 主键索引
    
    Query->>SecondaryIndex: 1. 查找name='alice'
    SecondaryIndex->>SecondaryIndex: 找到主键值=10
    SecondaryIndex->>PrimaryIndex: 2. 回表查询id=10
    PrimaryIndex->>PrimaryIndex: 读取完整数据
    PrimaryIndex->>Query: 3. 返回结果
    
    Note over Query,PrimaryIndex: 总共2次IO

南北绿豆:"这就是为什么覆盖索引性能更好——不需要回表!"


🎯 问题3:什么字段适合做主键?

面试标准答案

推荐:自增ID

CREATE TABLE `order` (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,  ← 推荐
  order_no VARCHAR(32),
  user_id BIGINT
);

优点

  • ✅ 趋势递增,顺序插入,不会页分裂
  • ✅ 占用空间小(8字节)
  • ✅ 查询性能好
  • ✅ 所有二级索引占用空间小

不推荐:业务字段(如订单号)

-- ❌ 不推荐
CREATE TABLE `order` (
  order_no VARCHAR(32) PRIMARY KEY,  ← 不推荐
  user_id BIGINT
);

缺点

  • ❌ 字符串主键,占用空间大(32字节)
  • ❌ 可能无序,导致页分裂
  • ❌ 所有二级索引都要存32字节的order_no

不推荐:UUID

-- ❌ 不推荐
CREATE TABLE `order` (
  id VARCHAR(36) PRIMARY KEY,  ← UUID
  order_no VARCHAR(32)
);

缺点(详见上一篇文章):

  • ❌ 无序,频繁页分裂
  • ❌ 占用空间大(36字节)
  • ❌ 索引碎片率高

推荐:自增ID + 唯一索引

-- ✅ 推荐
CREATE TABLE `order` (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,  ← 自增ID做主键
  order_no VARCHAR(32) UNIQUE,           ← 业务字段建唯一索引
  user_id BIGINT
);

优点

  • ✅ 主键性能好(自增ID)
  • ✅ 业务唯一性保证(唯一索引)
  • ✅ 两全其美

🎯 问题4:为什么主键推荐用自增ID而不是UUID?

面试标准答案(详见第8篇文章):

UUID的4个问题

  1. 无序插入 → 频繁页分裂 → 性能下降90%
  2. 索引碎片率高 → 范围查询慢10倍
  3. 占用空间大 → 所有索引体积增加4.5倍
  4. 回表代价高 → 随机IO,性能差5倍

自增ID的优点

  1. 顺序插入 → 无页分裂 → 性能好
  2. 占用空间小 → 8字节
  3. 查询性能好 → 物理连续

分布式场景:用雪花算法(趋势递增 + 全局唯一)


🎯 问题5:B+树的叶子节点是单向还是双向?

面试标准答案

双向链表

结构

非叶子节点(索引层):
          [50]
         /    \
    [20,35]    [65,80]

叶子节点(数据层,双向链表):
NULL ← [10][25][40][55][70][90] → NULL

为什么是双向?

阿西噶阿西:"因为要支持ORDER BY DESC(倒序查询)!"

-- 正序查询(从左往右)
SELECT * FROM user WHERE id > 10 ORDER BY id ASC LIMIT 10;

-- 倒序查询(从右往左)
SELECT * FROM user WHERE id > 10 ORDER BY id DESC LIMIT 10;

如果是单向链表:倒序查询需要先找到最右边,再往回找(效率低)。

双向链表:可以双向遍历,支持正序和倒序。


🎯 问题6:联合索引ABC,现在有个查询条件A = XXX and C < XXX,索引怎么走?

面试标准答案

只能用到A列,C列用不上

原因:违反最左前缀原则(跳过了B列)。

-- 联合索引
CREATE INDEX idx_abc ON table(a, b, c);

-- 查询
WHERE a = 1 AND c < 100

-- 索引使用情况:
- a = 1:✅ 走索引
- b:❌ 跳过了
- c < 100:❌ 因为跳过了b,c也用不上

EXPLAIN验证

EXPLAIN SELECT * FROM table WHERE a = 1 AND c < 100\G

key: idx_abc
key_len: 4  ← 只用了a列(假设INT4字节)
Extra: Using index condition

正确写法

-- 如果经常查a和c,建新索引
CREATE INDEX idx_a_c ON table(a, c);

-- 或者调整查询,把b也加上
WHERE a = 1 AND b IS NOT NULL AND c < 100;

🎯 问题7:MySQL主键是聚簇索引吗?

面试标准答案

InnoDB:是,主键就是聚簇索引
MyISAM:不是,主键也是非聚簇索引

InnoDB的存储结构

主键索引(聚簇索引):
叶子节点存储完整数据

二级索引(非聚簇索引):
叶子节点存储主键值

MyISAM的存储结构

主键索引(非聚簇索引):
叶子节点存储数据的物理地址

数据文件(.MYD):
独立存储,按插入顺序

查询流程:
1. 在主键索引找到物理地址
2. 根据物理地址读取数据文件

对比

引擎主键索引类型数据存储位置
InnoDB聚簇索引和主键索引在一起
MyISAM非聚簇索引独立的.MYD文件

🎯 问题8:表中1个字段,你主键用自增ID还是UUID,为什么?

面试标准答案

单机场景:自增ID

原因

  1. 顺序插入,无页分裂,性能好
  2. 占用空间小(8字节 vs 36字节)
  3. 二级索引占用空间小
  4. 查询性能好

分布式场景:雪花算法

原因

  1. 全局唯一
  2. 趋势递增(避免页分裂)
  3. 占用空间小(8字节)
  4. 本地生成,不依赖数据库

不推荐UUID

  1. 无序,频繁页分裂
  2. 占用空间大
  3. 性能差(详见第8篇文章)

🎯 问题9:如果聚簇索引的数据更新,它的存储要不要变化?

面试标准答案

分情况讨论

情况1:更新非主键列(不变)

-- 更新非主键列
UPDATE user SET name = 'bob' WHERE id = 10;

-- 存储位置不变
-- 只是在原位置修改数据

原因:主键没变,在B+树中的位置没变。


情况2:更新主键列(要变)

-- 更新主键
UPDATE user SET id = 100 WHERE id = 10;

-- 存储位置变化:
1. 删除id=10的数据(从原位置删除)
2. 插入id=100的数据(插入到新位置)
3. 可能触发页分裂/页合并

原因:主键变了,在B+树中的位置必须变。

时序图

sequenceDiagram
    participant Client
    participant BPlusTree as B+树
    participant OldPos as 原位置(id=10)
    participant NewPos as 新位置(id=100)

    Client->>BPlusTree: UPDATE user SET id=100 WHERE id=10
    BPlusTree->>OldPos: 1. 找到id=10的数据
    OldPos->>OldPos: 2. 标记删除
    BPlusTree->>NewPos: 3. 在id=100的位置插入数据
    Note over NewPos: 可能触发页分裂
    BPlusTree->>Client: 4. 返回成功

阿西噶阿西:"所以不要更新主键!性能很差,而且可能触发页分裂。"


🎯 问题10:Mysql中的索引是怎么实现的?

面试标准答案

InnoDB用B+树实现索引

B+树的特性

1. 所有数据都在叶子节点
2. 非叶子节点只存索引键(不存数据)
3. 叶子节点是双向链表(支持范围查询)
4. 树的高度通常是3-4层

一棵3层B+树能存多少数据?

假设:
- 数据页大小:16KB
- 主键BIGINT:8字节
- 指针:6字节
- 每行数据:1KB

非叶子节点:
每页能存:16KB / (8B + 6B) ≈ 1170个索引键

叶子节点:
每页能存:16KB / 1KB = 16行数据

3层B+树:
第1层(根):1页
第2层:1170页
第3层(叶子):1170 * 1170 = 1,368,900页

总数据量:1,368,900 * 16 ≈ 2100万行

结论:3层B+树能存约2100万数据

哈吉米:"所以为什么用B+树而不是哈希、红黑树?"

南北绿豆:"三个原因:"

数据结构优点缺点为什么不用
哈希等值查询O(1)不支持范围查询MySQL大量范围查询(>、<、BETWEEN)
红黑树平衡,查询O(log n)树太高,IO次数多1000万数据,树高23层(23次IO)
B+树树矮胖,IO少,支持范围查询插入可能分裂✅ 最优选择

B+树优势

  • ✅ 3-4层就能存储千万级数据
  • ✅ 叶子节点链表,范围查询快
  • ✅ 非叶子节点不存数据,每页能存更多索引键

🎯 问题11:查询数据时,到了B+树的叶子节点,之后的查找数据是如何做?

面试标准答案

聚簇索引(主键索引)

1. 根据B+树找到叶子节点
2. 叶子节点就是完整数据,直接返回

示例:
SELECT * FROM user WHERE id = 10;

流程:
根节点 → 中间节点 → 叶子节点[10, alice, 25, ...] → 返回数据

总IO次数:3次(假设3层树)

非聚簇索引(二级索引)

1. 根据B+树找到叶子节点
2. 叶子节点存的是主键值,读取主键值
3. 拿着主键值回到主键索引(回表)
4. 在主键索引的叶子节点找到完整数据

示例:
SELECT * FROM user WHERE name = 'alice';

流程:
二级索引根节点 → 中间节点 → 叶子节点[alice, 10] → 读取主键10
→ 主键索引根节点 → 中间节点 → 叶子节点[10, alice, 25, ...] → 返回数据

总IO次数:6次(两棵树各3层)

时序图

graph TD
    A[SELECT * FROM user WHERE name='alice'] --> B{走哪个索引?}
    B -->|主键索引| C[在主键B+树中查找]
    C --> D[找到叶子节点]
    D --> E[直接返回数据 总IO:3次]
    
    B -->|二级索引| F[在二级索引B+树中查找]
    F --> G[找到叶子节点]
    G --> H[读取主键值=10]
    H --> I[回表:在主键B+树中查找id=10]
    I --> J[找到叶子节点]
    J --> K[返回数据 总IO:6次]
    
    style E fill:#90EE90
    style K fill:#FFE4B5

阿西噶阿西:"这就是为什么覆盖索引重要——避免回表,减少IO!"


🎯 问题12:B+树的好处是什么?

面试标准答案

3大核心优势

优势1:树矮胖,IO次数少

对比:

红黑树(1000万数据):
树高:log₂(1000万) ≈ 23层
查询IO次数:23次

B+树(1000万数据):
树高:3-4层
查询IO次数:3-4次

性能差距:6倍IO

优势2:非叶子节点不存数据,扇出大

如果非叶子节点存数据:
每页只能存:16KB / (8B主键 + 1KB数据) ≈ 16个

如果非叶子节点不存数据(B+树):
每页能存:16KB / (8B主键 + 6B指针) ≈ 1170个

扇出:1170 / 16 = 73倍
树高更低,IO更少

优势3:叶子节点是链表,范围查询快

-- 范围查询
SELECT * FROM user WHERE id >= 10 AND id <= 100;

-- B+树执行流程:
1. 定位到id=10的叶子节点
2. 沿着链表顺序扫描到id=100
3. 只需要顺序IO

-- 如果是红黑树:
1. 查找id=10
2. 查找id=11
3. 查找id=12
...
需要91次树遍历,91次随机IO

🎯 问题13:说说B+树和B树的区别

面试标准答案

特性B树B+树
数据存储所有节点都存数据只有叶子节点存数据
叶子节点不连接双向链表连接
非叶子节点存数据 + 索引只存索引
范围查询需要中序遍历叶子节点顺序扫描
查询稳定性不稳定(可能在非叶子节点找到)稳定(都在叶子节点)

为什么MySQL用B+树?

  1. 叶子节点链表 → 范围查询快
  2. 非叶子节点不存数据 → 扇出大,树更矮
  3. 所有数据在叶子节点 → 查询性能稳定

🎯 问题14:B+树的叶子节点都可以存哪些东西?

面试标准答案

聚簇索引(主键索引)

叶子节点存储:完整的数据行

叶子节点内容:
[主键值, 列1, 列2, 列3, ..., 所有列的数据]

示例:
[10, 'alice', 25, '13800138000', '2024-10-07 10:00:00']

非聚簇索引(二级索引)

叶子节点存储:索引列的值 + 主键值

叶子节点内容:
[索引列的值, 主键值]

示例(索引是name):
['alice', 10]
['bob', 25]
['charlie', 40]

对比

索引类型叶子节点内容是否需要回表
聚簇索引完整数据行❌ 不需要
非聚簇索引索引列值 + 主键值✅ 需要回表

🎯 问题15:为什么MySQL不用跳表?

面试标准答案

跳表的问题

  1. 不够稳定:跳表是概率数据结构,查询性能不稳定
  2. 不适合磁盘存储:跳表适合内存,不适合磁盘IO
  3. 范围查询不如B+树:B+树的叶子节点是链表,更适合范围扫描

B+树的优势

  1. 确定性算法(性能稳定)
  2. 适合磁盘存储(减少IO次数)
  3. 范围查询优秀

跳表的应用:Redis的有序集合(ZSet)用的是跳表(因为Redis是内存数据库)。


🎯 问题16:如果聚簇索引的数据更新,它的存储要不要变化?

面试标准答案(详见问题4):

分情况

  • 更新非主键列:存储位置不变
  • 更新主键列:存储位置变化(删除+插入)

建议:不要更新主键(性能差,可能页分裂)


🎯 问题17:什么字段适合加索引?为啥?

面试标准答案

适合加索引的字段

  1. WHERE条件中的列

    SELECT * FROM user WHERE status = 1;  -- status适合加索引
    
  2. JOIN条件中的列

    SELECT * FROM order o JOIN user u ON o.user_id = u.id;  -- user_id适合加索引
    
  3. ORDER BY的列

    SELECT * FROM order ORDER BY create_time DESC;  -- create_time适合加索引
    
  4. 选择性高的列(不重复值多)

    -- 手机号(选择性99%)✅ 适合
    -- 性别(选择性0.02%)❌ 不适合单独建索引
    
  5. 频繁查询的列

不适合加索引的字段

场景原因
更新频繁的列每次更新都要更新索引,开销大
选择性低的列(性别、状态)回表代价大于全表扫描
TEXT/BLOB大字段索引占用空间太大
查询很少的列索引浪费空间

🎉 结束语

晚上10点,哈吉米把面试高频题都复习了一遍。

哈吉米:"原来索引的问题这么多!聚簇索引、非聚簇索引、B+树、驱动表……"

南北绿豆:"对,索引是MySQL的核心,面试必问。"

阿西噶阿西:"记住几个关键点:主键用自增ID、被驱动表必须有索引、B+树叶子节点是双向链表、不要更新主键。"

哈吉米:"明天二面,我有信心了!"

南北绿豆:"加油!把这些原理讲清楚,面试官肯定满意!"


记忆口诀

聚簇索引数据存,非聚簇引存主键
B+树叶子是双向,范围查询顺序扫
主键推荐自增ID,UUID无序页分裂
小表驱动大表快,被驱动表索引建
子查询改JOIN写,性能提升几十倍


希望这篇文章能帮你搞定索引相关的面试题!把这些原理讲清楚,offer就是你的了!💪