窗口函数解决GROUP BY无法处理的保留原始行并计算分组内指标的问题,如ROW_NUMBER()取每组最新记录、SUM() OVER()做高效累计统计,避免GROUP BY歧义和子查询性能瓶颈。
ROW_NUMBER()、RANK()、SUM() OVER() 这些窗口函数不是为了“替代” GROUP BY,而是解决它根本干不了的事——比如你既要保留每条原始记录,又要算分组内排名、累计值、前后行差值。
直接硬套 GROUP BY 做这类事,轻则结果错(Expression #2 of SELECT list is not in GROUP BY clause),重则查得慢、改不动、加个新条件就得重写三层嵌套。
MySQL 8.0 和 PostgreSQL 都默认拒绝这种写法:
SELECT user_id, order_id, MAX(created_at) FROM orders GROUP BY user_id;因为
order_id 没出现在 GROUP BY 里,也没被聚合,数据库无法保证它和 MAX(created_at) 来自同一行。真正可控的做法是用窗口函数打标再过滤:
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) 为每个用户按时间倒序编号WHERE rn = 1 精确拿到最新那条完整记录RANK() 或 DENSE_RANK(),避免漏掉并列项SUM() OVER() 替代关联子查询做累计统计想查每个订单的「用户历史总消费」,传统写法常是关联子查询:
SELECT o1.*, (SELECT SUM(amount) FROM orders o2 WHERE o2.user_id = o1.user_id AND o2.created_at <= o1.created_at) AS cum_amount FROM orders o1;数据量一上来就明显变慢,且难以加索引优化。
窗口函数写法简洁又高效:
SELECT *, SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at ROWS UNBOUNDED PRECEDING) AS cum_amount FROM orders;
ROWS UNBOUNDED PRECEDING 明确指定从分组开头累加到当前行(user_id, created_at) 可走索引扫描,不排序NULL,注意 SUM() 会跳过它们;需要保留 NULL 行位置时,COUNT(*) OVER(...) 更稳妥LAG() 和 LEAD() 替代自连接计算时间差或环比查“每个用户两次登录间隔”,用自连接要写 JOIN + ON + WHERE,逻辑绕、性能差、还容易漏边界。
窗口函数一行搞定:
SELECT user_id, login_time, login_time - LAG(login_time) OVER (PARTITION BY user_id ORDER BY login_time) AS gap FROM logins;
LAG(login_time, 1) 默认取前 1 行,可显式写成 LAG(login_time, 1, '1970-01-01'::timestamp) 设定默认值,避免首行返回 NULL
7 或 14,比写多个 LEFT JOIN 清晰得多ORDER BY 必须明确,否则 LAG/LEAD 结果不可控;对时间字段建议加 NULLS LAST 防止空值干扰顺序ROWS 还是 RANGE?两者语义不同,选错会导致结果偏差。例如移动平均:
SUM(amount) OVER (ORDER BY created_at ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)是取最近 7 行(物理行数);而
RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW 是取时间跨度内的所有行——哪怕某天没数据,也不会补空。RANGE,但要求 ORDER BY 字段类型支持区间运算(如 timestamp、integer)ROWS 更快、更确定,适合固定行数滑动(如 Top N、滚动计数)RANGE 对 text 或 jsonb 字段不生效,会静默退化为 ROWS,这点容易被忽略实际用的时候,别只盯着函数名——PARTITION BY 是否匹配索引、ORDER BY 是否带 NULLS 处理、帧子句是否写死范围,这些细节才是性能和结果正确的关键。