还在用子查询硬凑?窗口函数才是 SQL 真正的神器
很多人写 SQL,一碰到“分组后再取明细”“每组 Top N”“累计求和”“同比环比”“取上一条/下一条记录”这类需求,就开始疯狂套子查询。
结果通常是这样的:
说得难听点,很多子查询不是业务复杂,而是写法原始。
如果你还在靠子查询硬凑这些分析型需求,那窗口函数大概率就是你一直没认真用过的那把刀。
窗口函数的价值,不是“语法高级”,而是它能把“统计结果”和“原始明细”同时留在一张结果集里。
这件事,看起来只是写法变化,实际会直接改变你组织 SQL 的方式。
先说人话。
普通聚合函数,比如:
select dept_id, avg(salary)
from emp
group by dept_id;它会把多行压成一行。你得到了部门平均工资,但员工明细没了。
而窗口函数做的是:
不压缩行数,但允许你在每一行上看到某个“窗口范围内”的统计结果。
比如:
select
emp_name,
dept_id,
salary,
avg(salary) over(partition by dept_id) as dept_avg_salary
from emp;你会得到这样的效果:
这就是窗口函数的核心爽点:
明细不丢,统计也有。
很多以前必须靠子查询、临时表、自连接绕出来的需求,到这里基本就通了。
over()窗口函数的灵魂在 over()。
常见结构长这样:
函数名() over(
partition by 分组字段
order by 排序字段
rows between ... and ...
)你可以把它粗暴理解成:
partition by:先按什么维度分组看order by:组内按什么顺序算rows between:窗口范围到底覆盖哪些行不是每次都要写全。
你完全可以从这三个层次去理解:
partition by表示“按组统计,但不丢明细”。
partition by + order by表示“组内有顺序”,于是可以做排名、累计值、上一条下一条。
表示“精确限定计算范围”,比如最近 3 行、当前行到首行之类。
这套脑子一旦装上,很多 SQL 会突然变简单。
下面这几类,是窗口函数最值回票价的地方。
这是最经典的一个。
每个部门里,找出工资最高的 3 个人。
比如先求一个排序,再在外层筛。
select *
from (
select
emp_id,
emp_name,
dept_id,
salary,
row_number() over(
partition by dept_id
order by salary desc
) as rn
from emp
) t
where rn <= 3;row_number() 不是全表排,而是:
dept_id 分组salary desc 排序这样每个部门就都有自己的 1、2、3、4……
最后只保留 rn <= 3 即可。
row_number()
rank()
dense_rank()它们的区别在于并列名次怎么处理:
row_number():不管并列,强行唯一编号rank():并列占同一名次,后面跳号dense_rank():并列占同一名次,但后面不跳号如果你连“组内 Top N”都还在拿子查询硬拼,那窗口函数确实该补了。
想看每个员工工资,同时知道:
通常是先 group by dept_id 算一张部门统计表,再 join 回员工表。
能写,但绕。
select
emp_id,
emp_name,
dept_id,
salary,
avg(salary) over(partition by dept_id) as dept_avg_salary,
max(salary) over(partition by dept_id) as dept_max_salary,
min(salary) over(partition by dept_id) as dept_min_salary
from emp;如果还想继续做判断:
select
emp_id,
emp_name,
dept_id,
salary,
avg(salary) over(partition by dept_id) as dept_avg_salary,
case
when salary > avg(salary) over(partition by dept_id) then '高于均值'
when salary < avg(salary) over(partition by dept_id) then '低于均值'
else '等于均值'
end as salary_level
from emp;因为你根本不用先造一张中间表再回连。
统计信息直接贴在每条明细上。
读起来更顺,维护也更轻。
这类需求在报表、增长、交易分析里极其常见。
按日期看每天销售额,同时想知道累计销售额。
select
dt,
amount,
sum(amount) over(
order by dt
rows between unbounded preceding and current row
) as cumulative_amount
from sales_daily;这句的意思是:
于是你每一行都能看到“截至当天的累计值”。
select
city,
dt,
amount,
sum(amount) over(
partition by city
order by dt
rows between unbounded preceding and current row
) as city_cumulative_amount
from sales_daily;这就是窗口函数真正像神器的地方:
既保留每天明细,又直接拿到累计结果。
而且语义非常直白,不需要外层再套一堆相关子查询。
很多业务分析,本质就是“和前一个状态比一下”。
比如:
select
user_id,
order_time,
amount,
lag(amount, 1) over(
partition by user_id
order by order_time
) as prev_amount,
lead(amount, 1) over(
partition by user_id
order by order_time
) as next_amount
from orders;这里:
lag() 取前一条lead() 取后一条如果你要算和上一单的差值:
select
user_id,
order_time,
amount,
amount - lag(amount, 1) over(
partition by user_id
order by order_time
) as diff_from_prev
from orders;这种需求如果用子查询或者自连接,不是不能写,而是写出来通常又臭又长。
窗口函数则是原地解决。
做经营分析、用户增长、GMV 报表时,这类需求几乎天天见。
按月统计销售额,并计算相较上月的增长率。
select
month,
revenue,
lag(revenue, 1) over(order by month) as prev_revenue,
round(
(revenue - lag(revenue, 1) over(order by month))
/ nullif(lag(revenue, 1) over(order by month), 0),
4
) as mom_growth
from monthly_revenue;虽然这里看着重复写了几次 lag(),但逻辑非常清楚:
你甚至可以先包一层,让 SQL 更干净:
select
month,
revenue,
prev_revenue,
round((revenue - prev_revenue) / nullif(prev_revenue, 0), 4) as mom_growth
from (
select
month,
revenue,
lag(revenue, 1) over(order by month) as prev_revenue
from monthly_revenue
) t;注意,这种“先用窗口函数算,再外层做过滤或二次计算”的写法,和“用子查询硬拼分析逻辑”不是一回事。
窗口函数是先把分析字段算出来,外层只是消费结果。
这比一层层去模拟顺序关系,干净太多。
每个用户,取第一次下单记录。
select *
from (
select
user_id,
order_id,
order_time,
amount,
row_number() over(
partition by user_id
order by order_time asc
) as rn
from orders
) t
where rn = 1;如果要最后一次下单,只要把排序改成 desc。
这个需求在用户首购、首登、首次付费、最后活跃时间等分析里非常常见。
以前很多人会这么干:
group by user_id 求 min(order_time)当然能做。
但窗口函数写法更自然,因为它本来就是“在组内排顺序,再取第几条”。
这里要说句实在话。
窗口函数不是所有 SQL 都该上。
如果你的需求只是简单聚合:
select dept_id, count(*)
from emp
group by dept_id;那就没必要硬用窗口函数装高级。
但只要需求开始出现这些关键词:
那你就应该先想窗口函数,而不是本能地往子查询里钻。
它最适合的不是 CRUD 场景,而是分析型场景。
因为大多数人记住的是语法,没记住问题类型。
正确姿势不是背:
row_number() 怎么写lag() 有几个参数sum() over() 的格式是什么而是先识别:
这个问题本质上是不是“组内排序”或“顺序比较”或“明细上贴统计值”?
只要你能把业务问题识别成这几类,窗口函数就会自己跳出来。
换句话说:
窗口函数难的不是语法,而是思维切换。
你得从“先聚合,再拼回去”的老套路,切换到“在明细结果上直接计算分析字段”。
这一步一旦跨过去,很多 SQL 会突然从“施工现场”变成“人能读的东西”。
以后看到需求,你可以先问自己三个问题:
如果是,优先想窗口函数。
如果是,优先想 lag() / lead()。
如果是,优先想 row_number() / rank() / dense_rank()。
你会发现,很多以前以为必须上子查询的题,其实都可以被窗口函数正面拿下。
不是因为它能让你在面试里显得高级。
而是因为真实工作里,凡是稍微像样一点的数据分析、经营分析、用户行为分析、报表加工,窗口函数出现的频率都很高。
你不用它,很多题也能写。
但代价通常是:
而窗口函数真正厉害的地方在于:
它能让“复杂分析逻辑”写得像正常人说话。
这不是炫技,这是工程质量。
所以别再一上来就拿子查询硬凑了。
很多时候,不是需求难,是你手里那把刀太旧。
窗口函数,差不多该换上了。
最常用的其实就这些:
row_number():组内唯一编号rank():组内排名,允许并列,后续跳号dense_rank():组内排名,允许并列,但不跳号sum() over():累计求和 / 窗口求和avg() over():窗口平均值max() over() / min() over():窗口最大最小值lag():取前一条lead():取后一条先把这几个吃透,已经能解决 80% 的分析型 SQL 问题。
阅读原文:原文链接