臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

SQL 优化不是看到慢就加索引,而是先找到慢 SQL,再用执行计划确认时间花在哪里,修改后用同样条件重新验证。

本文以 MySQL 8.4 为准。相关基础可先看 04.MySQL 索引 和 09.EXPLAIN。

二、🧭 理论:什么是慢查询

2.1 慢查询不只是“执行超过几秒”

MySQL 慢查询日志会记录执行时间达到 long_query_time,并满足扫描行数等条件的语句。默认情况下,慢查询日志没有开启,long_query_time 默认是 10 秒。

实际工作中还要关注:

  • 单次很慢的 SQL,例如报表查询需要 8 秒。
  • 单次只慢一点、但每秒执行几千次的 SQL。
  • 返回数据很少,却扫描大量数据的 SQL。
  • 因锁等待而变慢的 SQL。

记忆顺序

先定位,后分析,再修改,最后验证。

三、⚙️ 理论:SQL 为什么会慢

3.1 常见原因

原因白话解释重点检查
缺少合适索引为找少量数据扫描了大量行EXPLAIN 的 type、key、rows
索引失效有索引,但 SQL 写法使它难以使用函数、隐式类型转换、联合索引顺序
返回或处理数据过多排序、分组、关联的数据量太大Rows_examined、临时表、额外排序
执行计划估算不准优化器以为数据很少,实际很多EXPLAIN ANALYZE 的预估行数与实际行数
锁等待SQL 本身不复杂,但在等其他事务Lock_time、事务持续时间

下面用 orders 订单表分别举例。执行计划中的行数和耗时是为了帮助理解而简化的示意值,实际结果由表结构、数据分布和统计信息决定。

3.1.1 缺少合适索引:为了 20 行扫描 100 万行

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 1001;

假设 orders 有 100 万行,user_id 没有索引,可能得到:

typepossible_keyskeyrowsfilteredExtra
ALLNULLNULL100000010.00Using where

这样阅读:

  • type = ALL:从头到尾扫描整张表。
  • key = NULL:没有使用索引。
  • rows = 1000000:预计检查约 100 万行,不是最终返回行数。
  • Using where:读出数据后,还要继续判断 user_id = 1001。

如果这个查询高频使用,可以评估为 user_id 建立索引:

CREATE INDEX idx_orders_user_id ON orders (user_id);

再次 EXPLAIN,可能变为:

typepossible_keyskeyrowsExtra
refidx_orders_user_ididx_orders_user_id20NULL

扫描量从约 100 万行降到约 20 行,才是这个索引真正带来的价值。创建前仍需检查重复索引、表大小和写入影响。

3.1.2 索引失效:在索引列上使用函数

假设 created_at 已有索引 idx_orders_created_at:

EXPLAIN
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-10-15';

可能得到:

typepossible_keyskeyrowsExtra
ALLNULLNULL1000000Using where

普通索引保存的是原始 created_at 值,而查询要先对每行执行 DATE(created_at)。优化器可能无法直接用原索引定位日期,于是退化为全表扫描。

把函数计算改成原字段的范围查询:

EXPLAIN
SELECT *
FROM orders
WHERE created_at >= '2026-10-15 00:00:00'
  AND created_at <  '2026-10-16 00:00:00';

执行计划可能变为:

typepossible_keyskeyrowsExtra
rangeidx_orders_created_atidx_orders_created_at12000Using index condition

type = range 表示只扫描索引中这一天对应的范围。这里不用 23:59:59 作为结束时间,可以避免遗漏带小数秒的数据。

3.1.3 处理数据过多:分组后还要排序

EXPLAIN
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
ORDER BY total_amount DESC;

可能得到:

typekeyrowsExtra
ALLNULL1000000Using temporary; Using filesort

执行过程大致是:

扫描 100 万行订单
→ 按 user_id 分组并计算 SUM
→ 把中间结果放入内部临时表
→ 按 total_amount 再排序
→ 返回全部用户
  • Using temporary:需要内部临时表保存分组结果。
  • Using filesort:需要额外排序;名称不代表一定写入磁盘。
  • rows 很大:排序之前已经处理了大量订单。

如果业务只要前 10 名,应先加 LIMIT 10 减少返回结果,但它不保证省掉前面的全量聚合。数据量很大且查询频繁时,通常还要评估汇总表、定时预计算,或能覆盖筛选条件的索引,而不是只盯着 filesort。

3.1.4 执行计划估算不准:预计 100 行,实际 10 万行

普通 EXPLAIN 只有估算值。要比较估算与实际,可以使用:

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE status = 'PENDING';

简化后的树形结果可能类似:

Index lookup on orders using idx_orders_status (status='PENDING')
  (cost=35 rows=100)
  (actual time=0.5..180.0 rows=100000 loops=1)

重点对比两个 rows:

  • rows=100:优化器执行前预计返回 100 行。
  • actual ... rows=100000:实际返回 10 万行。
  • 估算相差 1000 倍,可能导致优化器继续选择不合适的索引或关联顺序。

