作为一名长期与MySQL打交道的开发者,我始终认为EXPLAIN是SQL优化的第一道门槛。它就像数据库引擎的X光机,能够透视SQL语句的执行路径。当遇到慢查询时,EXPLAIN总能给我最直接的线索。
记得有一次排查一个页面加载缓慢的问题,通过EXPLAIN发现一个看似简单的联表查询竟然进行了全表扫描。添加适当索引后,查询时间从2.3秒降到了0.02秒。这种优化带来的性能提升,往往比升级硬件配置更有效。
使用EXPLAIN最简单的方式就是在SELECT语句前加上EXPLAIN关键字:
sql复制EXPLAIN SELECT * FROM products WHERE category_id = 5 AND price > 100;
输出结果通常包含以下关键字段:
| 字段 | 说明 | 优化关注度 |
|---|---|---|
| id | 查询标识符,相同id表示同一执行单元 | ★★ |
| select_type | 查询类型(SIMPLE, PRIMARY, SUBQUERY等) | ★★★ |
| table | 当前行对应的表名 | ★ |
| partitions | 匹配的分区 | ★ |
| type | 访问类型,从const到ALL性能递减 | ★★★★★ |
| possible_keys | 可能使用的索引 | ★★★ |
| key | 实际使用的索引 | ★★★★★ |
| key_len | 使用的索引长度 | ★★★ |
| ref | 索引的哪一列被使用 | ★★ |
| rows | 预估需要检查的行数 | ★★★★ |
| filtered | 按条件过滤后剩余行的百分比 | ★★★ |
| Extra | 额外信息(Using filesort等) | ★★★★★ |
type字段是判断查询效率的最重要指标之一,以下是常见的访问类型及其性能影响:
sql复制EXPLAIN SELECT * FROM users WHERE id = 1;
sql复制EXPLAIN SELECT * FROM users JOIN orders ON users.id = orders.user_id;
sql复制EXPLAIN SELECT * FROM products WHERE category_id = 5;
sql复制EXPLAIN SELECT * FROM logs WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31';
实际经验:当type出现index或ALL时,就应该考虑优化索引了。我曾经通过将一个ALL查询优化为ref,使查询时间从1200ms降到15ms。
key字段显示实际使用的索引,possible_keys则显示可能使用的索引。当这两个字段不一致时,说明MySQL优化器没有选择你认为合适的索引。
案例:有一个用户查询经常超时
sql复制EXPLAIN SELECT * FROM user_activities
WHERE user_id = 1001 AND activity_date > '2023-06-01';
发现possible_keys有idx_user和idx_date,但实际使用了idx_date。通过FORCE INDEX强制使用复合索引后,查询效率提升8倍。
rows字段是估算值,但能反映查询的规模。我通常这样评估:
一个实际案例:一个报表查询rows显示1,200,000行,通过添加复合索引和调整查询条件,最终降到800行。
Extra字段中的以下信息需要特别注意:
Using filesort:表示需要额外排序
Using temporary:使用了临时表
Using index:使用了覆盖索引,是好的表现
Using where:在存储引擎层进行了过滤
我曾遇到一个案例:Extra显示"Using temporary; Using filesort",导致查询耗时5秒。通过创建复合索引(order_date, status),查询时间降到0.2秒。
索引选择性 = 不重复的索引值数量 / 表记录总数。选择性越高,索引效率越好。
计算选择性的方法:
sql复制SELECT
COUNT(DISTINCT column_name) / COUNT(*)
FROM table_name;
经验法则:
遵循"最左前缀"原则,我通常这样设计复合索引:
案例:对于查询
sql复制SELECT id, name FROM products
WHERE category_id = 5
AND price > 100
ORDER BY create_time DESC;
最佳索引可能是:(category_id, price, create_time)
联表查询时,重点关注:
我曾经优化过一个三表联查,通过调整JOIN顺序和添加适当索引,将执行时间从8秒降到0.3秒。
原始查询:
sql复制EXPLAIN SELECT o.*, u.username
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid'
AND o.create_time > '2023-01-01'
ORDER BY o.amount DESC
LIMIT 100;
问题诊断:
优化方案:
优化后效果:查询时间从4.2秒降到0.15秒
原始查询:
sql复制EXPLAIN SELECT tag_id, COUNT(*) as count
FROM article_tags
GROUP BY tag_id
HAVING count > 10
ORDER BY count DESC;
问题诊断:
优化方案:
索引不是越多越好:每个索引都会影响写入性能。我曾经维护的一个表有15个索引,导致INSERT速度极慢,精简到5个后写入性能提升6倍。
注意隐式类型转换:如字符串字段用数字查询会导致索引失效
sql复制-- 假设user_code是varchar类型
EXPLAIN SELECT * FROM users WHERE user_code = 123; -- 索引失效
EXPLAIN SELECT * FROM users WHERE user_code = '123'; -- 使用索引
定期分析表:使用ANALYZE TABLE更新统计信息,帮助优化器做出更好决策
考虑使用FORCE INDEX:当优化器选择不当时,可以强制使用特定索引
关注索引合并:当出现Using union时,说明MySQL合并了多个索引扫描结果
除了EXPLAIN,MySQL还提供其他性能分析工具:
EXPLAIN FORMAT=JSON:获取更详细的执行计划信息
sql复制EXPLAIN FORMAT=JSON SELECT * FROM large_table WHERE ...;
EXPLAIN ANALYZE(MySQL 8.0+):显示实际执行统计信息
sql复制EXPLAIN ANALYZE SELECT * FROM large_table WHERE ...;
慢查询日志:记录执行时间超过阈值的查询
ini复制slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
Performance Schema:监控服务器事件
sql复制SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC LIMIT 10;
sys Schema:提供友好的性能视图
sql复制SELECT * FROM sys.schema_unused_indexes;
SELECT * FROM sys.statements_with_full_table_scans;
在实际工作中,我通常会先用慢查询日志定位问题SQL,然后用EXPLAIN分析执行计划,最后结合Performance Schema验证优化效果。这套组合拳帮助我解决了90%以上的数据库性能问题。