子查询在select列表中必须为标量,即返回一行一列;否则报错“subquery returns more than 1 row”;需用where精确限定、聚合函数兜底、显式关联外层字段,并处理null和除零;推荐优先使用窗口函数替代以提升性能与健壮性。

子查询放在 SELECT 列表里必须是标量子查询
直接在 SELECT 后写子查询,数据库要求它只能返回「一行一列」——也就是一个确定的值。如果子查询返回多行(比如漏了 WHERE 或用了聚合但没加 GROUP BY),会报错:Subquery returns more than 1 row。
常见错误写法:SELECT id, (SELECT COUNT(*) FROM orders) / (SELECT COUNT(*) FROM users) AS ratio FROM users —— 这看似没问题,但一旦外层 users 表为空,除数为 0;更隐蔽的是,如果子查询里引用了外层字段却没关联,MySQL 可能静默退化为相关子查询,性能骤降。
- 确保子查询用
WHERE精确限定到单值,或用MAX()/MIN()/COUNT(*)等聚合函数兜底 - 需要关联外层时,必须显式写出关联条件,例如:
(SELECT SUM(amount) FROM orders o WHERE o.user_id = u.id) - 避免在子查询中重复计算全表汇总(如总订单数),应提前算好或改用窗口函数
用子查询算占比时,NULL 和除零必须手动处理
占比 = 当前值 / 总和,但总和可能是 0,当前值也可能是 NULL。数据库不会自动帮你转成 0 或跳过,NULL / 100 结果仍是 NULL,5 / 0 直接报错 Division by zero。
正确做法是用 COALESCE 和条件判断包裹分母:
SELECT
dept,
COUNT(*) AS cnt,
COALESCE(
COUNT(*) * 100.0 / NULLIF((SELECT COUNT(*) FROM employees), 0),
0
) AS pct
FROM employees
GROUP BY dept;
-
NULLIF(expr1, expr2)在分母为 0 时返回NULL,避免除零 -
COALESCE(..., 0)把最终的NULL(来自除零或分子为NULL)转成 0 - 乘以
100.0而非100,防止整数除法截断(尤其在 PostgreSQL、SQL Server 中)
替代方案:窗口函数比子查询更稳更快
只要数据库支持(MySQL 8.0+、PostgreSQL、SQL Server、Oracle),优先用 SUM() OVER() 替代子查询算占比。它一次扫描完成全局汇总,没有重复执行、不触发相关子查询、天然规避除零问题。
等价写法:
SELECT dept, COUNT(*) AS cnt, COUNT(*) * 100.0 / SUM(COUNT(*)) OVER() AS pct FROM employees GROUP BY dept;
- 子查询方式对每个分组都重跑一遍全表统计;窗口函数只算一次总和,性能差距在百万级数据上非常明显
- 窗口函数中
SUM(COUNT(*)) OVER()是合法的:先按GROUP BY聚合,再对聚合结果开窗 - 如果需排除某些分组再算占比(比如剔除 dept IS NULL),子查询容易漏条件,而窗口函数可配合
FILTER(PostgreSQL)或嵌套 CTE 更清晰控制范围
带条件的动态汇总别硬套子查询
想算「每个部门中薪资 > 平均薪资的人数占比」,有人会写两层子查询:(SELECT COUNT(*) FROM employees e2 WHERE e2.dept = e1.dept AND e2.salary > (SELECT AVG(salary) FROM employees)) / (SELECT COUNT(*) FROM employees e3 WHERE e3.dept = e1.dept) —— 这不仅慢,而且内层平均值是全表平均,不是部门平均。
真正动态,得让子查询「感知外层分组」:
- 第一层必须关联:
(SELECT AVG(salary) FROM employees e2 WHERE e2.dept = e1.dept) - 但这样每行都触发一次子查询,N 行 × N 次扫描,O(N²) 复杂度
- 更优解是先用 CTE 算出各部门平均薪资,再 JOIN 关联,或者直接用窗口函数:
AVG(salary) OVER(PARTITION BY dept)
复杂占比逻辑一旦涉及多层依赖或条件汇总,子查询很快变成维护黑洞——该拆就拆,该换窗口就换,别硬扛。










