☰
深入理解MySQL子查询:执行顺序、索引与性能优化
2026/10/21 16:04:55 网站建设 项目流程

先交代结论:子查询不难,难的是大多数人把“能跑出结果”当成了“理解原理”。这篇我不打算做语法手册逐条罗列,而是按我自己的理解方式,把 MySQL 子查询从“是什么”到“为什么这么写”,再到“什么时候该打散重写”,全部串起来讲一遍。适合正在准备面试的开发同学、写业务 SQL 偶尔卡壳的后端,以及想搞明白 EXPLAIN 输出里那几个奇怪字段的人。

1. 从执行顺序开始理解子查询

很多人在子查询上栽跟头,不是因为不知道语法,而是因为搞不清楚子查询里面的表数据和外层查询到底是什么关系。MySQL 处理一条 SQL 的时候,执行顺序和书写顺序完全不是一回事。先记住这个顺序:FROM 子句先执行,然后是 WHERE、GROUP BY、HAVING,最后是 SELECT 和 ORDER BY。也就是说,普通查询里 WHERE 中引用的列,来自 FROM 已经确定好的表集合,这是“拿一张表的数据按照条件去过滤”的思维模型。

但子查询加入之后,这个顺序会变得复杂。最常见的误区是:以为子查询是“先执行完得到一张临时表,再拿这张表去做外层匹配”。这种理解对 FROM 子句里的表子查询是对的,但对 WHERE 子句里的条件子查询并不完全正确,因为 MySQL 有大量优化策略,会把子查询改写、展开、半连接化,实际执行路径往往和你写出来的逻辑结构已经不一样了。

我个人建议初学者换一种模型来理解:把子查询看成“一种依赖关系”。外层查询每准备处理一行数据时,如果碰到了子查询,就要看看这个子查询是否和外层这行数据相关。如果相关,就带着外层的条件去查一次;如果完全不相关,MySQL 会优先把它计算成常量。这种依赖视角能帮你解释为什么有些子查询慢,也能解释为什么加一个索引之后性能突飞猛进。

先建立一个最基本的分类框架,后面每个类型我会单独展开:

  • 标量子查询:返回一行一列,只有单个值。
  • 行子查询:返回一行多列。
  • 表子查询:返回多行多列,经常用在 FROM 里当派生表。
  • 条件子查询:用 IN、EXISTS、ANY、ALL 等做条件判断。

分类不是目的,分类是为了判断“这个子查询会执行多少次、能不能被优化、能不能被改写”。

2. 标量子查询:最常用也最容易踩坑

标量子查询的意思是:子查询的结果是单个值。它可以出现在任何期望一个表达式的位置,比如 SELECT 列表、WHERE 比较条件、ORDER BY 排序,甚至 HAVING 条件。

举个业务里非常常见的场景。用户表里有用户的注册时间,订单表里有下单记录,你现在要查“每个用户最近一次下单的金额”。很多人的第一反应是 JOIN 订单表然后取 MAX,但如果你对聚合和分组不熟,用标量子查询反而更直观:

SELECT u.user_id, u.user_name, (SELECT o.amount FROM orders o WHERE o.user_id = u.user_id ORDER BY o.create_time DESC LIMIT 1) AS last_amount FROM users u;

这段 SQL 的核心逻辑是:对外层 users 表的每一行,MySQL 都去 orders 表找到属于这个用户的最新一条订单,取出金额。这个逻辑非常直白,写起来没有理解成本。但代价是:如果 users 表有 1000 行,子查询理论上就要跑 1000 次,这就是所谓的逐行执行。

这里有几个必须注意的细节:

  • 标量子查询只能返回一行一列,如果返回多行,MySQL 直接报错 ER_SUBQUERY_NO_1_ROW。
  • 返回 NULL 是允许的,不报错,但外层会拿 NULL 参与运算,这个坑很隐蔽。
  • 有没有索引,性能差别可能到几百倍。上面例子里的 orders 表必须要有 (user_id, create_time) 联合索引,才能让 ORDER BY + LIMIT 1 走索引快速定位。

