MySQL 慢 SQL 优化实战

在数据库运维中,慢 SQL 往往是影响业务响应速度的 “隐形杀手”。有时明明表结构和索引看起来没问题,查询却迟迟跑不出结果。本文以一个真实案例为例,拆解慢 SQL 的优化思路,带你掌握从分析到解决的全流程技巧。

一、问题浮现:一条 “看似正常” 的慢查询

开发同学反馈,某业务统计 SQL 执行耗时长达 42 秒,严重影响用户体验。这条 SQL 的功能是统计满足以下条件的发票申请记录数:

  1. 主表invoice_sales_application(简称表 a)中shop_order_id'23060919546335%'开头;
  2. 或表 a 中is_merge=1,且关联的明细表invoice_sales_application_detail(简称表 b)中存在相同application_noinvoice_category,且表 b 的shop_order_id'23060919546335%'开头、del_flag=0

原 SQL 如下:

 
SELECT count(0)
FROM invoice_sales_application a
WHERE (
    a.shop_order_id LIKE '23060919546335%'
    OR (
        EXISTS (
            SELECT 1
            FROM invoice_sales_application_detail b
            WHERE a.application_no = b.application_no
              AND a.invoice_category = b.invoice_category
              AND b.del_flag = 0
              AND b.shop_order_id LIKE '23060919546335%'
        )
        AND a.is_merge = 1
    )
)
 

查看执行计划发现:表 a 触发了全表扫描(type: ALL),需要扫描 116 万行数据;表 b 虽用到了索引(type: ref),但整体效率极低。明明表 a 有idx_shop_order_id索引,表 b 有uk_appno_invcategory联合索引,为何会出现全表扫描?

二、根源剖析:SQL 结构中的 “隐形陷阱”

通过对 SQL 逻辑和执行计划的拆解,发现两个关键问题:

  1. OR 条件导致索引失效
    原 SQL 中OR连接了两个条件:表 a 的shop_order_id匹配,以及表 b 的shop_order_id匹配 + 表 a 的is_merge=1。MySQL 优化器在处理OR时,若两侧条件涉及不同表或字段,往往无法有效利用索引,只能选择全表扫描覆盖所有可能的匹配情况。
  2. EXISTS 子查询的间接影响
    虽然EXISTS子查询本身可以通过表 b 的索引高效定位数据,但外层OR的存在使得优化器无法将表 a 的索引与子查询结果 “联动”,最终仍需扫描全表才能判断每条记录是否满足任一条件。

三、优化迭代:两步实现 “量级级” 提速

针对上述问题,我们分阶段调整 SQL 结构,逐步消除性能瓶颈。
第一步:将 EXISTS 转为 INNER JOIN,消除子查询嵌套
EXISTS子查询的核心是判断 “是否存在关联记录”,这一逻辑可通过INNER JOIN实现,且JOIN更易被优化器识别索引关联。改写后 SQL 如下:

 
SELECT count(0)
FROM invoice_sales_application a
INNER JOIN invoice_sales_application_detail b 
  ON a.application_no = b.application_no
  AND a.invoice_category = b.invoice_category
  AND b.del_flag = 0
WHERE (
    a.shop_order_id LIKE '23060919546335%'
    OR (b.shop_order_id LIKE '23060919546335%' AND a.is_merge = 1)
)
 

执行计划显示:表 a 仍为全表扫描(type: ALL),原因是OR条件未消除,优化器仍无法确定如何使用索引。但这一步为后续优化奠定了基础 —— 通过JOIN将两表关联逻辑显性化。
第二步:用 UNION 拆分 OR 条件,释放索引效能
OR的本质是 “满足条件 A 或条件 B”,可拆分为两个独立查询后用UNION合并结果(UNION会自动去重,适合计数场景)。拆分后,每个子查询的条件更简单,优化器能精准匹配索引:
-- 子查询1:满足表a的shop_order_id条件
SELECT count(*)
FROM invoice_sales_application a
INNER JOIN invoice_sales_application_detail b 
  ON a.application_no = b.application_no
  AND a.invoice_category = b.invoice_category
  AND b.del_flag = 0
WHERE a.shop_order_id = '23060919546335' 
  AND a.del_flag = 0

UNION

-- 子查询2:满足表b的shop_order_id和表a的is_merge条件
SELECT count(*)
FROM invoice_sales_application a
INNER JOIN invoice_sales_application_detail b 
  ON a.application_no = b.application_no
  AND a.invoice_category = b.invoice_category
  AND b.del_flag = 0
WHERE b.shop_order_id = '23060919546335' 
  AND a.is_merge = 1 
  AND a.del_flag = 0;
 

优化效果

  • 执行计划中,表 a 和表 b 均使用索引(type: ref/eq_ref),消除全表扫描;
  • 执行时间从 42 秒降至 18 毫秒,性能提升 2300 倍!

四、优化原理:为什么这些改动有效?

  1. JOIN vs EXISTS
    EXISTS是 “半连接”(仅判断存在性),INNER JOIN是 “全连接”(返回匹配记录),但在计数场景中效果等价。JOIN将关联条件写入ON子句,更易被优化器识别为 “可通过索引关联”,减少不必要的行扫描。
  2. UNION vs OR
    OR会迫使优化器考虑 “最坏情况”(需覆盖所有条件的交集),可能放弃索引;而UNION将条件拆分后,每个子查询的过滤逻辑更清晰,优化器可针对单一条件选择最优索引(如本例中表 a 的idx_shop_order_id和表 b 的idx_shop_order_id)。
  3. 索引的 “有效触发”
    当条件中存在LIKE '前缀%'时,可触发前缀索引;但OR会破坏这种 “确定性”,导致优化器认为 “索引可能无法覆盖所有情况”,从而选择全表扫描。

五、扩展技巧:慢 SQL 优化的通用思路

  1. 优先看执行计划
    通过explain查看type(访问类型,ALL表示全表扫描,ref/eq_ref表示索引有效)、rows(预估扫描行数)、key(实际使用的索引),定位性能瓶颈。
  2. 拆分复杂条件
    对于OR连接的多条件,用UNION拆分;对于嵌套子查询,用JOIN扁平化,降低优化器的计算复杂度。
  3. 索引设计配合 SQL 结构
    OR拆分后的子查询条件单独创建索引(如本例中表 a 和表 b 的shop_order_id索引),确保每个子查询都能命中索引。

六、总结

慢 SQL 优化的核心不是 “盲目加索引”,而是通过调整 SQL 结构,让优化器能高效利用已有索引。本例中,从EXISTSJOIN的转换,再到ORUNION的拆分,每一步都围绕 “消除索引失效场景” 展开。记住:好的 SQL 不仅要 “实现功能”,更要 “顺应优化器的逻辑”,让数据查询如虎添翼。

posted on 2025-08-06 10:44  数据库那些事儿  阅读(243)  评论(0)    收藏  举报