MySQL 调优:如何给SQL做体检

目录


一条慢查询引发的问题

你刚接手一个项目,发现某个列表页加载要五六秒。你打开慢查询日志,找到了那条 SQL:

SELECT * FROM orders WHERE user_id = 1001 AND status = 'paid' ORDER BY create_time DESC LIMIT 20;

表里有几百万条数据,user_idstatus 都有索引,看起来不该这么慢。但你又不敢随便加索引,万一加错了,写入性能反而会下降。

这种场景在日常开发中太常见了。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 temporaryUsing 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|               |         |      |                                              |
+----+------+---------------+----------------+---------+------+------+----------------------------------------------+

逐字段分析:

  1. type = ref:走了索引等值查询,还行
  2. key = idx_user_status:用了联合索引 (user_id, status),user_id 和 status 都能命中
  3. rows = 8500:预估扫描 8500 行,这个数字偏大
  4. 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_idstatus 是等值查询,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 调优的核心思路,不仅能提升系统性能,也能帮助我们在面试中更从容地应对相关问题。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