这种情况可能由数据分布突变或统计信息不准确导致。可以进一步检查数据分布和统计信息,再决定是否更新统计信息、调整索引或改写查询,不能看到估算错误就直接使用 FORCE INDEX。

EXPLAIN ANALYZE 会真正执行 SQL

它会真实消耗 CPU、I/O 和内存。重查询应优先在测试环境、只读副本或限制后的数据范围中执行。

3.1.5 锁等待:执行计划正常,SQL 仍然很慢

事务 A:修改订单 1001,但一直没有提交
事务 B:也要修改订单 1001,只能等待事务 A 释放锁

事务 B 的语句可能是:

UPDATE orders
SET status = 'PAID'
WHERE id = 1001;

它的 EXPLAIN 可能很正常:

typepossible_keyskeyrowsExtra
rangePRIMARYPRIMARY1Using where

主键只定位一行,说明慢的原因不是扫描量。EXPLAIN 描述的是执行计划,不会告诉你这条语句实际等待锁多久。这时应改查会话和锁等待:

SHOW PROCESSLIST;
 
SELECT *
FROM performance_schema.data_lock_waits;

如果发现事务 B 正在等待事务 A,应处理长事务、缩短事务持锁时间或修正事务边界,而不是继续添加索引。

3.2 优化的核心目标

优化通常是在减少三样东西:

扫描的行数 → 中间结果的数据量 → SQL 的执行次数

索引只是手段之一。索引过多还会占用空间,并增加 INSERT、UPDATE、DELETE 的维护成本。

四、🚀 实践:从定位到验证

4.1 前置准备

准备一条已确认较慢的 SELECT,并记录它的执行时间、调用频率、返回行数和扫描行数。生产环境优先使用脱敏参数,在测试环境或只读副本分析。

查看慢查询相关配置:

SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'slow_query_log_file';

生产环境注意

开启日志、修改全局参数、创建索引都属于有副作用的操作,需要评估磁盘、写入压力和变更窗口,不能直接在生产库尝试。

4.2 可以拿来干什么

定位到慢 SQL 后,先执行普通 EXPLAIN,观察访问方式、实际使用的索引和预计扫描行数:

EXPLAIN
SELECT id, user_id, status, created_at
FROM orders
WHERE user_id = 1001
  AND status = 1
ORDER BY created_at DESC
LIMIT 20;

重点看:

  • type 是否出现大表全表扫描 ALL。
  • key 是否使用了预期索引。
  • rows 是否远大于最终返回的 20 行。
  • Extra 是否出现大数据量的 Using filesort 或 Using temporary。

4.3 完整实践:分析、优化、验证

假设查询经常按 user_id、status 筛选,再按 created_at 倒序取最近 20 条。原来只有单列索引,仍需扫描和排序较多数据。

可以评估联合索引:

-- 先使用等值查询列,再放排序列,使 MySQL 更容易直接按索引顺序取前 20 条。
CREATE INDEX idx_orders_user_status_created
ON orders (user_id, status, created_at DESC);

创建索引前要先确认:目标库表、现有重复索引、数据量、磁盘空间和写入影响。创建后使用相同参数重新执行:

EXPLAIN ANALYZE
SELECT id, user_id, status, created_at
FROM orders
WHERE user_id = 1001
  AND status = 1
ORDER BY created_at DESC
LIMIT 20;

验证结果时对比:

  1. 实际耗时是否下降。
  2. 实际扫描行数是否减少。
  3. 是否消除了大量额外排序。
  4. 高并发下的整体吞吐是否改善。
  5. 新索引是否明显拖慢写入。

EXPLAIN ANALYZE 会真实执行 SQL,重查询应先在测试环境或只读副本验证。

五、🎤 面试时怎么流利地说

5.1 回答路线

定位慢 SQL → EXPLAIN 分析 → 针对原因优化 → 对比验证 → 评估副作用

5.2 30 秒标准回答

遇到慢查询,我会先通过监控或慢查询日志定位 SQL,结合执行时间、调用频率和扫描行数判断优先级。然后用 EXPLAIN 或 EXPLAIN ANALYZE 检查访问方式、索引、扫描行数、排序和临时表,再针对性调整索引或改写 SQL。优化后要用相同数据和参数复测,同时观察写入成本,不能只凭“使用了索引”就认定优化成功。

5.3 高频追问与短答

为什么用了索引还是慢?

可能索引区分度低、扫描范围仍然很大、需要大量回表或排序,也可能慢在锁等待。要结合实际扫描行数和耗时判断。

是不是索引越多越好?

不是。索引会占用空间,并增加写操作的维护成本,应尽量让少量高价值索引覆盖常用查询。

六、📌 总结

  • 慢 SQL 既看单次耗时,也看执行频率和资源消耗。
  • 优化前先定位瓶颈,不要直接改 SQL 或加索引。
  • EXPLAIN 看预估计划,EXPLAIN ANALYZE 看实际执行。
  • 优化的核心是减少扫描、减少中间数据、减少不必要调用。
  • 修改后必须复测,并评估索引对写入的影响。

七、🔗 官方资料