Skip to content

MySQL索引

索引是 MySQL 性能优化里最重要、也最容易被误用的知识点。索引的作用不是“让所有查询都变快”,而是让数据库用更少的数据页找到目标数据。

先记住一句话:

索引本质上是为了减少扫描范围,但它也会增加写入成本和存储成本。

学习目标

学完这一页,你要能做到:

  1. 解释索引为什么能让查询少读页,而不是只背“索引快”。
  2. 画出 InnoDB B+Tree、聚簇索引、二级索引和回表过程。
  3. 解释最左前缀为什么成立,范围查询为什么会影响后续列利用。
  4. 区分覆盖索引解决什么问题,以及为什么覆盖索引仍然可能慢。
  5. 识别常见索引失效或效果变差场景,并能改写 SQL。
  6. 根据商业系统的查询场景设计联合索引,而不是给字段无脑建索引。
  7. EXPLAIN 验证索引是否真正被优化器选择。

为什么需要索引

假设一张用户表有 1000 万行,你要查用户名为 tom 的用户。

没有索引时:

mermaid
flowchart TD
    A["查询 username = tom"] --> B["从第一行开始扫描"]
    B --> C["逐行比较 username"]
    C --> D["扫描大量数据页"]
    D --> E["找到目标或扫描结束"]

有索引时:

mermaid
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 使用聚簇索引组织数据:

对比MyISAMInnoDB
主键索引叶子节点数据地址整行数据
二级索引叶子节点数据地址索引列 + 主键值
是否支持事务不支持支持
是否支持行锁不支持,主要表锁支持行锁

InnoDB 二级索引为什么保存主键值,而不是物理地址:

  1. 数据页分裂、移动时,物理位置可能变化。
  2. 保存主键值可以通过聚簇索引再次定位完整行。
  3. 代价是查询非索引字段时可能需要回表。

B+Tree 索引结构

mermaid
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 <--> G

B+Tree 适合数据库索引的原因:

  1. 树高较低,查找数据时磁盘 IO 次数少。
  2. 叶子节点按 key 有序,适合范围查询和排序。
  3. 非叶子节点只保存索引 key 和页指针,单页可以容纳更多指针。
  4. 叶子节点之间有链表,方便顺序扫描。

为什么用 B+Tree 不用 B-Tree

先区分两个概念:

结构数据保存在哪里叶子节点是否链表相连范围查询
B-Tree内部节点和叶子节点都可以保存数据通常不要求叶子链表范围查询需要中序遍历,过程更复杂
B+Tree数据都在叶子节点,内部节点只做目录叶子节点通常双向链表相连找到起点后沿叶子链表顺序扫描

可以把 B-Tree 理解成“每一层都可能放真实数据”,B+Tree 理解成“上面几层只是目录,真实数据统一放在最底层叶子页”。

B-Tree 的问题

mermaid
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

mermaid
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 <--> G

MySQL InnoDB 使用 B+Tree 的核心原因:

原因解释
树更矮内部页只放 key 和指针,同一页能容纳更多目录项
IO 更少树高通常很低,查一行只需要少量页访问
范围查询强找到范围起点后,沿叶子页链表顺序扫描
排序友好叶子节点天然按索引 key 有序
分页友好order by + limit 可以利用索引顺序
预读友好顺序扫描叶子页更容易利用磁盘和 Buffer Pool 预读
查询路径稳定所有数据都在叶子层,从根到叶路径更一致

例如:

sql
select *
from orders
where user_id = 1001
  and created_at >= '2026-07-01'
  and created_at < '2026-08-01'
order by created_at;

如果有联合索引:

sql
create index idx_user_time on orders(user_id, created_at);

B+Tree 可以先定位到 user_id = 1001created_at 满足范围的第一个叶子位置,然后沿叶子链表向后扫描,直到范围结束。这个过程非常适合范围查询和排序。

为什么不用红黑树或 AVL 树

