MySQL一个简单概念:索引下推和索引查询流程

索引下推(Index Condition Pushdown)


一、从最基础的概念开始:数据库是如何查数据的?

在正式讲索引下推之前,我们需要先建立一些基础认知。

1.1 数据库中的数据存在哪里?

数据库里的数据最终都存储在磁盘上。你可以把磁盘想象成一个巨大的仓库,数据一行一行地存放在里面。当你执行一条 SQL 查询语句时,数据库需要从这个仓库里把符合条件的数据找出来,返回给你。

最笨的方式就是:从头到尾把仓库里的每一行数据都取出来看一看,符合条件的留下,不符合的扔掉。这种方式叫做全表扫描

全表扫描的问题很明显:如果仓库里有一亿行数据,而你只需要其中十行,数据库却要把一亿行数据全部看一遍,效率极低。

1.2 索引是什么?

为了解决全表扫描效率低的问题,数据库引入了索引这个概念。

索引就像是一本书的目录。如果你想在一本 500 页的书里找"索引下推"这个词,你不会从第 1 页翻到第 500 页,而是直接查目录,目录告诉你这个词在第 238 页,你直接翻到第 238 页就行了。

数据库的索引也是一样的道理。它是一种特殊的数据结构(MySQL 中最常用的是 B+ 树 ),这个数据结构存储了某一列(或某几列)的值,以及这些值对应的数据行在磁盘上的位置(我们叫它主键值 或者行指针)。

有了索引,数据库就可以:

  1. 先去索引里快速找到满足部分条件的行的位置;
  2. 再根据这个位置去磁盘(或者主表)里取出完整的数据行。

1.3 两种常见的索引类型

在 MySQL InnoDB 引擎中,索引主要分为两种:

主键索引(聚簇索引)

  • 索引的叶子节点直接存储了整行数据。
  • 通过主键查询时,找到索引节点,就找到了完整数据,不需要额外的步骤。

非主键索引(二级索引 / 辅助索引)

  • 索引的叶子节点存储的是索引列的值 + 主键值,并不存储完整数据。
  • 通过二级索引查询时,找到索引节点后,只能得到主键值,还需要拿着这个主键值再去主键索引中查一次,才能得到完整的行数据。
  • 这个"拿着主键再去查一次"的过程,有一个专业名词叫做回表(Back to Table)

二、什么是联合索引?

索引下推主要发生在联合索引的场景下,所以我们必须先搞清楚联合索引是什么。

2.1 联合索引的定义

联合索引,就是把多个列组合在一起建立的索引。

比如我们有一张用户表 users

sql 复制代码
CREATE TABLE users (
    id       INT PRIMARY KEY,
    name     VARCHAR(50),
    age      INT,
    city     VARCHAR(50),
    email    VARCHAR(100)
);

我们在 nameage 两个列上建立联合索引:

sql 复制代码
CREATE INDEX idx_name_age ON users(name, age);

2.2 联合索引是如何存储数据的?

联合索引在 B+ 树中,是按照索引列的顺序依次排序的。

对于 (name, age) 这个联合索引,排序规则是:

  1. 先按 name 排序
  2. name 相同时,再按 age 排序

假设表里有以下数据:

id name age city
1 张三 25 北京
2 张三 28 上海
3 李四 22 广州
4 王五 30 北京
5 张三 32 深圳

那么 idx_name_age 这个联合索引中,叶子节点按照如下顺序存储:

复制代码
(李四, 22) -> id=3
(王五, 30) -> id=4
(张三, 25) -> id=1
(张三, 28) -> id=2
(张三, 32) -> id=5

可以看到:所有"张三"被放在一起,然后在"张三"内部,按 age 从小到大排列。

2.3 最左前缀原则

联合索引有一个重要的使用规则叫最左前缀原则:查询条件必须从联合索引最左边的列开始,才能使用到这个索引。

  • WHERE name = '张三' ------ 可以用索引(使用了最左列 name)
  • WHERE name = '张三' AND age = 25 ------ 可以用索引(使用了 name 和 age)
  • WHERE age = 25 ------ 不能用这个索引(跳过了最左列 name)

三、没有索引下推时,查询是怎么执行的?

现在我们有了足够的背景知识,来看一个具体的查询:

sql 复制代码
SELECT * FROM users WHERE name LIKE '张%' AND age = 28;

这条 SQL 的意思是:查找所有姓名以"张"开头、并且年龄等于 28 的用户。

3.1 执行流程分析(不使用索引下推)

