之前在实习中接触过一些 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 优化很难有一套固定答案。
很多时候还是要结合执行计划、数据量和具体业务场景去分析。
这篇文章主要也是把之前实际接触过的一些问题重新整理了一遍,算是对这部分内容的一次复盘。
总结
到此这篇关于sql优化实战总结之一次讲清索引、深分页、join、n+1的文章就介绍到这了,更多相关sql优化索引、深分页、join、n+1内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论