MySQL 索引为什么能加快查询?B+Tree 到底是什么?

我们经常会看到一句话:

给查询字段加索引,可以提高查询速度。

例如:

sql 复制代码
SELECT *
FROM user
WHERE id = 500000;

如果:

text 复制代码
id

上有索引,查询通常会很快。

如果没有索引,数据量一大,查询性能就可能明显下降。

那么问题来了:

text 复制代码
索引到底是什么?

为什么加了索引以后查询会变快?

为什么 InnoDB 的索引通常使用 B+Tree?

这篇我们就从最简单的一次查询开始,把这些问题串起来。


1. 没有索引会发生什么?

假设现在有一张:

text 复制代码
user

表。

里面有:

text 复制代码
100 万条数据

我们执行:

sql 复制代码
SELECT *
FROM user
WHERE age = 25;

假设:

text 复制代码
age

字段没有索引。

数据库怎么找到:

text 复制代码
age = 25

的数据?

最直接的方法就是:

text 复制代码
从第一条开始看
      ↓
age 是不是 25?
      ↓
不是
      ↓
继续下一条

也就是:

text 复制代码
第 1 行
 ↓
第 2 行
 ↓
第 3 行
 ↓
...
第 1000000 行

这种方式就是:

text 复制代码
全表扫描

也就是常说的:

text 复制代码
Full Table Scan

如果数据很少:

text 复制代码
100 条

可能感觉不到什么问题。

但是如果变成:

text 复制代码
100 万条

1000 万条

1 亿条

每次都大量扫描数据,查询成本就会越来越高。


2. 索引其实很像书的目录

假设我们有一本:

text 复制代码
1000 页

的书。

现在想找:

text 复制代码
MySQL 索引

这一章。

如果没有目录:

text 复制代码
第 1 页
第 2 页
第 3 页
...

只能慢慢翻。

但是如果书前面有目录:

text 复制代码
MySQL 基础 ........ 10

事务 .............. 120

索引 .............. 260

锁机制 ............ 400

那么:

text 复制代码
我要找索引
   ↓
目录
   ↓
260 页

直接跳过去。

数据库索引也是类似的思想。

可以简单理解成:

索引是一种额外的数据结构,用来帮助数据库更快地定位数据。

原来:

text 复制代码
直接扫描数据

变成:

text 复制代码
先查索引
   ↓
找到数据位置
   ↓
读取对应数据

所以索引本质上就是:

text 复制代码
空间换时间

需要额外存储一份索引结构,换取更高的查询效率。


3. 为什么不能直接把数据排好序?

假设有这些用户 ID:

text 复制代码
1
2
3
4
5
6
7
8
9
10

因为已经有序。

找:

text 复制代码
id = 8

是不是可以使用:

text 复制代码
二分查找

当然可以。

例如:

text 复制代码
1 2 3 4 5 6 7 8 9 10
        ↑
       先找中间

然后不断缩小范围。

二分查找的查询效率非常高。

但是数据库的数据并不是:

text 复制代码
只查询

还需要不断:

text 复制代码
INSERT

UPDATE

DELETE

如果每插入一条数据,都要维护一个连续的大数组:

text 复制代码
1 2 3 4 5 6 7 8

现在插入:

text 复制代码
3.5

后面的数据可能都需要移动。

所以数据库需要一种:

text 复制代码
既方便查找

又方便插入和删除

的数据结构。

于是我们自然会想到:

text 复制代码

4. 二叉搜索树可以吗?

假设使用:

text 复制代码
Binary Search Tree

也就是:

text 复制代码
二叉搜索树

规则:

text 复制代码
左边 < 当前节点 < 右边

例如:

text 复制代码
        8
       / \
      4   12
     / \  / \
    2  6 10 14

如果要找:

text 复制代码
10

过程:

text 复制代码
8
↓
10 > 8
↓
走右边

12
↓
10 < 12
↓
走左边

10

