为什么你的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 condition 或 type: 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 可以让优化器选择更好的连接顺序和索引策略,避免物化带来的额外开销。


总结:一张表帮你自查

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

最后说一句

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

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

相关推荐
现任明教教主~1 小时前
SpringBoot+Vue学生宿舍管理系统源码 带部署文档
vue.js·spring boot·后端
码事漫谈8 小时前
骂AI,会让它做得更好吗?
后端
陈随易8 小时前
bm2,Node.js 与 Bun 项目部署新选择
前端·后端·程序员
架构技术专栏9 小时前
AI Agent 框架怎么选:从一次制度查询拆出技术边界
后端·面试
明月_清风12 小时前
SaaS 的三层挣钱逻辑,正在被 AI 从第一层击穿
人工智能·后端
谁在黄金彼岸13 小时前
nginx服务化五套方案原理解剖与选型
后端
谁在黄金彼岸13 小时前
WinSW在Win7上失败真相-实测与修复
后端
花间相见13 小时前
【后端开发|Redis进阶01】—— Redis分布式锁全解:从SETNX到Redisson看门狗,再到RedLock
后端·面试
llqbzllll13 小时前
A2A 1.0 到底和 MCP 差在哪:从 Agent Card、Task 到 Java 最小互操作
后端
花间相见13 小时前
【后端开发|JVM基础01】—— Java垃圾回收全解:从分代内存模型到各收集器的分代职责
后端