关于 NULL 的坑,我今天多说一句。你在 SELECT 列表里写一个返回 NULL 的标量子查询,结果只是显示 NULL,问题不大。但如果写的是WHERE amount = (SELECT ...),这时 outer 这一行永远过滤不掉,因为任何值和 NULL 比较都是“未知”,不是 true,也不是 false。这个逻辑很多人误解,总以为过滤条件是“不等于就筛掉”,数据库里实际的处理是“NULL 比较不成立就筛掉”。

排查这个问题有个体验很明显的做法:把条件从=改成IS NOT DISTINCT FROM(MySQL 8.0 支持),或者改用 JOIN 配合空值判断。

标量子查询的性能模型可以用一句话总结:外层每行执行一次子查询,子查询本身是否高效取决于能不能通过索引快速收敛。所以写之前先问自己:这个子查询条件会不会命中索引?如果命中不了,就要考虑改写。

3. IN 子查询是否真的慢

IN 子查询是业务代码中出现频率最高的子查询类型。举个例子,查“在最近 7 天内有订单的用户信息”:

SELECT user_id, user_name FROM users WHERE user_id IN (SELECT user_id FROM orders WHERE create_time >= NOW() - INTERVAL 7 DAY);

很多人下意识觉得 IN 子查询一定比 JOIN 慢,这个说法太绝对了。MySQL 对 IN 子查询的优化策略在不同的版本里有明显差异。MySQL 5.5 时代,IN 子查询确实经常拉胯,主要问题在于 MySQL 会物化整个子查询结果,再拿外层逐行去匹配,而且当时的物化没有索引,等于双重全表扫描。但到了 MySQL 5.6 之后引入半连接优化,再到 5.7 和 8.0,IN 子查询的执行计划经常被改写成 semijoin,实际执行效率和 JOIN 已经非常接近。

什么叫半连接优化?简单解释一下。普通的 JOIN 如果一张用户有 3 张订单,用户会被关联出 3 行,最终结果里会出现重复用户。IN 子查询的逻辑本质上只需要判断“存不存在”,不关心匹配了几次,因此 MySQL 会对这种查询做去重语义的 JOIN,只取一张订单匹配就停下,这就是 semi join 的核心。

有一类 IN 子查询确实需要注意,就是子查询里带 LIMIT。MySQL 的优化器碰到 LIMIT 子查询时,往往没法把它改写成半连接,只能退化成逐行执行,复杂情况下性能会很差。比如:

SELECT * FROM users WHERE user_id IN (SELECT user_id FROM orders ORDER BY create_time DESC LIMIT 10);

这种写法语义上是可以理解的:先找出最近下单的 10 个用户,再查这些用户信息。但实际上 MySQL 执行起来非常拧巴,它不太容易把这个 LIMIT 小结果集转化为高效查询,往往需要物化。如果你追求性能,更好的写法是先把那 10 个用户查出来,然后外层用 JOIN 关联,或者直接拆成两条 SQL 在业务代码里拼一下。

关于 NOT IN 的坑要单独说。子查询结果集里只要有 NULL,NOT IN 的结果就是空集,一条数据都不会返回。这个非常让人意外,但就是 NULL 三值逻辑导致的必然结果。举个具体例子:

SELECT * FROM users WHERE user_id NOT IN (SELECT user_id FROM orders WHERE status = 'cancelled');

如果 orders 表里有任何一行的 user_id 是 NULL,整个查询结果就是空。因为“user_id 不等于某个值”和“user_id 不等于 NULL”一起判断时,NULL 的比较结果不是真也不是假,是“未知”,而 WHERE 只保留“真”的行。所以只要有一个 NULL,这个查询就废了。

解决方式有两种,一是改写为 NOT EXISTS,二是把子查询加一层WHERE user_id IS NOT NULL过滤。对于只需要判断不存在的业务,我都会直接建议换成 NOT EXISTS,因为语义不会有重复行问题,也不会有隐式去重开销。

4. EXISTS 与相关子查询的执行细节

EXISTS 子查询的写法比较特殊,它不关心子查询返回什么列,只看子查询有没有返回行。MySQL 优化器对 EXISTS 的处理也很有意思,只要子查询的 SELECT 列表里写的是常量或者 1,它就会忽略这个选择列表,只做存在性判断。

先看典型写法:

SELECT u.user_id, u.user_name FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND o.amount > 1000 );

