MySQL 慢 SQL 优化实战
在数据库运维中,慢 SQL 往往是影响业务响应速度的 “隐形杀手”。有时明明表结构和索引看起来没问题,查询却迟迟跑不出结果。本文以一个真实案例为例,拆解慢 SQL 的优化思路,带你掌握从分析到解决的全流程技巧。
一、问题浮现:一条 “看似正常” 的慢查询
开发同学反馈,某业务统计 SQL 执行耗时长达 42 秒,严重影响用户体验。这条 SQL 的功能是统计满足以下条件的发票申请记录数:
- 主表
invoice_sales_application(简称表 a)中shop_order_id以'23060919546335%'开头; - 或表 a 中
is_merge=1,且关联的明细表invoice_sales_application_detail(简称表 b)中存在相同application_no和invoice_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 逻辑和执行计划的拆解,发现两个关键问题:
-
OR 条件导致索引失效
原 SQL 中OR连接了两个条件:表 a 的shop_order_id匹配,以及表 b 的shop_order_id匹配 + 表 a 的is_merge=1。MySQL 优化器在处理OR时,若两侧条件涉及不同表或字段,往往无法有效利用索引,只能选择全表扫描覆盖所有可能的匹配情况。 -
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 倍!
四、优化原理:为什么这些改动有效?
-
JOIN vs EXISTS
EXISTS是 “半连接”(仅判断存在性),INNER JOIN是 “全连接”(返回匹配记录),但在计数场景中效果等价。JOIN将关联条件写入ON子句,更易被优化器识别为 “可通过索引关联”,减少不必要的行扫描。 -
UNION vs OR
OR会迫使优化器考虑 “最坏情况”(需覆盖所有条件的交集),可能放弃索引;而UNION将条件拆分后,每个子查询的过滤逻辑更清晰,优化器可针对单一条件选择最优索引(如本例中表 a 的idx_shop_order_id和表 b 的idx_shop_order_id)。 -
索引的 “有效触发”
当条件中存在LIKE '前缀%'时,可触发前缀索引;但OR会破坏这种 “确定性”,导致优化器认为 “索引可能无法覆盖所有情况”,从而选择全表扫描。
五、扩展技巧:慢 SQL 优化的通用思路
-
优先看执行计划
通过explain查看type(访问类型,ALL表示全表扫描,ref/eq_ref表示索引有效)、rows(预估扫描行数)、key(实际使用的索引),定位性能瓶颈。 -
拆分复杂条件
对于OR连接的多条件,用UNION拆分;对于嵌套子查询,用JOIN扁平化,降低优化器的计算复杂度。 -
索引设计配合 SQL 结构
为OR拆分后的子查询条件单独创建索引(如本例中表 a 和表 b 的shop_order_id索引),确保每个子查询都能命中索引。
六、总结
慢 SQL 优化的核心不是 “盲目加索引”,而是通过调整 SQL 结构,让优化器能高效利用已有索引。本例中,从
EXISTS到JOIN的转换,再到OR到UNION的拆分,每一步都围绕 “消除索引失效场景” 展开。记住:好的 SQL 不仅要 “实现功能”,更要 “顺应优化器的逻辑”,让数据查询如虎添翼。
浙公网安备 33010602011771号