只需要访问几个节点。

看起来已经很好了。


5. 普通二叉搜索树有一个严重问题

假设数据不是:

text 复制代码
8
4
12
2
6
...

这样的顺序插入。

而是:

text 复制代码
1
2
3
4
5
6
7

按照二叉搜索树插入以后:

text 复制代码
1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6

这棵树已经退化成:

text 复制代码
链表

查找:

text 复制代码
6

还是需要:

text 复制代码
1 → 2 → 3 → 4 → 5 → 6

性能非常差。

所以普通二叉搜索树:

text 复制代码
不够稳定

6. 那用 AVL 树或者红黑树呢?

既然普通二叉搜索树可能退化,那可以使用:

text 复制代码
AVL Tree

Red-Black Tree

这些:

text 复制代码
平衡二叉树

例如:

text 复制代码
        4
      /   \
     2     6
    / \   / \
   1   3 5   7

树的高度能够控制得比较低。

查找复杂度通常可以保持在:

text 复制代码
O(log n)

在内存数据结构中已经非常优秀。

Java 中很多地方就会使用:

text 复制代码
红黑树

例如 HashMap 在一定条件下会把链表转换成红黑树。

那为什么数据库索引不直接用红黑树呢?


7. 数据库真正昂贵的是什么?

理解这一点非常重要。

普通程序中的树通常放在:

text 复制代码
内存

而数据库中的大量数据最终需要存储在:

text 复制代码
磁盘 / SSD

CPU 在内存中比较几个数字:

text 复制代码
非常快

但是从磁盘读取数据:

text 复制代码
相对昂贵得多

因此数据库索引设计最重要的问题之一不是:

text 复制代码
少比较几次

而是:

尽量减少存储页访问和 I/O 次数。


8. 二叉树最大的问题:树太高

二叉树每个节点最多:

text 复制代码
2 个子节点

所以如果数据非常多:

text 复制代码
1000 万条

树就会变得比较高。

例如可以想象:

text 复制代码
Root
 ↓
Level 2
 ↓
Level 3
 ↓
Level 4
 ↓
Level 5
 ...

查询一条数据,需要沿着树:

text 复制代码
一层一层往下找

如果这些节点分布在不同的数据页中:

text 复制代码
访问一层
   ↓
可能需要读取一个页

再访问一层
   ↓
再读取一个页

树越高:

text 复制代码
潜在 I/O 次数越多

所以数据库希望:

text 复制代码
树尽量矮

于是出现了一个很直接的思路:

一个节点不要只放两个孩子,能不能一次放几十个、几百个甚至更多?

这就是:

text 复制代码
多路平衡搜索树

的思路。


9. B-Tree 出现了

B-Tree 和二叉树最大的区别之一就是:

text 复制代码
一个节点可以有多个子节点

例如:

text 复制代码
                [20 | 40 | 60]
              /      |      |      \
           <20    20~40   40~60    >60

不再是:

text 复制代码
左

右

两个方向。

而可能有很多分支。

这样同样数量的数据:

text 复制代码
树高度会大幅下降

例如二叉树可能需要很多层:

text 复制代码
Root
 ↓
2
 ↓
3
 ↓
4
 ↓
5

而多路树可能只有:

text 复制代码
Root
 ↓
Middle
 ↓
Leaf

几层。

对于数据库来说:

text 复制代码
树越矮
    ↓
需要访问的页越少
    ↓
I/O 次数越少
    ↓
查询越快

这已经非常适合数据库了。


10. 那为什么 InnoDB 更常用 B+Tree?

B+Tree 可以看作:

text 复制代码
B-Tree 的一种变体

它针对数据库和文件系统这类场景做了非常适合范围查询和页式存储的设计。

一个简化后的 B+Tree:

text 复制代码
                    [20 | 40]
                   /    |    \
                  /     |     \
          [1...19] [20...39] [40...]
              ↓        ↓        ↓
           Leaf     Leaf     Leaf

