之前在实习中接触过一些 SQL 优化相关的工作,当时也实际排查和处理过一些慢查询。隔了一段时间重新整理一遍,反而理解得更清楚了一些。
之前学习 SQL 优化时,接触比较多的是一些常见规则,比如:
- 尽量使用索引
- 避免不必要的全表扫描
- 注意联合索引的使用
- 尽量减少扫描的数据量
这些原则本身没有问题,但真正放到具体 SQL 里,情况往往没有这么简单。
比如有时候明明走了索引,查询还是比较慢;有时候测试数据量不大时没什么问题,数据量上来之后性能差距就比较明显。
所以这篇文章主要想结合之前实际接触过的一些场景,整理一下自己对 SQL 优化的理解。
主要涉及:
索引
执行计划
深分页
JOIN
N+1
SQL 调用次数
一、遇到慢 SQL,先看执行计划
遇到 SQL 性能问题时,比起直接修改 SQL 或者增加索引,我觉得先看看执行计划会更合适。
常用的是:
EXPLAIN SELECT ...
MySQL 8.0 也可以使用:
EXPLAIN ANALYZE SELECT ...
平时主要可以关注:
type
key
rows
Extra
其中 rows 是一个比较直观的参考。
比如一条 SQL 最后只返回几十条数据,但执行过程中预计需要扫描几十万甚至几百万行,那通常就值得继续往下排查。
所以对 SQL 优化,我目前比较直观的一个理解是:
尽量减少数据库需要扫描和处理的数据量。
当然,执行计划显示使用了索引,也不能直接说明这条 SQL 就没有优化空间。
二、有索引,不一定代表索引合适
之前排查 SQL 时遇到过一种情况:
表上的索引其实不少,但查询性能依然不太理想。
继续分析后会发现,问题并不一定是“没有索引”,而可能是:
现有索引和实际查询方式并不匹配。
例如有一个联合索引:
INDEX(user_id, status, create_time)
查询是:
SELECT id, order_no, status, create_time
FROM orders
WHERE user_id = 10001
AND status = 1
ORDER BY create_time DESC
LIMIT 20;
这种情况下,索引字段和查询条件整体比较匹配。
但如果实际查询主要是:
WHERE status = 1
前面的联合索引就不一定能很好地发挥作用。
所以联合索引不能只看:
哪些字段经常出现在 WHERE 中
还需要结合具体 SQL:
WHERE 条件是什么
ORDER BY 如何排序
有没有 JOIN
查询频率怎么样
数据量有多大
再决定索引应该怎么设计。
简单来说:
索引还是要尽量围绕实际查询场景来设计。
三、走了索引,为什么还是会慢?
一开始比较容易有一个误区:
SQL 只要走索引,性能应该就不会太差。
实际上并不一定。
例如:
SELECT *
FROM orders
WHERE status = 1;
假设 status 上已经有索引。
整张表有 1000 万条数据:
status = 0 100万
status = 1 700万
status = 2 200万
查询:
WHERE status = 1
即使使用了 status 索引,最终可能还是需要处理大量数据。
因为这个字段本身的区分度比较低,索引并没有过滤掉太多数据。
像:
订单号
手机号
用户ID
业务唯一ID
通常区分度比较高。
而:
状态
性别
是否删除
是否启用
这类字段区分度通常会低一些。
不过这也不代表低区分度字段一定不能放进索引。
例如:
INDEX(user_id, status, create_time)
这里的 status 作为联合索引的一部分,在特定查询场景下完全可能是合理的。
所以是否需要索引、索引怎么设计,还是需要结合具体 SQL 和数据分布来看。
四、几种比较常见的索引使用问题
之前排查过程中,下面几种情况相对比较常见。
1. 对索引字段进行函数计算
例如:
WHERE DATE(create_time) = '2026-08-01'
如果 create_time 本身有索引,这种写法就需要注意。
一般可以改成范围查询:
WHERE create_time >= '2026-08-01 00:00:00'
AND create_time < '2026-08-02 00:00:00'
类似的还有:
YEAR(create_time)
LEFT(phone, 3)
对于数据量比较大的查询,这类写法都值得多看一眼。
2. 隐式类型转换
例如数据库字段是:
phone VARCHAR(20)
查询却写成:
WHERE phone = 13800138000
而不是:
WHERE phone = '13800138000'
这种类型不一致的问题比较容易被忽略,也可能影响索引的使用方式。
3. 前置模糊查询
比如:
WHERE name LIKE '%张三%'
这种查询普通 B+Tree 索引一般比较难发挥作用。
如果业务本身有大量模糊搜索需求,继续在普通索引上调整可能也不是最合适的方案。
可以结合实际场景考虑:
全文索引
Elasticsearch
OpenSearch
之类更适合搜索的方案。
所以 SQL 优化有时候不只是修改 SQL,也要考虑:
这个查询需求本身是否适合直接交给关系型数据库处理。
五、深分页问题
深分页也是比较典型的一类性能问题。
例如:
SELECT id, order_no, create_time
FROM orders
ORDER BY id
LIMIT 1000000, 20;
数据量比较小时可能看不出明显差别。
但 OFFSET 越大,需要跳过的数据也越多,查询耗时就可能逐渐增加。
如果业务场景允许,可以考虑使用游标分页。
例如:
SELECT id, order_no, create_time
FROM orders
WHERE id > 9527
ORDER BY id
LIMIT 20;
其中 9527 是上一页最后一条数据的 ID。
相比深 OFFSET,这种方式不需要每次都跳过前面的大量数据,在数据量较大的情况下通常会稳定一些。
当然,游标分页也有自己的限制。
比如它不太适合:
直接跳到第 1000 页
所以具体使用哪种分页方式,还是要看业务场景。
后台管理系统通常需要页码跳转,而信息流、评论列表、滚动加载一类场景,用游标分页可能会更合适。
六、JOIN 优化,先看数据量和索引
JOIN 查询出现性能问题时,可以先关注几个比较直接的地方:
JOIN 字段有没有索引
参与 JOIN 的数据量有多大
过滤条件能不能提前缩小数据范围
例如:
SELECT o.id, o.order_no
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.level = 5
AND u.status = 1;
这条 SQL 可以重点关注:
users 经过过滤后还剩多少数据
orders.user_id 是否有合适的索引
执行过程中预计扫描多少行
相比一开始就调整 JOIN 顺序,我觉得先把数据量、索引和过滤条件确认清楚会更直观一些。
很多 JOIN 性能问题,最后还是离不开几个因素:
数据量
过滤效果
索引
执行计划
七、有时候问题并不在单条 SQL
这一点也是之前排查性能问题时印象比较深的地方。
假设一条 SQL 执行只需要:
3ms
单独看其实很快。
但是如果一个请求里面执行了 300 次,最终耗时依然不会低。
所以除了单条 SQL 的耗时,还需要关注:
一次请求到底执行了多少条 SQL。
这里比较典型的就是 N+1 查询。
例如先查询 100 个订单:
SELECT id, user_id, order_no
FROM orders
LIMIT 100;
之后代码再根据每个订单的 user_id 单独查询用户信息。
最终就可能变成:
1 次订单查询
+
100 次用户查询
=
101 次 SQL
这些 SQL 单独看可能都不慢,但数据库交互次数比较多。
这种场景可以根据实际情况考虑:
JOIN
批量 IN 查询
一次查询后在代码中组装
尽量减少数据库访问次数。
八、批量操作也值得注意
和 N+1 类似,有些性能问题不完全是 SQL 写得慢,而是调用方式不太合适。
例如需要插入 1000 条数据。
如果循环执行:
INSERT ...
INSERT ...
INSERT ...
会产生大量数据库交互。
一般可以考虑批量 INSERT,或者使用数据库驱动提供的 batch 能力。
例如:
INSERT INTO user(name)
VALUES
('A'),
('B'),
('C');
相比逐条执行,批量处理通常可以减少网络交互和 SQL 执行次数。
所以看数据库性能时,我觉得除了单条 SQL 耗时,SQL 的执行次数也值得关注。
九、索引也不是越多越好
刚接触 SQL 优化时,很容易把“增加索引”看成最直接的解决方式。
但索引本身也是有成本的。
比如一张表上逐渐出现:
user_id
status
create_time
user_id + status
user_id + create_time
status + create_time
如果针对每一种查询不断增加索引,最后索引数量可能会越来越多。
索引虽然能够提升部分查询效率,但 INSERT、UPDATE、DELETE 时同样需要维护索引,而且索引本身也会占用存储空间。
所以索引比较多时,也可以看看:
是否存在重复索引
是否存在高度重合的联合索引
是否有基本没有使用的索引
增加索引之前,最好能够明确:
这个索引具体是在解决哪一条或者哪一类 SQL。
十、整理下来,我觉得可以按照这个思路排查
如果遇到 SQL 性能问题,可以大致按照下面几个方向来看。
1. 先确认瓶颈是不是真的在数据库
接口慢,并不意味着 SQL 一定慢。
可以结合:
APM
慢查询日志
接口日志
数据库监控
先确认具体耗时点。
2. 看 SQL 执行次数
不要只关注单条 SQL 的耗时。
有时候:
100 次 × 5ms
比一条偶发的慢 SQL 更值得处理。
3. 看 EXPLAIN / EXPLAIN ANALYZE
重点关注:
使用了什么索引
预计或实际扫描多少数据
JOIN 怎么执行
有没有额外排序
有没有临时表
如果扫描数据量明显大于最终返回的数据,就可以继续分析索引和过滤条件。
4. 检查索引和查询是否匹配
结合:
WHERE
JOIN
ORDER BY
GROUP BY
看看现有索引是否符合实际查询方式。
5. 尽量减少扫描的数据量
例如原本:
扫描 100 万行
最终返回 20 行
优化以后变成:
扫描几十行
最终返回 20 行
这种变化通常比较能说明优化是否有效。
6. SQL 本身已经比较快,再看调用方式
例如:
有没有 N+1
能不能批量查询
有没有重复查询
能不能减少数据库调用
是否适合使用缓存
这一部分其实已经不完全属于 SQL 本身,而是接口和系统层面的性能问题了。
十一、最后的一些理解
重新整理这些内容之后,我觉得 SQL 优化里很多规则都不太适合直接套用。
比如:
IN 一定慢
OR 一定不能用
JOIN 一定比子查询快
Using filesort 一定有问题
全表扫描一定需要优化
这些说法都比较绝对。
实际情况还是和数据量、数据分布、索引以及执行计划有关。
例如一张只有几百条数据的小表,即使全表扫描,实际开销可能也并不大。
WHERE id IN (1, 2, 3, 4, 5)
这种查询,也没有必要仅仅因为使用了 IN 就一定修改。
所以我觉得 SQL 优化更重要的是:
先看实际执行情况,再判断问题在哪里。
如果简单归纳一下,可以先关注三个问题:
扫描了多少数据?
一共执行了多少次 SQL?
实际耗时主要在哪里?
再根据具体问题选择:
增加或调整索引
修改 SQL
优化分页方式
减少 JOIN 的数据量
解决 N+1
减少数据库调用次数
SQL 优化很难有一套固定答案。
很多时候还是要结合执行计划、数据量和具体业务场景去分析。
这篇文章主要也是把之前实际接触过的一些问题重新整理了一遍,算是对这部分内容的一次复盘。





