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+树结构。你看一遍就能懂。
你看,查询一个值,比如 55,过程是这样的:
- 从根节点开始,55 比 50 大,比 120 小,所以走中间的分支。
- 到达内部节点,55 比 70 小,走左边分支。
- 到达叶子节点,找到 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 上建了二级索引,过程是这样的:
- 先查 name 索引树,找到主键 id。
- 再用主键 id 回表查聚簇索引,拿到整行数据。
这就是所谓的「回表查询」。多了一次 I/O,所以二级索引查询比主键查询慢一点点。
核心建议:我建议你在建表时,尽量使用自增整数作为主键。这样 B+树在插入新数据时,只需要在叶子节点末尾追加,不需要频繁地分裂节点。用 UUID 做主键?嗯,那会导致索引频繁分裂,性能会差很多。
1.5 什么时候该建索引?
最后,我分享几条实战经验。什么时候该建索引,什么时候不该建,我踩过的坑可不少。
该建索引的场景:
- 经常出现在 WHERE 条件中的列
- 经常用于 JOIN 连接的列
- 经常用于 ORDER BY 排序的列
- 经常用于 GROUP BY 分组的列
- 数据量大的表(超过 10 万行)
不该建索引的场景:
- 数据量很小的表(几千行以内,全表扫描更快)
- 频繁写入的列(索引维护成本高)
- 区分度很低的列(比如性别,只有男/女,查出来还是一大片)
- 很少出现在查询条件中的列
避坑指南:我曾经在一个日志表上,给 status 字段建了索引。status 只有 3 个值:成功、失败、处理中。结果查询还是慢。为什么?因为区分度太低,查「成功」能查出 80% 的数据,数据库觉得还不如全表扫描。嗯,从那以后,我建索引前都会先算一下区分度。