MySQL 调优:如何给SQL做体检
MySQL 调优:如何给SQL做体检
目录
- 一条慢查询引发的问题
- EXPLAIN 是什么
- EXPLAIN 的用法
- EXPLAIN 输出字段详解
- type 列:访问类型的性能排序
- Extra 列:隐藏的关键信息
- 实战:用 EXPLAIN 诊断一个慢查询
- 常见优化思路速查表
- 小结
一条慢查询引发的问题
你刚接手一个项目,发现某个列表页加载要五六秒。你打开慢查询日志,找到了那条 SQL:
SELECT * FROM orders WHERE user_id = 1001 AND status = 'paid' ORDER BY create_time DESC LIMIT 20;
表里有几百万条数据,user_id 和 status 都有索引,看起来不该这么慢。但你又不敢随便加索引,万一加错了,写入性能反而会下降。
这种场景在日常开发中太常见了。SQL 写法千变万化,表结构各不相同,数据量也各有差异。**我们需要一种手段,能在不真正执行查询的情况下,看到 MySQL 打算怎么执行这条 SQL。**这就是 EXPLAIN 的作用。
EXPLAIN 是什么
一句话:EXPLAIN 是 MySQL 提供的查询执行计划分析工具。
打个比方。你去医院体检,医生不会上来就给你开刀,而是先做一系列检查——血常规、CT、心电图,拿到报告后才知道哪里有问题、怎么治。EXPLAIN 就是 SQL 的"体检报告",它告诉你 MySQL 打算怎么执行这条查询:用不用索引、扫描多少行、需不需要排序和临时表。
有了这份报告,你就能判断一条 SQL 到底慢在哪里,然后对症下药。
EXPLAIN 的用法
用法非常简单,在 SELECT 语句前面加一个 EXPLAIN 关键字:
EXPLAIN SELECT * FROM orders WHERE user_id = 1001;
执行后会返回一张表,每一行代表 MySQL 访问一个表的执行计划。对于单表查询通常只有一行,多表 JOIN 会有多行。
我们先建一张测试表,后面的例子都基于它:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL,
amount DECIMAL(10, 2),
create_time DATETIME NOT NULL,
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_user_status (user_id, status),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB;
EXPLAIN 输出字段详解
执行 EXPLAIN 后,返回的字段比较多,我们挑最重要的几个来说。
| 字段 | 含义 | 关注程度 |
|---|---|---|
| id | 查询序号,标识 SELECT 的执行顺序 | 了解即可 |
| select_type | 查询类型(SIMPLE、PRIMARY、SUBQUERY 等) | 一般 |
| table | 访问的表名 | 了解即可 |
| partitions | 匹配的分区 | 分区表才关注 |
| type | 访问类型,决定查询效率 | 重点关注 |
| possible_keys | 可能用到的索引 | 一般 |
| key | 实际用到的索引 | 重点关注 |
| key_len | 索引使用的字节长度 | 联合索引时关注 |
| ref | 索引关联的列或常量 | 一般 |
| rows | 预估扫描行数 | 重点关注 |
| filtered | 过滤后剩余行的百分比 | 一般 |
| Extra | 额外信息 | 重点关注 |
重点关注四个字段:type、key、rows、Extra。 它们基本决定了一个查询的性能表现。
type 列:访问类型的性能排序
type 列表示 MySQL 用了什么方式去查找数据,这是判断查询好坏的核心指标。它的值从好到差排列如下:
| type 值 | 含义 | 性能 |
|---|---|---|
| system | 表只有一行,特殊场景 | 最好 |
| const | 主键或唯一索引等值查询,最多一行 | 极好 |
| eq_ref | JOIN 时驱动表每行在被驱动表中通过主键/唯一索引匹配一行 | 很好 |
| ref | 非唯一索引等值查询,返回多行 | 好 |
| range | 索引范围扫描(BETWEEN、>、<、IN) | 较好 |
| index | 全索引扫描,遍历整棵索引树 | 一般 |
| ALL | 全表扫描,逐行遍历 | 差 |
一般要求至少达到 range 级别,理想情况是 ref 或更好。 如果看到 ALL,基本可以确定需要优化。
用几个例子来看:
-- type = const:主键等值查询,最多一行
EXPLAIN SELECT * FROM orders WHERE id = 1;
-- type = ref:普通索引等值查询
EXPLAIN SELECT * FROM orders WHERE user_id = 1001;
-- type = range:索引范围查询
EXPLAIN SELECT * FROM orders WHERE user_id BETWEEN 1000 AND 2000;
-- type = ALL:全表扫描,没有可用索引
EXPLAIN SELECT * FROM orders WHERE amount > 100;
-- amount 上没有索引,只能全表扫描
Extra 列:隐藏的关键信息
Extra 列是 EXPLAIN 中信息量最大的字段,它描述了 MySQL 在执行查询时的额外操作。常见的值有:
| Extra 值 | 含义 | 好坏 |
|---|---|---|
| Using index | 覆盖索引,不需要回表 | 好 |
| Using index condition | 索引下推,在存储引擎层提前过滤 | 好 |
| Using where | Server 层过滤,条件列不在索引中 | 一般 |
| Using temporary | 使用了临时表 | 差,需优化 |
| Using filesort | 使用了额外的排序操作 | 差,需优化 |
| Using join buffer | JOIN 时使用了连接缓冲区 | 一般,可能缺索引 |
重点看两个:Using temporary 和 Using filesort。 它们意味着 MySQL 需要在查询过程中创建临时表或做额外排序,数据量大时会严重影响性能。
以下就是一个典型的"踩雷"查询:
-- 按 status 分组,按 create_time 排序
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status ORDER BY create_time;
这条查询的 Extra 很可能出现 Using temporary; Using filesort——因为 GROUP BY 需要临时表来去重聚合,ORDER BY 的字段和分组字段不一致又触发了额外排序。
优化思路:用联合索引 (status, create_time) 让分组和排序都能走索引,避免临时表和额外排序。
实战:用 EXPLAIN 诊断一个慢查询
回到开头的那条 SQL,我们来用 EXPLAIN 做一次完整的诊断。
EXPLAIN SELECT * FROM orders WHERE user_id = 1001 AND status = 'paid' ORDER BY create_time DESC LIMIT 20;
假设 EXPLAIN 输出如下:
+----+------+---------------+----------------+---------+------+------+----------------------------------------------+
| id | type | possible_keys | key | key_len | rows | Extra |
+----+------+---------------+----------------+---------+------+------+----------------------------------------------+
| 1 | ref | idx_user_id, | idx_user_status| 16 | 8500 | Using where; Using filesort |
| | | idx_user_status| | | | |
+----+------+---------------+----------------+---------+------+------+----------------------------------------------+
逐字段分析:
- type = ref:走了索引等值查询,还行
- key = idx_user_status:用了联合索引
(user_id, status),user_id 和 status 都能命中 - rows = 8500:预估扫描 8500 行,这个数字偏大
- Extra = Using where; Using filesort:出现了
Using filesort,说明ORDER BY create_time没走索引,MySQL 在拿到 8500 条结果后还要做一次额外排序
问题定位到了:虽然 WHERE 条件走了索引,但排序字段 create_time 不在索引里,触发了 filesort。
优化方案:把排序字段也加到索引里,改成联合索引 (user_id, status, create_time)。
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);
再跑一次 EXPLAIN:
+----+------+---------------------+---------+------+------+----------------------------------------------+
| id | type | key | key_len | rows | Extra |
+----+------+---------------------+---------+------+------+----------------------------------------------+
| 1 | ref | idx_user_status_time| 16 | 8500 | Using where |
+----+------+---------------------+---------+------+------+----------------------------------------------+
Using filesort 消失了。MySQL 现在可以直接按索引顺序读取数据,不需要额外排序。
但注意 rows 还是 8500,说明扫描行数没变——因为联合索引 (user_id, status, create_time) 中,user_id 和 status 是等值查询,create_time 是排序用的,不影响扫描行数。但省掉了排序操作,在 ORDER BY + LIMIT 的场景下性能提升是很明显的。
常见优化思路速查表
拿到 EXPLAIN 报告后,根据不同的"症状"选择对应的优化手段:
| 症状 | EXPLAIN 表现 | 优化方向 |
|---|---|---|
| 全表扫描 | type = ALL | 检查 WHERE 条件列是否有索引,考虑加索引 |
| 索引没用上 | key = NULL | 检查是否违反最左前缀原则、是否有函数/类型转换 |
| 扫描行数过多 | rows 值很大 | 索引选择性低,考虑用更精确的索引或调整查询条件 |
| 额外排序 | Extra = Using filesort | ORDER BY 字段加入索引 |
| 临时表 | Extra = Using temporary | GROUP BY / DISTINCT 字段加入索引 |
| 回表太多 | type = ref 但 rows 大 | 考虑覆盖索引,避免 SELECT * |
| 索引区分度低 | rows 接近总行数 | 检查索引列的数据分布,考虑删除低效索引 |
除了看 EXPLAIN,还有几个通用的优化原则值得记住:
1. 避免 SELECT *
SELECT * 会取出所有字段,无法使用覆盖索引,每条记录都要回表。只取需要的字段,既能减少回表,也能减少网络传输。
-- 不好:回表取所有字段
SELECT * FROM orders WHERE user_id = 1001;
-- 好:只取需要的字段,可能走覆盖索引
SELECT order_no, status FROM orders WHERE user_id = 1001;
2. 避免在索引列上做函数或类型转换
-- 不好:对索引列用函数,索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2025;
-- 好:改写为范围查询,走索引
SELECT * FROM orders WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01';
3. 联合索引遵循最左前缀原则
联合索引 (a, b, c) 相当于同时建了 (a)、(a, b)、(a, b, c) 三个索引。查询条件必须从最左列开始连续使用,索引才能生效。
-- 走索引
WHERE a = 1 AND b = 2 AND c = 3;
WHERE a = 1 AND b = 2;
WHERE a = 1;
-- 不走索引(跳过了 a)
WHERE b = 2 AND c = 3;
小结
EXPLAIN 是 MySQL 调优的第一步,也是最重要的一步。 在不确定一条 SQL 该不该加索引、索引有没有生效的时候,跑一个 EXPLAIN,看 type、key、rows、Extra 这四个字段,基本就能判断查询的健康程度。
调优的本质是要理解 MySQL 的执行逻辑:它怎么选索引、怎么排序、怎么回表。EXPLAIN 把这些决策过程暴露给你,剩下的就是根据"症状"对症下药。此外,Mysql调优也是后端面试的一个热点话题,掌握 MySQL 调优的核心思路,不仅能提升系统性能,也能帮助我们在面试中更从容地应对相关问题。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)