其中最值得理解的有三个特点。


11. 特点一:非叶子节点主要保存索引信息

在 B+Tree 中:

text 复制代码
非叶子节点

主要用于:

text 复制代码
导航

例如:

text 复制代码
           [20 | 40]
          /    |    \

这些值告诉数据库:

text 复制代码
小于 20 往左

20~40 往中间

大于等于 40 往右

真正的数据记录或记录定位信息主要集中在:

text 复制代码
叶子节点

这样做有什么好处?

因为:

text 复制代码
非叶子节点不需要塞完整记录

同一个数据页中就可以放:

text 复制代码
更多索引项

于是一个节点能够拥有:

text 复制代码
更多分支

结果:

text 复制代码
分支越多
   ↓
树越矮
   ↓
查询所需页访问越少

12. 一个节点能放多少个分支?

假设一个数据页大小为:

text 复制代码
16KB

InnoDB 默认页大小通常就是:

text 复制代码
16KB

如果一个内部索引项只占:

text 复制代码
十几字节

那么一个页里可能容纳:

text 复制代码
上千个索引项

假设简单估算:

text 复制代码
一个节点可以有 1000 个分支

第一层:

text 复制代码
1000

第二层:

text 复制代码
1000 × 1000
=
100 万

第三层再扩展:

text 复制代码
10 亿级别

当然真实情况会受到:

text 复制代码
主键大小

页利用率

记录大小

页结构

等因素影响。

但它说明了一个核心事实:

B+Tree 的分支因子很大,因此即使数据量很大,树通常仍然可以保持较低高度。

这正是数据库索引非常需要的特性。


13. 特点二:数据集中在叶子节点

假设查询:

sql 复制代码
SELECT *
FROM user
WHERE id = 35;

B+Tree:

text 复制代码
             [20 | 40]
             /   |   \
                 ↓
             [20...39]
                 ↓
                 35

数据库通过内部节点不断缩小范围。

最终到:

text 复制代码
叶子节点

找到数据。

而内部节点:

text 复制代码
只负责导航

这让 B+Tree 的内部节点能够存放更多索引项。


14. 特点三:叶子节点之间通常是有序连接的

这是 B+Tree 特别适合数据库的另一个重要原因。

叶子节点并不是完全独立的。

可以理解成:

text 复制代码
Leaf 1
  ↔
Leaf 2
  ↔
Leaf 3
  ↔
Leaf 4

里面的数据整体按照索引键有序。

例如:

text 复制代码
[1 2 3 4]
     ↔
[5 6 7 8]
     ↔
[9 10 11 12]

这对:

text 复制代码
范围查询

特别友好。


15. 为什么范围查询特别适合 B+Tree?

比如执行:

sql 复制代码
SELECT *
FROM user
WHERE id BETWEEN 100 AND 200;

数据库先通过 B+Tree 找到:

text 复制代码
100

的位置。

然后:

text 复制代码
100
 ↓
101
 ↓
102
 ↓
...
200

沿着叶子节点的有序结构向后扫描即可。

可以简单理解成:

text 复制代码
B+Tree
   ↓
快速定位 100
   ↓
叶子节点顺序扫描
   ↓
一直扫到 200

而不需要:

text 复制代码
100 查一次

101 再从根节点查一次

102 再从根节点查一次

所以像:

sql 复制代码
>

<

BETWEEN

ORDER BY

范围扫描

这类场景,都能很好地利用 B+Tree 的有序结构。


16. B-Tree 和 B+Tree 最大区别是什么?

入门阶段没必要死记特别复杂的定义。

重点记两个区别即可。

B-Tree

内部节点:

text 复制代码
也可能保存实际数据

可以理解成:

text 复制代码
       [数据]
       /    \
   [数据]  [数据]

B+Tree

内部节点主要:

text 复制代码
保存索引和导航信息

真正的数据集中:

text 复制代码
叶子节点

