窗口函数能替代相关子查询,因其在单次扫描中完成分组内计算,避免每行重复执行内层查询的o(n²)开销;而相关子查询需为外层每行重新执行一次内层查询,导致性能差、逻辑嵌套深。

为什么窗口函数能替代相关子查询
因为相关子查询每次都要为外层每一行重新执行一次内层查询,性能差、逻辑嵌套深;而窗口函数在单次扫描中就能完成分组内计算,既保持逻辑清晰,又避免重复扫描。关键前提是:你要算的值必须能基于当前行所在分区(PARTITION BY)和排序(ORDER BY)推导出来,而不是依赖任意其他行的独立条件判断。
ROW_NUMBER() 和 RANK() 替代“取每组第一条”的子查询
常见错误写法是用 SELECT * FROM t1 WHERE id = (SELECT MIN(id) FROM t2 WHERE t2.group_id = t1.group_id) —— 这会触发全表关联+多次子查询。换成窗口函数更直接:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY created_at DESC) AS rn
FROM orders
) ranked
WHERE rn = 1;
注意点:
-
ROW_NUMBER()保证唯一编号,适合“严格取一条”;RANK()会在并列时跳号,DENSE_RANK()不跳号,按需选 - 如果原子查询里有
WHERE条件(比如只取 status = 'paid' 的第一条),必须先过滤再开窗,不能把条件塞进OVER子句 - MySQL 8.0+、PostgreSQL、SQL Server 2005+ 支持;SQLite 3.25+ 也支持,但旧版不认
用 SUM() OVER() 或 AVG() OVER() 替代聚合子查询
比如想查每个用户的订单总额,同时保留每笔订单明细,传统写法要 JOIN 聚合结果;窗口函数一行搞定:
SELECT user_id, order_id, amount,
SUM(amount) OVER (PARTITION BY user_id) AS total_per_user
FROM orders;
这里容易踩的坑:
- 没写
PARTITION BY就变成全表累计,不是按用户汇总 - 如果还要按时间排序做累计(如“到当前行为止的用户累计消费”),得加
ORDER BY created_at,否则默认无序,结果不可靠 - Oracle 和 PostgreSQL 对空值处理一致;但 MySQL 在
ORDER BY含 NULL 时可能排序不稳定,建议显式写ORDER BY created_at ASC NULLS LAST(如果版本支持)
相关子查询无法被窗口函数替代的典型场景
窗口函数不是万能的。以下情况仍得用子查询或 CTE:
- 需要跨不同分组做比较,比如“找出销售额高于公司平均值的员工”——窗口函数算不出全局平均,得先用子查询或 CTE 算出均值再 JOIN
- 子查询含非确定性函数,如
NEWID()或RANDOM(),窗口函数不允许在OVER中调用 - 逻辑依赖多层嵌套条件,例如 “该订单的客户上一笔订单金额 > 当前订单金额”,这时需要 LAG + 条件判断,但若涉及复杂状态机,窗口函数表达力有限
真正难的是识别哪些“看起来像能开窗”的逻辑,其实隐含了非局部依赖——这时候硬套窗口函数反而让 SQL 更难懂、更难调。











