最近在准备 Java 面试,发现慢 SQL 优化几乎是绕不过去的一类题。

但很多文章有个问题:要么全是概念,要么上来就是一堆索引术语,看完还是不知道真实项目里到底该怎么排查。

这篇文章我想换个写法,不背八股,直接用一个常见的后台列表接口来讲:

  • 慢 SQL 是怎么定位出来的
  • EXPLAIN 到底要看什么
  • 为什么有些 SQL 加了索引还是慢
  • 真实项目里慢 SQL 优化通常怎么做

文中的业务案例做了抽象处理,但思路来自我自己实习时做过的一次慢 SQL 优化。实际项目里,我结合慢查询日志和 EXPLAIN 分析执行计划,优化多条慢 SQL 的索引和查询方式,把数据库查询性能提升了大约 40%,某核心接口的响应时间从 1.5s 降到了 900ms。

如果你是校招生,或者刚工作一两年,这篇文章应该会比较适合你。


1. 先说结论:慢 SQL 优化不是“看见慢就加索引”

这是很多人最容易踩的坑。

慢 SQL 优化的正确顺序通常是:

  1. 先定位慢 SQL
  2. 再分析执行计划
  3. 找出真正的瓶颈点
  4. 最后决定是改 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 里有索引,但 keyNULL,说明索引根本没用上。

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 变成 rangeref
  • key 有没有真的命中目标索引
  • 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 面试,我们可以一起把这些高频问题吃透。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