而叶子节点又:

text 复制代码
有序连接

简单表示:

text 复制代码
        [Index]
       /       \
  [Index]     [Index]
     ↓           ↓
Leaf ↔ Leaf ↔ Leaf

所以 B+Tree 特别适合:

text 复制代码
磁盘页存储

范围查询

排序

大量数据查询

17. MySQL 所有索引都是 B+Tree 吗?

不是。

这个说法需要特别注意。

MySQL 是:

text 复制代码
数据库管理系统

底层可以使用不同的:

text 复制代码
Storage Engine

也就是存储引擎。

比如最常见的:

text 复制代码
InnoDB

InnoDB 中最常见的索引结构是:

text 复制代码
B+Tree

但并不是说:

text 复制代码
MySQL 所有索引永远只有 B+Tree

例如还有:

text 复制代码
Hash 类索引能力

全文索引

空间索引

不同引擎和不同索引类型,底层结构可能不同。

所以更加严谨的说法应该是:

InnoDB 中常见的普通索引和主键索引通常使用 B+Tree 组织。


18. InnoDB 的主键索引还有一个特殊点

假设有表:

sql 复制代码
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    age INT
);

InnoDB 的主键索引:

text 复制代码
PRIMARY KEY

不仅仅是:

text 复制代码
id → 数据地址

那么简单。

它的叶子节点存放的是:

text 复制代码
完整行记录

可以简化理解:

text 复制代码
             主键 B+Tree
                  ↓
        ┌──────────────┐
        │ id = 1       │
        │ name = 张三   │
        │ age = 20     │
        └──────────────┘

这种结构叫:

text 复制代码
聚簇索引

也就是:

text 复制代码
Clustered Index

可以简单理解成:

数据本身就是按照主键索引的 B+Tree 组织起来的。

所以:

sql 复制代码
SELECT *
FROM user
WHERE id = 100;

通过主键索引找到叶子节点以后:

text 复制代码
基本就找到了完整行数据

19. 普通索引又有什么不同?

假设再建立一个:

sql 复制代码
CREATE INDEX idx_age
ON user(age);

这个:

text 复制代码
idx_age

属于:

text 复制代码
二级索引

它的 B+Tree 叶子节点主要保存:

text 复制代码
age
+
主键 id

例如:

text 复制代码
age = 20
   ↓
id = 100

所以执行:

sql 复制代码
SELECT *
FROM user
WHERE age = 20;

可能先:

text 复制代码
idx_age B+Tree
    ↓
找到 id = 100

然后再:

text 复制代码
主键 B+Tree
    ↓
找到完整行数据

这个过程就是我们后面会经常听到的:

text 复制代码
回表

也就是:

先通过二级索引找到主键,再通过主键索引找到完整数据。

这也是为什么理解 B+Tree 之后,后面学习:

text 复制代码
回表

覆盖索引

联合索引

会容易很多。


20. 索引为什么不能越多越好?

既然索引可以提高查询速度,那是不是:

text 复制代码
每个字段都建一个索引

最好?

当然不是。

因为索引本质上也是:

text 复制代码
一份额外的数据结构

例如插入:

sql 复制代码
INSERT INTO user ...

不仅要写入:

text 复制代码
表数据

还要维护:

text 复制代码
主键索引

age 索引

name 索引

其他索引

所以:

text 复制代码
索引越多
   ↓
INSERT 成本越高

UPDATE 成本越高

DELETE 成本越高

同时还会:

text 复制代码
占用更多磁盘空间

所以索引实际上是在:

text 复制代码
查询速度

和:

text 复制代码
写入成本 + 存储成本

之间做权衡。

这也是为什么:

索引不是越多越好,而应该建立在真正有查询价值的字段和查询组合上。


21. 为什么低区分度字段不一定适合单独建索引?

假设:

text 复制代码
gender

字段只有:

text 复制代码
男

女

两种值。

100 万条数据:

