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

举报
yd_261707509 发表于 2026/08/30 08:45:29 2026/08/30
【摘要】 为什么你的SQL查询慢?这7个执行计划陷阱90%的人都踩过你有没有遇到过这种情况:明明给字段建了索引,执行计划却偏偏走全表扫描;SQL 在生产环境慢得离谱,在测试环境却飞快;加了 LIMIT 100,查询还是要跑十几秒……问题往往不在 SQL 本身,而在执行计划(Execution Plan)。执行计划是数据库优化器给出的"查询路线"。它一旦选错,再好的 SQL 也会跑成灾难。今天我们就来拆...

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

你有没有遇到过这种情况:
明明给字段建了索引,执行计划却偏偏走全表扫描;
SQL 在生产环境慢得离谱,在测试环境却飞快;
加了 LIMIT 100,查询还是要跑十几秒……
问题往往不在 SQL 本身,而在执行计划(Execution Plan)
执行计划是数据库优化器给出的"查询路线"。它一旦选错,再好的 SQL 也会跑成灾难。
今天我们就来拆解 7 个最常见的执行计划陷阱,每一个都可能让你的查询慢上十倍甚至百倍。

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

这是最经典、也最隐蔽的陷阱。
-- user_id 是 VARCHAR 类型,但查询时传了数字
SELECT * FROM orders WHERE user_id = 12345;
你以为走了索引?实际上数据库会做隐式类型转换:
-- 优化器实际执行的逻辑类似这样
SELECT * FROM orders WHERE CAST(user_id AS UNSIGNED) = 12345;
对索引列做了函数运算,索引直接失效,全表扫描。
怎么发现?看执行计划里有没有 Using index conditiontype: ALL
正确写法:
SELECT * FROM orders WHERE user_id = '12345';
排查技巧:​ 执行 EXPLAIN 后,关注 type 列。如果是 ALL(全表扫描)或 index(全索引扫描),而你的 possible_keys 里有可用索引,大概率是类型不匹配。

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

SELECT * FROM orders
WHERE user_id = '12345'
   OR status = 'pending';
user_id 有索引,status 也有索引。你觉得优化器会怎么选?
答案是:它可能两个都不用,直接全表扫描。
原因是 MySQL 的优化器在面对 OR 条件时,如果涉及多个不同字段,很难合并两个索引的范围扫描,往往会选择代价最低的全表扫描。
优化方案:用 UNION 拆开
SELECT * FROM orders WHERE user_id = '12345'
UNION
SELECT * FROM orders WHERE status = 'pending';
这样每个子查询都能独立使用各自的索引,性能差距可能从 几秒到几毫秒

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

MySQL 8.0 引入了 Index Skip Scan,听起来很美好——当联合索引的前导列不在 WHERE 条件中时,优化器可以"跳过"它。
但现实很骨感:
-- 联合索引是 (gender, age)
SELECT * FROM users WHERE age = 25;
优化器确实可能用 Skip Scan,但它的原理是 按前导列分组遍历。如果 gender 的基数很低(比如只有 M/F),那还行;但如果前导列基数高,Skip Scan 的开销可能比全表扫描还大。
正确做法:调整索引顺序,把高区分度的列放前面
-- 改为 (age, gender)
CREATE INDEX idx_age_gender ON users(age, gender);

陷阱四:LIMIT 的"伪优化"

很多人以为加了 LIMIT 查询就会快:
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100;
如果 created_at 没有索引,数据库会:
  1. 全表扫描
  2. 把所有数据排序
  3. 取前 100 条
排序(filesort)的开销可能比扫描本身还大。
优化方案:给排序列建索引
CREATE INDEX idx_created_at ON orders(created_at);
这样优化器可以直接走索引的倒序扫描,取满 100 条就停,根本不需要排序。

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

这是生产环境最常见的"灵异事件":
同样的 SQL,昨天还跑得好好的,今天突然慢了。
原因很可能是 统计信息过期
当表的数据分布发生剧烈变化(比如一次性删除了大量数据、批量插入),优化器的统计信息没有及时更新,导致它错误地认为:
  • 某个条件能过滤掉 99% 的数据(实际只过滤了 10%)
  • 某个索引的区分度很高(实际已经很低)
结果就是选了一个看似便宜、实际很贵的执行计划。
解决方式:
-- MySQL
ANALYZE TABLE orders;

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

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

SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
这行 SQL 的问题在于:数据库必须先读取并丢弃前 1,000,000 条记录
即使走了索引,它也要遍历索引的前 100 万条,然后才能拿到你要的 20 条。
优化方案:游标分页(Keyset Pagination)
-- 假设上次查到的最后一条 id 是 1000000
SELECT * FROM orders
WHERE id > 1000000
ORDER BY id
LIMIT 20;
这样优化器可以直接从 id = 1000000 的位置开始扫描,跳过前面的所有数据,性能提升是数量级的。

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

SELECT * FROM orders
WHERE user_id IN (
    SELECT user_id FROM vip_users WHERE level > 5
);
在某些 MySQL 版本中,优化器会把子查询 物化(Materialize)​ 成一个临时表,然后再做 JOIN。如果 vip_users 数据量大,这个临时表会非常大,而且无法使用索引。
优化方案:改写成 JOIN
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 一下。看看它到底打算怎么跑,和实际预期是否一致。大多数性能问题,在执行计划里都写得清清楚楚。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。