MySQL索引
索引是 MySQL 性能优化里最重要、也最容易被误用的知识点。索引的作用不是“让所有查询都变快”,而是让数据库用更少的数据页找到目标数据。
先记住一句话:
索引本质上是为了减少扫描范围,但它也会增加写入成本和存储成本。
学习目标
学完这一页,你要能做到:
- 解释索引为什么能让查询少读页,而不是只背“索引快”。
- 画出 InnoDB B+Tree、聚簇索引、二级索引和回表过程。
- 解释最左前缀为什么成立,范围查询为什么会影响后续列利用。
- 区分覆盖索引解决什么问题,以及为什么覆盖索引仍然可能慢。
- 识别常见索引失效或效果变差场景,并能改写 SQL。
- 根据商业系统的查询场景设计联合索引,而不是给字段无脑建索引。
- 用
EXPLAIN验证索引是否真正被优化器选择。
为什么需要索引
假设一张用户表有 1000 万行,你要查用户名为 tom 的用户。
没有索引时:
flowchart TD
A["查询 username = tom"] --> B["从第一行开始扫描"]
B --> C["逐行比较 username"]
C --> D["扫描大量数据页"]
D --> E["找到目标或扫描结束"]有索引时:
flowchart TD
A["查询 username = tom"] --> B["从索引 B+Tree 根节点开始"]
B --> C["逐层定位叶子节点"]
C --> D["找到 username 对应主键"]
D --> E["按需回表读取完整行"]区别在于:没有索引是“从头找”,有索引是“按有序目录找”。
索引类型
MySQL 中常见索引可以从不同角度分类:
| 角度 | 类型 | 说明 |
|---|---|---|
| 数据结构 | B+Tree 索引 | InnoDB 最常见,支持等值、范围、排序 |
| 数据结构 | Hash 索引 | 适合等值查询,不适合范围查询,Memory 引擎常见 |
| 数据结构 | 全文索引 | 适合文本分词搜索 |
| 约束 | 主键索引 | 唯一且非空,一个表一个 |
| 约束 | 唯一索引 | 保证字段或字段组合唯一 |
| 字段数量 | 单列索引 | 一个字段组成 |
| 字段数量 | 联合索引 | 多个字段组成 |
实际开发中最重要的是 InnoDB 的 B+Tree 索引。
InnoDB 与 MyISAM 索引区别
MyISAM 的索引文件和数据文件分离,索引叶子节点保存数据文件地址。
InnoDB 使用聚簇索引组织数据:
| 对比 | MyISAM | InnoDB |
|---|---|---|
| 主键索引叶子节点 | 数据地址 | 整行数据 |
| 二级索引叶子节点 | 数据地址 | 索引列 + 主键值 |
| 是否支持事务 | 不支持 | 支持 |
| 是否支持行锁 | 不支持,主要表锁 | 支持行锁 |
InnoDB 二级索引为什么保存主键值,而不是物理地址:
- 数据页分裂、移动时,物理位置可能变化。
- 保存主键值可以通过聚簇索引再次定位完整行。
- 代价是查询非索引字段时可能需要回表。
B+Tree 索引结构
flowchart TD
A["根节点<br/>索引键 + 页指针"] --> B["内部节点 1"]
A --> C["内部节点 2"]
B --> D["叶子节点 1<br/>key 从小到大"]
B --> E["叶子节点 2<br/>key 从小到大"]
C --> F["叶子节点 3<br/>key 从小到大"]
C --> G["叶子节点 4<br/>key 从小到大"]
D <--> E
E <--> F
F <--> GB+Tree 适合数据库索引的原因:
- 树高较低,查找数据时磁盘 IO 次数少。
- 叶子节点按 key 有序,适合范围查询和排序。
- 非叶子节点只保存索引 key 和页指针,单页可以容纳更多指针。
- 叶子节点之间有链表,方便顺序扫描。
为什么用 B+Tree 不用 B-Tree
先区分两个概念:
| 结构 | 数据保存在哪里 | 叶子节点是否链表相连 | 范围查询 |
|---|---|---|---|
| B-Tree | 内部节点和叶子节点都可以保存数据 | 通常不要求叶子链表 | 范围查询需要中序遍历,过程更复杂 |
| B+Tree | 数据都在叶子节点,内部节点只做目录 | 叶子节点通常双向链表相连 | 找到起点后沿叶子链表顺序扫描 |
可以把 B-Tree 理解成“每一层都可能放真实数据”,B+Tree 理解成“上面几层只是目录,真实数据统一放在最底层叶子页”。
B-Tree 的问题
flowchart TD
A["根节点<br/>key + 数据 + 指针"] --> B["内部节点<br/>key + 数据 + 指针"]
A --> C["内部节点<br/>key + 数据 + 指针"]
B --> D["叶子节点<br/>key + 数据"]
B --> E["叶子节点<br/>key + 数据"]B-Tree 并不是不能做数据库索引,它也能查找、插入和删除。但对磁盘数据库来说,它有几个不如 B+Tree 的地方。
| 问题 | 为什么影响数据库 |
|---|---|
| 内部节点存数据 | 同样 16KB 页能放的 key 和指针变少 |
| 分叉数变少 | 树可能更高,查询需要更多随机 IO |
| 查询路径不稳定 | 数据可能在内部节点,也可能在叶子节点 |
| 范围扫描不方便 | 需要在树上反复中序遍历,不如叶子链表顺序扫 |
| 不利于预读 | B+Tree 叶子页连续扫描更适合磁盘和 Buffer Pool 预读 |
数据库查询的瓶颈通常不是 CPU 比较几次,而是“读了多少页、做了多少随机 IO”。B+Tree 让内部节点更像目录页,一页可以放更多目录项,树更矮,IO 次数更少。
B+Tree 为什么更适合 MySQL
flowchart TD
A["根页<br/>key + 子页指针"] --> B["内部页<br/>key + 子页指针"]
A --> C["内部页<br/>key + 子页指针"]
B --> D["叶子页 1<br/>真实记录"]
B --> E["叶子页 2<br/>真实记录"]
C --> F["叶子页 3<br/>真实记录"]
C --> G["叶子页 4<br/>真实记录"]
D <--> E
E <--> F
F <--> GMySQL InnoDB 使用 B+Tree 的核心原因:
| 原因 | 解释 |
|---|---|
| 树更矮 | 内部页只放 key 和指针,同一页能容纳更多目录项 |
| IO 更少 | 树高通常很低,查一行只需要少量页访问 |
| 范围查询强 | 找到范围起点后,沿叶子页链表顺序扫描 |
| 排序友好 | 叶子节点天然按索引 key 有序 |
| 分页友好 | order by + limit 可以利用索引顺序 |
| 预读友好 | 顺序扫描叶子页更容易利用磁盘和 Buffer Pool 预读 |
| 查询路径稳定 | 所有数据都在叶子层,从根到叶路径更一致 |
例如:
select *
from orders
where user_id = 1001
and created_at >= '2026-07-01'
and created_at < '2026-08-01'
order by created_at;如果有联合索引:
create index idx_user_time on orders(user_id, created_at);B+Tree 可以先定位到 user_id = 1001 且 created_at 满足范围的第一个叶子位置,然后沿叶子链表向后扫描,直到范围结束。这个过程非常适合范围查询和排序。
为什么不用红黑树或 AVL 树
红黑树、AVL 树适合内存数据结构,但不适合磁盘数据库主索引。
| 结构 | 为什么不适合磁盘索引 |
|---|---|
| 红黑树 | 二叉树分叉太少,数据量大时树高很高,随机 IO 多 |
| AVL 树 | 平衡更严格,旋转维护成本高,树高仍比 B+Tree 高 |
| 跳表 | 适合内存和部分 LSM 场景,不如 B+Tree 适合页式磁盘范围扫描 |
| Hash | 等值查询快,但不支持范围、排序、最左前缀 |
数据库索引要围绕“页”设计。InnoDB 默认页大小通常是 16KB,B+Tree 一个节点就是一个页或页的一部分,能最大化一次 IO 带回来的有效目录信息。
为什么不用 Hash 做主索引
Hash 索引适合:
where id = 1001但不适合:
where id > 1001
order by id
where name like 'tom%'
where user_id = 1001 and created_at between ... and ...因为 Hash 打散了顺序,无法天然支持范围查询、排序、前缀匹配和分页。商业系统里这些查询太常见,所以 InnoDB 的主力索引结构是 B+Tree。
使用 B-Tree 或 B+Tree 的数据库有哪些
这里要特别注意一个术语问题:很多数据库文档会把索引统称为 B-Tree index,但工程实现往往是 B+Tree、B*Tree 或 B-Tree 变体。也就是说,文档叫 B-Tree,不一定等于教材里“内部节点也保存完整数据”的纯 B-Tree。
| 数据库或引擎 | 官方或常见叫法 | 更准确理解 |
|---|---|---|
| MySQL InnoDB | B+Tree 索引 | 聚簇索引和二级索引都是 B+Tree 思路 |
| MySQL MyISAM | B-Tree 索引 | 非聚簇,索引叶子保存数据文件地址,工程上偏 B+Tree/B-Tree 变体 |
| PostgreSQL | B-tree index | 文档称 B-tree,工程上是适合范围扫描的 B-tree 变体 |
| Oracle | B-tree index | 常说 B-tree,实际是数据库工程化 B-tree/B*Tree 体系 |
| SQL Server | B-tree index | 聚集索引、非聚集索引使用 B+Tree 类结构 |
| SQLite | B-tree | 表和索引基于 B-tree 页结构,工程实现接近 B+Tree 思路 |
| MongoDB WiredTiger | B-tree based index | WiredTiger 使用 B-tree 类索引结构 |
更实用的记法:
- MySQL InnoDB 明确按 B+Tree 理解。
- PostgreSQL、Oracle、SQL Server 文档常说 B-tree,但都是数据库工程里的 B-tree 家族变体,不要死扣教材定义。
- 现代磁盘型关系数据库主流都是 B-Tree/B+Tree 家族,因为它们适合页式存储、范围扫描、排序和低树高。
- LSM-Tree 家族常见于写入吞吐更高的系统,比如 RocksDB、LevelDB、HBase、Cassandra、TiKV 底层 RocksDB。这类不是传统 B+Tree 主索引思路。
为什么 Oracle、PostgreSQL、SQL Server 说自己使用 B-Tree
这个问题最容易误解。很多资料会把它讲成:
MySQL 用 B+Tree,Oracle、PostgreSQL、SQL Server 用 B-Tree。
这个说法太粗糙。更准确的理解是:数据库文档里的 B-Tree 往往是一个索引家族名,不是在严格区分教材里的 B-Tree 和 B+Tree。
flowchart TD
A["数据库文档里的 B-Tree"] --> B["教材 B-Tree"]
A --> C["B+Tree"]
A --> D["B*Tree / 工程变体"]
C --> E["页式存储"]
D --> E
E --> F["高扇出、低树高"]
E --> G["叶子层有序扫描"]
E --> H["适合等值、范围、排序"]数据库为什么喜欢 B-Tree 家族,而不是普通二叉树、Hash 或纯内存结构,核心原因有四个:
| 原因 | 原理 | 如果不用会怎样 |
|---|---|---|
| 高扇出 | 一个页或块里能放很多 key 和指针,树高很低 | 二叉树树高太高,查一次可能产生大量随机 IO |
| 有序 | key 按顺序组织,可以从小到大扫描 | Hash 虽然等值快,但范围查询、排序、分页很弱 |
| 页式存储友好 | 数据库按 page/block 读写,B-Tree 家族天然以页为节点 | 结构如果不贴合页,Buffer Cache、预读、刷盘效率都会差 |
| 写读平衡 | 支持插入、删除、页分裂、页合并和并发控制 | 只优化读或只优化写,都不适合 OLTP 混合负载 |
所以它们不是因为“教材 B-Tree 比 B+Tree 更适合数据库”才这样命名,而是因为数据库行业长期把这一类多路平衡搜索树统称为 B-Tree。
| 数据库 | 为什么文档叫 B-Tree | 真实工程特点 |
|---|---|---|
| PostgreSQL | 默认通用索引类型就叫 B-tree index | 基于页的高并发 B-tree 实现,支持等值、范围、排序、ORDER BY、MIN/MAX 等访问方式 |
| Oracle | 常见索引类型叫 B-tree index | 索引由 root block、branch block、leaf block 组成,leaf block 保存 key 和 ROWID,叶子块有序,适合范围扫描 |
| SQL Server | 聚集索引和非聚集索引文档通常描述为 B-tree 结构 | 有 root、intermediate、leaf level;聚集索引叶子层就是数据行,非聚集索引叶子层保存 key 和 row locator |
以 SQL Server 为例,聚集索引和非聚集索引的叶子层含义不同:
flowchart TD
A["SQL Server B-Tree 根层"] --> B["中间层"]
B --> C["聚集索引叶子层"]
B --> D["非聚集索引叶子层"]
C --> E["真实数据行"]
D --> F["索引 key + 行定位器"]
F --> G["再定位到堆表行或聚集索引行"]这和 MySQL InnoDB 的主键聚簇索引、二级索引回表思想很像:叶子层决定最终能不能直接拿到数据。区别在于不同数据库的页格式、行定位方式、并发控制、锁、版本管理、日志和优化器实现不同。
PostgreSQL、Oracle、SQL Server 使用 B-Tree 家族的共同收益:
- 等值查询可以从根页逐层定位到叶子页。
- 范围查询可以定位起点后继续顺序扫描。
- 排序字段和索引顺序一致时,可以减少额外排序。
MIN、MAX可以沿索引最左或最右快速定位。- 多列索引可以利用最左前缀做条件收敛。
- 页分裂和页合并能支持持续写入,不需要整棵树重建。
也要知道它们的代价:
| 代价 | 说明 |
|---|---|
| 写入成本增加 | 插入、更新索引列、删除都要维护索引结构 |
| 页分裂 | 随机插入可能导致页分裂,带来额外 IO 和碎片 |
| 存储成本 | 每个索引都是额外的数据结构 |
| 统计信息依赖 | 优化器是否选择索引,依赖统计信息和成本估算 |
| 不适合低选择性字段滥建 | 性别、状态这类字段单独建索引,可能过滤效果很差 |
面试遇到“为什么 Oracle、PostgreSQL、SQL Server 使用 B-Tree”时,不要只回答“因为查询快”。可以这样拆:
Oracle、PostgreSQL、SQL Server 文档里常说 B-tree index,但这里的 B-tree 更像数据库索引家族名,不一定是教材里内部节点也保存完整数据的普通 B-Tree。它们选择 B-tree 家族,是因为数据库按页或块管理数据,高扇出可以降低树高,减少随机 IO;key 有序可以支持等值、范围、排序、分页、MIN/MAX;页式结构也方便 Buffer Cache、并发控制、页分裂和恢复。Oracle 的 B-tree index 有 root、branch、leaf block,leaf 保存 key 和 ROWID;SQL Server 的聚集索引和非聚集索引有 root、intermediate、leaf level,聚集索引叶子层是数据行,非聚集索引叶子层保存 key 和行定位器;PostgreSQL 默认 B-tree 索引支持等值、范围和排序访问。所以不能简单说它们使用普通 B-Tree 而 MySQL 使用 B+Tree,更准确地说,它们都属于 B-tree family,只是实现细节不同。面试怎么答
可以这样回答:
MySQL InnoDB 使用 B+Tree,核心原因是数据库索引要减少磁盘 IO。B+Tree 的非叶子节点只保存 key 和页指针,一页能放更多目录项,树更矮;所有真实数据都在叶子节点,查询路径稳定;叶子节点按 key 有序并通过链表相连,非常适合范围查询、排序和分页。B-Tree 的内部节点也可能保存数据,会降低单页能容纳的 key 数量,范围查询也没有 B+Tree 叶子链表顺序扫描方便。Hash 虽然等值查询快,但不支持范围和排序,红黑树这种二叉树树高太高,不适合磁盘页式索引。很多数据库文档会说 B-tree index,例如 PostgreSQL、Oracle、SQL Server,但工程上通常都是 B-tree 家族变体;MySQL InnoDB 按 B+Tree 理解最准确。聚簇索引、二级索引、覆盖索引
| 索引类型 | 叶子节点保存内容 | 查询特点 |
|---|---|---|
| 聚簇索引 | 整行数据 | 通过主键查询最快 |
| 二级索引 | 索引列 + 主键值 | 查询非索引列通常需要回表 |
| 覆盖索引 | 查询字段都在索引中 | 不需要回表 |
示例表:
create table user_info (
id bigint primary key,
name varchar(64) not null,
age int not null,
city varchar(64) not null,
key idx_city_age (city, age)
) engine = InnoDB default charset = utf8mb4;覆盖索引查询:
explain
select city, age
from user_info
where city = '杭州';city、age 都在 idx_city_age 中,通常不需要回表。
可能回表的查询:
explain
select name
from user_info
where city = '杭州';name 不在 idx_city_age 中,需要先通过二级索引找到主键,再回聚簇索引读取 name。
联合索引最左前缀
联合索引 (a, b, c) 的排序不是分别按三个字段建三棵树,而是按 (a, b, c) 这个顺序整体排序。
flowchart TD
A["联合索引 (a,b,c)"] --> B["先按 a 排序"]
B --> C["a 相同再按 b 排序"]
C --> D["a、b 都相同再按 c 排序"]所以 (a, b, c) 可以较好支持:
where a = ?
where a = ? and b = ?
where a = ? and b = ? and c = ?
where a = ? order by b通常不能高效支持:
where b = ?
where c = ?
where b = ? and c = ?原因是跳过 a 后,b 在整棵树里不是全局有序的。你不能拿一本按“省、市、区”排序的通讯录,直接高效查询所有“某个市”的记录,因为不同省下面都有这个市名。
范围查询对联合索引的影响
联合索引中,范围条件后面的列通常不能继续用于精确定位。
create index idx_user_time_status
on orders(user_id, created_at, status);查询:
explain
select *
from orders
where user_id = 1001
and created_at >= '2026-01-01'
and status = 1;这里 user_id 可以精确定位,created_at 是范围扫描,status 可能不能继续用于缩小索引扫描范围,只能在扫描后过滤。
索引顺序要根据业务查询设计:
| 查询特点 | 索引顺序建议 |
|---|---|
| 多个等值条件 | 等值列优先,顺序可结合区分度和排序需求 |
| 范围条件 | 范围列一般放在等值列后 |
| 排序字段 | 尽量让索引顺序兼顾 order by |
| 高频分页 | 索引要服务过滤和排序,避免大范围 filesort |
索引失效或效果变差的场景
1. 对索引列使用函数
-- 可能无法有效使用 created_at 索引
select *
from orders
where date(created_at) = '2026-01-01';
-- 更推荐
select *
from orders
where created_at >= '2026-01-01 00:00:00'
and created_at < '2026-01-02 00:00:00';原因:B+Tree 中保存的是原始 created_at 值,不是 date(created_at) 计算结果。
2. 隐式类型转换
-- phone 是 varchar,但条件给了数字
select *
from user_info
where phone = 13800138000;如果发生隐式转换,可能导致索引效果变差。字符串字段要加引号:
select *
from user_info
where phone = '13800138000';3. 前缀模糊匹配
-- 前面有 %,B+Tree 不知道从哪里开始查
select *
from article
where title like '%mysql';
-- 后缀匹配可以利用前缀定位
select *
from article
where title like 'mysql%';4. 返回数据太多
即使命中索引,如果要返回表中大部分数据,优化器也可能选择全表扫描,因为“走索引 + 大量回表”不一定比全表扫描便宜。
select *
from orders
where status = 1;如果 status = 1 占 90% 数据,单独给 status 建索引通常价值不高。
索引设计流程
flowchart TD
A["收集高频 SQL"] --> B["拆解 where / join / order by / group by"]
B --> C["区分等值、范围、排序字段"]
C --> D["设计联合索引顺序"]
D --> E["用 EXPLAIN 验证执行计划"]
E --> F{"扫描行数和回表是否可接受"}
F -- "否" --> G["调整索引或改写 SQL"]
G --> E
F -- "是" --> H["上线后观察慢 SQL 和索引使用情况"]设计索引时不要从字段出发,而要从 SQL 出发。
错误思路:
这个表有 user_id、status、created_at,那我给每个字段都建一个索引。更好的思路:
高频查询是按 user_id 查最近订单,所以设计 (user_id, created_at)。
后台还会按 status 查最近订单,所以再评估 (status, created_at) 是否必要。商业项目如何设计索引
商业系统里,索引不是围绕“表有哪些字段”设计,而是围绕“页面怎么查、接口怎么查、批处理怎么扫”设计。
以医疗数据采集与资产平台为例:
| 场景 | 高频 SQL 特点 | 推荐索引思路 |
|---|---|---|
| 资产详情 | asset_no = ? | asset_no 唯一索引,保证业务唯一并快速点查 |
| 医院资产列表 | hospital_id + status + created_at desc limit | (hospital_id, status, created_at, id) |
| 采集任务抢占 | status + next_execute_time limit | (status, next_execute_time, id),配合条件更新或锁 |
| 用户最近订单 | user_id + order by created_at | (user_id, created_at, id) |
| 后台按状态筛选 | tenant_id + status + created_at | 优先加租户或机构边界,避免单独低选择性 status |
| 报表统计 | 大范围 group by | 不要只靠普通索引,考虑汇总表、数仓或离线任务 |
| 模糊搜索 | %keyword% | B+Tree 不适合,考虑全文索引或 Elasticsearch |
一个典型资产表:
create table medical_asset (
id bigint primary key auto_increment,
asset_no varchar(64) not null,
hospital_id bigint not null,
department_id bigint,
status tinyint not null,
asset_name varchar(128) not null,
created_at datetime not null,
updated_at datetime not null,
unique key uk_asset_no(asset_no),
key idx_hospital_status_created(hospital_id, status, created_at, id),
key idx_department_status(department_id, status)
) engine = InnoDB default charset = utf8mb4;列表查询:
explain
select id, asset_no, asset_name, status, created_at
from medical_asset
where hospital_id = 1001
and status = 1
order by created_at desc, id desc
limit 20;为什么索引这样设计:
hospital_id是业务边界,先缩小到某医院。status是等值筛选,在医院范围内继续缩小。created_at, id服务排序和稳定分页。id让同一时间的数据排序稳定,也方便游标分页。- 没有把所有展示字段都塞进索引,因为索引太宽会拖慢写入和占用缓存。
如果页面需要展示更多详情字段,不要为了覆盖索引把大字段都放进索引。列表页查少量字段,详情页按主键再查完整数据,通常更稳。
索引设计为什么要考虑写入成本
每个二级索引都是一棵额外的 B+Tree。插入一行数据时,不只是写聚簇索引,还要写所有相关二级索引。
flowchart TD
A["insert 一行订单"] --> B["写聚簇索引 B+Tree"]
A --> C["写订单号唯一索引"]
A --> D["写用户时间索引"]
A --> E["写状态时间索引"]
A --> F["写商户状态索引"]
F --> G["索引越多,写入维护越重"]索引过多会带来:
| 成本 | 解释 |
|---|---|
| 写入变慢 | 每次插入、删除、更新索引列都要维护多棵 B+Tree |
| 页分裂增加 | 随机写入多个索引页,可能触发更多页分裂 |
| Buffer Pool 压力 | 热数据缓存被大量索引页挤占 |
| 磁盘和备份变大 | 索引也是数据,备份、恢复、DDL 都受影响 |
| 优化器选择复杂 | 重复或相似索引多,计划选择更难稳定 |
所以索引治理要定期做:
show index from medical_asset;结合慢日志、performance_schema、业务访问路径判断哪些索引高频使用,哪些索引长期无效或重复。
排查:为什么建了索引还是慢
flowchart TD
A["建了索引但 SQL 慢"] --> B["看 EXPLAIN 的 key"]
B --> C{"是否使用预期索引"}
C -- "否" --> D["查最左前缀、函数、隐式转换、统计信息"]
C -- "是" --> E["看 rows 和 filtered"]
E --> F{"扫描是否很大"}
F -- "是" --> G["缩小业务条件或重设计联合索引"]
F -- "否" --> H["看 Extra"]
H --> I{"是否 filesort/temporary/大量回表"}
I -- "是" --> J["调整 order by/group by/覆盖字段"]
I -- "否" --> K["继续查锁等待、IO、Buffer Pool、返回量"]常见原因:
| 原因 | 本质 | 处理 |
|---|---|---|
用了索引但 rows 很大 | 索引只能定位到很宽的范围 | 加业务边界、调整联合索引 |
Using index 仍慢 | 覆盖索引只减少回表,不减少扫描 | 看 大表覆盖索引仍然慢 |
Using filesort | 索引顺序不能满足排序 | 让联合索引兼顾过滤和排序 |
| 函数包裹索引列 | B+Tree 保存原值,不保存函数结果 | 改成范围查询或生成列索引 |
| 隐式类型转换 | 类型转换破坏索引利用 | 参数类型和字段类型一致 |
| 返回比例太高 | 大量回表不如顺序扫描 | 接受全表扫描或改变查询方式 |
| 深分页 | offset 需要跳过大量记录 | 游标分页 |
| 锁等待 | 慢不在扫描,而在等待 | 查事务和锁 |
面试标准回答
MySQL 索引为什么快
MySQL InnoDB 常用 B+Tree 索引。数据库按页读写数据,B+Tree 的非叶子节点保存 key 和页指针,一页能放很多目录项,所以树高低,查询一条记录通常只需要少量页访问。叶子节点按 key 有序并通过链表连接,所以范围查询、排序和分页可以从范围起点顺序扫描。索引快的本质不是 CPU 比较少,而是减少扫描的数据页和随机 IO。最左前缀为什么成立
联合索引 `(a,b,c)` 不是给 a、b、c 分别建三棵树,而是按 a 排序,a 相同再按 b 排序,a 和 b 都相同再按 c 排序。因此查询从最左列开始时,可以利用这棵树的有序性逐步缩小范围;如果跳过 a 直接查 b,b 在全局并不是有序的,就很难高效定位。范围条件后面的列通常不能继续用于精确定位,也是因为范围扫描已经确定了一段叶子区间,后续列只能在区间内过滤或部分优化。覆盖索引为什么不一定快
覆盖索引表示查询字段都在二级索引中,不需要回表。它解决的是少回表,不代表少扫描。如果查询范围很大、低选择性字段命中大量数据、深分页需要跳过很多行、排序分组很重,或者覆盖索引太宽导致索引页很多,即使 Extra 出现 Using index 也可能慢。排查时要看 rows、Rows_examined、Extra、返回行数和业务条件。索引是不是越多越好
不是。每个索引都是一棵额外的 B+Tree,会占磁盘和 Buffer Pool。插入、删除、更新索引列时都要维护这些索引,索引越多写入越慢,页分裂和缓存压力也越大。索引应该围绕高频 SQL 的 where、join、order by、group by 设计,并用 EXPLAIN 和慢日志验证收益,定期清理重复和无用索引。实战 Demo:订单列表索引
建表:
create table orders (
id bigint primary key auto_increment,
order_no varchar(64) not null,
user_id bigint not null,
status tinyint not null,
amount decimal(10, 2) not null,
created_at datetime not null,
unique key uk_order_no (order_no),
key idx_user_created (user_id, created_at),
key idx_status_created (status, created_at)
) engine = InnoDB default charset = utf8mb4;用户订单列表:
explain
select id, order_no, amount, created_at
from orders
where user_id = 1001
order by created_at desc
limit 20;适合索引:idx_user_created(user_id, created_at)。
后台按状态看最近订单:
explain
select id, order_no, user_id, amount, created_at
from orders
where status = 1
order by created_at desc
limit 50;适合索引:idx_status_created(status, created_at),但前提是 status = 1 的选择性不要太差。如果 90% 都是 status = 1,这个索引的收益可能有限。
索引不是越多越好
每增加一个索引,都有成本:
flowchart TD
A["新增索引"] --> B["占用磁盘空间"]
A --> C["insert 要写更多 B+Tree"]
A --> D["update 索引列要维护更多结构"]
A --> E["优化器选择成本变复杂"]索引过多的典型问题:
- 写入变慢。
- 磁盘占用变大。
- Buffer Pool 缓存被更多索引页占用。
- 很多索引长期没人用,维护成本高。
设计建议
- 主键尽量短、稳定、递增,避免频繁页分裂。
- 高频查询优先设计联合索引,不要为每个字段都单独建索引。
- 区分度高的字段更适合作为索引前缀,但还要结合排序和范围条件。
- 写多读少的表要控制索引数量。
- 查询字段尽量明确,避免无脑
select *。 - 所有索引设计都要用
EXPLAIN验证。 - 上线后继续看慢日志和真实访问模式,索引不是一次设计永远不变。
小结
索引学习的关键不是背“最左前缀、索引失效”这些词,而是从 B+Tree 的有序性理解它们:
- B+Tree 按索引定义顺序排序。
- 跳过最左列会失去全局有序性。
- 范围查询会改变后续列的可利用方式。
- 查询字段不在二级索引里可能需要回表。
- 返回数据太多时,走索引不一定划算。
关联知识点
| 知识点 | 继续学习什么 |
|---|---|
| B+Tree、B-Tree 与数据库索引家族 | 为什么 MySQL 用 B+Tree,其他数据库为什么叫 B-tree |
| InnoDB 存储结构 | 页、表空间、聚簇索引、二级索引 |
| EXPLAIN | 判断索引是否真的被优化器选择 |
| 优化器原理 | 为什么有索引也可能不用 |
| 大表覆盖索引仍然慢 | 覆盖索引的边界和大表治理 |
| 数据库优化 | 慢 SQL 综合优化流程 |
继续学习时建议阅读 EXPLAIN:索引是否真的生效,要用执行计划验证,并理解 rows、filtered、key_len 这些参数是优化器根据统计信息和索引结构估算出来的。如果已经走了覆盖索引但大表查询仍然慢,继续看 大表覆盖索引仍然慢。
