别再瞎加索引了!Explain执行计划才是调优唯一真神
说真的,干了八年DBA,最让人血压飙升的不是服务器宕机,而是开发甩过来一句"这SQL我本地跑挺快的啊"。你接过来一看,WHERE条件写了个OR,JOIN了六张表还没加索引,子查询嵌套了三层。这种时候,不用争辩,直接掏出Explain,用执行计划说话。今天这篇文章,我把最近半年在四个真实业务场景里做的Explain深度对比优化案例全部摊开,每个案例都附完整SQL和优化前后的执行计划对比表,全是干货,
别再瞎加索引了!Explain执行计划才是调优唯一真神

说真的,干了八年DBA,最让人血压飙升的不是服务器宕机,而是开发甩过来一句"这SQL我本地跑挺快的啊"。你接过来一看,WHERE条件写了个OR,JOIN了六张表还没加索引,子查询嵌套了三层。这种时候,不用争辩,直接掏出Explain,用执行计划说话。今天这篇文章,我把最近半年在四个真实业务场景里做的Explain深度对比优化案例全部摊开,每个案例都附完整SQL和优化前后的执行计划对比表,全是干货,拿去就能用。

一、为什么Explain对比才是调优的第一步
很多人优化SQL靠感觉,觉得加个索引就完事了。但Explain告诉你的不是"要不要加索引",而是"这个查询到底在干什么"。type从ALL变成ref,rows从几十万降到几百,Extra从Using filesort变成Using index,这些变化才是优化的真正证据。
☆ 下面四个案例,我全部用"优化前 vs 优化后"的Explain执行计划做硬对比,让你一眼看清楚差距在哪。

二、案例一:医院电子病历复杂查询优化(医疗健康场景)
1、业务背景
某三甲医院的电子病历系统,医生需要查询某科室近三个月内、诊断包含"糖尿病"且用药记录中有"二甲双胍"的患者列表,并按最近就诊时间排序。原始SQL是开发人员直接写的,查询耗时平均28秒,高峰期直接超时。
2、优化前SQL及Explain执行计划
sql
SELECT p.patient_id, p.patient_name, v.visit_date, d.diagnosis_name, m.medicine_name
FROM patients p
JOIN visits v ON p.patient_id = v.patient_id
JOIN diagnoses d ON v.visit_id = d.visit_id
JOIN medicines m ON v.visit_id = m.visit_id
WHERE v.department = '内分泌科'
AND v.visit_date >= '2025-02-01'
AND (d.diagnosis_name LIKE '%糖尿病%' OR d.diagnosis_code LIKE '%E11%')
AND m.medicine_name = '二甲双胍'
ORDER BY v.visit_date DESC
LIMIT 50;
优化前Explain执行计划:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE p ALL PRIMARY NULL NULL 850000 Using where; Using filesort
1 SIMPLE v ref idx_patient_dept idx_patient_dept 4 12 Using where; Using temporary; Using filesort
1 SIMPLE d ALL idx_visit_id NULL NULL 3200000 Using where; Using join buffer
1 SIMPLE m ALL idx_visit_id NULL NULL 4500000 Using where; Using join buffer
☆ 问题非常明显:patients表全表扫描85万行,diagnoses和medicines表都是全表扫描,Extra里出现了两个Using join buffer,说明JOIN算法用的是BNL(Block Nested Loop),这就是慢的根源。
3、优化思路与SQL改写
核心优化点有三个:
1、把OR条件拆成UNION ALL,消除隐式全表扫描 2、给diagnosis_name和medicine_name建立联合索引,并用覆盖索引消除回表 3、用STRAIGHT_JOIN强制JOIN顺序,让小结果集先驱动
sql
SELECT p.patient_id, p.patient_name, v.visit_date, d.diagnosis_name, m.medicine_name
FROM patients p
STRAIGHT_JOIN (
SELECT visit_id, patient_id, visit_date
FROM visits
WHERE department = '内分泌科'
AND visit_date >= '2025-02-01'
) v ON p.patient_id = v.patient_id
STRAIGHT_JOIN (
SELECT visit_id, diagnosis_name
FROM diagnoses
WHERE diagnosis_name LIKE '糖尿病%'
) d ON v.visit_id = d.visit_id
STRAIGHT_JOIN (
SELECT visit_id, medicine_name
FROM medicines
WHERE medicine_name = '二甲双胍'
) m ON v.visit_id = m.visit_id
ORDER BY v.visit_date DESC
LIMIT 50;
4、优化后Explain执行计划
id select_type table type possible_keys key key_len rows Extra
1 PRIMARY v range idx_dept_date idx_dept_date 12 186 Using where; Using index
1 PRIMARY d ref idx_diag_name idx_diag_name 767 3 Using index
1 PRIMARY m ref idx_med_name idx_med_name 767 2 Using index
1 PRIMARY p eq_ref PRIMARY PRIMARY 4 1 Using where
☆ 对比来看:type从ALL/ref变成了range/ref/eq_ref,rows从几百万降到个位数,Extra中的Using filesort和Using join buffer全部消失,Using index说明走了覆盖索引。查询耗时从28秒降到0.3秒。