text 复制代码
男:50 万

女:50 万

执行:

sql 复制代码
SELECT *
FROM user
WHERE gender = '男';

即使建立索引:

text 复制代码
一次仍然要取出大约 50 万条数据

索引能够帮你快速找到:

text 复制代码
这些记录从哪里开始

但后面依然要读取大量数据。

所以这类字段:

text 复制代码
区分度很低

单独建立索引的收益可能有限。

而像:

text 复制代码
身份证号

手机号

用户 ID

这种:

text 复制代码
不同值很多

的字段,通常具有更好的选择性。

当然,是否使用某个索引最终还会由优化器根据:

text 复制代码
数据分布

成本估算

查询条件

综合判断。

所以不能简单说:

text 复制代码
低区分度字段绝对不能建索引

而应该说:

低选择性字段单独建立 B+Tree 索引,很多查询场景下收益可能比较有限。


22. 把整个过程串起来

现在回头看:

sql 复制代码
SELECT *
FROM user
WHERE id = 500000;

如果没有索引:

text 复制代码
user 表
  ↓
大量扫描数据
  ↓
找到 500000

如果有主键 B+Tree:

text 复制代码
B+Tree Root
     ↓
中间节点
     ↓
叶子节点
     ↓
id = 500000
     ↓
完整行数据

核心区别就是:

text 复制代码
全表扫描

        VS

利用有序索引结构不断缩小查找范围

B+Tree 又通过:

text 复制代码
一个节点多个分支

让:

text 复制代码
树非常矮

从而减少页访问。

同时:

text 复制代码
叶子节点有序连接

又让范围查询非常方便。

这就是 B+Tree 非常适合数据库索引的重要原因。


23. 面试时怎么回答为什么 MySQL 索引用 B+Tree?

如果面试中被问:

为什么 MySQL 索引通常使用 B+Tree?

不要只回答:

text 复制代码
因为 B+Tree 查询快

可以从三个方向回答。

第一:

text 复制代码
B+Tree 是多路平衡搜索树

一个节点可以有大量子节点,所以:

text 复制代码
树高度低

能够减少磁盘页访问和 I/O。

第二:

text 复制代码
内部节点主要保存索引导航信息

单个页可以容纳更多索引项,进一步提高分支因子。

第三:

text 复制代码
叶子节点有序连接

因此:

text 复制代码
范围查询

顺序扫描

排序

都比较高效。

相关推荐
天天进步20151 小时前
Pixelle-Video 源码解析 #17:TTS 语音生成:Edge-TTS、Index-TTS 如何接入?
前端·数据库
鸽芷咕1 小时前
异构数据同步,凭什么敢说数据不丢?——拆解金仓KFS的全周期一致性校验
数据库
海浪仙人掌1 小时前
账龄分析的定义是什么?账龄分析有什么作用?
数据库
ACP广源盛139246256731 小时前
M6/M5 Pro Mac mini 端侧 AI 落地@ACP#IX6024 PCIe2.0 交换芯片在轻量化 AI 服务中的机会与应用场景
大数据·数据库·人工智能·嵌入式硬件·macos·开源
ly76891 小时前
深入解析 MySQL 间隙锁:从加锁规则到死锁案例
数据库·mysql·死锁·间隙锁·next-key lock·事务隔离
LRL_1 小时前
7x24小时不停机:基于 Apache SeaTunnel 实现 Oracle to Oracle 实时 CDC 同步全实战
数据库·oracle·apache
不剪发的Tony老师1 小时前
Navop:一款工具搞定数据库、SSH、SFTP、远程桌面、AI Agent
运维·数据库·ssh
程序员-Benothing1 小时前
MySQL 中 DELETE、DROP 和 TRUNCATE 的区别是什么?
数据库·mysql
lhldsg1 小时前
幼儿托管系统开发实战指南:从需求分析到架构设计全流程解析
数据库·数据仓库·需求分析