臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

索引就像一本书的目录:没有目录就要一页页翻,有目录就能先找到页码,再直接翻到目标内容。

面试先记住一句话:索引用额外的存储空间和写入成本,换取更快的查询。

二、📚 先看一个生活例子

2.1 没有索引:一本本找书

假设图书馆有 10 万本书,你要找《Java 入门》。

如果图书没有目录,你只能:

第 1 本 → 不是
第 2 本 → 不是
第 3 本 → 不是
……
直到找到《Java 入门》

这就像数据库的全表扫描:从第一行开始检查,直到找完所有数据。

2.2 有索引:先查目录

如果图书馆有一份按书名排序的目录:

《Java 入门》 → 图书编号 101
《MySQL 实战》→ 图书编号 205
《Redis 设计》→ 图书编号 309

现在找书分成两步:

  1. 在书名目录中找到《Java 入门》,得到图书编号 101。
  2. 根据编号 101,找到这本书的完整信息和位置。

2.3 图书馆和 MySQL 的对应关系

图书馆MySQL
一本书的完整信息表中的一行数据
书名目录普通索引或二级索引
图书编号主键
先查目录,再根据编号找书先查二级索引,再回表
一本本翻找全表扫描

三、⚙️ 索引到底怎么工作

3.1 先看完整关系

先不要把几个名词混在一起:

名称白话理解主要作用
MySQL整个数据库软件接收 SQL,管理数据库和表
存储引擎MySQL 内部负责实际存取数据的模块决定数据、索引、事务和锁怎么管理
InnoDBMySQL 默认使用的存储引擎管理表数据,支持事务、行锁和崩溃恢复
InnoDB 表交给 InnoDB 管理的表表的数据和索引由 InnoDB 保存
索引表的快速查询目录减少需要扫描的数据
B+Tree组织常见索引的数据结构让 InnoDB 快速定位和范围查找

把它们连起来就是:

MySQL
└── 数据库
    └── 表
        └── 由 InnoDB 存储引擎管理
            ├── 保存表数据
            └── 建立索引
                └── 常见索引使用 B+Tree 组织

执行一条查询时,大致过程是:

SQL 发给 MySQL
→ MySQL 分析 SQL 并选择执行方案
→ InnoDB 根据索引查找数据
→ B+Tree 帮助定位数据页
→ 查询结果返回给 MySQL

一句话记忆

MySQL 是整个系统,InnoDB 是负责存取表数据的模块,索引是查询目录,B+Tree 是组织这份目录的方法。

3.2 索引和 B+Tree 有什么关系

先把两个概念分开:

概念白话理解解决什么问题
索引数据库里的目录用来加快查询
B+Tree组织这份目录的方法让 MySQL 能快速定位和范围查找

可以类比成一本书:

索引     = 书后面的目录
B+Tree   = 目录按照什么结构排列

所以两者不是同一个东西。索引是要实现的功能,B+Tree 是实现索引的一种数据结构。

MySQL 可以使用不同结构实现不同索引。在常见的 InnoDB 表中,主键索引、普通索引和唯一索引通常使用 B+Tree;全文索引和空间索引则使用其他结构。

为了加快查询
→ 建立索引
→ 索引需要一种便于查找的数据结构
→ InnoDB 常用 B+Tree

因此,下面先认清常见索引名称,再了解 B+Tree 长什么样。

3.2.1 常见索引类型

先记住:索引可以从不同角度分类,所以这些名称不是完全并列的。

分类角度常见类型这个角度在问什么
按叶子节点存什么聚簇索引、二级索引完整行数据放在哪里
按作用和限制主键索引、普通索引、唯一索引、全文索引、空间索引这个索引用来做什么
按使用几列单列索引、联合索引一个索引包含几列

面试最常问下面几种:

索引名称大白话理解
主键索引根据主键找数据;主键不能重复,也不能是空值。在 InnoDB 中,它通常也是聚簇索引
普通索引只负责加快查询,不要求索引值唯一,例如给书名建立索引
唯一索引一边加快查询,一边限制非空索引值不能重复,例如用户手机号
联合索引把多列合在一个索引中,例如 (类别, 作者, 书名)
全文索引从大段文字中搜索词语,类似文章内容搜索
空间索引查询地图坐标、区域等空间数据,普通业务较少使用

还有两个名字会经常出现:

  • 聚簇索引:叶子节点直接保存完整行数据。InnoDB 表通常只有一个,后面会单独讲。
  • 二级索引:聚簇索引以外的索引。普通索引、唯一索引和联合索引通常都属于二级索引。

