窗口函数适用于不改变行数的组内统计、排序排名、前后行计算等场景,而子查询适用于需过滤、存在性判断或跨粒度聚合等必须改变行集的情况。

子查询和窗口函数在 SQL 中都能解决“基于分组或整体数据计算衍生值”的问题,但它们的执行逻辑、适用场景和性能表现差异明显。窗口函数不是子查询的简单替代品,而是在特定需求下更高效、更清晰的表达方式;反过来,很多子查询也无法被窗口函数直接替代。
能用窗口函数替代子查询的常见场景
当子查询用于在同一查询中引用**当前行上下文相关的聚合结果**(如每组的平均值、累计和、排名等),且不改变原始行数时,窗口函数通常是更优选择:
-
组内统计复用:比如“查出每个员工工资,并显示其部门平均工资”。用子查询需关联或嵌套聚合;用
AVG(salary) OVER (PARTITION BY dept)直接、可读、通常更快。 -
排序与排名类计算:如“每个部门薪资前三的员工”。子查询常需自连接 + 计数比较,易出错且难维护;
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)简洁准确。 -
前后行参考(滑动计算):如“每位员工工资与上一位入职员工的差值”。子查询需自连接加条件模拟序号;
LAG(salary) OVER (ORDER BY hire_date)原生支持,无歧义。
子查询不可被窗口函数替代的情况
窗口函数作用于已确定的结果集,无法改变查询的“行集范围”或“过滤逻辑”,以下情况必须用子查询(或 CTE、JOIN 等):
-
过滤依赖聚合结果:如“只显示工资高于部门平均值的员工”。窗口函数可算出部门均值,但
WHERE不能直接引用窗口函数结果;需子查询/CTE 先算均值再 JOIN 或 WHERE 过滤。 -
多表关联中的非对称条件:例如“找出所有没有订单的客户”,需用
NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);窗口函数无法表达存在性否定逻辑。 - 跨粒度聚合后再次聚合:如“统计每个城市中高薪员工(>5000)占比”。需先按人判断是否高薪,再按城市汇总比例——这需要两层聚合,窗口函数只能做一层,必须嵌套子查询或 CTE。
性能与可读性的实际权衡
窗口函数通常比等效的相关子查询快,因为避免了对每一行重复执行内部查询;但若误用于需过滤的场景而强行绕过限制(如用 CASE + 窗口函数伪造过滤),反而导致逻辑混乱、性能下降。
- 数据库优化器对窗口函数有专门优化路径(如流式处理、内存排序缓冲),而相关子查询可能触发 N+1 扫描。
- 窗口函数语义明确(
PARTITION BY、ORDER BY、帧子句),比多层嵌套子查询更易理解与调试。 - 部分老版本数据库(如 MySQL 5.7 及更早)不支持窗口函数,此时子查询是唯一选择;升级后应逐步重构为窗口函数提升可维护性。
什么时候该选哪个?一个快速判断法
问自己两个问题:
- 输出行数是否必须等于原表(或 JOIN 后基础行集)?→ 是,则优先考虑窗口函数。
- 是否需要基于聚合结果进一步
WHERE、HAVING、JOIN或改变行集?→ 是,则必须用子查询(或 CTE)。
二者也常配合使用:子查询(或 CTE)负责缩小/构造数据集,窗口函数在其上做精细分析。这种组合既清晰又高效。










