为什么你的SQL查询慢?这7个执行计划陷阱90%的人都踩过

为什么你的SQL查询慢?这7个执行计划陷阱90%的人都踩过

你有没有遇到过这种情况:

明明给字段建了索引,执行计划却偏偏走全表扫描;

SQL 在生产环境慢得离谱,在测试环境却飞快;

加了 LIMIT 100,查询还是要跑十几秒......

问题往往不在 SQL 本身,而在执行计划(Execution Plan)

执行计划是数据库优化器给出的"查询路线"。它一旦选错,再好的 SQL 也会跑成灾难。

今天我们就来拆解 7 个最常见的执行计划陷阱,每一个都可能让你的查询慢上十倍甚至百倍。


陷阱一:隐式类型转换,索引直接失效

这是最经典、也最隐蔽的陷阱。

sql 复制代码
-- user_id 是 VARCHAR 类型,但查询时传了数字
SELECT * FROM orders WHERE user_id = 12345;

你以为走了索引?实际上数据库会做隐式类型转换:

sql 复制代码
-- 优化器实际执行的逻辑类似这样
SELECT * FROM orders WHERE CAST(user_id AS UNSIGNED) = 12345;

对索引列做了函数运算,索引直接失效,全表扫描。

怎么发现?看执行计划里有没有 Using index conditiontype: ALL

正确写法:

ini 复制代码
SELECT * FROM orders WHERE user_id = '12345';

排查技巧: ​ 执行 EXPLAIN 后,关注 type 列。如果是 ALL(全表扫描)或 index(全索引扫描),而你的 possible_keys 里有可用索引,大概率是类型不匹配。


陷阱二:OR 条件让优化器"摆烂"

ini 复制代码
SELECT * FROM orders
WHERE user_id = '12345'
   OR status = 'pending';

user_id 有索引,status 也有索引。你觉得优化器会怎么选?

答案是:它可能两个都不用,直接全表扫描。

原因是 MySQL 的优化器在面对 OR 条件时,如果涉及多个不同字段,很难合并两个索引的范围扫描,往往会选择代价最低的全表扫描。

优化方案:用 UNION 拆开

sql 复制代码
SELECT * FROM orders WHERE user_id = '12345'
UNION
SELECT * FROM orders WHERE status = 'pending';

这样每个子查询都能独立使用各自的索引,性能差距可能从 几秒到几毫秒


陷阱三:索引跳跃扫描(Index Skip Scan)的假象

MySQL 8.0 引入了 Index Skip Scan,听起来很美好------当联合索引的前导列不在 WHERE 条件中时,优化器可以"跳过"它。

但现实很骨感:

sql 复制代码
-- 联合索引是 (gender, age)
SELECT * FROM users WHERE age = 25;

优化器确实可能用 Skip Scan,但它的原理是 按前导列分组遍历 。如果 gender 的基数很低(比如只有 M/F),那还行;但如果前导列基数高,Skip Scan 的开销可能比全表扫描还大。

正确做法:调整索引顺序,把高区分度的列放前面

scss 复制代码
-- 改为 (age, gender)
CREATE INDEX idx_age_gender ON users(age, gender);

陷阱四:LIMIT 的"伪优化"

很多人以为加了 LIMIT 查询就会快:

sql 复制代码
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100;

如果 created_at 没有索引,数据库会:

  1. 全表扫描
  2. 把所有数据排序
  3. 取前 100 条

排序(filesort)的开销可能比扫描本身还大。

优化方案:给排序列建索引

scss 复制代码
CREATE INDEX idx_created_at ON orders(created_at);

这样优化器可以直接走索引的倒序扫描,取满 100 条就停,根本不需要排序。


陷阱五:统计信息过期,优化器"瞎选"

这是生产环境最常见的"灵异事件":

同样的 SQL,昨天还跑得好好的,今天突然慢了。

原因很可能是 统计信息过期

