子查询应优先写在where中,用in/exists/比较运算符连接;select中子查询须为标量;from中子查询必须加别名;相关子查询性能取决于索引而非本身。

子查询写在 WHERE 里,别硬塞 JOIN
WHERE 后面跟 IN、EXISTS 或比较运算符(比如 =)接子查询,是最常用也最安全的起点。JOIN 容易因多对一关系重复主表行,导致聚合结果翻倍;而子查询天然“单值上下文”,逻辑更干净。
常见错误现象:SUM(amount) * COUNT(DISTINCT user_id) 算出离谱总数——其实是 JOIN 拉平后重复计数了。
- 用
WHERE user_id IN (SELECT user_id FROM vip_users WHERE level > 3)替代 LEFT JOIN + 过滤 -
EXISTS比IN更适合大表关联,尤其子查询带索引字段时,数据库常能提前终止扫描 - 子查询返回多行但外层用
=会报错:ERROR: more than one row returned by a subquery used as an expression,这时要么加LIMIT 1(需确认业务允许),要么换IN
SELECT 列表里的子查询必须标量
SELECT 后直接写子查询,它只能返回**一行一列**,否则语法直接失败。这不是性能问题,是 SQL 标准强制约束。
使用场景:给每行补一个动态计算值,比如“该用户最近一笔订单金额”“部门平均薪资(不拉平数据)”。
- 写法必须是:
(SELECT amount FROM orders WHERE user_id = u.id ORDER BY created_at DESC LIMIT 1) - 别漏掉关联条件(如
user_id = u.id),否则变成全表扫描+笛卡尔积 - MySQL 8.0+ 和 PostgreSQL 支持 LATERAL,可让子查询引用前面的表别名,但老版本得靠 JOIN 模拟,反而更重
- 这类子查询在结果集每行都执行一次,数据量大时可能变慢——先确认是否真需要实时计算,还是能预聚合到临时表
FROM 子句中的子查询要起别名
所有出现在 FROM 后的子查询,无论嵌套多深,都必须用 AS alias_name 显式命名,否则多数数据库(PostgreSQL、SQL Server)直接报错,MySQL 5.7+ 也强制要求。
参数差异:不同数据库对“列名可见性”的处理略有不同,但别名是唯一通用解法。
- 正确:
FROM (SELECT user_id, SUM(amount) s FROM orders GROUP BY user_id) AS order_summary - 错误:
FROM (SELECT ...) WHERE s > 100—— 外层 WHERE 看不到子查询里的s,必须通过别名访问:order_summary.s > 100 - 嵌套三层以上时,别名要能区分层级,比如
raw_data→agg_by_user→ranked,别全叫t1/t2 - CTE(
WITH)本质是具名子查询,可读性更好,但某些旧版 MySQL 不支持;若必须兼容,就老实用括号子查询+别名
相关子查询性能差?先看有没有索引,再想改写
相关子查询(即子查询里引用了外层表字段)每次都要重新执行,看起来吓人,但现代优化器其实很擅长处理它——前提是被引用的字段有索引。
容易踩的坑:盲目用 JOIN 替换,结果引入重复行或 NULL 行,后续还得去重/过滤,代码更难懂,性能未必提升。
- 检查执行计划里子查询部分是否走了索引扫描(
Index Scan或ref),而不是Seq Scan或ALL - 如果子查询只查单个值且外键明确,把子查询逻辑下推到 JOIN ON 条件里,有时比相关子查询快(例如用
LEFT JOIN latest_order ON u.id = latest_order.user_id AND latest_order.rn = 1) - PostgreSQL 中,
NOT EXISTS通常比NOT IN快,因为后者遇到 NULL 就整个表达式为 NULL,可能意外过滤掉有效行 - 真正拖慢的往往不是子查询本身,而是外层没加
WHERE导致全表扫描——先缩小主表范围,再跑子查询
子查询真正的复杂点不在语法,而在「哪一层该算、哪一层该过滤」。同一个业务逻辑,放在 WHERE、SELECT 或 FROM,语义和性能可能天差地别;而人最容易忽略的,是子查询里那个看似无关紧要的 ORDER BY ... LIMIT 1——它没有索引支撑时,就是全表排序。