在没有索引下推的情况下,MySQL 的执行过程分为两个角色:存储引擎层Server 层

  • 存储引擎层:负责实际的数据存储和索引的维护,是真正和磁盘打交道的地方。
  • Server 层:负责 SQL 解析、优化、以及执行查询逻辑(包括 WHERE 条件的过滤)。

在旧的执行方式下,两层的分工是这样的:

第一步:存储引擎层使用索引定位数据

存储引擎接到查询请求,使用 idx_name_age 联合索引,根据 name LIKE '张%' 这个条件,在 B+ 树中找到所有 name 以"张"开头的索引项:

复制代码
(张三, 25) -> id=1
(张三, 28) -> id=2
(张三, 32) -> id=5

找到了 3 个匹配的索引项。

第二步:逐条回表

存储引擎对找到的每一个索引项,都要拿着主键值去主键索引(完整数据)里查一次,取出完整的行数据,然后返回给 Server 层。

  • 拿着 id=1,回表查询,得到 {id:1, name:'张三', age:25, city:'北京', email:'...'}
  • 拿着 id=2,回表查询,得到 {id:2, name:'张三', age:28, city:'上海', email:'...'}
  • 拿着 id=5,回表查询,得到 {id:5, name:'张三', age:32, city:'深圳', email:'...'}

第三步:Server 层过滤

Server 层拿到这 3 行完整数据后,再用 age = 28 这个条件进行过滤,只保留 age 等于 28 的行:

最终结果:{id:2, name:'张三', age:28, city:'上海', email:'...'}

3.2 问题在哪里?

你有没有发现一个浪费?

在第二步,存储引擎回表了 3 次(id=1, id=2, id=5 各一次),取出了 3 条完整数据。但其中 id=1 和 id=5 的数据,在第三步被 Server 层丢弃了,因为它们的 age 不等于 28。

也就是说,id=1 和 id=5 的回表操作是多余的、浪费的

关键点来了:age 这个条件,实际上在索引里就已经有了 !你看索引里存的是 (张三, 25)(张三, 28)(张三, 32),age 的值就在索引节点里。

既然索引里就能看出 age=25 和 age=32 都不等于 28,为什么不在回表之前就把它们排除掉,只回表查 age=28 的那一条呢?

这就是索引下推要解决的问题。


四、索引下推:把条件"下推"到存储引擎层

4.1 索引下推的核心思想

索引下推(Index Condition Pushdown,简称 ICP)的核心思想是:

把原本由 Server 层负责的、可以在索引层面就判断的 WHERE 条件,提前下沉到存储引擎层去执行,从而减少不必要的回表次数。

"下推"这个词,说的就是把条件从上层(Server 层)推到下层(存储引擎层)。

4.2 使用索引下推时,查询如何执行?

还是同样的查询:

sql 复制代码
SELECT * FROM users WHERE name LIKE '张%' AND age = 28;

第一步:存储引擎层使用索引定位候选项

同样地,存储引擎用 idx_name_age 索引,找到所有 name 以"张"开头的索引项:

复制代码
(张三, 25) -> id=1
(张三, 28) -> id=2
(张三, 32) -> id=5

第二步:在索引层面直接判断 age 条件(这是关键!)

开启索引下推后,存储引擎不急着回表 ,而是先拿着手头的索引数据,继续判断剩余的 WHERE 条件 age = 28

  • (张三, 25):age=25,不等于 28,直接跳过,不回表
  • (张三, 28):age=28,等于 28,需要回表
  • (张三, 32):age=32,不等于 28,直接跳过,不回表

第三步:只对满足全部索引条件的行进行回表

只有 id=2 满足条件,存储引擎只回表一次,取出完整数据:

{id:2, name:'张三', age:28, city:'上海', email:'...'}

第四步:Server 层接收数据

Server 层接到存储引擎返回的数据,已经是过滤后的结果了,直接返回给用户。

4.3 前后对比总结

步骤 无索引下推 有索引下推
索引扫描 扫描 3 条索引项 扫描 3 条索引项
在索引层过滤 age ❌ 不过滤 ✅ 过滤掉 2 条
回表次数 3 次 1 次
返回给 Server 层 3 行完整数据 1 行完整数据
Server 层过滤 过滤后剩 1 行 直接就是 1 行

回表次数从 3 次降低到 1 次,节省了 2 次磁盘 I/O 操作。

在实际业务中,如果符合第一个索引列条件(name LIKE '张%')的数据有 10 万条,但同时满足 age = 28 的只有 1000 条,那么索引下推可以减少 99000 次回表操作,性能提升是极其显著的。


五、索引下推的适用条件

索引下推并不是在所有情况下都会生效,它有一些具体的适用条件。