例如,(手机号) 可以同时是“单列索引 + 唯一索引 + 二级索引”;(类别, 作者) 可以同时是“联合索引 + 普通索引 + 二级索引”。它们并不冲突,只是分类角度不同。

面试先记住

最常见的是主键索引、普通索引、唯一索引和联合索引。聚簇索引与二级索引是在说明数据怎么存,不要和前面四种硬分成同一组。

3.3 B+Tree 的数据结构长什么样

B+Tree 这个名字可以先不记。先看 MySQL 怎么从 9 个编号里找到 50。

假设一页最多只放 3 个编号,MySQL 会把排好序的编号分成三组:

A 组:5、10、20
B 组:30、40、50
C 组:60、70、80

然后再准备一张“总目录”,记录每个范围应该去哪个组:

要找的编号应该去哪一组
小于 30A 组
30~59B 组
大于等于 60C 组

现在执行下面的查询:

SELECT * FROM book WHERE id = 50;

只需要三步:

用文字再走一遍:

  1. 要找的编号是 50。
  2. 总目录告诉我们:50 属于 30~59,应该去 B 组。
  3. B 组只有 30、40、50,很快就能找到 50。

如果没有总目录,就要从 A 组开始,把 5、10、20、30、40、50…… 一个个比较。

现在再把例子换成专业名称:

刚才的例子B+Tree 中的名称作用
总目录根节点告诉 MySQL 下一步应该去哪一组
A、B、C、D 数据组叶子节点保存真正的索引记录
总目录放不下所有分组信息,再增加分目录中间节点数据特别多时,继续缩小范围

所以这棵简化的 B+Tree 实际上就是:

查找 50 时只走这条路线:总目录 → 分目录 1 → B 组 → 找到 50。其他分目录和数据组不用检查。

只需要记住

B+Tree 就是“总目录 + 分组数据”。查询时先用总目录确定去哪一组,再到那一组里找具体数据。数据特别多时,会在总目录和数据组之间再增加几层目录。

上面的数字是为了理解而设计的简化案例。真实 MySQL 的一个节点能够保存很多索引值,但查找思路是一样的。

3.3.1 数据库具体怎么分

先别想数据库,可以把它想成一个放号码卡的仓库:

  • 索引记录:一张张排好顺序的号码卡。
  • 数据页:装号码卡的盒子。InnoDB 的数据页默认是 16KB。
  • 上层目录:记录“从哪个号码开始,要去哪个盒子找”。

假设每个盒子最多放 4 张卡片,现在有:

数据页 A:[10、20、30、40]  ← 已经装满

这时再放入 50,A 已经塞不下了。MySQL 会拿一个新盒子 B,把这批卡片分开放:

数据页 A:[10、20]
数据页 B:[30、40、50]

多出 B 后,目录也要记住它:

10 → 去数据页 A
30 → 去数据页 B

以后再有新的数据页,目录就继续增加记录。如果目录自己也放满了,在这个简化例子里,MySQL 就把目录也拆成两页,再在上面增加一张“总目录”。

整个过程只要记住四步:

号码卡越来越多
→ 一个数据页装满,拆成两个数据页
→ 上层目录记下新数据页的位置
→ 目录页也满了,再拆目录并增加一层

下面这张图,就是把这个过程完整画出来:

B+Tree 数据页和目录页分裂完整过程

一句话记忆

一个数据页满了,通常会由一页拆成两页;每多一个数据页,上层目录就要多记一个位置;目录页没有空间记录新位置时,目录也会分裂,B+Tree 就会长高一层。

真实边界取决于索引值、记录大小、页面剩余空间和插入顺序,不是提前固定好的。

3.4 BTree 与 B+Tree:结构、区别与查找原理

3.4.1 BTree 是什么

**BTree(B 树)是一种多路平衡查找树。**它不是二叉树;“多路”表示一个节点可以连接多个子节点,“平衡”表示所有叶子节点都在同一层。

还是用图书编号理解:一个节点可以保存多个排好序的编号,用这些编号把查找范围分成几段。下面用“编号 → 图书记录”表示一条数据:

                    [30 → 图书记录]
                     ↙           ↘
 [10 → 图书记录,20 → 图书记录]   [40 → 图书记录,50 → 图书记录]

