MySQL慢查询是数据库性能问题的头号杀手。一条低效SQL可能导致整库响应变慢,甚至触发连接池耗尽。EXPLAIN是MySQL提供的执行计划分析工具,通过解读执行计划可以精确判断索引使用情况、扫描行数和连接策略。本文以真实慢查询案例为驱动,覆盖执行计划解读、索引设计和优化实战。
EXPLAIN执行计划字段详解
EXPLAIN是MySQL查询优化器的输出,展示SQL将如何执行。理解每个字段的含义是优化的前提。
-- 创建测试表和数据
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
amount DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB;
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_name VARCHAR(128) NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
INDEX idx_order_id (order_id)
) ENGINE=InnoDB;
-- 执行EXPLAIN
EXPLAIN SELECT o.order_no, o.amount, oi.product_name, oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
WHERE o.user_id = 10086
AND o.status = 1
ORDER BY o.created_at DESC
LIMIT 20;
EXPLAIN输出字段详解:
+----+--------+-------------+-------+------+-------------------+-------------------+---------+----------+------+----------+------------------------------------+
| id | select | table | type | key | key_len | ref | rows | filtered | Extra |
+----+--------+-------------+-------+------+-------------------+-------------------+---------+----------+------------------------------------+
| 1 | SIMPLE | o | ref | idx_user_id | 8 | const | 156 | 3.33 | Using where; Using filesort |
| 1 | SIMPLE | oi | ref | idx_order_id| 9 | test.o.id | 3 | 100.00 | NULL |
+----+--------+-------------+-------+------+-------------------+-------------------+---------+----------+------------------------------------+
各字段含义:
- id:查询标识符。相同id表示在同一层执行,子查询id递增。
- select_type:SIMPLE(简单查询)、PRIMARY(最外层)、SUBQUERY(子查询)、DERIVED(派生表)。
- type:访问类型,性能从好到差:system > const > eq_ref > ref > range > index > ALL。ALL表示全表扫描,必须优化。
- key:实际使用的索引。NULL表示未使用索引。
- key_len:索引使用的字节数。可用于判断复合索引使用了几个字段。
- rows:预估扫描行数。越小说明索引越有效。
- filtered:过滤后剩余的行百分比。3.33表示扫描156行后只有约5行满足条件。
- Extra:额外信息。Using index(覆盖索引)、Using filesort(文件排序)、Using temporary(临时表)是需要关注的信号。
type字段深度解读与优化目标
type字段是判断查询效率的核心指标。各类型的具体含义和优化目标:
-- ALL:全表扫描,最差情况
EXPLAIN SELECT * FROM orders WHERE DATE(created_at) = '2026-07-23';
-- type=ALL,rows=1000000(全表扫描)
-- 问题:在索引列上使用函数导致索引失效
-- 优化:改为范围查询
EXPLAIN SELECT * FROM orders
WHERE created_at >= '2026-07-23 00:00:00'
AND created_at < '2026-07-24 00:00:00';
-- type=range,rows=1200(索引范围扫描)
-- index:全索引扫描,比ALL好但仍需优化
EXPLAIN SELECT COUNT(*) FROM orders;
-- type=index,扫描整个索引树
-- ref:非唯一索引等值匹配
EXPLAIN SELECT * FROM orders WHERE user_id = 10086;
-- type=ref,使用idx_user_id索引
-- eq_ref:唯一索引等值匹配(JOIN场景最优)
EXPLAIN SELECT * FROM orders o
JOIN order_items oi ON o.id = oi.order_id;
-- type=eq_ref,使用主键关联
-- const:主键或唯一索引等值查询
EXPLAIN SELECT * FROM orders WHERE id = 1;
-- type=const,最多匹配一行
Extra字段关键信号与优化策略
Extra字段包含执行计划的额外信息,三个关键信号直接影响性能:
Using filesort(文件排序)
表示MySQL无法使用索引完成排序,需要在内存或磁盘中排序。数据量大时严重影响性能。
-- 触发filesort的查询
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 ORDER BY created_at DESC;
-- Extra: Using where; Using filesort
-- 问题:idx_user_id索引不包含created_at,排序无法利用索引
-- 优化:创建复合索引
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at);
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 ORDER BY created_at DESC;
-- Extra: Using where; Using index (filesort消失)
Using temporary(临时表)
MySQL需要创建临时表来处理查询。常见于GROUP BY、DISTINCT和UNION操作。
-- 触发temporary的查询
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Extra: Using temporary; Using filesort
-- 优化:创建覆盖索引
ALTER TABLE orders ADD INDEX idx_status (status);
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Extra: Using index (temporary和filesort都消失)
Using index(覆盖索引)
查询所需的所有字段都能从索引中获取,无需回表读取数据行。这是最优的查询方式。
-- 覆盖索引示例
ALTER TABLE orders ADD INDEX idx_user_status_amount (user_id, status, amount);
EXPLAIN SELECT user_id, status, amount FROM orders WHERE user_id = 10086;
-- Extra: Using index (覆盖索引,无需回表)
-- 注意:SELECT * 会破坏覆盖索引
EXPLAIN SELECT * FROM orders WHERE user_id = 10086;
-- Extra: NULL (需要回表,因为查询了索引外的字段)
复合索引设计与最左前缀原则
复合索引的列顺序决定了索引的可用性。MySQL遵循最左前缀原则:查询条件必须从索引的最左列开始连续匹配。
-- 复合索引 (user_id, status, created_at)
-- 索引有效的情况:
SELECT * FROM orders WHERE user_id = 10086; -- 使用1列
SELECT * FROM orders WHERE user_id = 10086 AND status = 1; -- 使用2列
SELECT * FROM orders WHERE user_id = 10086 AND status = 1
AND created_at > '2026-07-01'; -- 使用3列(范围)
-- 索引部分有效:
SELECT * FROM orders WHERE user_id = 10086
AND created_at > '2026-07-01';
-- 只使用user_id列,跳过了status列(中间列缺失)
-- 索引完全无效:
SELECT * FROM orders WHERE status = 1; -- 缺少最左列
SELECT * FROM orders WHERE created_at > '2026-07-01'; -- 缺少最左列
通过key_len可以验证复合索引用了几个列:
-- key_len计算规则:
-- BIGINT: 8字节, TINYINT: 1字节, DATETIME: 5字节
-- 非空字段+1字节, 可变长度字段+2字节
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1;
-- key_len = 8(user_id) + 1(status) = 9
-- 如果status字段允许NULL,key_len = 8 + 1 + 1(NULL标志) = 10
-- 通过key_len判断索引利用率
-- 索引(user_id, status, created_at)最大key_len = 8+1+5 = 14
-- 实际key_len=9说明只用了前两列
慢查询日志配置与分析实战
生产环境通过慢查询日志自动捕获低效SQL:
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒记录
SET GLOBAL log_queries_not_using_indexes = ON;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 使用mysqldumpslow分析慢查询日志
-- 按总耗时排序
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
-- 按次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
-- 按返回行数排序
mysqldumpslow -s r -t 10 /var/log/mysql/slow.log
-- 或使用pt-query-digest(更强大)
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
pt-query-digest的分析结果会按SQL指纹聚合,展示每类SQL的执行次数、平均耗时、总耗时占比和95分位延迟,帮助快速定位TOP N慢查询。
索引优化决策checklist
面对慢查询,按以下流程决策是否需要新增或调整索引:
- EXPLAIN查看type字段:ALL或index需优化
- 检查WHERE条件字段是否有索引
- 检查ORDER BY/GROUP BY字段是否在索引中
- 检查是否有函数操作导致索引失效(DATE()、UPPER()、类型转换等)
- 检查LIKE查询是否以%开头(’%keyword’无法用索引)
- 评估复合索引列顺序:等值条件在前,范围条件在后
- 确认是否可使用覆盖索引避免回表
- 评估索引维护成本:写多读少的表需控制索引数量
单表索引数量建议不超过5个,每个索引都会增加INSERT/UPDATE/DELETE的维护成本。优先优化查询SQL本身(减少SELECT *、避免SELECT COUNT(*)频繁执行、大表分页用游标方案替代LIMIT OFFSET),再考虑添加索引。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-zhi-xing-ji-hua-shen-du-jie-xi-man-cha-xun-zhen-duan/