这里面 EXISTS 子查询引用了外层表 u 的字段,这种叫相关子查询。执行逻辑稍微绕一点:外层每取一行,就带着这一行的 user_id 去 orders 表查有没有匹配的记录,匹配到一条就算 EXISTS 为真,不再继续扫描同一用户的后面记录。

这里有个性能常识要建立起来:相关子查询不一定是坏事。如果 orders 表上有 (user_id, amount) 联合索引,这种 EXISTS 写法往往非常快,因为它本质上变成了一个索引查找,还有提前停止的效果。反过来,如果 orders 表没索引,每行用户都要全表扫一次 orders,那就会非常慢。

实战中什么时候坚持用 EXISTS 而不是 IN?我总结三点经验:

  • 子查询涉及多表关联,结果集有重复风险时,EXISTS 语义更稳。
  • 外层表小、内层表大时,EXISTS 通常更优。先扫描外层每行,然后内层通过索引精确匹配,性能可控。
  • 需要判断“存在但不关心具体值”,EXISTS 直接省掉物化开销。

反过来如果是外层表大、内层表小,IN 的物化策略会让内层结果先算成一个小临时表,再走索引关联,可能会更优。当然这只是经验方向,实际还是要用 EXPLAIN 验证。

再强调一个相关子查询容易犯的错:关联条件写漏。很多人写 EXISTS 时,只顾着写内层的过滤条件,忘记了把外层表相关的关联字段放在 WHERE 里,结果子查询变成完全独立的查询。这时候 EXISTS 结果恒为真,查询就变成了全表返回,数据行数瞬间爆炸。

5. 行子查询与元组比较

行子查询返回一行多列,MySQL 支持用元组比较一次比较多个列。这个功能很多人不熟悉,但某些场景下确实有用。

比如订单表里有一个逻辑:查询“下单时间和金额都和某笔标杆订单相同的订单”。普通写法需要两个独立条件:

SELECT * FROM orders WHERE create_time = (SELECT create_time FROM orders WHERE order_id = 100) AND amount = (SELECT amount FROM orders WHERE order_id = 100);

用行子查询可以合并成一个条件:

SELECT * FROM orders WHERE (create_time, amount) = (SELECT create_time, amount FROM orders WHERE order_id = 100);

这种写法更紧凑,但实际执行效率不一定比两个独立标量子查询更高,因为优化器需要额外处理元组比较。如果你要查的是标杆订单本身,直接用 JOIN 更清晰。行子查询的真正价值在于:当多个列之间同时具备业务约束,写在一起能表达更强的语义。

不过要谨慎一点:MySQL 的行比较表达式在索引匹配上的优化能力比较弱。两个普通标量子查询各走各的索引,反而可能更好。所以行子查询适合业务逻辑上强相关的列,不适合为了凑语法而硬写。

6. FROM 子句派生表与 LATERAL 关联

写在 FROM 子句里的子查询叫派生表,这是另一种理解模型。它是把子查询的结果先算成一张临时的结果集合,然后当作一个普通表参与外层查询。MySQL 对派生表的处理有两种方式:合并(merge)和物化(materialize)。

MySQL 5.7 之前,很多派生表会被直接物化成临时表,这就造成了性能瓶颈。后来优化器对简单的派生表会优先做合并处理,即把派生表 SQL 合并到外层查询中,直接基于底层表做过滤。判断方式是看执行计划里 Derived 表的访问类型有没有被优化器展开。如果派生表里有 LIMIT、ORDER BY、聚合函数这类操作,合并往往失效,此时就会物化。

我在具体项目里踩过的典型坑是派生表排序失效。有人写:

SELECT * FROM (SELECT * FROM orders ORDER BY create_time DESC) t GROUP BY t.user_id;

直觉上会觉得:先按时间排序,再分组,分组取的就是每个用户最近的订单。但 MySQL 执行时,优化器可能认为子查询里的 ORDER BY 是无意义的,因为在物化派生表时排序并不能保证外层 GROUP BY 的有序性,直接就把排序丢掉了。这是一类比较经典的“逻辑推理被优化器打破”的问题。

解决方式通常是把语义表达得更明确,比如用窗口函数:

SELECT user_id, amount, create_time FROM ( SELECT user_id, amount, create_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) rn FROM orders ) t WHERE rn = 1;