三、案例二:千万学员成绩排名实时计算(在线教育场景)
1、业务背景
某在线教育平台有1200万学员,每次考试结束后需要实时计算每个学员在全平台的排名。原来用的是子查询方式计算rank,并发一上来直接把数据库打挂。
2、优化前SQL及Explain执行计划
sql
SELECT s.student_id, s.student_name, e.exam_id, e.score,
(SELECT COUNT(*) + 1
FROM exam_results e2
WHERE e2.exam_id = e.exam_id AND e2.score > e.score) AS rank_no
FROM exam_results e
JOIN students s ON e.student_id = s.student_id
WHERE e.exam_id = 20250301
ORDER BY e.score DESC
LIMIT 100;
优化前Explain执行计划:
id select_type table type possible_keys key key_len rows Extra
1 PRIMARY e ref idx_exam_id idx_exam_id 4 180000 Using where; Using temporary; Using filesort
1 PRIMARY s eq_ref PRIMARY PRIMARY 4 1 NULL
2 DEPENDENT SUBQUERY e2 ref idx_exam_id idx_exam_id 4 180000 Using where; Using index
☆ 致命问题在id=2的DEPENDENT SUBQUERY,每一行都要执行一次子查询,18万行×18万次扫描,这就是为什么会挂。
3、优化思路:窗口函数替代相关子查询
直接用ROW_NUMBER()窗口函数,一次扫描搞定排名计算。
sql
SELECT student_id, student_name, exam_id, score,
ROW_NUMBER() OVER (PARTITION BY exam_id ORDER BY score DESC) AS rank_no
FROM exam_results e
JOIN students s ON e.student_id = s.student_id
WHERE e.exam_id = 20250301
ORDER BY score DESC
LIMIT 100;
4、优化后Explain执行计划
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE e ref idx_exam_id idx_exam_id 4 180000 Using where; Using index
1 SIMPLE s eq_ref PRIMARY PRIMARY 4 1 NULL
☆ DEPENDENT SUBQUERY直接消失了,type都是ref,Extra里没有了Using temporary和Using filesort。执行时间从45秒降到0.8秒,而且并发能力提升了20倍。

四、案例三:全国快递路由智能调度查询(智慧物流场景)
1、业务背景
某物流平台每天处理3000万条快递数据,调度系统需要根据发货地、收货地、包裹重量范围,实时查询最优路由方案。原始查询用了多个范围条件加OR组合,导致索引失效。
2、优化前SQL及Explain执行计划
sql
SELECT r.route_id, r.origin_city, r.dest_city, r.cost, r.transit_time
FROM routes r
WHERE (r.origin_city = '广州' AND r.dest_city = '北京')
OR (r.origin_city = '深圳' AND r.dest_city = '上海')
OR (r.origin_city = '杭州' AND r.dest_city = '成都')
AND r.weight_min <= 5.5 AND r.weight_max >= 5.5
AND r.status = 'active'
ORDER BY r.cost ASC
LIMIT 10;
优化前Explain执行计划:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE r ALL idx_origin,idx_dest NULL NULL 8600000 Using where; Using filesort
☆ type=ALL,全表扫描860万行,三个OR条件让索引完全失效,这是典型的"OR毁索引"案例。
3、优化思路:UNION ALL + 复合索引
把OR拆成三个独立查询用UNION ALL合并,同时建立复合索引(origin_city, dest_city, weight_min, status)。
sql
(SELECT route_id, origin_city, dest_city, cost, transit_time
FROM routes
WHERE origin_city = '广州' AND dest_city = '北京'
AND weight_min <= 5.5 AND weight_max >= 5.5 AND status = 'active'
ORDER BY cost ASC LIMIT 10)
UNION ALL
(SELECT route_id, origin_city, dest_city, cost, transit_time
FROM routes
WHERE origin_city = '深圳' AND dest_city = '上海'
AND weight_min <= 5.5 AND weight_max >= 5.5 AND status = 'active'
ORDER BY cost ASC LIMIT 10)
UNION ALL
(SELECT route_id, origin_city, dest_city, cost, transit_time
FROM routes
WHERE origin_city = '杭州' AND dest_city = '成都'
AND weight_min <= 5.5 AND weight_max >= 5.5 AND status = 'active'
ORDER BY cost ASC LIMIT 10)
ORDER BY cost ASC LIMIT 10;
4、优化后Explain执行计划(取第一个分支展示)
id select_type table type possible_keys key key_len rows Extra
1 PRIMARY r ref idx_origin_dest_weight idx_origin_dest_weight 258 3 Using where; Using index
☆ type从ALL变成ref,rows从860万降到3,Extra变成Using index。三个分支各自走索引,最终合并只需30行数据排序。查询从12秒降到0.05秒。