这是用于理解的简化 B 树:

  • 查 30:在根节点就能找到对应的图书记录。
  • 查 20:先判断 20 < 30,进入左侧子节点,再找到记录。
  • 查 50:先判断 50 > 30,进入右侧子节点,再找到记录。

**B 树的内部节点和叶子节点都可以保存键及其对应的数据或记录引用。**这里的“数据”不一定是完整行,取决于具体实现。

为什么数据库会考虑这种结构?数据库通常按页读取数据。如果一个节点能放在一个页面中,一次读取就可以拿到多个键,排除大量无关范围。与每个节点最多只有两个子节点的二叉树相比,B 树可以拥有更多分支,通常更矮,从而减少沿树查找时需要访问的页面。

数据增加或删除时,B 树会通过节点分裂、合并或键的调整维持结构,并不是把新数据一直挂在最下面,让某条分支无限变长。

3.4.2 B+Tree 是什么,与 BTree 有什么区别

B+Tree(B+ 树)把数据记录集中到叶子节点,内部节点只保存用于指路的分隔键和子节点指针;叶子节点之间按键的顺序连接。

可以把它理解为“上层只有目录,最底层才放内容”:

                 [分隔键:30、60]
                   ↙     ↓     ↘
叶子节点: [10、20] ↔ [30、40、50] ↔ [60、70、80]
            带记录       带记录          带记录

这里约定:小于 30 走左边,30 ≤ 编号 < 60 走中间,大于等于 60 走右边。图中内部节点的 30、60 只是路标,对应记录仍保存在叶子节点中;具体分隔键的表示方式取决于实现。

对比点B 树B+ 树
数据记录放在哪里内部节点和叶子节点都可以保存集中在叶子节点
内部节点做什么指路,也可以直接找到数据记录只保存分隔键和子节点指针,负责指路
单条查询可能在内部节点提前命中需要走到叶子节点才能拿到记录
叶子节点的连接标准结构不依赖叶子链表叶子节点按键的顺序连接
范围查询通常需要在树中进行中序遍历定位起点后,沿叶子节点顺序扫描
相同页面大小下的分支数量内部节点携带数据,通常能容纳的分支较少内部节点更精简,通常能容纳更多分支

以查询图书编号 30~70 为例,B+ 树的处理过程是:

从根节点找到 30 所在的叶子节点
→ 读取 30、40、50
→ 沿叶子节点的链接进入下一页
→ 读取 60、70
→ 遇到 80,超过范围,结束扫描

这里不需要为每条记录重新从根节点出发。B 树也支持范围查询,只是通常要在树中跨节点遍历,不能说它“无法查范围”。

**InnoDB 采用 B+ 树结构的主要原因:内部节点能容纳更多分支,树通常较矮;叶子节点有序连接,便于范围查询和顺序扫描。**真实查询是否发生磁盘读取,还取决于相关页面是否已经在内存缓存中,不能把树高直接当作每次查询的磁盘 I/O 次数。

两个容易混淆的地方

① B+ 树并不保证所有查询都比 B 树快,B 树的单条查询可能在内部节点提前命中。
② InnoDB 聚簇索引的叶子节点保存完整行,二级索引的叶子节点保存索引列和主键值,所以“叶子节点存数据”不等于“所有索引的叶子都存整行”。

MySQL 官方文档有时用广义的 B-tree 称呼这类索引;面试中讨论 InnoDB 的具体组织方式时,通常称为 B+Tree。不要仅根据文档中的名称,就把它理解成上面“内部节点也保存数据记录”的经典 B 树。

3.4.3 B+Tree 如何一层层缩小范围

数据较少时,一张总目录就够了。真实数据库可能有几百万行,一张目录放不下所有分组信息,于是 MySQL 会增加“分目录”。

每经过一层目录,就会排除大量不相关的数据。这就是“一层层缩小范围”。

B+Tree 适合数据库的原因,面试说出下面三点就够了:

  • 每个节点能保存很多目录项,所以树通常不高。
  • 查找时逐层缩小范围,减少磁盘读取。
  • 最下面的数据按顺序排列,适合范围查询。

3.5 主键索引:编号后面就是完整数据

假设有一张图书表:

book(id, title, author, shelf)

叶子节点,就是 B+Tree 最底下的那一层。

把 B+Tree 想成一棵倒过来的树:

最上面:根节点       → 总目录
中间:中间节点       → 分目录
最下面:叶子节点     → 真正存放索引记录的地方

