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 对齐。
外层行数大时列表里放两个子查询就是两倍的查找次数;先把外层条件写好压到小结果集,或直接改写成一次聚合的连接写法。