上周排查一个慢查询时,我盯着EXPLAIN输出看了半小时才恍然大悟——原来这个全表扫描问题早就在执行计划里暴露无遗。作为MySQL性能调优的"X光机",EXPLAIN能直观展示查询的骨骼结构,但90%的开发者只停留在看type列是否出现ALL的层面。今天我们就来解剖这个最熟悉的陌生人。
执行计划本质上就是MySQL优化器给出的查询路线图。当你说SELECT时,MySQL会考虑数百种执行路径:先查哪个表?用哪个索引?要不要排序?EXPLAIN就是把最终选择的执行方案打印给你看。我见过太多团队在索引上胡乱堆砌,却从不看执行计划反馈,就像蒙着眼睛调参。
先看这个典型执行计划案例:
sql复制EXPLAIN SELECT * FROM orders
WHERE user_id = 100 AND status = 'paid'
ORDER BY create_time DESC;
输出结果各列含义如下:
| 列名 | 示例值 | 深度解读 |
|---|---|---|
| id | 1 | 查询序列号,子查询时会递增 |
| select_type | SIMPLE | 简单查询还是UNION等复杂操作 |
| table | orders | 访问的表名,有别名时显示别名 |
| partitions | NULL | 匹配的分区 |
| type | ref | 关键指标!从最优到最差:system > const > eq_ref > ref > range > index > ALL |
| possible_keys | user_status | 可能选用的索引 |
| key | user_status | 实际使用的索引 |
| key_len | 8 | 使用的索引长度(字节数) |
| ref | const,const | 与索引比较的列或常量 |
| rows | 50 | 预估需要检查的行数 |
| filtered | 10.00 | 条件过滤的百分比 |
| Extra | Using where | 额外信息,常见有Using filesort、Using temporary等 |
关键经验:type列是性能的"体温计",出现index/ALL时必须警惕;rows列是成本估算的"秤",数值异常增大往往预示性能问题。
Extra列藏着魔鬼细节,常见值及其影响:
Using filesort:需要额外排序,常见于ORDER BY没有索引支持时。我曾优化过一个查询,添加组合索引后排序时间从800ms降到20ms。
Using temporary:创建临时表,大数据量时可能引发磁盘IO。某次分页查询因此导致OOM,改为延迟关联解决。
Using index:覆盖索引扫描,性能最佳状态。上次优化将5个字段的SELECT改为只查索引字段,QPS提升3倍。
Using where:存储引擎检索行后做二次过滤。发现某查询虽然用了索引但仍有大量Using where,调整索引列顺序后过滤比例从70%降到5%。
根据执行计划反馈,我总结的索引设计原则:
最左前缀原则:索引(a,b,c)能优化WHERE a=? AND b=?,但无法优化WHERE b=? AND c=?。曾有个项目错误调整列顺序,导致索引失效。
三星索引标准:
避免过度索引:每个额外索引都会降低写性能。某电商表曾建了20多个索引,写入延迟高达200ms,精简后降到50ms。
用户查询场景:
sql复制SELECT * FROM products
WHERE category='electronics'
AND price > 1000
ORDER BY sales_volume DESC
LIMIT 10;
初始执行计划显示type: range, Extra: Using filesort。经过三次迭代优化:
第一版索引:(category)
第二版索引:(category, price)
最终索引:(category, sales_volume, price)
血泪教训:联合索引列顺序应该 = WHERE等值条件列 + ORDER BY列 + WHERE范围条件列。曾因顺序弄反导致索引失效,线上查询超时。
遇到一个"诡异"的慢查询:
sql复制EXPLAIN SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);
执行计划显示:
优化方案:
sql复制-- 改为JOIN写法
EXPLAIN SELECT u.* FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;
优化后:
典型分页查询:
sql复制EXPLAIN SELECT * FROM comments
WHERE post_id = 123
ORDER BY create_time DESC
LIMIT 10000, 10;
问题诊断:
优化方案:
sql复制-- 采用"书签法"
EXPLAIN SELECT * FROM comments
WHERE post_id = 123
AND create_time < '2023-06-01' -- 上次查询的最后一条时间
ORDER BY create_time DESC
LIMIT 10;
优化效果:
推荐两个诊断利器:
某次使用Workbench发现优化器错误选择了索引,通过FORCE INDEX解决。
在生产环境需持续关注:
特殊场景下的控制方法:
sql复制-- 强制使用特定索引
EXPLAIN SELECT * FROM orders FORCE INDEX(user_status)
WHERE user_id = 100 AND status = 'paid';
-- 忽略索引
EXPLAIN SELECT * FROM orders IGNORE INDEX(create_time)
WHERE create_time > '2023-01-01';
-- 关闭优化器特性
SET optimizer_switch='block_nested_loop=off';
慎用提示:除非能证明优化器选择错误,否则不要干预。有次FORCE INDEX导致查询从50ms暴涨到2s,因为数据分布已变化。