之所以叫“叶子”,是因为它已经到了树的末端,下面不会再连接其他节点。上面的根节点和中间节点主要负责带路,最后真正要找的内容在叶子节点中。

对于 InnoDB 的主键索引,叶子节点中放的是“主键 + 完整行数据”。例如:

叶子节点中的一条记录:
101 → Java 入门、张老师、A-03 书架

id 是主键。InnoDB 的主键索引叶子节点保存完整行数据,所以通过主键找书时,可以直接拿到书名、作者和书架位置。

3.5.1 聚簇索引是什么

聚簇索引,就是把索引和完整行数据放在一起。

还是用图书馆来理解:普通目录只告诉你书在哪里;聚簇索引更像书架本身已经按照图书编号排好,找到编号时,完整的书也就在这里。

在 InnoDB 的主键索引中,最下面的叶子节点大致可以理解成:

101 → [Java 入门,张老师,A-03]
205 → [MySQL 实战,李老师,B-05]
309 → [Redis 设计,王老师,B-08]

左边的 101、205、309 是主键,右边是对应的完整行数据。因此,按照主键查询时,找到主键索引的叶子节点,就已经拿到了完整数据,不需要再去别的地方找一次。

聚簇索引
= 按主键排好的 B+Tree
= 叶子节点直接保存完整行数据

一张表的完整数据只能按照一种主要方式组织,所以一张 InnoDB 表通常只有一个聚簇索引。表有主键时,主键索引通常就是聚簇索引。

不同索引的叶子里放的东西不同

主键索引的叶子节点保存完整行数据;普通索引的叶子节点主要保存“索引列 + 主键值”,下一节会接着讲。

3.6 二级索引和回表:先拿编号,再找完整数据

现在给 title 建一个普通索引:

CREATE INDEX idx_title ON book(title);

这个书名索引的叶子节点主要保存:

书名 + 主键 id

执行下面的查询:

SELECT * FROM book WHERE title = 'Java 入门';

查找过程如下:

最后“再去主键索引查一次”的动作,就叫回表。

面试记法

二级索引像书名目录,目录里记录了图书编号;根据编号再去拿完整图书信息,就是回表。

3.7 覆盖索引:目录里的信息已经够了

如果只查询主键:

SELECT id FROM book WHERE title = 'Java 入门';

书名索引中已经有 title 和 id,可以直接返回结果,不需要再去主键索引拿完整行。这就是覆盖索引。

一句话区分:

  • 需要索引之外的列:回表。
  • 查询需要的列都在索引中:覆盖索引,不回表。

3.8 联合索引:像按“类别、作者、书名”排目录

假设建立联合索引:

CREATE INDEX idx_category_author_title
ON book(category, author, title);

它的排列方式是:

因此下面这些条件容易利用该索引查找:

WHERE category = '编程'
 
WHERE category = '编程' AND author = '张老师'
 
WHERE category = '编程'
  AND author = '张老师'
  AND title = 'Java 入门'

但如果只查:

WHERE author = '张老师'

就像你拿到的是一份“先按类别排列”的目录,却跳过类别直接找作者。不同类别下都可能有张老师,只能到处翻,无法很好地利用这份目录快速定位。

这就是最左前缀原则:联合索引 (a, b, c) 通常要从最左边的 a 开始使用。

3.9 为什么不能给每列都建索引

目录也需要维护。每新进一本书,不但要把书放到书架,还要更新书名目录、作者目录、类别目录。

数据库也是一样:

  • 索引会占用磁盘空间。
  • 新增、修改、删除数据时,也要更新索引。
  • 索引越多,写入成本越高。

所以不是索引越多越好,而是要为经常查询、确实能减少扫描的条件设计索引。

四、🎤 面试怎么回答

4.1 什么是索引

索引类似书的目录,是数据库为加快查询建立的有序数据结构。没有索引可能需要全表扫描;有索引可以快速缩小查找范围。它会占用额外空间,增删改时也要维护,所以不是越多越好。

4.2 为什么使用 B+Tree

B 树和 B+ 树都是多路平衡查找树。B 树的内部节点也能保存数据记录;B+ 树的内部节点只保存分隔键和指针,数据记录集中在叶子节点。这样内部节点通常能容纳更多分支,树更矮;叶子节点之间又有有序链接,便于范围扫描,所以 InnoDB 使用 B+ 树结构。B 树的单条查询可能提前命中,不能说 B+ 树在所有场景都更快。

