sql优化方案

执行计划:sql查询type是range,但是rows扫描行数太多怎么办?extra使用索引下推

type=range说明索引用对了,但rows太大说明索引" selectivity(选择性)"不够,或者我们的查询范围本身就太大了。

Extra里出现Using index condition(索引下推,ICP)是个好消息,说明MySQL已经尽力在存储引擎层帮你过滤数据了。但面对几十万甚至上百万的rows,依然会很慢。

🔍 第一步:确认"扫描行数多"的根本原因

先用EXPLAIN看两个关键信息,这决定了你的优化方向:

  1. key_len(索引长度)

    • 如果你的联合索引是 (a, b, c),但WHERE条件里只用了a,那么key_len就只算a的长度。

    • 后果 :索引只定位到了a这个大范围,然后在这个大范围内扫描所有行去匹配bc

  2. 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=1b>10的所有数据。

    • 优化 :将索引改为 (a, c, b)。这样,ac都能精准匹配,只有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变成rangeref,但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页的数据)。这是很多性能问题背后的根本原因。