返回首页面试题

面试必备:MySQL 索引结构详解

2026年03月25日9 min read

面试必备:MySQL 索引结构详解

为什么需要索引?

没有索引:全表扫描,10万条数据查一条,要扫 10万次。

有索引:二分查找,10万条数据查一条,最多 17 次。

无索引:  [1][2][3][4][5][6][7][8][9][10]...  → 查找 100000 次
            ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓
           找到目标要扫描全表

有索引:  [查找目标在第 50000 位]
            ↓ 二分
           [前半区|后半区]  → 只用查 17 次!

索引底层:数据结构演进

1. 哈希索引

键值对,O(1) 查找
┌────────┬────────┐
│  key   │ value  │
├────────┼────────┤
│ hash  │  0x123 │
│ code  │  0x456 │
│ name  │  0x789 │
└────────┴────────┘

缺点

  • 不支持范围查询 WHERE id > 10
  • 不支持排序
  • 哈希冲突多时性能退化

适用:等值查询,如 Redis

2. B-Tree(平衡多路查找树)

        [17 | 35]
       /    |     \
    [8]   [17]   [35|46]
   / | \  / \    / | \
 [1][4][8] [15][16][18][35][46][78]

特点

  • 每个节点有多个子节点(不像二叉树只有两个)
  • 所有叶子节点在同一层
  • 自平衡

3. B+Tree(MySQL 采用)

                    [17 | 35]
                   /    |     \
           ┌──────┘     │     └──────┐
           ▼            ▼            ▼
      [17|35]      [17|35]      [17|35]
         ↓            ↓            ↓
    ┌────┴────┐  ┌────┴────┐  ┌────┴────┐
    ↓         ↓  ↓         ↓  ↓         ↓
  [所有数据]  [所有数据]  [所有数据]  ← 叶子节点包含所有数据

B+Tree vs B-Tree

特性B-TreeB+Tree
叶子节点存放所有数据只存放索引,叶子链表连接
非叶子节点存放数据只存放索引
搜索最多 O(log n)最多 O(log n)
范围查询需要回表叶子链表遍历即可
查询效率不稳定(深度可能不同)稳定(所有叶子深度相同)

MySQL InnoDB 的 B+Tree 结构

                        ┌─────────────────┐
                        │    根节点/索引    │
                        │   [主键索引页]    │
                        └────────┬─────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              ▼                  ▼                  ▼
    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
    │  索引页 1   │    │  索引页 2   │    │  索引页 3   │
    │ [1][101][201]│    │ [51][151][251]│   │ [76][176][276]│
    └──────┬──────┘    └──────┬──────┘    └──────┬──────┘
           │                  │                  │
           ▼                  ▼                  ▼
   ┌───────────────┐  ┌───────────────┐  ┌───────────────┐
   │  叶子节点     │←→│  叶子节点     │←→│  叶子节点     │
   │ [1][name]...  │  │ [101][name]... │  │ [201][name]... │
   │ [2][name]...  │  │ [102][name]... │  │ [202][name]... │
   │ [3][name]...  │  │ [103][name]... │  │ [203][name]... │
   └───────────────┘  └───────────────┘  └───────────────┘

MySQL 存储

  • 16KB 为一页
  • B+Tree 深度一般 2-3 层(千万级数据)
  • 3 层 B+Tree 可索引 20 亿条数据

聚簇索引 vs 非聚簇索引

这是 MySQL 面试的重中之重!

聚簇索引(Clustered Index)

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

InnoDB 表:
├─ 主键索引 → 叶子节点存整行数据(聚簇索引)
└─ 其他索引 → 叶子节点存主键值(非聚簇索引/辅助索引)

查询:
SELECT * FROM user WHERE id = 1
  → 直接走主键索引
  → 叶子节点就是数据,无需回表
  → 只需要一次查询!

非聚簇索引(辅助索引/二级索引)

定义:叶子节点存储索引列 + 主键值。

查询:
SELECT * FROM user WHERE name = '张三'
  → 先走 name 索引
  → 叶子节点找到主键 id = 5
  → 再回到主键索引找完整数据(回表)
  → 需要两次查询!

两者对比

特性聚簇索引非聚簇索引
叶子节点存储完整数据存储索引+主键
数量一个表只有一个可以有多个
查找直接获取数据可能需要回表
插入可能有页分裂直接插入

回表查询

-- 先在 name 索引中查找
SELECT * FROM user WHERE name = '张三'

