MySQL慢查询是数据库性能问题的核心来源。EXPLAIN命令输出查询执行计划,揭示优化器选择的访问路径、索引使用情况和扫描行数估算。读懂EXPLAIN输出并正确调整索引,是数据库性能调优的基本功。本文通过三个真实慢查询案例演示完整的优化流程。
EXPLAIN输出字段解读:type、key、rows与Extra列含义
执行 EXPLAIN SELECT 后输出的核心列:
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1;
type列(访问类型,性能从好到差):const > eq_ref > ref > range > index > ALL。ALL表示全表扫描,必须优化。range表示索引范围扫描。ref表示通过非唯一索引等值查询。
key列:实际使用的索引名。NULL表示未使用索引。
rows列:优化器估算的需要扫描的行数。rows越小说明索引过滤效果越好。
Extra列:额外信息。Using index(覆盖索引,无需回表)是理想状态;Using filesort(文件排序)和 Using temporary(临时表)是需要消除的信号。
案例一:隐式类型转换导致索引失效
线上发现订单查询接口偶发超时,慢查询日志记录:
# 慢查询日志
# Query_time: 3.2s Lock_time: 0.0s Rows_sent: 1 Rows_examined: 2800000
SELECT * FROM orders WHERE order_no = 2026080500001234;
order_no字段建有唯一索引,但查询耗时3.2秒,扫描280万行。执行EXPLAIN:
EXPLAIN SELECT * FROM orders WHERE order_no = 2026080500001234;
-- 结果
-- type: ALL | key: NULL | rows: 2800000 | Extra: Using where
type为ALL,索引完全未生效。查看表结构:order_no字段类型为VARCHAR(32),但查询传入的是整数2026080500001234而非字符串。MySQL对整型和字符串比较时,将字符串列转换为数值——这导致全表扫描。
-- 查看字段类型
DESC orders;
-- order_no | varchar(32) | YES | MUL
-- 修正:传入字符串值
EXPLAIN SELECT * FROM orders WHERE order_no = '2026080500001234';
-- type: const | key: uk_order_no | rows: 1 | Extra: NULL
修正后查询从3.2秒降至0.2毫秒。根因在于ORM框架的参数绑定未指定字符串类型。检查MyBatis MapperXML中的参数类型,确保 #{orderNo} 对应Java String类型。
案例二:多列查询索引顺序与最左前缀原则
用户订单列表查询,WHERE条件包含user_id(等值)和create_time(范围排序):
-- 慢查询:1.8秒,扫描15万行
SELECT * FROM orders
WHERE user_id = 10086
AND create_time >= '2026-07-01'
AND create_time < '2026-08-01'
ORDER BY create_time DESC
LIMIT 20;
EXPLAIN SELECT ... (同上);
-- type: ref | key: idx_user_id | rows: 150000 | Extra: Using where; Using filesort
只有idx_user_id单列索引起作用。Extra列出现 Using filesort,表示ORDER BY无法利用索引排序,MySQL需要内存排序15万行后取前20条。
创建联合索引(user_id在前,create_time在后):
ALTER TABLE orders ADD INDEX idx_user_create (user_id, create_time);
EXPLAIN SELECT ... (同上);
-- type: range | key: idx_user_create | rows: 3200
-- Extra: Using index condition
修正后rows从15万降至3200,filesort消失。联合索引满足最左前缀原则:user_id等值匹配走索引定位,create_time范围扫描利用索引有序性直接ORDER BY,无需额外排序。
联合索引字段顺序原则:等值查询字段在前,范围查询字段在后,范围查询后的字段无法利用索引。如果还有status等值过滤条件,应为 (user_id, status, create_time)。
案例三:覆盖索引消除回表与分页深度优化
后台分页查询,深分页时性能急剧下降:
-- 第一页:0.3秒
SELECT * FROM orders ORDER BY id DESC LIMIT 0, 20;
-- 第10000页:4.5秒
SELECT * FROM orders ORDER BY id DESC LIMIT 200000, 20;
深分页问题在于MySQL需要扫描前200020行,丢弃前200000行只返回20行。优化方法:延迟关联,先通过覆盖索引查出主键,再关联查询完整数据:
-- 优化后深分页查询:0.4秒
SELECT t.* FROM orders t
INNER JOIN (
SELECT id FROM orders ORDER BY id DESC LIMIT 200000, 20
) tmp ON t.id = tmp.id;
子查询 SELECT id FROM orders 只读取主键列,走主键索引的覆盖扫描(Using index),IO量极小。外层JOIN通过20个主键值精确回表,避免扫描200020行完整数据行。
如果只需返回部分字段而非全部,直接创建覆盖索引:
-- 需求:查询订单列表只需返回order_no、user_id、amount、status
-- 创建覆盖索引
ALTER TABLE orders ADD INDEX idx_cover_list (id, order_no, user_id, amount, status);
-- 查询直接走覆盖索引,无需回表
SELECT id, order_no, user_id, amount, status
FROM orders ORDER BY id DESC LIMIT 200000, 20;
-- Extra: Using index
索引维护与监控:定期审查冗余索引和未使用索引
索引不是越多越好——写入操作需同步更新所有索引,过多索引拖慢INSERT/UPDATE。定期通过sys.schema_unused_indexes视图排查从未使用的索引:
-- 查询从未使用的索引
SELECT * FROM sys.schema_unused_indexes
WHERE object_schema = 'your_db';
-- 查询冗余索引
SELECT * FROM sys.schema_redundant_indexes
WHERE table_schema = 'your_db';
删除冗余索引前,确认该索引未被任何查询使用:检查慢查询日志、应用代码中的ORM映射,确认后逐步下线。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-man-cha-xun-you-hua-shi-zhan-explain-zhi-xing-ji-hua/