news 2026/5/23 19:49:17

MySQL 慢查询优化实战:从 EXPLAIN 分析到索引设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL 慢查询优化实战:从 EXPLAIN 分析到索引设计

在后端开发中,接口响应慢并不一定是代码逻辑的问题。很多时候,真正拖慢系统的是数据库查询。

一个接口可能只写了几行代码,但背后执行的 SQL 如果没有走索引,或者索引设计不合理,就可能让数据库扫描大量数据,最终导致接口响应时间从几十毫秒变成几秒甚至十几秒。

慢查询通常会带来以下问题:

  • 接口响应时间变长;

  • 数据库 CPU 占用升高;

  • 连接池被占满;

  • 业务高峰期请求堆积;

  • 用户体验明显下降;

  • 严重时可能拖垮整个服务。

本文将从实战角度出发,围绕 MySQL 慢查询优化展开,重点讲清楚:

  1. 什么是慢查询;

  2. 如何定位慢 SQL;

  3. 如何使用 EXPLAIN 分析执行计划;

  4. 索引为什么能提升查询性能;

  5. 联合索引如何设计;

  6. 哪些写法会导致索引失效;

  7. 慢查询优化的常见思路。


一、什么是慢查询?

慢查询,简单来说,就是执行时间超过预期的 SQL。

在 MySQL 中,可以通过慢查询日志记录执行较慢的 SQL。

比如系统中有一个查询用户订单的接口:

SELECT * FROM orders WHERE user_id = 10001 ORDER BY created_at DESC LIMIT 20;

如果orders表只有几千条数据,查询可能非常快。

但如果orders表已经有几千万条数据,并且没有合适的索引,这条 SQL 就可能变成慢查询。

数据库需要扫描大量记录,再进行排序,最后取出前 20 条数据。

对于线上系统来说,一条慢 SQL 的影响不只是“这一次查询慢”,更严重的是它会占用数据库资源,影响其他正常请求。


二、开启慢查询日志

在 MySQL 中,可以通过以下命令查看慢查询日志是否开启:

SHOW VARIABLES LIKE 'slow_query_log';

查看慢查询时间阈值:

SHOW VARIABLES LIKE 'long_query_time';

如果long_query_time设置为 1,表示执行时间超过 1 秒的 SQL 会被记录为慢查询。

可以临时开启慢查询日志:

SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1;

查看慢查询日志文件位置:

SHOW VARIABLES LIKE 'slow_query_log_file';

需要注意的是,线上环境修改这些参数要谨慎,最好结合实际运维规范进行配置。


三、准备测试表

为了方便说明,假设有一张订单表:

CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, status TINYINT NOT NULL, amount DECIMAL(10, 2) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL );

常见查询场景包括:

-- 查询某个用户的订单列表 SELECT * FROM orders WHERE user_id = 10001 ORDER BY created_at DESC LIMIT 20; -- 查询某个状态下的订单 SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 20; -- 根据订单号查询订单详情 SELECT * FROM orders WHERE order_no = '202605220001'; -- 查询某个用户某种状态的订单 SELECT * FROM orders WHERE user_id = 10001 AND status = 1 ORDER BY created_at DESC LIMIT 20;

这些 SQL 看起来都很简单,但如果索引设计不合理,数据量变大后都会变慢。


四、使用 EXPLAIN 分析执行计划

优化 SQL 前,不要凭感觉猜测。

正确做法是先使用EXPLAIN查看执行计划。

例如:

EXPLAIN SELECT * FROM orders WHERE user_id = 10001 ORDER BY created_at DESC LIMIT 20;

执行结果中常见字段包括:

id select_type table type possible_keys key key_len ref rows Extra

其中最关键的是以下几个字段。

1. type

type表示访问类型,常见值从好到差大致如下:

const eq_ref ref range index ALL

如果看到ALL,通常表示全表扫描,需要重点关注。

例如:

type: ALL rows: 5000000

这说明 MySQL 可能要扫描 500 万行数据,查询性能通常不会好。

2. key

key表示实际使用的索引。

如果keyNULL,说明没有使用索引。

3. rows

rows表示 MySQL 预计需要扫描的行数。

这个值越大,查询成本通常越高。

4. Extra

Extra中可能出现一些重要信息。

常见内容包括:

Using where Using index Using filesort Using temporary

其中需要特别注意:

Using filesort Using temporary

这通常意味着排序或临时表成本较高,可能需要优化索引或 SQL。


五、给 user_id 添加索引

对于下面这条 SQL:

SELECT * FROM orders WHERE user_id = 10001 ORDER BY created_at DESC LIMIT 20;

如果没有索引,数据库需要扫描整张表。

可以先添加一个普通索引:

CREATE INDEX idx_user_id ON orders(user_id);