4.3 什么是回表和覆盖索引

InnoDB 的二级索引保存索引列和主键值。先查二级索引拿到主键,再去主键索引拿完整行叫回表;如果查询需要的列在二级索引中已经都有了,就能直接返回,这叫覆盖索引。

4.4 使用联合索引需要注意什么

先按这条路线回答:最左前缀 → 范围条件 → 列的排列顺序 → 覆盖索引和维护成本。

联合索引首先要注意最左前缀原则,例如 (a,b,c) 通常可以利用 a、a+b、a+b+c 来定位数据。其次要注意范围条件,遇到 >、< 等范围比较后,后面的列通常不能继续缩小同一次索引扫描的范围,但仍可能参与过滤。设计索引时,要根据常用查询来排列字段,综合考虑等值条件、范围条件、排序和字段区分度。最后,可以利用覆盖索引减少回表,但不要为了覆盖所有查询,把索引建得过宽、过多,否则会增加存储和写入成本。

4.4.1 最左前缀:看索引列顺序,不看 WHERE 书写顺序

对于联合索引 (a,b,c),可以理解为先按 a 排序,a 相同再按 b,最后按 c。

查询条件通常如何利用索引定位
a = 1利用第一列 a
a = 1 AND b = 2利用连续的前两列 a,b
a = 1 AND b = 2 AND c = 3利用完整的 a,b,c
a = 1 AND c = 3通常先利用 a 定位,跳过 b 后,不能直接把 a,c 当成连续前缀
b = 2缺少最左列,通常无法按普通最左前缀方式直接定位

WHERE b = 2 AND a = 1 与 WHERE a = 1 AND b = 2 都可以利用 a,b。最左前缀约束的是索引定义中的列顺序,不要求把 WHERE 条件按这个顺序书写。

这里说的是常规索引定位方式。跳过最左列不代表数据库绝对不会扫描这个索引,特定情况下还可能使用跳跃扫描等优化;入门时先掌握常规规则。

4.4.2 范围条件:前一列用了 >,后一列通常只能筛选结果

这一条到底要注意什么

联合索引 (班级, 学号) 中,如果班级是等值条件,可以继续利用学号定位;如果班级用了 > 这样的范围条件,学号通常不能继续缩小这次索引扫描的范围,但仍能筛选结果。不要看到两列都写在 WHERE 中,就认为两列都能帮助减少扫描。

**会导致什么?**数据库可能需要扫描很多不符合学号条件的索引记录,再把它们过滤掉。例如,扫描 4950 条记录,最后只返回 99 条。查询结果仍然正确,但额外的读取和筛选会增加开销,可能让查询变慢。

**先看档案怎么摆。**档案按“先班级、再学号”排列,而不是把所有班的同一学号放在一起:

1班1号、1班2号、1班3号
2班1号、2班2号、2班3号
3班1号、3班2号、3班3号

情况一:找“2 班的 3 号”。

班级已经固定,直接找到 2 班,再按有序的学号定位到 3 号。两个条件都能帮助缩小查找位置。

先定位:2班1号、2班2号、【2班3号】
再定位:                 【2班3号】

情况二:找“班级大于 1,且学号是 3”的所有档案。

“班级大于 1”先确定从 2 班开始,到这些班级的档案结束。扫描这段档案时,实际遇到的是:

2班1号 → 2班2号 → 2班3号 → 3班1号 → 3班2号 → 3班3号
  跳过      跳过      保留      跳过      跳过      保留

这里“学号是 3”仍然有用:它决定哪些档案留下。但两个 3 号没有挨在一起,不能用一个更小的连续区间,把其他学号都排除在扫描范围之外。

**这就是要注意的区别:写了“学号是 3”,不代表只需要扫描各班的 3 号。**在这里讨论的普通连续扫描中,仍会遇到其他学号,再把它们筛掉。

**对应到 SQL。**假设学生表 student 有联合索引 (class_no, student_no),以下只分析条件,不需要实际执行:

-- 班级是等值条件:可以继续用学号定位。
SELECT class_no, student_no FROM student
WHERE class_no = 2 AND student_no = 3;
 
-- 班级是范围条件:先扫描班级大于 1 的区间,再筛选学号。
SELECT class_no, student_no FROM student
WHERE class_no > 1 AND student_no = 3;
条件班级这一列的作用学号这一列的作用
class_no = 2 AND student_no = 3定位到 2 班继续定位到 3 号
class_no > 1 AND student_no = 3确定班级大于 1 的扫描范围扫描时筛出 3 号,通常不能继续缩小这个范围

