在MySQL数据库性能调优领域,EXPLAIN命令就像外科医生的X光机,它能让我们直观看到SQL语句的执行路径和资源消耗情况。我处理过数百个性能案例,90%的慢查询问题都可以通过正确解读EXPLAIN输出找到突破口。
执行计划展示的是MySQL优化器选择的查询路径,包含几个关键维度:表的读取顺序、访问方法、可能使用的索引、预估行数和额外操作等。这些信息共同构成了查询执行的"路线图"。举个例子,当看到type列出现ALL(全表扫描)时,就像发现汽车导航规划了一条绕城路线,这时就需要检查是否缺少合适的索引。
id字段表示查询中SELECT语句的执行顺序。相同id按从上到下执行,不同id值越大越先执行。在分析包含子查询或UNION的复杂SQL时,这个字段特别有用。我曾遇到一个案例,两个看似独立的子查询因为id相同导致意外地顺序执行,改为不同id后性能提升5倍。
select_type揭示查询类型,常见的有:
SIMPLE:简单SELECT(不含子查询或UNION)PRIMARY:最外层SELECTSUBQUERY:子查询中的第一个SELECTDERIVED:派生表的SELECT(FROM子句中的子查询)table字段显示访问的表名或别名。当看到<derivedN>格式时,说明使用了派生表,这往往是性能瓶颈点。有个实际案例:一个报表查询因为多层派生表导致临时表膨胀到20GB,改为JOIN后执行时间从120秒降到3秒。
type列是最需要关注的性能指标,它表示表的访问方式,性能从优到劣排序为:
sql复制system > const > eq_ref > ref > range > index > ALL
possible_keys和key分别显示可能使用的索引和实际使用的索引。当这两个字段值不一致时,说明优化器没有选择你认为合适的索引。上周处理的一个案例中,一个本该使用复合索引的查询却走了全表扫描,原因是索引字段顺序与查询条件不匹配。
rows是预估需要检查的行数,这个值基于统计信息计算得出。当实际执行发现rows估值严重偏离时(比如预估100行实际扫描100万行),可能需要运行ANALYZE TABLE更新统计信息。
最左前缀原则是复合索引设计的核心。假设有索引(a,b,c),以下条件能利用索引:
WHERE a=1 AND b=2 AND c=3WHERE a=1 AND b=2WHERE a=1但以下条件无法充分利用索引:
WHERE b=2(缺少最左列a)WHERE a=1 AND c=3(跳过了b列)索引选择性是指索引列不同值的数量与表记录数的比值。高选择性的列(如用户ID)更适合建索引,低选择性的列(如性别)建索引效果差。有个电商项目曾为"订单状态"字段建索引,结果该索引从未被使用,因为90%订单都处于"已完成"状态。
隐式类型转换:当查询条件与字段类型不匹配时,如WHERE varchar_col=123会导致索引失效。去年排查的一个生产问题就是因此导致全表扫描,改为WHERE varchar_col='123'后QPS从50提升到2000。
使用函数或运算:WHERE YEAR(create_time)=2023会使create_time上的索引失效,应改为范围查询WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'。
OR条件不当:WHERE a=1 OR b=2如果a和b字段都有索引,MySQL通常不会使用索引。解决方案是改为UNION ALL:
sql复制SELECT * FROM t WHERE a=1
UNION ALL
SELECT * FROM t WHERE b=2
当查询的所有列都包含在索引中时,就无需回表查数据行,这称为覆盖索引。通过EXPLAIN的Extra列出现Using index可以确认。在一个用户分析系统中,我把查询从SELECT *改为只查询索引包含的列,查询时间从800ms降到80ms。
创建覆盖索引的技巧:
SHOW INDEX FROM table检查索引基数MySQL 5.6引入的索引条件下推(ICP)特性,能在存储引擎层提前过滤数据。通过EXPLAIN看到Using index condition表示启用了ICP。在某物流系统中,启用ICP后一个复杂查询的IO操作减少了70%。
启用ICP的条件:
分析一个实际生产慢查询:
sql复制SELECT o.order_id, u.username
FROM orders o JOIN users u ON o.user_id=u.user_id
WHERE o.create_time > '2023-01-01'
AND o.status = 'completed'
ORDER BY o.amount DESC
LIMIT 100;
初始EXPLAIN显示:
status索引,但需要扫描50万行优化步骤:
(status, create_time, amount)sql复制SELECT o.order_id, u.username
FROM (SELECT order_id, user_id FROM orders
WHERE create_time > '2023-01-01'
AND status = 'completed'
ORDER BY amount DESC
LIMIT 100) o
JOIN users u ON o.user_id=u.user_id;
优化后执行计划显示:
常见的分页查询性能问题:
sql复制SELECT * FROM large_table ORDER BY id LIMIT 1000000, 10;
优化方案:
sql复制SELECT t.* FROM large_table t
JOIN (SELECT id FROM large_table ORDER BY id LIMIT 1000000, 10) tmp
ON t.id=tmp.id;
sql复制SELECT * FROM large_table
WHERE id > last_max_id
ORDER BY id LIMIT 10;
过度依赖EXPLAIN估算:rows列只是基于统计信息的估算值,当发现实际执行时间与预期不符时,应该使用EXPLAIN ANALYZE(MySQL 8.0+)获取实际执行数据。
忽视临时表和文件排序:Extra列出现Using temporary和Using filesort时,对于大表查询可能是性能杀手。解决方案包括优化GROUP BY字段顺序、增加合适的索引等。
索引越多越好:每个索引都会增加写操作开销。我曾优化过一个表有15个索引,实际只有5个常用。通过SELECT * FROM sys.schema_unused_indexes可以找出无用索引。
忽视索引维护:长期运行的数据库会出现索引碎片,定期执行OPTIMIZE TABLE或ALTER TABLE ... ENGINE=InnoDB可以重建索引。有个客户案例显示,重建索引后查询性能提升了40%。
建立性能基准监控体系:
performance_schema监控慢查询SHOW GLOBAL STATUS中的关键指标long_query_time为合理值(建议从1秒开始)pt-index-usage工具分析索引使用情况自动化优化建议:
sql复制-- 查找冗余索引
SELECT * FROM sys.schema_redundant_indexes;
-- 查找全表扫描的查询
SELECT * FROM sys.statements_with_full_table_scans;
-- 查找未使用索引
SELECT * FROM sys.schema_unused_indexes;
在最近的一个金融项目中,通过持续监控发现某核心查询性能逐渐下降,分析发现是由于数据分布变化导致索引失效。调整索引后,该查询的TP99从1200ms降到了150ms。