这个写法是明确告诉 MySQL:每个用户取排名第一的记录。窗口函数在 8.0 里优化得比较好,比依赖派生表排序再聚合的写法稳定得多。

如果你的 MySQL 是 8.0.14 以上版本,还能用 LATERAL 派生表。这个特性解决的核心问题是:派生表能不能引用外层表的列。正常情况下派生表是独立的,无法引用外层查询的列,而 LATERAL 允许子查询像相关子查询一样依赖外层数据。

举个例子,每个用户查最近三条订单:

SELECT u.user_id, u.user_name, recent_orders.order_id, recent_orders.amount FROM users u, LATERAL ( SELECT order_id, amount FROM orders o WHERE o.user_id = u.user_id ORDER BY create_time DESC LIMIT 3 ) AS recent_orders;

这个写法的执行思路是:外层取一个用户,带着这个用户的条件去执行 LATERAL 子查询,然后与结果组合。相比传统 JOIN 场景,它天然支持“每组取前 N 条”这种需求。

7. 子查询与 JOIN 的优化抉择

总会有读者问:子查询和 JOIN 到底选哪个?我的回答是:语义优先,性能靠 EXPLAIN 说话。子查询和 JOIN 在 SQL 语义上经常等价,但 MySQL 优化器处理的路线不同,实际性能在特定数据分布下会有很大差异。

先用一个业务例子对比。查“从未下单的用户”,有两种主流写法:

-- 写法 A:NOT IN 子查询 SELECT * FROM users WHERE user_id NOT IN (SELECT DISTINCT user_id FROM orders); -- 写法 B:NOT EXISTS 相关子查询 SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id); -- 写法 C:LEFT JOIN 过滤 NULL SELECT u.* FROM users u LEFT JOIN orders o ON u.user_id = o.user_id WHERE o.user_id IS NULL;

三种写法语义上等价,但底层执行路径完全不同。写法 A 的问题前面说过,一旦子查询结果里有 NULL 就会全军覆没;写法 B 贴近存在性判断;写法 C 是典型的反连接模式,MySQL 8.0 对 NOT EXISTS 也支持反连接优化。实际到底选哪种,我会先做小数据量验证,再对比执行计划。

有一个常见的误区是认为 JOIN 永远比子查询快。这个结论不能滥用。如果子查询能命中索引,MySQL 的 semi join 优化非常高效;JOIN 如果产生大量重复中间结果,反而增加过滤成本。比如:orders 表里一个用户 100 条订单,JOIN 之后先膨胀到 100 倍再过滤,子查询直接用 EXISTS 提前终止,后者明显更好。

遇到性能瓶颈时,我没有一套固定模板,但有一个固定的排查顺序:

  1. 先看 EXPLAIN 里有没有 select_type 为 SUBQUERY、DEPENDENT SUBQUERY 或 MATERIALIZED 的节点。
  2. 再看有没有 possible_keys 和 key,确认子查询涉及的关联列有没有走索引。
  3. 最后看 rows 估算,对比各步骤扫描量的数量级。
  4. 如果子查询节点是 DEPENDENT SUBQUERY,访问类型是 ALL,说明存在大量逐行扫描,优先考虑改写。
  5. 如果优化器把子查询物化,但物化结果没有索引,MySQL 会建立一个自动生成的临时表索引,这时要确认物化临时表的数据量,数据量过大要果断改写。

8. 相关子查询的几个典型坑位

相关子查询在查看执行计划时经常出现 select_type 为 DEPENDENT SUBQUERY,这个标识的意思是:子查询每次执行都依赖外层数据。这并不一定代表性能差,但确实意味着这个子查询会执行多次,它的成本需要乘以扫描次数。

我总结一下相关子查询最常见的三个问题场景:

第一个是内层表缺少索引。这个不用多说了,每次执行都变成全表扫描,扫描次数又等于外层行数,复杂度直接变成外层行数乘以全表行数。在数据量上万之后立刻卡顿,SQL 上线的 load 测试里一眼就能发现。

第二个是外层表驱动选择错误。MySQL 的优化器通常会选小表作为驱动表,但相关子查询的关联字段如果在外层表上没有过滤条件,优化器也会无从下手。如果你知道某个表作为驱动表更合适,可以在改写时加上范围过滤条件,给优化器明确提示。