换回联合索引 (a,b,c),同样的道理是:a = 1 AND b > 10 AND c = 3 中,a、b 确定扫描范围,c 通常负责进一步过滤。不能把“c 不能继续缩小范围”说成“c 完全没用”。

这会导致什么?扫描的数据可能很多,但最后返回的数据很少,查询也就可能更慢。结果仍然正确,影响的是查询成本。

假设学校有 100 个班,每班学号都是 1~50。使用联合索引 (班级, 学号) 查询“班级大于 1,且学号是 3”时:

最终需要:2~100 班各自的 3 号,共 99 条记录。
 
按前面讲的连续范围扫描方式:
扫描 2~100 班的学生:99 个班 × 每班 50 人 = 4950 条索引记录
→ 检查每条记录的学号
→ 过滤掉其他学号
→ 最后返回 99 条记录

也就是说,**虽然只返回 99 条,却可能扫描、检查 4950 条索引记录。**过滤掉的记录也消耗了读取和判断的时间;数据越多,这种额外开销就越值得关注。实际耗时还取决于缓存、记录大小和数据库选择的执行方式,不是扫描量增加多少倍,耗时就一定增加多少倍。

如果索引顺序换成 (学号, 班级),同一学号的学生会集中排列:

学号 1:1班、2班、3班……100班
学号 2:1班、2班、3班……100班
学号 3:1班、2班、3班……100班

这时可以先定位到“学号 3”,再找到其中“班级大于 1”的部分,目标记录就连在一起了。在这个简化例子中,可以直接扫描需要的约 99 条索引记录,而不是前面的 4950 条。

为什么要注意范围条件

不是担心查错,而是避免“返回的数据很少,实际扫描的数据却很多”。后面的列能筛选结果,不代表它能减少前面需要扫描的记录。

**设计索引时怎么用这个知识?**如果高频查询恰好是“学号等于某个值,班级大于某个值”,可以考虑 (学号, 班级):先集中找到这个学号,再在其中按班级查范围。但是否调整顺序还要看其他查询,不能只为这一条 SQL 就直接交换索引列。

面试可以这样说

使用联合索引时,要注意范围条件对后续列的影响。例如 (a,b,c) 中,a 等值、b 大于某值时,c 通常不能继续缩小扫描范围,但仍可参与过滤。设计索引时,可以结合高频查询考虑把等值条件列放在范围条件列之前。

这里讲的是 > 对普通连续索引扫描的影响,不是所有运算符都能套用同一条口诀;>=、BETWEEN、IN 等条件需要具体分析。

4.4.3 列顺序:按实际查询设计

如果经常按“类别 + 作者”查图书,(category, author) 可以同时支持按类别查,以及按类别和作者查。如果经常只按作者查,这个顺序通常不合适,需要重新考虑查询需求和索引设计。

设计时先看高频查询的过滤条件,再综合考虑范围查询、排序和区分度。**不要机械地认为“区分度最高的列一定放第一位”。**例如查询总是同时对两列做等值匹配时,把高区分度列放前面并不必然更快,还要考虑其他查询能否复用索引前缀。

4.4.4 覆盖与成本:够用就好

如果查询需要的列已经全部在联合索引中,就可能通过覆盖索引避免回表。但多加一列,也会让索引占用更多空间,增加增删改时的维护成本。

已有 (a,b) 时,单独的普通索引 (a) 可能重复,需要结合实际使用判断;如果单列索引承担唯一性约束等职责,就不能仅凭前缀重复直接删除。

面试记忆

左边要连续,范围要分清;列顺序看查询,覆盖要算成本。

五、📌 快速回顾

索引       = 书的目录
全表扫描   = 从第一页开始逐页翻
主键索引   = 编号后面直接放完整数据
二级索引   = 目录中保存“索引列 + 主键”
回表       = 目录拿到主键后,再找完整数据
覆盖索引   = 目录中的信息已经够用,不必回表
联合索引   = 多级目录,要从最左边开始查

最适合面试复述的一句话:

索引通过有序的 B+Tree 减少数据扫描;InnoDB 二级索引查完整行可能需要回表,覆盖索引可以避免回表;联合索引要注意最左前缀。

相关笔记:05.MySQL 事务、06.MySQL 隔离级别、08.MySQL 锁、Elasticsearch 入门

六、🔗 官方资料