红黑树、AVL 树适合内存数据结构,但不适合磁盘数据库主索引。

结构为什么不适合磁盘索引
红黑树二叉树分叉太少,数据量大时树高很高,随机 IO 多
AVL 树平衡更严格,旋转维护成本高,树高仍比 B+Tree 高
跳表适合内存和部分 LSM 场景,不如 B+Tree 适合页式磁盘范围扫描
Hash等值查询快,但不支持范围、排序、最左前缀

数据库索引要围绕“页”设计。InnoDB 默认页大小通常是 16KB,B+Tree 一个节点就是一个页或页的一部分,能最大化一次 IO 带回来的有效目录信息。

为什么不用 Hash 做主索引

Hash 索引适合:

sql
where id = 1001

但不适合:

sql
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 InnoDBB+Tree 索引聚簇索引和二级索引都是 B+Tree 思路
MySQL MyISAMB-Tree 索引非聚簇,索引叶子保存数据文件地址,工程上偏 B+Tree/B-Tree 变体
PostgreSQLB-tree index文档称 B-tree,工程上是适合范围扫描的 B-tree 变体
OracleB-tree index常说 B-tree,实际是数据库工程化 B-tree/B*Tree 体系
SQL ServerB-tree index聚集索引、非聚集索引使用 B+Tree 类结构
SQLiteB-tree表和索引基于 B-tree 页结构,工程实现接近 B+Tree 思路
MongoDB WiredTigerB-tree based indexWiredTiger 使用 B-tree 类索引结构

更实用的记法:

  1. MySQL InnoDB 明确按 B+Tree 理解
  2. PostgreSQL、Oracle、SQL Server 文档常说 B-tree,但都是数据库工程里的 B-tree 家族变体,不要死扣教材定义。
  3. 现代磁盘型关系数据库主流都是 B-Tree/B+Tree 家族,因为它们适合页式存储、范围扫描、排序和低树高。
  4. 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

mermaid
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 BYMIN/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 为例,聚集索引和非聚集索引的叶子层含义不同:

mermaid
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 家族的共同收益:

  1. 等值查询可以从根页逐层定位到叶子页。
  2. 范围查询可以定位起点后继续顺序扫描。
  3. 排序字段和索引顺序一致时,可以减少额外排序。
  4. MINMAX 可以沿索引最左或最右快速定位。
  5. 多列索引可以利用最左前缀做条件收敛。
  6. 页分裂和页合并能支持持续写入,不需要整棵树重建。

也要知道它们的代价:

代价说明
写入成本增加插入、更新索引列、删除都要维护索引结构
页分裂随机插入可能导致页分裂,带来额外 IO 和碎片
存储成本每个索引都是额外的数据结构
统计信息依赖优化器是否选择索引,依赖统计信息和成本估算
不适合低选择性字段滥建性别、状态这类字段单独建索引,可能过滤效果很差

面试遇到“为什么 Oracle、PostgreSQL、SQL Server 使用 B-Tree”时,不要只回答“因为查询快”。可以这样拆:

text
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,只是实现细节不同。

面试怎么答

可以这样回答:

text
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 理解最准确。

聚簇索引、二级索引、覆盖索引

索引类型叶子节点保存内容查询特点
聚簇索引整行数据通过主键查询最快
二级索引索引列 + 主键值查询非索引列通常需要回表
覆盖索引查询字段都在索引中不需要回表

示例表:

sql
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;

覆盖索引查询:

sql
explain
select city, age
from user_info
where city = '杭州';

cityage 都在 idx_city_age 中,通常不需要回表。

可能回表的查询:

sql
explain
select name
from user_info
where city = '杭州';

name 不在 idx_city_age 中,需要先通过二级索引找到主键,再回聚簇索引读取 name

联合索引最左前缀

联合索引 (a, b, c) 的排序不是分别按三个字段建三棵树,而是按 (a, b, c) 这个顺序整体排序。

