1. 为什么我们需要关注EXPLAIN?
我第一次接触EXPLAIN是在处理一个电商平台的订单查询问题时。当时有个页面加载需要8秒,用户投诉不断。当我用EXPLAIN分析这条查询时,发现它竟然进行了全表扫描——一个包含300万条记录的表!这就是为什么每个SQL开发者都应该掌握EXPLAIN:它能让你看到数据库引擎背后的思考过程。
EXPLAIN是MySQL提供的一个强大工具,它展示了优化器将如何执行你的查询。通过它,你可以:
- 查看查询是否使用了索引
- 了解表的读取顺序
- 估算需要检查的行数
- 识别性能瓶颈
- 验证索引是否被有效利用
提示:在生产环境分析慢查询时,务必使用EXPLAIN而不是盲目添加索引。我曾见过一个案例,添加索引后性能反而下降了30%,因为优化器选择了错误的执行计划。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. EXPLAIN输出列完全解读
2.1 核心字段解析
当我们执行EXPLAIN SELECT * FROM users WHERE age > 30时,会得到类似这样的输出:
code复制+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | users | NULL | ALL | age_index | NULL | NULL | NULL | 1000 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
让我们深入每个字段:
type列(最重要的性能指标):
- system:表中只有一行数据
- const:通过主键或唯一索引查询
- eq_ref:关联查询中使用的索引是主键或唯一键
- ref:使用非唯一索引查找
- range:索引范围扫描
- index:全索引扫描
- ALL:全表扫描(需要避免)
rows列:估算需要检查的行数。我曾在优化一个报表查询时,发现这个值显示1,200,000行,而实际表只有50万行——这说明统计信息已经过时,需要执行ANALYZE TABLE。
2.2 Extra字段的隐藏信息
Extra列经常被忽视,但它包含关键信息:
Using index:表示使用了覆盖索引(查询的列都包含在索引中)Using filesort:需要额外排序(可能需添加索引优化)Using temporary:使用了临时表(常见于GROUP BY和DISTINCT)Using where:在存储引擎检索行后进行了过滤
注意:看到
Using filesort不一定总是坏事。对于小结果集,内存排序可能比索引扫描更快。我曾优化一个查询,强制使用索引后反而慢了15%,因为结果集只有20行。
3. 实战:用EXPLAIN优化真实查询
3.1 案例一:电商平台商品搜索
原始查询:
sql复制SELECT p.* FROM products p
WHERE p.category_id = 5
AND p.price BETWEEN 100 AND 500
AND p.stock > 0
ORDER BY p.create_time DESC
LIMIT 20;
EXPLAIN显示type为ALL,进行了全表扫描。优化步骤:
- 添加复合索引:
sql复制ALTER TABLE products ADD INDEX idx_category_price_stock (category_id, price, stock);
- 修改查询(注意范围查询的位置):
sql复制SELECT p.* FROM products p
WHERE p.category_id = 5
AND p.stock > 0
AND p.price BETWEEN 100 AND 500
ORDER BY p.create_time DESC
LIMIT 20;
优化后EXPLAIN显示type变为range,rows从50,000降到320。
3.2 案例二:社交网络好友动态
原始查询:
sql复制SELECT p.id, p.content, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.user_id IN (
SELECT to_user_id FROM follows WHERE from_user_id = 100
)
ORDER BY p.create_time DESC
LIMIT 10;
问题在于IN子查询导致临时表。改为JOIN:
sql复制SELECT p.id, p.content, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
JOIN follows f ON p.user_id = f.to_user_id
WHERE f.from_user_id = 100
ORDER BY p.create_time DESC
LIMIT 10;
并添加索引:
sql复制ALTER TABLE follows ADD INDEX idx_from_to (from_user_id, to_user_id);
ALTER TABLE posts ADD INDEX idx_user_time (user_id, create_time);
执行时间从1.2秒降到0.05秒。
4. 高级EXPLAIN技巧
4.1 EXPLAIN FORMAT=JSON
MySQL 5.6+支持JSON格式输出,包含更多细节:
sql复制EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 100;
JSON输出包含成本估算、优化器决策等信息。例如:
json复制{
"query_block": {
"cost_info": {
"query_cost": "8.21"
},
"table": {
"access_type": "ref",
"possible_keys": ["idx_user"],
"key": "idx_user",
"used_key_parts": ["user_id"],
"cost_info": {
"read_cost": "7.41",
"eval_cost": "0.80",
"prefix_cost": "8.21",
"data_read_per_join": "1K"
}
}
}
}
4.2 EXPLAIN ANALYZE(MySQL 8.0+)
这是真正的游戏规则改变者——它实际执行查询并报告统计数据:
sql复制EXPLAIN ANALYZE SELECT * FROM products WHERE category_id = 5;
输出示例:
code复制-> Index lookup on products using idx_category (category_id=5) (cost=5.21 rows=32) (actual time=0.025..0.138 rows=28 loops=1)
可以看到估算和实际的差异。我最近用这个功能发现一个查询的rows估计偏差达300%,原因是统计信息不准确。
4.3 可视化工具推荐
对于复杂查询,可视化工具更直观:
- MySQL Workbench:内置可视化EXPLAIN
- Percona PMM:监控和查询分析
- JetProfiler:商业性能分析工具
我个人的工作流程是:先用Workbench快速查看执行计划,再用EXPLAIN ANALYZE验证实际性能。
5. 常见误区与最佳实践
5.1 不要过度索引
我曾接手一个系统,有27个索引在一个表上!每个INSERT都变慢了3倍。记住:
- 每个索引都会增加写操作开销
- 复合索引的顺序很重要(最左前缀原则)
- 定期检查未使用的索引
检查未使用索引的SQL:
sql复制SELECT * FROM sys.schema_unused_indexes
WHERE object_schema = 'your_db';
5.2 统计信息的重要性
EXPLAIN的估算基于统计信息。如果发现rows估算不准:
sql复制ANALYZE TABLE your_table;
对于大表,可以采样:
sql复制ANALYZE TABLE your_table UPDATE HISTOGRAM ON column1, column2;
5.3 参数化查询的影响
sql复制EXPLAIN SELECT * FROM products WHERE category_id = 5; -- 可能使用索引
EXPLAIN SELECT * FROM products WHERE category_id = 5 OR 1=1; -- 可能全表扫描
预处理语句通常能得到更好的执行计划。
5.4 分区表的EXPLAIN
查看分区使用情况:
sql复制EXPLAIN PARTITIONS SELECT * FROM sales WHERE sale_date BETWEEN '2023-01-01' AND '2023-01-31';
确保查询只访问必要的分区。
6. 与其他优化工具配合使用
6.1 慢查询日志
配置my.cnf:
code复制slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
分析工具:
bash复制mysqldumpslow -s t /var/log/mysql/mysql-slow.log
6.2 Performance Schema
查看历史查询性能:
sql复制SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;
6.3 sys Schema
内置的优化视图:
sql复制SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;
SELECT * FROM sys.schema_index_statistics
WHERE table_schema = 'your_db';
7. 不同MySQL版本的差异
7.1 MySQL 5.6 vs 5.7 vs 8.0
- 5.6:引入EXPLAIN FORMAT=JSON
- 5.7:优化器成本模型改进
- 8.0:EXPLAIN ANALYZE、不可见索引、直方图统计
7.2 执行计划稳定性
使用optimizer hints固定计划:
sql复制SELECT /*+ INDEX(products idx_category) */ * FROM products WHERE category_id = 5;
或使用SQL_BIND:
sql复制EXECUTE stmt USING @category_id;
8. 真实案例分析
8.1 案例三:分页查询优化
原始分页:
sql复制SELECT * FROM orders ORDER BY create_time DESC LIMIT 10000, 20;
优化方案(记住上次的最大值):
sql复制SELECT * FROM orders
WHERE create_time < '2023-06-01 00:00:00'
ORDER BY create_time DESC LIMIT 20;
8.2 案例四:JSON字段查询
MySQL 5.7+支持JSON字段:
sql复制EXPLAIN SELECT * FROM products
WHERE JSON_EXTRACT(specs, '$.weight') > 10;
添加生成列和索引:
sql复制ALTER TABLE products
ADD COLUMN weight DECIMAL(10,2) AS (JSON_EXTRACT(specs, '$.weight')) STORED,
ADD INDEX idx_weight (weight);
9. 监控与持续优化
建立性能基准:
sql复制CREATE TABLE query_benchmarks (
query_name VARCHAR(100),
avg_exec_time DECIMAL(10,6),
last_exec_time DECIMAL(10,6),
exec_count INT,
PRIMARY KEY (query_name)
);
定期检查执行计划变化:
sql复制SELECT * FROM plan_history
WHERE query_md5 = 'a1b2c3d4'
ORDER BY check_date DESC;
10. 个人经验分享
在过去的SQL优化工作中,我总结了这些经验法则:
- 看到ALL类型先考虑添加索引
- 范围查询后的索引列不会被使用
- ORDER BY和GROUP BY也需要考虑索引
- 有时重写查询比加索引更有效
- 使用IN()代替多个OR条件
- 大偏移量分页使用"记住位置"技术
- 定期检查索引使用情况
- 统计信息不准确会导致糟糕的执行计划
最后一个小技巧:在开发环境设置optimizer_switch='index_merge=off',因为生产环境中索引合并经常表现不稳定。