5.1 必须使用二级索引(非主键索引)

索引下推的价值在于减少回表。如果查询走的是主键索引(聚簇索引),索引节点本身就包含了完整的行数据,根本不需要回表,自然也就不需要索引下推了。

所以,索引下推只对二级索引有意义

5.2 WHERE 条件中必须有可以在索引上判断的列

如果 WHERE 条件里的列根本不在索引里,存储引擎就没有办法提前判断,条件只能由 Server 层来处理。

以我们的 idx_name_age (name, age) 为例:

  • age = 28:在索引里,可以下推 ✅
  • city = '上海':不在索引里,不能下推 ❌

5.3 索引下推对覆盖索引无效(也不需要)

覆盖索引是指:查询所需要的所有列,都在索引中包含了,不需要回表。

比如:

sql 复制代码
SELECT name, age FROM users WHERE name LIKE '张%' AND age = 28;

这条 SQL 只需要 nameage 两列,而这两列都在 idx_name_age 索引里,根本不需要回表。既然不需要回表,索引下推也就没有意义了(当然,对于覆盖索引,存储引擎仍然会在索引上做条件判断,但这属于"索引过滤",不涉及减少回表的优化)。

5.4 联合索引中,只有"非前导列"的条件才需要下推

在联合索引 (name, age) 中:

  • name 是前导列(第一列)
  • age 是非前导列(第二列)

name LIKE '张%' 这个条件,存储引擎在 B+ 树中定位数据时就已经用到了,这是索引的导航作用

age = 28 这个条件,在 B+ 树的导航过程中没有被用到(因为 name LIKE '张%' 是范围查询,确定范围后,age 的全局排序就没有意义了),只能作为"额外的过滤条件"------索引下推就负责在这里把它提前处理掉。


六、如何验证索引下推是否生效?

6.1 使用 EXPLAIN 查看执行计划

执行 EXPLAIN 可以查看 MySQL 对一条 SQL 的执行计划:

sql 复制代码
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age = 28;

在输出结果中,有一列叫 Extra,如果索引下推生效,你会看到:

复制代码
Using index condition

这就是索引下推生效的标志。

相比之下,如果是普通的索引扫描 + 回表(无下推),Extra 列通常是空的或者显示 Using where

6.2 使用 optimizer_switch 手动开关索引下推

MySQL 5.6 及以上版本默认开启索引下推。你也可以手动控制它:

sql 复制代码
-- 关闭索引下推
SET optimizer_switch = 'index_condition_pushdown=off';

-- 开启索引下推(默认)
SET optimizer_switch = 'index_condition_pushdown=on';

通过对比开关前后的执行计划和实际查询耗时,可以直观地感受索引下推带来的性能提升。


七、一个完整的例子,把所有内容串起来

让我们用一个更完整的例子,把上面所有内容串联起来理解。

场景设定

假设有一张电商平台的商品表:

sql 复制代码
CREATE TABLE products (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    brand       VARCHAR(50),   -- 品牌
    price       DECIMAL(10,2), -- 价格
    stock       INT,           -- 库存
    description TEXT           -- 商品描述
);

-- 在 brand 和 price 上建立联合索引
CREATE INDEX idx_brand_price ON products(brand, price);

表中有 100 万条商品数据。

查询需求

查找所有品牌名以"华"开头、价格在 1000 到 2000 之间的商品:

sql 复制代码
SELECT * FROM products WHERE brand LIKE '华%' AND price BETWEEN 1000 AND 2000;

执行过程分析

联合索引 idx_brand_price 中,数据排列方式:

先按 brand 排序,brand 相同时按 price 排序:

复制代码
...
(华为, 899.00)  -> id=10234
(华为, 1299.00) -> id=10235
(华为, 1999.00) -> id=10236
(华为, 2599.00) -> id=10237
(华硕, 1099.00) -> id=20100
(华硕, 1599.00) -> id=20101
(华硕, 3200.00) -> id=20102
...

不使用索引下推的过程:

  1. 存储引擎通过 brand LIKE '华%' 找到所有品牌以"华"开头的索引项,假设有 5 万条
  2. 对这 5 万条索引项全部回表,取出完整数据,共 5 万次 I/O
  3. Server 层对 5 万条数据再用 price BETWEEN 1000 AND 2000 过滤,假设最终只剩 8000 条
  4. 其中 4.2 万次回表是无意义的浪费

使用索引下推的过程:

  1. 存储引擎通过 brand LIKE '华%' 找到所有品牌以"华"开头的索引项,假设有 5 万条
  2. 在索引层面 直接判断 price BETWEEN 1000 AND 2000,5 万条中有 4.2 万条价格不在范围内,直接排除
  3. 只剩 8000 条满足两个条件,只对这 8000 条进行回表,共 8000 次 I/O
  4. 取出 8000 条完整数据返回给 Server 层

