执行计划:sql查询type是range,但是rows扫描行数太多怎么办?extra使用索引下推
type=range说明索引用对了,但rows太大说明索引" selectivity(选择性)"不够,或者我们的查询范围本身就太大了。
Extra里出现Using index condition(索引下推,ICP)是个好消息,说明MySQL已经尽力在存储引擎层帮你过滤数据了。但面对几十万甚至上百万的rows,依然会很慢。
🔍 第一步:确认"扫描行数多"的根本原因
先用EXPLAIN看两个关键信息,这决定了你的优化方向:
-
看
key_len(索引长度):-
如果你的联合索引是
(a, b, c),但WHERE条件里只用了a,那么key_len就只算a的长度。 -
后果 :索引只定位到了
a这个大范围,然后在这个大范围内扫描所有行去匹配b和c。
-
-
看
filtered(过滤百分比):- 如果
rows=100万,但filtered=5%,说明扫描100万行,最终只返回5万行,有95%的扫描是无效的,这是最需要优化的信号。
- 如果
🛠️ 第二步:四种实战优化方案(按优先级排序)
方案一:调整联合索引顺序(最有效,0成本)
如果rows大是因为索引没有完全匹配 查询条件,优先调整索引列的顺序,让它覆盖 你的WHERE条件。
-
核心原则 :将 "等值查询(=)" 的列放在最前面,把 "范围查询(<, >, BETWEEN)" 的列放在最后面。
-
示例:
-
当前SQL:
WHERE a = 1 AND b > 10 AND c = 5 -
当前索引:
(a, b, c)。因为b是范围查询,c的索引就失效了,扫描的行数就是a=1且b>10的所有数据。 -
优化 :将索引改为
(a, c, b)。这样,a和c都能精准匹配,只有b是范围扫描,扫描行数会大幅下降。
-
方案二:使用"覆盖索引" + 延迟关联(针对回表严重)
rows大的另一个隐形杀手是回表 。即使索引过滤出100万行数据,回表100万次到主键取数据,I/O开销极大。如果Extra里有Using index condition,说明没有Using index(覆盖索引)。
-
核心思路:利用"子查询"先走索引覆盖,只查出主键ID,再通过主键去关联取全部数据。
-
SQL改写示例:
sql
-- 原SQL(回表严重) SELECT * FROM orders WHERE create_time BETWEEN '2023-01-01' AND '2023-04-01' AND status = 1; -- 优化后(延迟关联) SELECT * FROM orders o INNER JOIN ( SELECT id FROM orders WHERE create_time BETWEEN '2023-01-01' AND '2023-04-01' AND status = 1 -- 这里只查主键,覆盖索引避免回表,速度极快 ) t ON o.id = t.id;注意 :子查询里要建立一个
(create_time, status, id)的联合索引,确保子查询是全索引覆盖的。
方案三:在应用层做"分页游标"改写(业务驱动)
如果你的range查询是为了"拉取近三个月所有订单",这是典型的大数据量导出 场景,不应该用limit做深分页,也不适合一次性查出。
-
做法 :放弃
OFFSET,改用游标查询。sql
-- 每次只查上一次的最大ID SELECT * FROM orders WHERE create_time BETWEEN '2023-01-01' AND '2023-04-01' AND id > #{lastMaxId} ORDER BY id ASC LIMIT 1000; -
效果 :让
type变成range或ref,但rows严格限制在LIMIT范围内,彻底解决了扫描行数问题。
方案四:强制通过"索引提示"引导优化器(特殊情况)
有时候MySQL优化器判断失误,认为全表扫描比你的range索引快(可能因为统计信息不准)。
-
做法 :使用
FORCE INDEX (idx_name)强制指定索引。sql
SELECT * FROM orders FORCE INDEX (idx_create_time) WHERE create_time BETWEEN ...; -
注意:这种方法不推荐作为长期方案,因为数据分布变化后,强制索引可能变得更慢,只适合紧急救火。
🧩 关于"索引下推 (ICP)"的特别说明
你看到Using index condition,说明MySQL 5.6+ 的特性已生效。这意味着存储引擎层 会利用索引中的status字段(假设在联合索引里)先把不满足条件的数据过滤掉,再回表。
-
现状:你已经有了ICP,说明存储引擎层已经尽力了。
-
瓶颈 :现在的瓶颈大概率在回表 和网络传输 上。所以你接下来的优化重点,应该放在上面的方案二(覆盖索引+延迟关联) 和方案三(游标分批) 上。
💎 总结与建议
针对你的情况,我给出一句口诀:
-
如果业务逻辑允许 :优先用游标分页(方案三),这是最彻底的根治手段。
-
如果必须一次性返回结果集 :优先调整联合索引顺序(方案一) ,并改写SQL用延迟关联(方案二),让子查询走覆盖索引,大幅减少回表开销。
最后,千万记得用EXPLAIN验证优化后的结果,重点关注rows是否下降,以及Extra里是否出现了Using index(这意味着覆盖索引,是最理想的状态)。如果行数依然很大,那就要反思这个"近三个月"的查询范围本身是否合理,是否应该加一些强制性的业务限制(比如只查询最近100页的数据)。这是很多性能问题背后的根本原因。