首页/数据/SQL 速查/SELECT 列表里的子查询

SELECT 列表里的子查询

在结果列里内嵌标量子查询得到附加信息(计数、最新值):可读性好,但逻辑上逐行执行,行数大时优先改 JOIN 或窗口函数。

SQL 速查数据更新于 2026-10-13

SELECT 列表里的子查询

在结果列里内嵌标量子查询,可以给每行附加统计信息(订单数、最近下单时间),写法直观、改写成本低。代价是逻辑上逐行执行:外层有多少行就执行多少次。外层 100 万行、列表里放两个子查询,就是 200 万次查找。行数大时应改写成 LEFT JOIN 聚合子查询或窗口函数,让数据一次扫描算完;改写后还要核对 LEFT JOIN 补 NULL 与聚合口径是否与原子查询一致。

基本信息

求值方式逻辑上外层每一行执行一次子查询
空值行为无匹配时子查询返回 NULL,可用 COALESCE 兜底
改写方向LEFT JOIN + GROUP BY、窗口函数、LATERAL
性能关键先用 WHERE 把外层结果集压小,再决定是否改写
报错条件标量子查询返回多行会直接报错

典型使用场景

  • 列表页显示每一项的关联数量(订单数、评论数)
  • 显示最近一次操作时间或状态
  • 小结果集上的快速补充统计,改写收益不明显
  • 临时排查时快速取关联信息,不必写复杂连接
  • 迁移或重构前先跑通语义,再按数据量决定是否改写

直观写法

直观写法
SELECT u.id, u.name,
       (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id)          AS order_cnt,
       (SELECT MAX(o.created_at) FROM orders o WHERE o.user_id = u.id) AS last_order
FROM users u
WHERE u.status = 1 AND u.id <= 100;   # 先用 WHERE 把外层行数压小

改写为一次聚合

改写为一次聚合
-- LEFT JOIN 聚合子查询:一次扫描算完,避免逐行执行
SELECT u.id, u.name,
       COALESCE(t.cnt, 0) AS order_cnt,          # 口径对齐:无匹配时展示 0
       t.last_order
FROM users u
LEFT JOIN (
    SELECT user_id, COUNT(*) AS cnt, MAX(created_at) AS last_order
    FROM orders
    GROUP BY user_id
) t ON t.user_id = u.id
WHERE u.status = 1;

写法与适用规模

写法执行方式适用规模
SELECT 列表子查询逻辑上逐行执行外层几十到几千行
LEFT JOIN 聚合子查询一次聚合后再连接外层几十万行以上
窗口函数一次扫描计算分组统计、明细行带组统计
LATERAL 子查询每行一次但内层可走索引需要「每组取一条」等场景
改写要注意口径差异:标量子查询无匹配时返回 NULL,换成 LEFT JOIN + COUNT 后返回 0;展示层对 NULL 与 0 的呈现通常不同,需要显式 COALESCE 对齐。
外层行数大时列表里放两个子查询就是两倍的查找次数;先把外层条件写好压到小结果集,或直接改写成一次聚合的连接写法。

易错点

  • 外层百万行仍用列表内子查询,等于百万次额外查找,接口超时
  • 改写成 LEFT JOIN 后没核对 NULL 与 0 的差异,展示层出现空值
  • 子查询返回多行导致运行时报错,上线后才暴露
  • 子查询过滤列没有索引,逐行执行退化成逐行全表扫描

数据来源与参考

  • MySQL 8.0 Reference Manual · Scalar Subqueries
  • PostgreSQL Documentation · Scalar Subqueries
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