MySQL慢查询优化实战:EXPLAIN执行计划解读与索引调优案例

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/

(0)
小编小编
上一篇 9小时前
下一篇 9小时前

相关推荐