窗口函数核心是将多层嵌套逻辑压缩至单次表扫描,缩短执行路径;如avg() over(partition by dept)替代关联子查询,row_number()配合子查询实现top n且需order by保证稳定性。

窗口函数不是“少写几行”的技巧,而是把原本需要多层嵌套、自连接或重复扫描的逻辑,压进单次表扫描里完成——代码量下降只是副产品,核心是执行路径变短了。
用 AVG() OVER(PARTITION BY) 替代关联子查询
查每个员工工资占部门平均工资的比例,传统写法要嵌套子查询,还容易漏加 WHERE 条件导致全表扫描:
- 错误写法:
(SELECT AVG(salary) FROM emp e2 WHERE e2.dept = e1.dept)—— 每行都触发一次子查询,执行计划里能看到多次 Seq Scan - 正确写法:
AVG(salary) OVER(PARTITION BY dept)—— 一次扫描,分组内直接算均值,结果列和原始行一一对应 - 如果部门字段名是
department_id,必须写成OVER(PARTITION BY department_id),写错字段名会导致分组失效,结果全乱 - MySQL 8.0+、PostgreSQL、SQL Server 2012+ 支持;MySQL 5.7 直接报错
FUNCTION AVG does not exist
ROW_NUMBER() 替代自连接取 Top N
查每个部门薪资前 3 的员工,自连接写法至少 15 行,且容易因 OR 条件漏匹配;窗口函数两层就搞定:
- 必须用子查询或 CTE 包裹,因为
WHERE无法直接过滤ROW_NUMBER()的结果 -
ORDER BY salary DESC, id DESC是关键:相同薪资时靠id保证排序稳定,否则每次执行结果可能不同 - 别用
RANK()或DENSE_RANK()代替ROW_NUMBER()—— 前两者会并列,WHERE rn 可能返回 5 行 - 如果只查单个部门(如
WHERE department_id = 101),窗口函数仍会扫描全表再过滤,此时自连接反而更快
LAG() 和 LEAD() 替代 ID 关联的自连接
计算相邻登录间隔、订单金额差值这类需求,传统写法依赖 JOIN o1 ON o2.id = o1.id + 1,但生产环境 ID 几乎不连续:
-
LAG(login_date) OVER (PARTITION BY user_id ORDER BY login_date)不依赖 ID,只认排序逻辑 - 必须显式写
ORDER BY login_date,否则LAG()的“上一行”无定义,PostgreSQL 会报错,MySQL 可能返回随机值 - 时间字段精度不够(如只有秒级)时,补唯一字段:
ORDER BY login_date, log_id -
LAG(amount, 1, 0)第三个参数是默认值,避免首行返回NULL干扰后续计算
真正难的不是写出窗口函数,而是判断该不该用——当原查询已有强过滤条件(比如 WHERE user_id IN (1,2,3)),窗口函数被迫扫全表再裁剪,反而更慢。执行计划里看到 Sort 节点没走索引,基本就说明它在硬盘上排序了。











