欢迎光临
我们一直在努力

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 调优的核心思路,不仅能提升系统性能,也能帮助我们在面试中更从容地应对相关问题。

    赞(0)
    未经允许不得转载:171主机测评 » MySQL 调优:给你的SQL做体检
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址