1. 索引基础:什么是索引、索引的优缺点、B+树索引结构图解

大家好,我是老张。今天咱们正式开始聊索引。

说实话,我见过太多开发同学,写 SQL 那叫一个溜,一遇到慢查询就抓瞎。问他们索引是啥,答得上来;问他们为啥建了索引还慢,就支支吾吾了。嗯,这章咱们就把索引的底裤扒干净。

1.1 什么是索引?

索引,说白了就是一本「书的目录」。你想想看,一本 500 页的技术书,没有目录,你想找「B+树」这个词,是不是得从头翻到尾?有了目录,直接翻到对应页码就行。

数据库里的索引也是这个道理。它是一张单独的数据结构,存储着「数据的位置」信息。查询时,数据库先查索引,找到数据在磁盘上的位置,再直接去取。这就避免了全表扫描——也就是把整张表从头到尾读一遍。

核心定义:索引是数据库管理系统中的一种数据结构,用于快速定位表中的特定数据行,而无需扫描整个表。

我个人习惯把索引比作「高速公路的出口指示牌」。没有指示牌,你得开到每个出口去看看是不是你要下的那个;有了指示牌,远远地就知道该在哪个出口减速了。

1.2 索引的优缺点

任何技术都有两面性,索引也不例外。我在项目中遇到过不少「索引滥用」的案例,建了一堆索引,结果写入慢得像蜗牛。

优点 缺点
大幅提升查询速度,尤其是大表 占用额外的磁盘空间
保证数据的唯一性(唯一索引) 降低数据写入(INSERT/UPDATE/DELETE)的速度
加速表与表之间的连接(JOIN) 需要额外维护,索引可能碎片化
减少查询中需要扫描的数据量 创建索引本身需要时间

避坑指南:我曾经接手过一个项目,一张只有 10 万行的表,建了 12 个索引。每次插入数据要等好几秒。后来我删掉了 8 个冗余索引,写入速度提升了 10 倍。记住:索引不是越多越好,够用就行。

为什么会这样?因为每次写入数据,数据库不仅要更新表数据,还要更新所有相关的索引。索引越多,维护成本越高。你想想看,一本书的目录如果每页都更新一次,那得多累?

1.3 B+树索引结构图解

聊到索引,就绕不开 B+树。这是 MySQL InnoDB 存储引擎默认的索引结构。很多同学觉得 B+树很神秘,其实拆开来看,一点都不复杂。

B+树是一种多路平衡查找树。它有两个核心特点:

  • 非叶子节点只存键值,不存数据。这样每个节点能存更多的键,树的高度就更低。
  • 叶子节点存数据,并且通过链表相连。这样范围查询就非常快——找到起点后,顺着链表往后读就行。

下面这张图,是我手绘的 B+树结构。你看一遍就能懂。

B+树索引结构示意图 B+树索引结构示意图 50 120 20 35 70 95 140 180 10 (data) 25 (data) 40 (data) 55 (data) 80 (data) 100 (data) 130 (data) 160 (data) 图解说明: • 蓝色节点:根节点,存储键值 50 和 120,不存数据 • 橙色节点:内部节点,存储键值,用于路由查询路径 • 绿色节点:叶子节点,存储实际数据(键 + 行指针) • 红色箭头:叶子节点之间的双向链表,支持高效范围查询 • 查询过程:从根节点开始,逐层向下,最终在叶子节点找到数据

你看,查询一个值,比如 55,过程是这样的:

  1. 从根节点开始,55 比 50 大,比 120 小,所以走中间的分支。
  2. 到达内部节点,55 比 70 小,走左边分支。
  3. 到达叶子节点,找到 55 对应的数据。

整个过程只需要 3 次 I/O。如果全表扫描,可能得几百上千次。这就是 B+树快的原因。

小技巧:B+树的高度通常只有 3-4 层。也就是说,哪怕一张表有上亿行数据,查询也只需要 3-4 次磁盘 I/O。我建议你在设计表结构时,尽量让索引键短小精悍,这样每个节点能存更多的键,树的高度就更低,查询更快。

1.4 聚簇索引 vs 非聚簇索引

聊完 B+树,咱们再聊聊两种常见的索引类型。这也是面试高频题。

特性 聚簇索引 非聚簇索引
数据存储 叶子节点直接存整行数据 叶子节点存主键值(回表)
数量限制 一张表只能有一个 一张表可以有多个
查询速度 主键查询极快,无需回表 可能需要回表,多一次 I/O
典型代表 InnoDB 的主键索引 InnoDB 的二级索引

我举个例子你就明白了。假设有一张用户表,主键是 id。你用 SELECT * FROM users WHERE id = 100 查询,走的是聚簇索引,直接就能拿到整行数据。

但如果你用 SELECT * FROM users WHERE name = '张三' 查询,而 name 上建了二级索引,过程是这样的:

  1. 先查 name 索引树,找到主键 id。
  2. 再用主键 id 回表查聚簇索引,拿到整行数据。

这就是所谓的「回表查询」。多了一次 I/O,所以二级索引查询比主键查询慢一点点。

核心建议:我建议你在建表时,尽量使用自增整数作为主键。这样 B+树在插入新数据时,只需要在叶子节点末尾追加,不需要频繁地分裂节点。用 UUID 做主键?嗯,那会导致索引频繁分裂,性能会差很多。

1.5 什么时候该建索引?

最后,我分享几条实战经验。什么时候该建索引,什么时候不该建,我踩过的坑可不少。

该建索引的场景:

  • 经常出现在 WHERE 条件中的列
  • 经常用于 JOIN 连接的列
  • 经常用于 ORDER BY 排序的列
  • 经常用于 GROUP BY 分组的列
  • 数据量大的表(超过 10 万行)

不该建索引的场景:

  • 数据量很小的表(几千行以内,全表扫描更快)
  • 频繁写入的列(索引维护成本高)
  • 区分度很低的列(比如性别,只有男/女,查出来还是一大片)
  • 很少出现在查询条件中的列

避坑指南:我曾经在一个日志表上,给 status 字段建了索引。status 只有 3 个值:成功、失败、处理中。结果查询还是慢。为什么?因为区分度太低,查「成功」能查出 80% 的数据,数据库觉得还不如全表扫描。嗯,从那以后,我建索引前都会先算一下区分度。