第三个是子查询里使用了函数包裹索引列:

WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND DATE(o.create_time) = '2024-01-01' );

create_time 上如果加了函数,索引直接失效。改成范围查询o.create_time >= '2024-01-01' AND o.create_time < '2024-01-02'才能走索引。这个属于老生常谈,但实际代码里出现的频率仍然很高。

9. 子查询中的聚合、HAVING 与 LIMIT 陷阱

子查询里写聚合函数非常常见,但也容易犯一个经典错误:在子查询里用 GROUP BY 取了多个分组,而外层期望它返回单值,最后运行时报错。这类问题出现的原因,是对子查询结果的行数预判不准。

另外,子查询中使用 LIMIT 时,要注意 MySQL 只支持在子查询末尾加 LIMIT。很多人想表达“每个分组内取 N 条”,直接写在子查询里是不对的。比如:

-- 错误想法:每个用户取最近一笔订单 SELECT * FROM users u LEFT JOIN ( SELECT * FROM orders ORDER BY create_time DESC LIMIT 1 ) o ON o.user_id = u.user_id;

这个子查询根本不是“每个用户取一笔”,而是“全局取一笔”,然后拿这一笔去 JOIN。执行结果就是:只有最近下单的那个用户有订单数据,其他用户全是 NULL。这类错误就是典型的“脑子里的语义写成了另一回事”。

想要每个用户取最近的订单,在 MySQL 8.0 里首选窗口函数。在 5.7 及以下,我的做法是先把每个用户最大时间找出来,再 JOIN 回订单表,或者用 GROUP BY 配合聚合函数获得第一行数据后再关联。

另一个容易踩的坑是 HAVING 引用了子查询的别名。MySQL 中 HAVING 的执行顺序晚于 SELECT,所以可以引用 SELECT 里定义的别名,但前提是这个别名来自同一查询块。如果别名定义在子查询内部,外层 HAVING 引用是无效的,要在外层重新计算一遍,或者把条件直接放到子查询内部。

10. 半连接与物化的执行计划阅读

看执行计划时,和子查询相关的几个关键字段需要重点关注。select_type 是最直观的,SUBQUERY 表示非相关子查询,DEPENDENT SUBQUERY 表示相关子查询,MATERIALIZED 表示优化器把子查询物化成一张临时表。访问类型里 type 为 eq_ref 或 ref 的说明依靠索引精确匹配,性能通常可控;type 为 ALL 就要警惕全表扫描。

当 MySQL 把 IN 子查询改写成 semi join 时,EXPLAIN 里第一个表会出现 Semi join 相关提示,或者在扩展的执行计划里看到 semijoin 策略一类的字样。这时说明优化器已经做了去重 JOIN,你不用再纠结原始写法是 IN 还是 JOIN。

半连接的执行策略有几种,MySQL 会根据数据特征选择:

  • Duplicate Weedout:通过临时表对重复结果去重,适合数据量中等的情况。
  • First Match:对驱动表每行匹配到第一条就停止,适合内层结果集中仅需判断存在的情况。
  • Loose Scan:基于索引做跳跃扫描,适合内层表索引结构适合快速去重的情况。
  • Materialize Lookup:物化子查询结果并建索引,再与驱动表关联。

这些策略不会由你指定,优化器自动选择,但理解它们能帮你解读 EXPLAIN 里的异常现象。

MySQL 8.0 中还可以用 EXPLAIN ANALYZE 查看实际执行耗时与循环次数。这是个很有用的工具,它会把每个算子的实际执行行数和耗时打出来,比 EXPLAIN 的估算更直观。

11. 子查询慢的原因排查实测

用一个模拟场景来演示排查思路。假设有一张订单表 orders,10 万行数据,要查出所有未发生过有效订单的用户。SQL 初级写法:

SELECT * FROM users WHERE user_id NOT IN (SELECT user_id FROM orders WHERE status = 'valid');

在数据量小的时候跑得飞快,但数据量上去之后就完全卡住。先用 EXPLAIN 查看,注意到 orders 子查询里没有可用索引,MySQL 需要物化整张订单表的 user_id 到一个临时表,然后再对 users 表全表扫描做 anti join,整体代价非常大。