mermaid
flowchart TD
    A["联合索引 (a,b,c)"] --> B["先按 a 排序"]
    B --> C["a 相同再按 b 排序"]
    C --> D["a、b 都相同再按 c 排序"]

所以 (a, b, c) 可以较好支持:

sql
where a = ?

where a = ? and b = ?

where a = ? and b = ? and c = ?

where a = ? order by b

通常不能高效支持:

sql
where b = ?

where c = ?

where b = ? and c = ?

原因是跳过 a 后,b 在整棵树里不是全局有序的。你不能拿一本按“省、市、区”排序的通讯录,直接高效查询所有“某个市”的记录,因为不同省下面都有这个市名。

范围查询对联合索引的影响

联合索引中,范围条件后面的列通常不能继续用于精确定位。

sql
create index idx_user_time_status
on orders(user_id, created_at, status);

查询:

sql
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. 对索引列使用函数

sql
-- 可能无法有效使用 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. 隐式类型转换

sql
-- phone 是 varchar,但条件给了数字
select *
from user_info
where phone = 13800138000;

如果发生隐式转换,可能导致索引效果变差。字符串字段要加引号:

sql
select *
from user_info
where phone = '13800138000';

3. 前缀模糊匹配

sql
-- 前面有 %,B+Tree 不知道从哪里开始查
select *
from article
where title like '%mysql';

-- 后缀匹配可以利用前缀定位
select *
from article
where title like 'mysql%';

4. 返回数据太多

即使命中索引,如果要返回表中大部分数据,优化器也可能选择全表扫描,因为“走索引 + 大量回表”不一定比全表扫描便宜。

sql
select *
from orders
where status = 1;

如果 status = 1 占 90% 数据,单独给 status 建索引通常价值不高。

索引设计流程

mermaid
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 出发。

错误思路:

text
这个表有 user_id、status、created_at,那我给每个字段都建一个索引。

更好的思路:

text
高频查询是按 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

一个典型资产表:

sql
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;

列表查询:

sql
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;

为什么索引这样设计:

  1. hospital_id 是业务边界,先缩小到某医院。
  2. status 是等值筛选,在医院范围内继续缩小。
  3. created_at, id 服务排序和稳定分页。
  4. id 让同一时间的数据排序稳定,也方便游标分页。
  5. 没有把所有展示字段都塞进索引,因为索引太宽会拖慢写入和占用缓存。

如果页面需要展示更多详情字段,不要为了覆盖索引把大字段都放进索引。列表页查少量字段,详情页按主键再查完整数据,通常更稳。

索引设计为什么要考虑写入成本

每个二级索引都是一棵额外的 B+Tree。插入一行数据时,不只是写聚簇索引,还要写所有相关二级索引。

mermaid
flowchart TD
    A["insert 一行订单"] --> B["写聚簇索引 B+Tree"]
    A --> C["写订单号唯一索引"]
    A --> D["写用户时间索引"]
    A --> E["写状态时间索引"]
    A --> F["写商户状态索引"]
    F --> G["索引越多,写入维护越重"]

索引过多会带来:

成本解释
写入变慢每次插入、删除、更新索引列都要维护多棵 B+Tree
页分裂增加随机写入多个索引页,可能触发更多页分裂
Buffer Pool 压力热数据缓存被大量索引页挤占
磁盘和备份变大索引也是数据,备份、恢复、DDL 都受影响
优化器选择复杂重复或相似索引多,计划选择更难稳定

所以索引治理要定期做:

sql
show index from medical_asset;

结合慢日志、performance_schema、业务访问路径判断哪些索引高频使用,哪些索引长期无效或重复。

排查:为什么建了索引还是慢

mermaid
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 索引为什么快

text
MySQL InnoDB 常用 B+Tree 索引。数据库按页读写数据,B+Tree 的非叶子节点保存 key 和页指针,一页能放很多目录项,所以树高低,查询一条记录通常只需要少量页访问。叶子节点按 key 有序并通过链表连接,所以范围查询、排序和分页可以从范围起点顺序扫描。索引快的本质不是 CPU 比较少,而是减少扫描的数据页和随机 IO。