-- 执行过程:
-- 1. 在 name 索引树中查找,找到 name='张三' 对应的主键 id=5
-- 2. 根据 id=5 回主键索引查找完整行数据
-- 3. 返回结果

如何减少回表?

-- 只查索引列,不需要回表(覆盖索引)
SELECT name FROM user WHERE name = '张三'

-- 使用索引覆盖
SELECT name, age FROM user WHERE name = '张三' AND age = 18
-- 如果 (name, age) 是联合索引,可以一次查询

最左前缀原则(联合索引)

-- 创建联合索引
CREATE INDEX idx_name_age ON user(name, age)

-- 查询命中索引的情况:WHERE name = '张三'          -- 命中 nameWHERE name = '张三' AND age = 18  -- 命中 name + ageWHERE name IN ('张三', '李四')    -- 命中 nameWHERE age = 18               -- 不命中(没有 name)WHERE age = 18 AND name = '张三'  -- 顺序错,但 MySQL 优化器会调整

索引失效的情况

-- 1. 左 LIKE 模糊匹配
SELECT * FROM user WHERE name LIKE '%张'  -- ✗ 索引失效
SELECT * FROM user WHERE name LIKE '张%'  -- ✓ 索引有效

-- 2. 使用函数/计算
SELECT * FROM user WHERE LEFT(name, 1) = '张'  -- ✗ 索引失效
SELECT * FROM user WHERE name = '张三'        -- ✓ 正常

-- 3. 类型转换
SELECT * FROM user WHERE name = 123  -- ✗ 索引失效(字符串列,参数是数字)

-- 4. OR 连接(OR 前后有非索引列)
SELECT * FROM user WHERE name = '张三' OR age = 18
-- ✗ name 有索引,age 没有,索引失效

-- 5. NOT / != / <>
SELECT * FROM user WHERE name != '张三'  -- ✗ 索引失效

-- 6. 隐式类型转换
ALTER TABLE user MODIFY phone VARCHAR(20);
SELECT * FROM user WHERE phone = 13800138000  -- ✗ 索引失效(字符串列用数字比较)

索引分类

类型说明
主键索引主键列,值唯一非空,聚簇索引
唯一索引值唯一,可以有 NULL
普通索引普通列
联合索引多个列的组合索引
全文索引文本内容搜索(MyISAM)
-- 主键索引
ALTER TABLE user ADD PRIMARY KEY(id);

-- 唯一索引
ALTER TABLE user ADD UNIQUE(email);

-- 普通索引
ALTER TABLE user ADD INDEX idx_name(name);

-- 联合索引
ALTER TABLE user ADD INDEX idx_name_age(name, age);

-- 全文索引(MyISAM)
ALTER TABLE article ADD FULLTEXT(title, content);

面试高频问题

Q1:为什么 MySQL 选 B+Tree 而不是 B-Tree?

  1. 范围查询友好:叶子节点链表连接,范围查询只需遍历链表
  2. 查询稳定:所有叶子节点深度相同,查询效率稳定
  3. 更适合磁盘:非叶子节点不存数据,可以放更多索引,降低树高

Q2:InnoDB 和 MyISAM 索引的区别?

特性InnoDBMyISAM
聚簇索引支持(主键)不支持
索引存储叶子节点存数据叶子节点存指针
数据存储.ibd 文件.MYD + .MYI
事务支持不支持

Q3:主键为什么建议用自增 ID?

  • 插入时追加到末尾,避免页分裂
  • 用 UUID 或字符串:插入位置不确定,可能导致大量页分裂

Q4:什么是页分裂?

InnoDB 以页为单位存储(16KB)。当插入数据导致页满时,需要分裂成两个页,影响性能。

页满了
┌────────────────────────┐
│ [1][2][3][4][5][6][7][8]│
└────────────────────────┘
        ↓ 插入 4.5
   需要分裂成两页
┌────────────┐ ┌────────────┐
│ [1][2][3][4]│ │[4.5][5][6]..│
└────────────┘ └────────────┘

自增 ID 追加到末尾,分裂概率低。


总结

索引结构:
├─ 哈希索引:O(1),不支持范围
├─ B-Tree:多路平衡树
└─ B+Tree:B-Tree 变种,MySQL 采用

B+Tree 特点:
├─ 叶子节点链表连接
├─ 非叶子节点只存索引
└─ 深度一般 2-3 层

聚簇 vs 非聚簇:
├─ 聚簇:叶子节点存完整数据
└─ 非聚簇:叶子节点存主键,需回表

最左前缀原则:联合索引从左开始匹配

评论区