我们经常会看到一句话:
给查询字段加索引,可以提高查询速度。
例如:
SELECT *
FROM user
WHERE id = 500000;
如果:
id
上有索引,查询通常会很快。
如果没有索引,数据量一大,查询性能就可能明显下降。
那么问题来了:
索引到底是什么?
为什么加了索引以后查询会变快?
为什么 InnoDB 的索引通常使用 B+Tree?
这篇我们就从最简单的一次查询开始,把这些问题串起来。
1. 没有索引会发生什么?
假设现在有一张:
user
表。
里面有:
100 万条数据
我们执行:
SELECT *
FROM user
WHERE age = 25;
假设:
age
字段没有索引。
数据库怎么找到:
age = 25
的数据?
最直接的方法就是:
从第一条开始看
↓
age 是不是 25?
↓
不是
↓
继续下一条
也就是:
第 1 行
↓
第 2 行
↓
第 3 行
↓
…
第 1000000 行
这种方式就是:
全表扫描
也就是常说的:
Full Table Scan
如果数据很少:
100 条
可能感觉不到什么问题。
但是如果变成:
100 万条
1000 万条
1 亿条
每次都大量扫描数据,查询成本就会越来越高。
2. 索引其实很像书的目录
假设我们有一本:
1000 页
的书。
现在想找:
MySQL 索引
这一章。
如果没有目录:
第 1 页
第 2 页
第 3 页
…
只能慢慢翻。
但是如果书前面有目录:
MySQL 基础 …….. 10
事务 ………….. 120
索引 ………….. 260
锁机制 ………… 400
那么:
我要找索引
↓
目录
↓
260 页
直接跳过去。
数据库索引也是类似的思想。
可以简单理解成:
索引是一种额外的数据结构,用来帮助数据库更快地定位数据。
原来:
直接扫描数据
变成:
先查索引
↓
找到数据位置
↓
读取对应数据
所以索引本质上就是:
空间换时间
需要额外存储一份索引结构,换取更高的查询效率。
3. 为什么不能直接把数据排好序?
假设有这些用户 ID:
1
2
3
4
5
6
7
8
9
10
因为已经有序。
找:
id = 8
是不是可以使用:
二分查找
?
当然可以。
例如:
1 2 3 4 5 6 7 8 9 10
↑
先找中间
然后不断缩小范围。
二分查找的查询效率非常高。
但是数据库的数据并不是:
只查询
还需要不断:
INSERT
UPDATE
DELETE
如果每插入一条数据,都要维护一个连续的大数组:
1 2 3 4 5 6 7 8
现在插入:
3.5
后面的数据可能都需要移动。
所以数据库需要一种:
既方便查找
又方便插入和删除
的数据结构。
于是我们自然会想到:
树
4. 二叉搜索树可以吗?
假设使用:
Binary Search Tree
也就是:
二叉搜索树
规则:
左边 < 当前节点 < 右边
例如:
8
/ \\
4 12
/ \\ / \\
2 6 10 14
如果要找:
10
过程:
8
↓
10 > 8
↓
走右边
12
↓
10 < 12
↓
走左边
10
只需要访问几个节点。
看起来已经很好了。
5. 普通二叉搜索树有一个严重问题
假设数据不是:
8
4
12
2
6
…
这样的顺序插入。
而是:
1
2
3
4
5
6
7
按照二叉搜索树插入以后:
1
\\
2
\\
3
\\
4
\\
5
\\
6
这棵树已经退化成:
链表
查找:
6
还是需要:
1 → 2 → 3 → 4 → 5 → 6
性能非常差。
所以普通二叉搜索树:
不够稳定
6. 那用 AVL 树或者红黑树呢?
既然普通二叉搜索树可能退化,那可以使用:
AVL Tree
Red-Black Tree
这些:
平衡二叉树
例如:
4
/ \\
2 6
/ \\ / \\
1 3 5 7
树的高度能够控制得比较低。
查找复杂度通常可以保持在:
O(log n)
在内存数据结构中已经非常优秀。
Java 中很多地方就会使用:
红黑树
例如 HashMap 在一定条件下会把链表转换成红黑树。
那为什么数据库索引不直接用红黑树呢?
7. 数据库真正昂贵的是什么?
理解这一点非常重要。
普通程序中的树通常放在:
内存
而数据库中的大量数据最终需要存储在:
磁盘 / SSD
CPU 在内存中比较几个数字:
非常快
但是从磁盘读取数据:
相对昂贵得多
因此数据库索引设计最重要的问题之一不是:
少比较几次
而是:
尽量减少存储页访问和 I/O 次数。
8. 二叉树最大的问题:树太高
二叉树每个节点最多:
2 个子节点
所以如果数据非常多:
1000 万条
树就会变得比较高。
例如可以想象:
Root
↓
Level 2
↓
Level 3
↓
Level 4
↓
Level 5
…
查询一条数据,需要沿着树:
一层一层往下找
如果这些节点分布在不同的数据页中:
访问一层
↓
可能需要读取一个页
再访问一层
↓
再读取一个页
树越高:
潜在 I/O 次数越多
所以数据库希望:
树尽量矮
于是出现了一个很直接的思路:
一个节点不要只放两个孩子,能不能一次放几十个、几百个甚至更多?
这就是:
多路平衡搜索树
的思路。
9. B-Tree 出现了
B-Tree 和二叉树最大的区别之一就是:
一个节点可以有多个子节点
例如:
[20 | 40 | 60]
/ | | \\
<20 20~40 40~60 >60
不再是:
左
右
两个方向。
而可能有很多分支。
这样同样数量的数据:
树高度会大幅下降
例如二叉树可能需要很多层:
Root
↓
2
↓
3
↓
4
↓
5
而多路树可能只有:
Root
↓
Middle
↓
Leaf
几层。
对于数据库来说:
树越矮
↓
需要访问的页越少
↓
I/O 次数越少
↓
查询越快
这已经非常适合数据库了。
10. 那为什么 InnoDB 更常用 B+Tree?
B+Tree 可以看作:
B-Tree 的一种变体
它针对数据库和文件系统这类场景做了非常适合范围查询和页式存储的设计。
一个简化后的 B+Tree:
[20 | 40]
/ | \\
/ | \\
[1…19] [20…39] [40…]
↓ ↓ ↓
Leaf Leaf Leaf
其中最值得理解的有三个特点。
11. 特点一:非叶子节点主要保存索引信息
在 B+Tree 中:
非叶子节点
主要用于:
导航
例如:
[20 | 40]
/ | \\
这些值告诉数据库:
小于 20 往左
20~40 往中间
大于等于 40 往右
真正的数据记录或记录定位信息主要集中在:
叶子节点
这样做有什么好处?
因为:
非叶子节点不需要塞完整记录
同一个数据页中就可以放:
更多索引项
于是一个节点能够拥有:
更多分支
结果:
分支越多
↓
树越矮
↓
查询所需页访问越少
12. 一个节点能放多少个分支?
假设一个数据页大小为:
16KB
InnoDB 默认页大小通常就是:
16KB
如果一个内部索引项只占:
十几字节
那么一个页里可能容纳:
上千个索引项
假设简单估算:
一个节点可以有 1000 个分支
第一层:
1000
第二层:
1000 × 1000
=
100 万
第三层再扩展:
10 亿级别
当然真实情况会受到:
主键大小
页利用率
记录大小
页结构
等因素影响。
但它说明了一个核心事实:
B+Tree 的分支因子很大,因此即使数据量很大,树通常仍然可以保持较低高度。
这正是数据库索引非常需要的特性。
13. 特点二:数据集中在叶子节点
假设查询:
SELECT *
FROM user
WHERE id = 35;
B+Tree:
[20 | 40]
/ | \\
↓
[20…39]
↓
35
数据库通过内部节点不断缩小范围。
最终到:
叶子节点
找到数据。
而内部节点:
只负责导航
这让 B+Tree 的内部节点能够存放更多索引项。
14. 特点三:叶子节点之间通常是有序连接的
这是 B+Tree 特别适合数据库的另一个重要原因。
叶子节点并不是完全独立的。
可以理解成:
Leaf 1
↔
Leaf 2
↔
Leaf 3
↔
Leaf 4
里面的数据整体按照索引键有序。
例如:
[1 2 3 4]
↔
[5 6 7 8]
↔
[9 10 11 12]
这对:
范围查询
特别友好。
15. 为什么范围查询特别适合 B+Tree?
比如执行:
SELECT *
FROM user
WHERE id BETWEEN 100 AND 200;
数据库先通过 B+Tree 找到:
100
的位置。
然后:
100
↓
101
↓
102
↓
…
200
沿着叶子节点的有序结构向后扫描即可。
可以简单理解成:
B+Tree
↓
快速定位 100
↓
叶子节点顺序扫描
↓
一直扫到 200
而不需要:
100 查一次
101 再从根节点查一次
102 再从根节点查一次
所以像:
>
<
BETWEEN
ORDER BY
范围扫描
这类场景,都能很好地利用 B+Tree 的有序结构。
16. B-Tree 和 B+Tree 最大区别是什么?
入门阶段没必要死记特别复杂的定义。
重点记两个区别即可。
B-Tree
内部节点:
也可能保存实际数据
可以理解成:
[数据]
/ \\
[数据] [数据]
B+Tree
内部节点主要:
保存索引和导航信息
真正的数据集中:
叶子节点
而叶子节点又:
有序连接
简单表示:
[Index]
/ \\
[Index] [Index]
↓ ↓
Leaf ↔ Leaf ↔ Leaf
所以 B+Tree 特别适合:
磁盘页存储
范围查询
排序
大量数据查询
17. MySQL 所有索引都是 B+Tree 吗?
不是。
这个说法需要特别注意。
MySQL 是:
数据库管理系统
底层可以使用不同的:
Storage Engine
也就是存储引擎。
比如最常见的:
InnoDB
InnoDB 中最常见的索引结构是:
B+Tree
但并不是说:
MySQL 所有索引永远只有 B+Tree
例如还有:
Hash 类索引能力
全文索引
空间索引
不同引擎和不同索引类型,底层结构可能不同。
所以更加严谨的说法应该是:
InnoDB 中常见的普通索引和主键索引通常使用 B+Tree 组织。
18. InnoDB 的主键索引还有一个特殊点
假设有表:
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
age INT
);
InnoDB 的主键索引:
PRIMARY KEY
不仅仅是:
id → 数据地址
那么简单。
它的叶子节点存放的是:
完整行记录
可以简化理解:
主键 B+Tree
↓
┌──────────────┐
│ id = 1 │
│ name = 张三 │
│ age = 20 │
└──────────────┘
这种结构叫:
聚簇索引
也就是:
Clustered Index
可以简单理解成:
数据本身就是按照主键索引的 B+Tree 组织起来的。
所以:
SELECT *
FROM user
WHERE id = 100;
通过主键索引找到叶子节点以后:
基本就找到了完整行数据
19. 普通索引又有什么不同?
假设再建立一个:
CREATE INDEX idx_age
ON user(age);
这个:
idx_age
属于:
二级索引
它的 B+Tree 叶子节点主要保存:
age
+
主键 id
例如:
age = 20
↓
id = 100
所以执行:
SELECT *
FROM user
WHERE age = 20;
可能先:
idx_age B+Tree
↓
找到 id = 100
然后再:
主键 B+Tree
↓
找到完整行数据
这个过程就是我们后面会经常听到的:
回表
也就是:
先通过二级索引找到主键,再通过主键索引找到完整数据。
这也是为什么理解 B+Tree 之后,后面学习:
回表
覆盖索引
联合索引
会容易很多。
20. 索引为什么不能越多越好?
既然索引可以提高查询速度,那是不是:
每个字段都建一个索引
最好?
当然不是。
因为索引本质上也是:
一份额外的数据结构
例如插入:
INSERT INTO user ...
不仅要写入:
表数据
还要维护:
主键索引
age 索引
name 索引
其他索引
所以:
索引越多
↓
INSERT 成本越高
UPDATE 成本越高
DELETE 成本越高
同时还会:
占用更多磁盘空间
所以索引实际上是在:
查询速度
和:
写入成本 + 存储成本
之间做权衡。
这也是为什么:
索引不是越多越好,而应该建立在真正有查询价值的字段和查询组合上。
21. 为什么低区分度字段不一定适合单独建索引?
假设:
gender
字段只有:
男
女
两种值。
100 万条数据:
男:50 万
女:50 万
执行:
SELECT *
FROM user
WHERE gender = '男';
即使建立索引:
一次仍然要取出大约 50 万条数据
索引能够帮你快速找到:
这些记录从哪里开始
但后面依然要读取大量数据。
所以这类字段:
区分度很低
单独建立索引的收益可能有限。
而像:
身份证号
手机号
用户 ID
这种:
不同值很多
的字段,通常具有更好的选择性。
当然,是否使用某个索引最终还会由优化器根据:
数据分布
成本估算
查询条件
综合判断。
所以不能简单说:
低区分度字段绝对不能建索引
而应该说:
低选择性字段单独建立 B+Tree 索引,很多查询场景下收益可能比较有限。
22. 把整个过程串起来
现在回头看:
SELECT *
FROM user
WHERE id = 500000;
如果没有索引:
user 表
↓
大量扫描数据
↓
找到 500000
如果有主键 B+Tree:
B+Tree Root
↓
中间节点
↓
叶子节点
↓
id = 500000
↓
完整行数据
核心区别就是:
全表扫描
VS
利用有序索引结构不断缩小查找范围
B+Tree 又通过:
一个节点多个分支
让:
树非常矮
从而减少页访问。
同时:
叶子节点有序连接
又让范围查询非常方便。
这就是 B+Tree 非常适合数据库索引的重要原因。
23. 面试时怎么回答为什么 MySQL 索引用 B+Tree?
如果面试中被问:
为什么 MySQL 索引通常使用 B+Tree?
不要只回答:
因为 B+Tree 查询快
可以从三个方向回答。
第一:
B+Tree 是多路平衡搜索树
一个节点可以有大量子节点,所以:
树高度低
能够减少磁盘页访问和 I/O。
第二:
内部节点主要保存索引导航信息
单个页可以容纳更多索引项,进一步提高分支因子。
第三:
叶子节点有序连接
因此:
范围查询
顺序扫描
排序
都比较高效。