最左前缀为什么成立

text
联合索引 `(a,b,c)` 不是给 a、b、c 分别建三棵树,而是按 a 排序,a 相同再按 b 排序,a 和 b 都相同再按 c 排序。因此查询从最左列开始时,可以利用这棵树的有序性逐步缩小范围;如果跳过 a 直接查 b,b 在全局并不是有序的,就很难高效定位。范围条件后面的列通常不能继续用于精确定位,也是因为范围扫描已经确定了一段叶子区间,后续列只能在区间内过滤或部分优化。

覆盖索引为什么不一定快

text
覆盖索引表示查询字段都在二级索引中,不需要回表。它解决的是少回表,不代表少扫描。如果查询范围很大、低选择性字段命中大量数据、深分页需要跳过很多行、排序分组很重,或者覆盖索引太宽导致索引页很多,即使 Extra 出现 Using index 也可能慢。排查时要看 rows、Rows_examined、Extra、返回行数和业务条件。

索引是不是越多越好

text
不是。每个索引都是一棵额外的 B+Tree,会占磁盘和 Buffer Pool。插入、删除、更新索引列时都要维护这些索引,索引越多写入越慢,页分裂和缓存压力也越大。索引应该围绕高频 SQL 的 where、join、order by、group by 设计,并用 EXPLAIN 和慢日志验证收益,定期清理重复和无用索引。

实战 Demo:订单列表索引

建表:

sql
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;

用户订单列表:

sql
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)

后台按状态看最近订单:

sql
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,这个索引的收益可能有限。

索引不是越多越好

每增加一个索引,都有成本:

mermaid
flowchart TD
    A["新增索引"] --> B["占用磁盘空间"]
    A --> C["insert 要写更多 B+Tree"]
    A --> D["update 索引列要维护更多结构"]
    A --> E["优化器选择成本变复杂"]

索引过多的典型问题:

  1. 写入变慢。
  2. 磁盘占用变大。
  3. Buffer Pool 缓存被更多索引页占用。
  4. 很多索引长期没人用,维护成本高。

设计建议

  1. 主键尽量短、稳定、递增,避免频繁页分裂。
  2. 高频查询优先设计联合索引,不要为每个字段都单独建索引。
  3. 区分度高的字段更适合作为索引前缀,但还要结合排序和范围条件。
  4. 写多读少的表要控制索引数量。
  5. 查询字段尽量明确,避免无脑 select *
  6. 所有索引设计都要用 EXPLAIN 验证。
  7. 上线后继续看慢日志和真实访问模式,索引不是一次设计永远不变。

小结

索引学习的关键不是背“最左前缀、索引失效”这些词,而是从 B+Tree 的有序性理解它们:

  1. B+Tree 按索引定义顺序排序。
  2. 跳过最左列会失去全局有序性。
  3. 范围查询会改变后续列的可利用方式。
  4. 查询字段不在二级索引里可能需要回表。
  5. 返回数据太多时,走索引不一定划算。

关联知识点

知识点继续学习什么
B+Tree、B-Tree 与数据库索引家族为什么 MySQL 用 B+Tree,其他数据库为什么叫 B-tree
InnoDB 存储结构页、表空间、聚簇索引、二级索引
EXPLAIN判断索引是否真的被优化器选择
优化器原理为什么有索引也可能不用
大表覆盖索引仍然慢覆盖索引的边界和大表治理
数据库优化慢 SQL 综合优化流程

继续学习时建议阅读 EXPLAIN:索引是否真的生效,要用执行计划验证,并理解 rowsfilteredkey_len 这些参数是优化器根据统计信息和索引结构估算出来的。如果已经走了覆盖索引但大表查询仍然慢,继续看 大表覆盖索引仍然慢