当表的数据分布发生剧烈变化(比如一次性删除了大量数据、批量插入),优化器的统计信息没有及时更新,导致它错误地认为:

  • 某个条件能过滤掉 99% 的数据(实际只过滤了 10%)
  • 某个索引的区分度很高(实际已经很低)

结果就是选了一个看似便宜、实际很贵的执行计划。

解决方式:

sql 复制代码
-- MySQL
ANALYZE TABLE orders;

-- PostgreSQL
ANALYZE orders;

另外,对于 MySQL 的 InnoDB,注意 innodb_stats_auto_recalc 是否开启。如果关闭了,统计信息不会自动更新。


陷阱六:深分页的" OFFSET 陷阱"

vbnet 复制代码
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;

这行 SQL 的问题在于:数据库必须先读取并丢弃前 1,000,000 条记录

即使走了索引,它也要遍历索引的前 100 万条,然后才能拿到你要的 20 条。

优化方案:游标分页(Keyset Pagination)

sql 复制代码
-- 假设上次查到的最后一条 id 是 1000000
SELECT * FROM orders
WHERE id > 1000000
ORDER BY id
LIMIT 20;

这样优化器可以直接从 id = 1000000 的位置开始扫描,跳过前面的所有数据,性能提升是数量级的。


陷阱七:子查询被"物化",临时表爆炸

sql 复制代码
SELECT * FROM orders
WHERE user_id IN (
    SELECT user_id FROM vip_users WHERE level > 5
);

在某些 MySQL 版本中,优化器会把子查询 物化(Materialize) ​ 成一个临时表,然后再做 JOIN。如果 vip_users 数据量大,这个临时表会非常大,而且无法使用索引。

优化方案:改写成 JOIN

sql 复制代码
SELECT o.*
FROM orders o
INNER JOIN vip_users v ON o.user_id = v.user_id
WHERE v.level > 5;

JOIN 可以让优化器选择更好的连接顺序和索引策略,避免物化带来的额外开销。


总结:一张表帮你自查

陷阱 核心问题 自查关键词
隐式类型转换 索引列被函数包裹 EXPLAINtype: ALL
OR 条件 多列 OR 导致索引失效 改写为 UNION
索引跳跃扫描 前导列基数高,Skip Scan 代价大 调整联合索引列顺序
LIMIT 无索引 filesort 排序开销大 EXPLAINUsing filesort
统计信息过期 优化器基于错误数据做决策 定期 ANALYZE TABLE
深分页 OFFSET 扫描并丢弃大量前置数据 改用游标分页
子查询物化 临时表无法走索引 改写为 JOIN

最后说一句

优化 SQL 的核心不是背规则,而是 看懂执行计划

养成习惯:写完 SQL,先 EXPLAIN 一下。看看它到底打算怎么跑,和实际预期是否一致。大多数性能问题,在执行计划里都写得清清楚楚。

相关推荐
lichenyang45336 分钟前
从「房间」到「实时通知」:用 NestJS + Socket.IO 实现团队邀请的完整工程实践
前端·后端
沙盘客1 小时前
AFSIM 示例解读(09)· 传感器全家桶 sensor_demos(下):ESM / SAR / 被动测向
c++·经验分享·后端
叫我Paul就好1 小时前
Spring 为何没有在 Java之外的地方存在?
java·后端·spring
掘金酱1 小时前
TRAE Work 实战帮征文 | 获奖名单公示
前端·人工智能·后端
用户69371750013842 小时前
前阵子刷屏的 Ox-Alpha 真身揭晓!GLM-5.3-Flash 上线,这价格真的杀疯了
前端·后端·github
四千岁2 小时前
抓包LangChain-了解LangChain的原理
前端·javascript·后端
泡海椒2 小时前
JQuick-Curl 快速上手:Maven 依赖与第一个 HelloWorld 请求
后端
QQ_21696290962 小时前
【源码编号:project86570】SpringBoot电影院在线选座售票系统:电影排片、在线选座、订单购票、后台管理完整实战
java·spring boot·后端
卷无止境2 小时前
FastAPI 实现 SSO,从协议原理到生产级落地
后端·python·fastapi