效果:回表次数从 5 万次降低到 8000 次,减少了 84% 的磁盘 I/O!


八、索引下推的本质是什么?

现在我们可以从更本质的角度来理解索引下推。

8.1 本质是职责的重新划分

在没有索引下推之前,MySQL 的分层架构中存在一个"浪费":

  • 存储引擎层只负责"根据索引导航,找到可能的数据行,逐一回表";
  • Server 层负责"拿到完整数据后,用 WHERE 条件过滤"。

这种分工方式,导致很多"其实在索引层面就可以判断为不符合条件的行",仍然被存储引擎层老老实实地回表取出来,再由 Server 层丢掉。

索引下推的出现,重新定义了存储引擎层的职责:存储引擎层不再是"只会导航"的底层工人,而是具备了"在索引上提前过滤"的能力。

8.2 本质是减少磁盘 I/O

回表操作之所以昂贵,是因为它意味着随机的磁盘 I/O。每次回表,都要根据主键值,去磁盘上的某个位置读取数据,而这些位置可能分散在磁盘的各个地方(随机 I/O 比顺序 I/O 慢得多)。

索引下推通过减少回表次数,直接减少了随机磁盘 I/O 的次数,这是它能显著提升查询性能的根本原因。

8.3 它适用于"索引列有剩余过滤能力"的场景

当你的联合索引中,第一列用于范围查询 (如 LIKE '张%'BETWEEN>< 等),此时第二列的排序在索引内部是局部有序而非全局有序的,B+ 树的导航能力对第二列的过滤作用有限。索引下推正是在这种场景下,充分利用了索引中存储的第二列(及以后的列)的值,进行额外的过滤,避免了大量无意义的回表。


九、总结

让我们用最简洁的语言,把索引下推的全部要点总结一遍:

是什么:

索引下推(ICP)是 MySQL 5.6 引入的一种查询优化技术,允许存储引擎在扫描索引时,就对 WHERE 条件中涉及索引列的条件进行判断,提前过滤掉不满足条件的索引项,从而减少回表次数。

解决什么问题:

解决了联合索引场景下,多余的回表操作带来的性能浪费问题。

核心机制:

将原本由 Server 层执行的、可以在索引层面判断的条件,"下推"到存储引擎层,在回表之前就完成过滤。

什么时候最有用:

  • 使用了联合索引;
  • WHERE 条件中第一列是范围条件(导致第二列在全局上无序);
  • 后续列的条件能过滤掉大量数据(选择性高);
  • 最终需要回表获取完整数据(非覆盖索引查询)。

如何验证:

使用 EXPLAIN 查看执行计划,Extra 列出现 Using index condition 表示索引下推已生效。

记住这一句话:

索引下推,就是让存储引擎在"拿着号码牌去仓库取货"之前,先把手里的号码牌过滤一遍,把明显不符合要求的号码牌直接扔掉,只拿着真正需要的号码牌去取货。

这样,跑腿取货的次数更少,查询自然就更快了。

相关推荐
本体智能2 小时前
从“查数”到“懂数”:本体语义层让数据分析真正智能化
数据库·数据挖掘·数据分析
爬山算法2 小时前
MongoDB(71)如何启用MongoDB身份验证?
数据库·mongodb·oracle
蚂蚁数据AntData2 小时前
DB-GPT V0.8.0 版本更新|范式跃迁:AI + Data 驱动的数据分析交互体验升级
大数据·数据库·人工智能·数据分析·开源
云边有个稻草人2 小时前
【MySQL】第十五节—事务隔离级别与 MVCC 机制深度解析
数据库·mysql事务·可重复读·模拟mvcc·如何理解隔离性·串行化·undo 日志
FinTech老王2 小时前
Oracle的CONNECT BY在国产数据库中的实现
数据库·oracle
Java后端的Ai之路2 小时前
3 天从入门到可视化监控:Elasticsearch 新手实战指南
大数据·数据库·elasticsearch·搜索引擎·向量数据库
l1t2 小时前
DeepSeek 总结的pgEdge for Postgres 的 MCP 服务器
服务器·数据库·postgresql·mcp
观测云2 小时前
观测云3月产品升级报告 | 网络设备自动发现、数据库深度分析上线,故障中心、仪表板、APM及管理能力等持续优化
网络·数据库·apm
志栋智能2 小时前
小而美的选择:低成本超自动化巡检工具
数据库·人工智能