看懂Using where
1.什么是Using where
在 MySQL 的EXPLAIN执行计划中,Extra列显示Using where是一个非常常见的信息。
简单来说,它的意思是:MySQL 服务器(Server 层)收到存储引擎(Storage Engine 层)返回的数据后,还需要再次根据WHERE条件进行一次数据的筛选(过滤)。
要彻底理解它,我们需要先看一眼 MySQL 的内部架构,以及它跟“有没有走索引”的区别。
1. 核心原理:Server 层与存储引擎的交互
MySQL 内部大致分为两层:
Server 层:负责 SQL 解析、优化、执行逻辑,以及最终的数据过滤。
存储引擎层(如 InnoDB):负责真正去磁盘或内存里读写数据。
当出现Using where时,数据的处理流程通常是这样的:
存储引擎把数据捞出来(可能是全表扫描的一行行数据,也可能是通过索引查到的多条数据)。
存储引擎把这些数据交还给Server 层。
Server 层拿着你的
WHERE条件,对这些数据进行逐一比对,把不符合条件的丢弃掉,留下符合条件的返回给客户端。
2. 常见场景分析
很多人误以为“只要写了WHERE子句就一定会显示Using where”,这是错误的。只有当存储引擎无法一次性完美解决所有过滤条件,不得不让 Server 层擦屁股时,才会显示Using where。
以下是几种典型场景:
场景 A:完全没走索引(全表扫描)
-- 假设 age 字段没有建任何索引 SELECT * FROM user WHERE age = 18;执行过程:存储引擎只能进行全表扫描(
type = ALL),把整张表的所有数据都读出来,一条一条丢给 Server 层。Server 层通过WHERE age = 18进行筛选。结果:
Extra会显示Using where。
场景 B:走了索引,但还有其他无法用索引过滤的条件
-- 假设 name 字段有索引,但 age 字段没有索引 SELECT * FROM user WHERE name = '张三' AND age = 18;执行过程:存储引擎通过
name索引,定位到所有叫“张三”的人的数据行(假设有 100 条),然后把这 100 条数据交给 Server 层。Server 层再通过WHERE age = 18过滤掉不符合年龄的人。结果:
type = ref(走了索引),同时Extra显示Using where(Server 层进行了二次过滤)。
反例:什么时候写了WHERE却不显示Using where?
-- 假设 id 是主键 SELECT * FROM user WHERE id = 10;执行过程:存储引擎通过主键索引,精准地只拿到了
id = 10的这一行数据。既然存储引擎返回的数据百分之百就是最终结果,Server 层就不需要再做任何过滤了。结果:
Extra栏是空白(NULL),不会显示Using where。
3.Using where是好是坏?需要优化吗?
Using where本身只是一个状态陈述,它无所谓好坏,关键要看它和谁组合:
性能很差 🚨:
type: ALL+Extra: Using where含义:全表扫描加服务器端过滤。说明表数据量大时会非常慢,必须优化(通常是给
WHERE后面的字段建索引)。
性能尚可/正常 ⚖️:
type: ref/range+Extra: Using where含义:走了索引范围查询,但 Server 层做了二次过滤。如果过滤掉的数据量很少,性能通常没问题;如果过滤掉的数据量极大(比如索引查出 10 万条,Server 过滤后只剩 1 条),说明索引区分度不够,可以考虑建立联合索引来让存储引擎一步到位。
总结口诀
Using where意味着:存储引擎给得太多了,Server 层不得不亲自动手筛选。
2.为什么主键的range也会触发Using where
这是一个极其经典且容易让人产生“索引焦虑”的问题。很多开发者一看到Using where就觉得是不是索引失效了,特别是当WHERE条件明明是主键范围(比如id > 10)时。
结论先行:完全正常,不需要任何优化!主键范围查询(type: range)时出现Using where,是因为MySQL 的架构分工决定的。
底层原因:Server 层要对边界进行“把控”
还记得我们前面提到的 MySQL 双层架构吗?
存储引擎层(InnoDB):负责根据主键的 B+ 树,快速定位到范围的起点(比如
id = 11),然后开始顺着叶子节点的双向链表,一条一条地向后读取数据。Server 层:负责逻辑控制。
当你执行类似SELECT * FROM user WHERE id > 10 AND id < 20的范围查询时,底层是这样互动的:
引擎层找到了
id = 11的行,传给 Server 层。Server 层拿到这行数据,看了一眼你的
WHERE条件,发现11 < 20符合要求,于是收下。引擎层继续读下一条
id = 12传给 Server 层……直到读到id = 20传给 Server 层。Server 层一比对,发现
20 < 20不成立,于是叫停引擎:“行了,别读了,范围到头了。”
💡核心点就在这里:> 只要在执行过程中,Server 层亲自参与了对数据进行
WHERE条件的评估、比对或边界控制,执行计划的Extra列就一定会打上Using where的标记。
为什么主键等值查询(id = 1)就没有Using where?
你可能会问:“那为什么写WHERE id = 1的时候,Extra 就是空白(NULL),没有Using where呢?”
WHERE id = 1(type: const):MySQL 的优化器非常聪明。在真正执行 SQL 之前(优化阶段),它就已经知道主键是唯一的,id = 1顶多只有一条数据。所以它直接去引擎层把这一条数据抓上来,当成一个“常量(const)”直接用。既然执行期间不需要再进行任何行与行之间的循环比对和边界判断,Server 层就不用挨个评估
WHERE条件,自然就没有Using where。WHERE id > 10(type: range):这是一个流式、循环的过程。引擎层并不知道什么时候该停下来,必须由 Server 层在每一次循环中拿着WHERE条件去把控边界。
对比总结
我们可以通过下表来看清它们的本质区别:
| 查询场景 | type 类型 | Extra 提示 | 实际含义 |
WHERE id = 1 | const | 空白 (NULL) | 一步到位查到唯一行,Server 层无需循环评估条件。 |
WHERE id > 10 | range | Using where | 走了主键索引范围扫描,Server 层在循环接收数据并控制边界。(非常高效,正常现象) |
(age无索引) | ALL | Using where | 没走索引(全表扫描),Server 层把整张表的数据挨个过滤了一遍。(性能很差,需要优化) |
💡 铁律
判断一个查询快不快,优先看type列,而不是被Using where吓退。只要type是range且key是PRIMARY,就说明你的主键索引正在全力以赴、极其高效地工作着!
3.为什么主键不走索引下推?
结论非常明确:不能。主键索引(聚簇索引)完全无法使用索引下推(ICP)。
MySQL 官方文档中明确指出:ICP 适用于二级索引(Secondary Index),不适用于聚簇索引(Clustered Index)。在 InnoDB 中,主键索引就是聚簇索引。
为什么主键不需要、也无法使用索引下推?我们可以从它的“核心目的”和“物理结构”来拆解。
1. 核心原因:索引下推的 KPI 是“减少回表”
我们在前面聊过,索引下推(ICP)诞生唯一的目的,就是为了让存储引擎在二级索引树上提前过滤,从而“减少回表(去主键树查数据)的次数”。
二级索引:叶子节点只有“索引列 + 主键值”。如果不下推,引擎层得把数据一条条拿去回表,传给 Server 层;如果下推了,引擎层自己在二级索引树上查完,过滤掉不符合的,省去了大量的回表开销。
主键索引:它的叶子节点本身就存放了整张表、最完整的行数据。既然引擎层在扫主键树的时候,手里已经拿到这一行的全部字段了,它就不需要去别的地方“回表”查数据。
既然根本没有“回表”这个动作,索引下推自然就毫无用武之地了。
2. 那么,主键范围查询时引擎层是怎么做的?
既然主键不能走索引下推,那像WHERE id > 10 AND id < 20这样的查询,引擎层是怎么工作的呢?
它会直接利用主键的 B+ 树:
引擎层快速定位到
id = 11的数据页。引擎层开始沿着叶子节点的双向链表,顺藤摸瓜,把整行整行的数据直接读出来,源源不断地送给 Server 层。
Server 层在上面接着,一条条比对,直到发现
id = 20超过了边界,宣告结束。
在这个过程中,虽然没有索引下推,但由于主键索引本身就是“一步到位”的(不需要二次寻址),它的执行效率依然是所有查询方式里最高的一种。
总结
我们可以用一句话彻底理清它们的关系:
索引下推(ICP)是专门给二级索引打的“补丁”,用来弥补二级索引数据不全、频繁回表的缺陷;而主键索引天生就拥有全量数据,不需要这个补丁。