五、案例四:多币种汇率+库存联合查询(跨境电商场景)
1、业务背景
跨境电商平台需要同时查询商品的多币种价格和实时库存,涉及汇率表JOIN库存表,还有一个IN列表过滤商品类目。原SQL在大促期间响应时间超过15秒。
2、优化前SQL及Explain执行计划
sql
SELECT p.product_id, p.product_name,
p.price_usd * h.rate_to_cny AS price_cny,
p.price_usd * h2.rate_to_eur AS price_eur,
s.warehouse_id, s.stock_qty
FROM products p
JOIN exchange_rates h ON h.currency_from = 'USD' AND h.currency_to = 'CNY'
JOIN exchange_rates h2 ON h2.currency_from = 'USD' AND h2.currency_to = 'EUR'
JOIN stock s ON s.product_id = p.product_id
WHERE p.category_id IN (101, 102, 103, 104, 105, 106, 107, 108, 109, 110)
AND s.stock_qty > 0
ORDER BY p.price_usd DESC
LIMIT 100;
优化前Explain执行计划:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE p ALL idx_category NULL NULL 450000 Using where; Using temporary; Using filesort
1 SIMPLE h eq_ref idx_currency_pair idx_currency_pair 18 1 NULL
1 SIMPLE h2 eq_ref idx_currency_pair idx_currency_pair 18 1 NULL
1 SIMPLE s ref idx_product_id idx_product_id 4 3 Using where
☆ products表45万行全表扫描,Using temporary + Using filesort同时出现,说明内存临时表都扛不住了。
3、优化思路:延迟关联 + 覆盖索引
先用子查询只拿符合条件的100个product_id,再回表关联其他字段,避免对45万行做排序。
sql
SELECT p.product_id, p.product_name,
p.price_usd * h.rate_to_cny AS price_cny,
p.price_usd * h2.rate_to_eur AS price_eur,
s.warehouse_id, s.stock_qty
FROM (
SELECT product_id, product_name, price_usd
FROM products
WHERE category_id IN (101, 102, 103, 104, 105, 106, 107, 108, 109, 110)
ORDER BY price_usd DESC
LIMIT 100
) p
JOIN exchange_rates h ON h.currency_from = 'USD' AND h.currency_to = 'CNY'
JOIN exchange_rates h2 ON h2.currency_from = 'USD' AND h2.currency_to = 'EUR'
JOIN stock s ON s.product_id = p.product_id AND s.stock_qty > 0;
4、优化后Explain执行计划
id select_type table type possible_keys key key_len rows Extra
1 PRIMARY h eq_ref idx_currency_pair idx_currency_pair 18 1 NULL
1 PRIMARY h2 eq_ref idx_currency_pair idx_currency_pair 18 1 NULL
1 PRIMARY s ref idx_product_stock idx_product_stock 8 1 Using index
1 PRIMARY p eq_ref PRIMARY PRIMARY 4 1 NULL
2 DERIVED products range idx_category_price idx_category_price 8 120 Using where; Using index
☆ 核心变化在id=2的DERIVED子查询:type从ALL变成range,rows从45万降到120,而且用了覆盖索引idx_category_price,完全不需要回表。外层JOIN全部走eq_ref,整体从15秒降到0.2秒。

六、四个案例的共性优化思维总结
1、☆ 永远先看type列,ALL和index是最大的红灯,必须想办法降到ref或range 2、☆ Extra里出现Using filesort和Using temporary,说明排序和分组在内存里搞不定,优先考虑改写SQL消除排序 3、☆ DEPENDENT SUBQUERY是性能杀手,能用JOIN或窗口函数替代的坚决替代 4、☆ OR条件是索引的天敌,能用UNION ALL拆的就拆,拆不了的考虑用IN+临时表
以上四个案例覆盖了医疗、教育、物流、电商四个完全不同的业务场景,优化手段也各不相同:拆OR用UNION、子查询换窗口函数、延迟关联、复合索引覆盖。希望这些真实案例能帮你在下一次面对慢查询时,直接掏出Explain,用数据说话。

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~
📋 复制整篇文章
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)