网站突然变卡、后台列表页要等好几秒才刷出来、服务器负载莫名升高——这些问题背后,十有八九藏着几条 MySQL 慢查询(执行时间超出预期的 SQL 语句)。很多站长面对慢查询的第一反应是升级服务器配置,但实际经验是:一条没走索引的 SQL,换再强的机器也只是在烧钱续命。这篇文章要解决的正是”如何用 EXPLAIN 找到慢查询的真正原因,并用联合索引把查询速度拉回来”这个问题。我们会按照”定位慢语句 → 读懂执行计划 → 设计联合索引 → 验证优化效果”的主线展开,每一步都给出可以直接执行的命令和判断标准,帮助你不需要数据库专家背景,也能独立完成一次完整的 MySQL 慢查询优化。

第一步:找到慢查询并确认瓶颈在哪里
优化从定位开始。MySQL 自带慢查询日志(slow query log,记录执行时间超过阈值的 SQL),先确认它是否开启:
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
如果 slow_query_log 是 OFF,可以临时开启并设置 1 秒阈值:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
跑一段时间后,用 mysqldumpslow 汇总日志,找出出现频率最高、耗时最长的语句。例如 mysqldumpslow -s t -t 5 /var/lib/mysql/hostname-slow.log 会列出累计耗时最高的 5 条 SQL。真实案例:一个外贸站的订单列表页平均 4.2 秒才加载完成,慢日志里同一条 SELECT ... WHERE customer_id = ? AND status = ? ORDER BY created_at DESC 每小时出现 800 多次——这就是典型的”单条不致命、总量拖垮服务器”场景,也是索引优化收益最大的类型。
拿到具体 SQL 之后,不要急着改代码或急着重启数据库。下一步要做的,是让 MySQL 自己解释它打算怎么执行这条语句。

第二步:用 EXPLAIN 读懂执行计划
EXPLAIN(执行计划分析命令)是 MySQL 提供的”透视镜”:在 SELECT 前面加上 EXPLAIN,MySQL 不会真的执行查询,而是返回它准备采用的执行路径。上面那条订单查询的分析结果,关键列如下:
EXPLAIN SELECT * FROM orders
WHERE customer_id = 1024 AND status = 'paid'
ORDER BY created_at DESC LIMIT 20;
| 列名 | 输出示例 | 含义 |
|---|---|---|
| type | ALL | 访问类型,ALL 表示全表扫描 |
| key | NULL | 实际使用的索引,NULL 表示没用上 |
| rows | 186543 | 预计扫描行数 |
| Extra | Using where; Using filesort | 附加操作,filesort 表示需要额外排序 |
看执行计划时,重点盯三个信号。第一个是 type:从优到差依次是 system > const > eq_ref > ref > range > index > ALL,如果看到 ALL 且表超过几万行,这条查询基本就是慢查询元凶。第二个是 rows:预计扫描 18 万行只为了取 20 条数据,扫描量是结果集的九千倍,明显是索引缺失。第三个是 Extra 里的 Using filesort 和 Using temporary,前者说明排序无法利用索引顺序,后者说明需要建临时表,两者都会随数据量增长线性变慢。
刚才的例子里三条信号全部命中:type=ALL、rows=18 万、Extra 带 filesort。结论很明确——orders 表缺一个能同时覆盖这三个查询条件的联合索引(由多个字段按顺序组成的复合索引)。接下来就是怎么设计它。
第三步:按最左前缀原则设计联合索引
联合索引的设计核心是最左前缀原则(联合索引按字段顺序逐级排序,查询条件必须从最左字段开始连续匹配才能用上索引)。可以把联合索引想象成一本电话簿:先按姓排序、姓相同再按名排序。你查”张”姓很快,查”张伟”更快,但只查名字叫”伟”的人,电话簿就帮不上忙了。
针对订单查询 WHERE customer_id = ? AND status = ? ORDER BY created_at DESC,推荐这样建索引:
ALTER TABLE orders
ADD INDEX idx_customer_status_created (customer_id, status, created_at);
字段顺序的确定有三个依据。第一,等值条件字段放前面:customer_id 和 status 都是等值匹配,按区分度高的在前排。第二,排序字段放最后:created_at 放在第三位后,前面两个字段定位到的数据天然按时间有序,Extra 里的 filesort 会直接消失,ORDER BY ... DESC LIMIT 20 只需读 20 行。第三,避免范围字段插中间:如果改成 (customer_id, created_at, status),created_at 的范围条件会截断后续字段的索引匹配,status 就用不上了。
设计时还要避开几个常见误区:在索引列上使用函数或运算(如 WHERE DATE(created_at) = '2026-10-14')会导致索引失效,应改写为 WHERE created_at >= '2026-10-14 00:00:00' AND created_at < '2026-10-15 00:00:00';前导模糊查询 LIKE '%关键词' 无法走索引,改成 LIKE '关键词%' 前缀匹配即可;联合索引也不是越多越好,每个索引都会拖慢写入并占用存储,一般建议单表索引不超过 5-6 个。

第四步:验证效果与线上变更注意事项
索引建好后,重新跑一遍 EXPLAIN 确认三个指标都改善了:
EXPLAIN SELECT * FROM orders
WHERE customer_id = 1024 AND status = 'paid'
ORDER BY created_at DESC LIMIT 20;
优化前后的对比非常直观:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| type | ALL | ref |
| key | NULL | idx_customer_status_created |
| rows | 186543 | 42 |
| Extra | Using where; Using filesort | Backward index scan; NULL |
扫描行数从 18.6 万降到 42,页面接口实测耗时从 4200ms 降到 18ms,降幅超过 99%。这正是”数据库性能问题优先查索引,而不是先升级硬件”的原因——同样的 VPS 主机(VPS,虚拟专用服务器)配置,索引调整前后的承载能力可能相差几十倍。
在生产环境执行索引变更时,有几点必须注意。ALTER TABLE ADD INDEX 在 MySQL 8.0 的 InnoDB 引擎下默认使用 INSTANT/INPLACE 方式,多数情况不锁写操作,但在数据量很大的表上仍建议避开业务高峰执行;上线前先在测试环境验证,并用 SHOW INDEX FROM orders; 确认索引已生效。如果你托管的是 WordPress 这类内容站点,数据库同样遵循这些规则,可以结合网站性能优化指南里的 TTFB 优化思路一起排查;使用独立服务器自建数据库的用户,还可以关注服务器性能优化分类下的更多实战经验。
总结与行动建议
回顾整个优化流程:先用慢查询日志锁定高频慢语句,再用 EXPLAIN 检查 type、rows、Extra 三个关键指标,然后按”等值在前、范围其次、排序在后”的顺序设计联合索引,最后重新执行 EXPLAIN 验证扫描行数是否下降到合理水平。这套方法不依赖特定框架或中间件,适用于几乎所有 MySQL 场景。
如果你正在排查类似问题,建议按这个顺序行动:先开启慢查询日志跑 24 小时拿到真实数据,再挑出耗时最长的 3 条语句逐一做 EXPLAIN 分析,确认索引问题后优先为最高频的查询补上联合索引。总结一句话:绝大多数慢查询的根源是索引设计,而不是硬件不够。如果你需要一台配置稳定、支持自主调优数据库参数的服务器来承载业务增长,可以考虑 Hostease 的虚拟主机或 VPS(虚拟专用服务器)方案,配合本文的索引优化方法,让数据库性能和硬件资源都用在刀刃上。