再次执行:

EXPLAIN SELECT * FROM orders WHERE user_id = 10001 ORDER BY created_at DESC LIMIT 20;

此时可能看到:

type: ref key: idx_user_id rows: 1000 Extra: Using where; Using filesort

这说明 MySQL 使用了user_id索引,扫描行数明显减少。

但仍然可能出现Using filesort

原因是索引只帮助数据库快速找到了user_id = 10001的数据,但这些数据仍然需要按照created_at排序。

如果某个用户有大量订单,排序成本仍然可能比较高。


六、使用联合索引优化排序

针对这个查询:

SELECT * FROM orders WHERE user_id = 10001 ORDER BY created_at DESC LIMIT 20;

更合适的索引是:

CREATE INDEX idx_user_created ON orders(user_id, created_at);

这个联合索引的含义是:

先按 user_id 排序 在相同 user_id 内,再按 created_at 排序

这样,MySQL 可以先定位到某个用户的订单范围,然后直接按照索引顺序读取数据。

如果执行计划中不再出现Using filesort,说明排序成本得到了优化。

更进一步,如果业务明确要求倒序查询,也可以创建降序索引:

CREATE INDEX idx_user_created_desc ON orders(user_id, created_at DESC);

不过是否需要显式创建降序索引,要结合 MySQL 版本和实际执行计划判断。


七、联合索引的最左前缀原则

联合索引有一个非常重要的规则:最左前缀原则。

例如创建索引:

CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);

这个索引可以支持以下查询:

WHERE user_id = 10001
WHERE user_id = 10001 AND status = 1
WHERE user_id = 10001 AND status = 1 ORDER BY created_at DESC

但不能很好支持:

WHERE status = 1

因为status不是联合索引的最左列。

联合索引可以理解为一本多级排序的字典:

先按 user_id 排 user_id 相同,再按 status 排 status 相同,再按 created_at 排

如果查询条件跳过了最左边的user_id,后面的索引列就很难被高效利用。


八、索引列顺序如何设计?

联合索引不是把查询字段随便放进去就可以。

索引列顺序通常要结合以下因素设计:

等值条件 范围条件 排序字段 字段区分度 查询频率

例如有一条常见 SQL:

SELECT * FROM orders WHERE user_id = 10001 AND status = 1 ORDER BY created_at DESC LIMIT 20;

可以考虑索引:

CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);

原因是:

  • user_id是等值条件;

  • status是等值条件;

  • created_at用于排序;

  • 查询需要按照时间取最新记录。

如果换成:

CREATE INDEX idx_created_user_status ON orders(created_at, user_id, status);

通常效果就不如前者。

因为查询首先要根据user_idstatus过滤,如果索引以created_at开头,可能无法高效定位目标数据范围。


九、范围查询对联合索引的影响

假设有如下索引:

CREATE INDEX idx_user_created_status ON orders(user_id, created_at, status);

查询:

SELECT * FROM orders WHERE user_id = 10001 AND created_at >= '2026-05-01' AND status = 1;

这里created_at是范围查询。

在联合索引中,范围查询之后的字段,通常无法继续充分利用索引进行精确过滤。

也就是说,status的索引利用效果可能受到影响。

所以如果查询更常见的是:

WHERE user_id = ? AND status = ? AND created_at >= ?

那么索引顺序更适合设计为:

CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);

一般经验是:

等值条件字段尽量放前面 范围查询字段尽量放后面 排序字段结合查询条件设计

十、覆盖索引:减少回表

假设有查询:

SELECT id, order_no, created_at FROM orders WHERE user_id = 10001 ORDER BY created_at DESC LIMIT 20;

如果创建索引:

CREATE INDEX idx_user_created_order ON orders(user_id, created_at, order_no);

查询所需字段都能从索引中拿到,不需要再回到主键索引查询完整行数据。

这种情况称为覆盖索引。

执行计划的Extra中可能会出现:

Using index

覆盖索引的优点是减少回表,提高查询性能。

但需要注意,覆盖索引不是字段越多越好。

如果索引包含太多列,会带来以下问题:

  • 占用更多磁盘空间;

  • 插入和更新成本上升;

  • 索引维护成本变高;

  • 缓存命中率可能下降。

所以覆盖索引适合高频、核心、字段较少的查询场景。


十一、哪些写法会导致索引失效?

1. 对索引列使用函数

错误示例:

SELECT * FROM orders WHERE DATE(created_at) = '2026-05-22';

这里对created_at使用了DATE()函数,可能导致索引无法正常使用。

更推荐写成范围查询:

SELECT * FROM orders WHERE created_at >= '2026-05-22 00:00:00' AND created_at < '2026-05-23 00:00:00';

2. 对索引列进行计算