接着改成 NOT EXISTS:

SELECT * FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND o.status = 'valid' );

配合在 orders 表上建立 (user_id, status) 联合索引之后,执行计划里可以看到 users 表作为驱动表,访问 orders 表时使用 ref 或 eq_ref,关联列有索引,扫描行数大幅下降。这样处理之后,查询时间从秒级别降到几十毫秒级别,效果非常直观。

这个案例说明:子查询本身不是问题,问题在于是否命中索引。每一条子查询都要当成独立的查询来审视,判断它能不能利用索引快速返回。

12. 子查询在 UPDATE、DELETE 中的应用

子查询不仅用于 SELECT,也常用于 UPDATE 和 DELETE。更新场景里比较常见的是把一张表的值更新为另一张表的聚合结果:

UPDATE users u SET u.total_amount = ( SELECT COALESCE(SUM(o.amount), 0) FROM orders o WHERE o.user_id = u.user_id );

这个写法逻辑很直观,但一定要警惕性能问题。它的执行模型就是逐行更新,每次更新都执行一次子查询,如果 inner 查询没有索引,更新几十万行数据会非常痛苦。我的经验是:小数据量可以用这种直白的写法保证逻辑不出错,大数据量尽量改成 JOIN 批量更新,一次性计算再做匹配。

MySQL 里 UPDATE 关联子查询还有一个限制:不允许在更新同一张表的同时,在子查询里又去读取同一张表。比如想更新 orders 表同时根据 orders 表自己分组统计的结果设置字段,MySQL 会直接报错。这时候只能改成临时表方式规避,或者分两步处理。

DELETE 里用 EXISTS 做条件过滤是常客。比如删除一个用户从未下过单的无效用户,写法模板如下:

DELETE FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id );

使用 DELETE 时,务必确认删除条件符合业务预期,最好先在 SELECT 里验证结果集再替换为 DELETE。建议操作前备份数据表或者开启事务,防止误删。

13. 常见子查询问题速查对照

实践过程中会遇到大量子查询相关报错和异常结果,这里把高频问题汇总成对照表,方便排查时快速定位。

现象根因解决方法
子查询返回多行导致报错标量子查询/行子查询返回超过一行加 LIMIT 1,或改为 IN/EXISTS
NOT IN 结果为空子查询结果包含 NULL改 NOT EXISTS,或过滤掉 NULL
EXISTS 恒真返回全表子查询未关联外层表字段补上 WHERE 关联条件
派生表排序失效优化器丢弃无意义的 ORDER BY改写子查询或使用窗口函数
子查询扫描行数巨大关联列无索引为关联列建联合索引
更新同一表报错子查询和更新目标同为一张表拆两步或改用临时表
函数包裹索引列导致慢查询索引失效改为范围查询或等价条件
子查询里 GROUP BY 取了多行外层期望单值明确聚合粒度,或改 EXISTS

这份对照表不是百分百覆盖,但覆盖了我见过的 80% 场景。剩下的问题基本都能通过 EXPLAIN ANALYZE 定位到具体算子。

14. 读完这篇之后应该建立的心智模型

子查询的书写只是第一层能力,真正有价值的是根据业务语义选择正确的查询模式。每次写子查询前,先问自己三个问题:

  • 子查询返回几行几列?这是单值、条件判断,还是表?决定写法。
  • 子查询与外表是否有依赖?决定它能否被优化器合并或物化。
  • 子查询的关联条件是否能命中索引?决定性能上能不能接受。

如果三句话能回答清楚,这个 SQL 写出来基本不会偏差太大。如果答不上来,先别急着写完整 SQL,先把表和索引结构摸清楚,或者直接把问题拆成两个小查询,在代码里做组合。虽然看起来多了一次数据库交互,但往往比写一个复杂到没人能维护的子查询更稳妥。

最后给一个我的个人习惯:任何带子查询的 SQL 上线前,我都会用 EXPLAIN 看一遍执行计划,有条件的话再用 EXPLAIN ANALYZE 跑一次真实执行,确认子查询里的表扫描次数和实际数据规模匹配。很多 SQL 性能问题都是上线前没看执行计划,上线后被慢查询日志打脸。这个习惯救过我很多次,值得你也试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询