欢迎光临
我们一直在努力

SQL 优化实战总结:索引、深分页、JOIN、N+1 一次讲清

之前在实习中接触过一些 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 优化很难有一套固定答案。

很多时候还是要结合执行计划、数据量和具体业务场景去分析。

这篇文章主要也是把之前实际接触过的一些问题重新整理了一遍,算是对这部分内容的一次复盘。

赞(0)
未经允许不得转载:171主机测评 » SQL 优化实战总结:索引、深分页、JOIN、N+1 一次讲清
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址