错误示例:

SELECT * FROM orders WHERE amount + 10 > 100;

更推荐写成:

SELECT * FROM orders WHERE amount > 90;

3. LIKE 前缀使用通配符

可以使用索引的情况:

SELECT * FROM orders WHERE order_no LIKE '202605%';

通常难以使用普通 B+Tree 索引的情况:

SELECT * FROM orders WHERE order_no LIKE '%0001';

因为 B+Tree 索引是按前缀有序的,前置通配符会破坏索引的有序查找能力。

4. 隐式类型转换

假设order_no是字符串类型:

order_no VARCHAR(64)

错误示例:

SELECT * FROM orders WHERE order_no = 202605220001;

更推荐:

SELECT * FROM orders WHERE order_no = '202605220001';

字段类型和查询值类型不一致时,可能发生隐式类型转换,影响索引使用。

5. OR 条件使用不当

例如:

SELECT * FROM orders WHERE user_id = 10001 OR status = 1;

如果两个字段没有合适索引,或者优化器判断成本较高,可能导致全表扫描。

这类 SQL 可以根据场景改写为UNION

SELECT * FROM orders WHERE user_id = 10001 UNION SELECT * FROM orders WHERE status = 1;

但是否一定更快,需要结合执行计划和数据量测试。


十二、不要滥用 SELECT *

很多慢查询中都存在一个问题:

SELECT *

SELECT *会带来几个问题:

  • 读取不必要的字段;

  • 增加网络传输量;

  • 更容易触发回表;

  • 表结构变化可能影响接口稳定性;

  • 不利于覆盖索引优化。

更推荐只查询需要的字段:

SELECT id, order_no, status, amount, created_at FROM orders WHERE user_id = 10001 ORDER BY created_at DESC LIMIT 20;

尤其是表中包含大字段时,例如:

TEXT LONGTEXT BLOB JSON

更应该避免在列表查询中直接SELECT *


十三、分页查询优化

很多系统中会使用如下分页:

SELECT * FROM orders ORDER BY created_at DESC LIMIT 100000, 20;

这种深分页查询非常容易变慢。

原因是 MySQL 需要先扫描并跳过前 100000 条数据,然后再取 20 条。

更好的方式是使用游标分页。

例如第一页:

SELECT id, order_no, created_at FROM orders ORDER BY created_at DESC, id DESC LIMIT 20;

下一页带上上一页最后一条记录的时间和 ID:

SELECT id, order_no, created_at FROM orders WHERE (created_at < '2026-05-22 10:00:00') OR (created_at = '2026-05-22 10:00:00' AND id < 123456) ORDER BY created_at DESC, id DESC LIMIT 20;

同时创建索引:

CREATE INDEX idx_created_id ON orders(created_at, id);

游标分页的优点是性能更稳定,适合信息流、订单列表、日志列表等场景。

缺点是不适合直接跳转到第 N 页。


十四、ORDER BY 优化

排序是慢查询中很常见的性能消耗点。

例如:

SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 20;

可以考虑索引:

CREATE INDEX idx_status_created ON orders(status, created_at);

这样 MySQL 可以先定位到status = 1的数据,再按created_at顺序读取。

如果没有合适索引,执行计划中可能出现:

Using filesort

这并不一定代表一定很慢,但当数据量较大时,需要重点关注。

常见优化方向包括:

让 WHERE 条件和 ORDER BY 字段组成联合索引 减少排序数据量 避免对大结果集排序 分页时尽量使用索引顺序读取

十五、COUNT 查询优化

很多列表接口会同时查询数据总数:

SELECT COUNT(*) FROM orders WHERE status = 1;

当数据量很大时,COUNT(*)也可能变慢。

优化思路包括:

1. 给过滤字段加索引

CREATE INDEX idx_status ON orders(status);

如果查询条件能走索引,统计成本通常会降低。

2. 避免高频实时精确统计

对于一些列表页,不一定每次都需要实时精确总数。

可以考虑:

缓存统计结果 异步更新计数 只展示是否有下一页 限制最大可翻页范围

3. 业务上接受近似值

在一些非强一致场景中,可以使用近似统计,而不是每次执行精确COUNT(*)


十六、索引不是越多越好

索引可以提升查询性能,但也会带来成本。

每增加一个索引,都会带来以下影响:

  • 插入数据时需要维护索引;

  • 更新索引列时需要调整索引结构;

  • 删除数据时也要维护索引;

  • 索引会占用额外磁盘空间;

  • 优化器需要在多个索引中选择执行路径。

所以索引设计需要克制。

不建议看到查询慢就盲目加索引。

更合理的流程是:

定位慢 SQL 分析执行计划 确认扫描行数和索引使用情况 结合业务查询频率设计索引 上线前测试效果 持续观察慢查询日志

