当相关子查询仅做聚合计算(如MAX、COUNT、AVG)且分组条件与外层表完全一致时,可无损改写为窗口函数;若含非等值条件、多表JOIN或动态Top-N,则难以直接替代。

什么时候该用窗口函数替代相关子查询
当相关子查询只做聚合计算(如 MAX()、COUNT()、AVG())且分组依据与外层表完全一致时,基本可以无损改写为窗口函数。典型场景是“查每个员工所在部门的最高薪资”这类问题——子查询里 WHERE dept_id = t1.dept_id 就是窗口分区的关键线索。
但若子查询含非等值条件(如 salary > t1.salary)、多表 JOIN 或涉及排序后取第 N 行(且 N 不固定),窗口函数很难直接替代,强行改写容易漏数据或逻辑错位。
改写三步走:定位分区、确认排序、选对函数
先看一个常见错误写法:SELECT *, (SELECT MAX(salary) FROM emp e2 WHERE e2.dept_id = e1.dept_id) AS max_sal FROM emp e1。它慢是因为每行都触发一次全表扫描。
对应窗口写法的核心是把子查询的 WHERE 条件转成 PARTITION BY,聚合函数套上 OVER():
SELECT *, MAX(salary) OVER (PARTITION BY dept_id) AS max_sal FROM emp;
-
PARTITION BY dept_id对应原子查询的WHERE e2.dept_id = e1.dept_id - 不需要
ORDER BY—— 因为MAX()不依赖顺序;但如果是ROW_NUMBER()或累计求和,就必须明确ORDER BY - 如果原查询还带
GROUP BY,注意窗口函数不减少行数,不能直接替换GROUP BY语句
容易踩的坑:NULL、去重、性能反升
窗口函数默认包含当前行,而某些相关子查询隐含过滤逻辑。比如统计“比当前员工薪资高的人数”:(SELECT COUNT(*) FROM emp e2 WHERE e2.dept_id = e1.dept_id AND e2.salary > e1.salary),改写时必须用 COUNT(*) FILTER (WHERE salary > e1.salary) OVER (PARTITION BY dept_id)(PostgreSQL)或 SUM(CASE WHEN salary > e1.salary THEN 1 ELSE 0 END) OVER (...)(通用写法)。
另一个陷阱是误用 DISTINCT:窗口函数不支持 OVER (PARTITION BY ... DISTINCT),想算部门内不同薪资数量,得先 DISTINCT ON (dept_id, salary) 再聚合,或用 COUNT(DISTINCT salary) OVER (PARTITION BY dept_id)(仅部分数据库支持,如 PostgreSQL 14+,MySQL 不支持)。
还有人发现改写后变慢了——大概率是因为没加索引。相关子查询可能走 dept_id + salary 联合索引,而窗口函数仍需全表扫描再内存排序,尤其在 ORDER BY 子句复杂时。
MySQL 和 SQL Server 的语法差异点
MySQL 8.0+ 支持标准窗口函数,但 RANK() 和 DENSE_RANK() 在 NULL 处理上和 PostgreSQL 不同,默认把 NULL 当最小值;SQL Server 则要求 OVER() 中 ORDER BY 必须存在(哪怕只是 ORDER BY (SELECT NULL))才能用 ROW_NUMBER()。
另外,SQL Server 的 OFFSET-FETCH 分页结合窗口函数时,容易和 TOP 混用出错;MySQL 对 FRAME 子句(如 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)支持较晚,5.7 完全不支持。
真正麻烦的是 Oracle:它的 MODEL 子句能干更复杂的事,但窗口函数语法和标准略有出入,比如 LAG() 默认返回 NULL 而不是报错,这点和多数引擎不同。

















