一条慢 SQL 把接口拖到 1.5s,我是怎么优化到 900ms 的?
最近在准备 Java 面试,发现慢 SQL 优化几乎是绕不过去的一类题。
但很多文章有个问题:要么全是概念,要么上来就是一堆索引术语,看完还是不知道真实项目里到底该怎么排查。
这篇文章我想换个写法,不背八股,直接用一个常见的后台列表接口来讲:
- 慢 SQL 是怎么定位出来的
EXPLAIN到底要看什么- 为什么有些 SQL 加了索引还是慢
- 真实项目里慢 SQL 优化通常怎么做
文中的业务案例做了抽象处理,但思路来自我自己实习时做过的一次慢 SQL 优化。实际项目里,我结合慢查询日志和 EXPLAIN 分析执行计划,优化多条慢 SQL 的索引和查询方式,把数据库查询性能提升了大约 40%,某核心接口的响应时间从 1.5s 降到了 900ms。
如果你是校招生,或者刚工作一两年,这篇文章应该会比较适合你。
1. 先说结论:慢 SQL 优化不是“看见慢就加索引”
这是很多人最容易踩的坑。
慢 SQL 优化的正确顺序通常是:
- 先定位慢 SQL
- 再分析执行计划
- 找出真正的瓶颈点
- 最后决定是改 SQL、改索引,还是改业务写法
也就是说,索引只是手段,不是答案。
真实工作里,一条 SQL 变慢,常见原因通常有这几类:
- 没有合适索引
- 索引建了,但 SQL 写法导致索引失效
- 联表方式不合理
- 查询字段太多,回表成本高
- 排序、分页、模糊查询本身就贵
- 数据量上来以后,原本能跑的 SQL 开始变慢
下面直接看案例。
2. 一个很常见的业务场景
假设我们有一个后台订单列表页,需要按状态、创建时间筛选订单,同时还能按用户名模糊搜索。
表结构简化后大概是这样:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL
);
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL
);
原始 SQL 可能长这样:
SELECT o.id, o.order_no, o.amount, o.status, o.create_time, u.username
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND DATE_FORMAT(o.create_time, '%Y-%m-%d') = '2025-09-01'
AND u.username LIKE '%zhang%'
ORDER BY o.create_time DESC
LIMIT 0, 20;
接口上线初期数据量不大,查询还能忍。
但订单数据涨到十几万、几十万以后,这条 SQL 就开始拖接口后腿了。接口平均响应时间到了 1.5s 左右,页面打开明显卡顿。
这时候真正应该做的,不是拍脑袋加索引,而是先看慢日志和执行计划。
3. 第一步:怎么定位到这条慢 SQL
如果系统已经开了 MySQL 慢查询日志,排查会轻松很多。
一般流程是:
- 先从慢查询日志里找到执行时间长、执行次数多的 SQL
- 再结合业务场景看这条 SQL 属于哪个接口
- 最后拿 SQL 去跑
EXPLAIN
如果你是在公司里做这件事,通常不会只盯着“最慢的一条”,还会关注两个维度:
- 单次执行特别慢的 SQL
- 单次不算特别慢,但调用特别频繁的 SQL
因为第二类 SQL 更容易把数据库整体拖慢。
4. 第二步:EXPLAIN 到底看什么
对这条 SQL 执行:
EXPLAIN
SELECT o.id, o.order_no, o.amount, o.status, o.create_time, u.username
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND DATE_FORMAT(o.create_time, '%Y-%m-%d') = '2025-09-01'
AND u.username LIKE '%zhang%'
ORDER BY o.create_time DESC
LIMIT 0, 20;
校招生阶段,你不用把 EXPLAIN 所有字段全背下来,但这几个一定要会看:
4.1 type
表示扫描方式,性能大致从好到差可以简单理解成:
const > ref > range > index > ALL
如果你看到 ALL,基本就要警觉了,通常意味着全表扫描。
4.2 key
表示实际用了哪个索引。
如果 possible_keys 里有索引,但 key 是 NULL,说明索引根本没用上。
4.3 rows
表示 MySQL 预估需要扫描多少行。
这个值越大,通常意味着代价越高。
4.4 Extra
这列很关键,常见几个关键词:
Using where:说明做了条件过滤Using index:说明可能走了覆盖索引Using filesort:说明额外排序,通常要重点关注Using temporary:说明用了临时表,也要小心
5. 这条 SQL 为什么慢
把上面的 SQL 拆开看,问题其实不少。
5.1 对索引列用了函数,容易导致索引失效
这一段是第一个典型问题:
DATE_FORMAT(o.create_time, '%Y-%m-%d') = '2025-09-01'
如果 create_time 上有索引,但你对它做了函数计算,MySQL 往往没法直接利用原索引。
更推荐写成时间范围查询:
o.create_time >= '2025-09-01 00:00:00'
AND o.create_time < '2025-09-02 00:00:00'
5.2 LIKE '%xxx%' 很难走普通 B+ 树索引
这一段也很常见:
u.username LIKE '%zhang%'
前面带 % 的模糊查询,普通索引通常帮不上太大忙。
如果业务允许,最好改成前缀匹配:
u.username LIKE 'zhang%'
如果业务必须支持“包含搜索”,那就要考虑别的方案,比如搜索引擎,或者接受它本身就是一个贵查询。
5.3 排序字段如果没有配合索引,也容易慢
ORDER BY o.create_time DESC
如果筛选条件和排序字段不能和索引设计配合起来,就很容易出现 Using filesort。
5.4 联表查询要看驱动表和过滤条件
有些 SQL 看起来慢是因为没索引,实际上慢在联表顺序不合理,或者过滤发生得太晚。
这也是为什么真实工作里优化 SQL 不能只看某一个点。
6. 我是怎么改的
6.1 先改 SQL 写法
第一步不是立刻建索引,而是先把明显不合理的写法改掉:
SELECT o.id, o.order_no, o.amount, o.status, o.create_time, u.username
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND o.create_time >= '2025-09-01 00:00:00'
AND o.create_time < '2025-09-02 00:00:00'
AND u.username LIKE 'zhang%'
ORDER BY o.create_time DESC
LIMIT 0, 20;
这里主要改了三点:
- 把
DATE_FORMAT改成时间范围查询 - 把
%zhang%改成zhang% - 明确只查需要的字段,不写
SELECT *
6.2 再补合适的索引
基于查询条件,比较自然的思路是给订单表建联合索引:
ALTER TABLE orders
ADD INDEX idx_status_ctime_uid (status, create_time, user_id);
给用户表补上用户名索引:
ALTER TABLE users
ADD INDEX idx_username (username);
这里要注意一个常见误区:不是字段出现在 WHERE 里,就一股脑全建单列索引。
真实项目里,联合索引往往比多个单列索引更有效,因为它更贴近 SQL 的实际筛选顺序。
6.3 优化前后我最关注什么
我会重点看这几个变化:
type有没有从ALL变成range或refkey有没有真的命中目标索引rows预估扫描行数有没有明显下降Extra里有没有减少Using filesort
如果这些指标没明显变好,通常说明优化方向还不对。
7. 为什么这次优化能生效
很多人写慢 SQL 优化文章,最后只给一句“加了索引,性能提升了”。
但真正值得你写进博客里的,是背后的原因。
这次优化本质上做了三件事:
7.1 让 MySQL 能用上索引
原来对 create_time 做函数处理,等于主动把索引优势削掉了。改成范围查询后,MySQL 才更容易走索引过滤。
7.2 让筛选和排序尽量顺着索引走
如果筛选字段和排序字段设计得合理,数据库就不需要先扫出一大堆数据,再额外做排序。
7.3 尽量减少无效扫描
慢 SQL 的核心问题很多时候不是“执行了一条 SQL”,而是“为了返回 20 条数据,扫描了几万甚至几十万条”。
当扫描行数降下来,性能提升通常就会比较明显。
8. 真实项目里,我不会只做这几步
上面的案例适合学习,也适合写博客。
但如果放到真实项目里,我一般还会继续看下面这些点:
8.1 这条 SQL 是“偶尔慢”,还是“高频慢”
如果它是后台页面偶尔查一次,那优化优先级没那么高。
但如果它是首页、大屏、定时任务或者高频接口里的核心 SQL,那就必须优先处理。
8.2 是 SQL 本身慢,还是业务把数据库用得太重
有些慢 SQL 表面上是查询问题,实际是接口设计问题。
比如:
- 页面一次初始化调了太多接口
- 一个接口里重复查相同数据
- 列表页带了过多联表字段
这时候只调 SQL,收益可能有限。
8.3 加索引有没有副作用
索引不是越多越好。
索引会带来:
- 写入成本上升
- 占用更多磁盘空间
- 维护成本增加
所以公司里一般不会因为一条慢 SQL 就随便往生产库上堆索引,通常还是要结合读写比例、表大小和调用频率一起评估。
9. 这类问题在面试里怎么答
如果面试官问你“你做过慢 SQL 优化吗”,不要一上来就说“我加了索引”。
更好的回答顺序是:
9.1 先讲定位方式
“我先通过慢查询日志定位到耗时高的 SQL,再用 EXPLAIN 看执行计划,重点关注扫描方式、命中的索引、扫描行数和额外排序情况。”
9.2 再讲分析思路
“我会先判断是索引缺失,还是 SQL 写法导致索引失效,比如函数操作、模糊查询、联表顺序不合理、排序分页代价高这些情况。”
9.3 最后讲优化动作
“我通常会从 SQL 改写和索引设计两个方向入手,比如把函数查询改成范围查询、给高频筛选字段建联合索引、减少不必要字段查询,再看执行计划和接口耗时有没有明显改善。”
如果你有真实经历,还可以顺手带一句:
“我在实习里做过类似优化,通过慢日志和 EXPLAIN 调整索引和查询方式,把数据库查询性能提升了大约 40%,某核心接口响应时间从 1.5s 降到了 900ms。”
这句话非常适合写在博客里,也非常适合写进面试回答里。
10. 我总结出的 5 条慢 SQL 排查经验
最后给大家总结 5 条我觉得最实用的经验:
10.1 先定位,再优化
没有慢日志、没有执行计划,优化很容易变成猜。
10.2 别迷信单列索引
很多场景下,联合索引比多个单列索引更有效。
10.3 警惕索引失效写法
像函数、计算、隐式类型转换、前导模糊匹配,都是高频坑点。
10.4 rows 很重要
一条 SQL 慢不慢,很多时候就看它为了返回结果到底扫了多少数据。
10.5 优化不是只盯数据库
有时候 SQL 只是表象,真正的问题在接口设计和业务调用方式。
11. 写在最后
慢 SQL 优化是一个特别适合写进博客,也特别适合拿去面试讲的话题。
因为它不像“背定义”那样空,能直接体现你有没有做过真实分析,有没有排查思路,能不能把数据库、SQL 和业务联系起来看。
如果你是校招生,我很建议你至少写一篇这类文章。哪怕你的案例不是特别复杂,只要你能把“怎么发现、怎么分析、怎么优化、优化后有什么结果”讲清楚,这篇文章就已经有价值了。
如果这篇文章对你有帮助,欢迎点个赞或者收藏。后面我也可以继续写一篇:
EXPLAIN 其实该怎么看联合索引为什么经常失效深分页为什么会拖慢接口
如果你也在准备 Java 面试,我们可以一起把这些高频问题吃透。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)