十七、慢查询优化的一般流程

实际项目中,可以按照以下步骤排查慢查询:

1. 从慢查询日志中找到高频慢 SQL 2. 查看 SQL 执行时间和扫描行数 3. 使用 EXPLAIN 分析执行计划 4. 判断是否使用了合适索引 5. 检查是否出现 Using filesort 或 Using temporary 6. 分析 WHERE、ORDER BY、GROUP BY 字段 7. 设计或调整联合索引 8. 改写不合理 SQL 9. 使用真实数据量测试 10. 上线后持续观察

这个流程比单纯“加索引”更可靠。

因为慢查询的原因可能不止一个,可能是索引问题,也可能是 SQL 写法、数据分布、分页方式、排序方式、表结构设计等问题。


十八、一个完整优化案例

假设慢 SQL 如下:

SELECT * FROM orders WHERE user_id = 10001 AND status = 1 ORDER BY created_at DESC LIMIT 20;

执行计划显示:

type: ALL key: NULL rows: 8000000 Extra: Using where; Using filesort

这说明:

没有使用索引 扫描了大量数据 还进行了额外排序

第一步,避免SELECT *

SELECT id, order_no, amount, status, created_at FROM orders WHERE user_id = 10001 AND status = 1 ORDER BY created_at DESC LIMIT 20;

第二步,创建联合索引:

CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);

再次查看执行计划,理想情况下可能变成:

type: ref key: idx_user_status_created rows: 50 Extra: Using where

如果查询字段能被索引覆盖,还可以进一步调整为:

CREATE INDEX idx_user_status_created_cover ON orders(user_id, status, created_at, order_no, amount);

但是否需要覆盖索引,要看查询频率和写入成本,不能盲目增加。


十九、总结

MySQL 慢查询优化不是简单地给字段加索引,而是一个系统性的分析过程。

核心思路可以概括为:

先定位慢 SQL 再分析执行计划 再判断索引是否合理 最后结合业务场景优化 SQL 和表结构

在实际开发中,最常见的优化点包括:

避免全表扫描 合理设计联合索引 遵守最左前缀原则 避免索引列上使用函数或计算 减少 SELECT * 优化深分页 减少不必要的排序 谨慎使用 COUNT 避免滥加索引

对于大多数业务系统来说,数据库性能问题往往不是一开始就出现的,而是在数据量增长后逐渐暴露出来。

因此,写 SQL 时不能只考虑“功能能不能跑”,还要考虑:

数据量变大后还能不能跑得快 查询是否能走索引 排序是否可控 分页是否稳定 索引维护成本是否合理

一个优秀的后端开发者,不仅要会写业务代码,也要能看懂执行计划,理解索引结构,并根据真实业务场景设计可持续的数据库查询方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/23 19:44:08

【Anaconda】使用指南及问题汇总(自用)

安装 1. Anaconda的下载与安装 除了安装路径修改&#xff0c;其他的一路默认就好 2. Anaconda修改环境变量 因为我们这一步才手动添加环境变量&#xff0c;所以第一步安装的时候不要让它自动配置环境变量了。 用户变量或者系统变量都可以。建议系统变量&#xff0c;方便后…

作者头像 李华
网站建设 2026/5/23 19:44:02

雷军:特斯拉是受人尊重的企业,我们与Model Y较量是八败两胜

雷递网 乐天 5月22日小米CEO雷军日前在与媒体沟通交流中表示&#xff0c;特斯拉是一个全球受人尊重的企业&#xff0c;ModelY也是全球销冠&#xff0c;但小米从造车的第一天起&#xff0c;米粉和大众对小米汽车的期待就是要超过特斯拉。“这个任务很难很难&#xff0c;我们去年…

作者头像 李华
网站建设 2026/5/23 19:33:36

如何快速搭建戴森球计划高效工厂:终极蓝图库使用指南

如何快速搭建戴森球计划高效工厂&#xff1a;终极蓝图库使用指南 【免费下载链接】FactoryBluePrints 游戏戴森球计划的**工厂**蓝图仓库 项目地址: https://gitcode.com/GitHub_Trending/fa/FactoryBluePrints FactoryBluePrints是戴森球计划游戏中最全面的工厂蓝图开源…

作者头像 李华
网站建设 2026/5/23 19:32:33

告别图形界面:用BaiduPCS命令行工具高效管理百度网盘

告别图形界面&#xff1a;用BaiduPCS命令行工具高效管理百度网盘 【免费下载链接】BaiduPCS 百度网盘命令行工具。The terminal utility for Baidu Network Disk. 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduPCS 你是否厌倦了百度网盘缓慢的网页界面和臃肿的客户…

作者头像 李华