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

总结:一张表帮你自查


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

最后说一句

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

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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