管理杂谈OA答疑ERP答疑教程搜索

还在用子查询硬凑?窗口函数才是 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 ...
)

你可以把它粗暴理解成:

不是每次都要写全。

你完全可以从这三个层次去理解:

1)只有 partition by

表示“按组统计,但不丢明细”。

2)partition by + order by

表示“组内有顺序”,于是可以做排名、累计值、上一条下一条。

3)再加窗口范围

表示“精确限定计算范围”,比如最近 3 行、当前行到首行之类。

这套脑子一旦装上,很多 SQL 会突然变简单。


三、最常见的 6 类场景,窗口函数几乎是降维打击

下面这几类,是窗口函数最值回票价的地方。


场景 1:每个分组内取 Top N

这是最经典的一个。

需求

每个部门里,找出工资最高的 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() 不是全表排,而是:

这样每个部门就都有自己的 1、2、3、4……

最后只保留 rn <= 3 即可。

相关函数顺手记一下

row_number()
rank
()
dense_rank
()

它们的区别在于并列名次怎么处理:

如果你连“组内 Top N”都还在拿子查询硬拼,那窗口函数确实该补了。


场景 2:在明细里直接看到部门平均值 / 最大值 / 最小值

需求

想看每个员工工资,同时知道:

传统写法

通常是先 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;

为什么这比子查询爽

因为你根本不用先造一张中间表再回连。

统计信息直接贴在每条明细上。

读起来更顺,维护也更轻。


场景 3:累计求和、累计人数、累计销售额

这类需求在报表、增长、交易分析里极其常见。

需求

按日期看每天销售额,同时想知道累计销售额。

窗口函数写法

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;

这就是窗口函数真正像神器的地方:

既保留每天明细,又直接拿到累计结果。

而且语义非常直白,不需要外层再套一堆相关子查询。


场景 4:取上一条、下一条记录

很多业务分析,本质就是“和前一个状态比一下”。

比如:

窗口函数写法

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;

这里:

如果你要算和上一单的差值:

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;

这种需求如果用子查询或者自连接,不是不能写,而是写出来通常又臭又长。

窗口函数则是原地解决。


场景 5:同比、环比、增长率

做经营分析、用户增长、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;

注意,这种“先用窗口函数算,再外层做过滤或二次计算”的写法,和“用子查询硬拼分析逻辑”不是一回事。

窗口函数是先把分析字段算出来,外层只是消费结果。

这比一层层去模拟顺序关系,干净太多。


场景 6:找每组的第一条 / 最后一条记录

需求

每个用户,取第一次下单记录。

窗口函数写法

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

这个需求在用户首购、首登、首次付费、最后活跃时间等分析里非常常见。

以前很多人会这么干:

当然能做。

但窗口函数写法更自然,因为它本来就是“在组内排顺序,再取第几条”。


四、窗口函数不是万能药,但它特别适合“分析型 SQL”

这里要说句实在话。

窗口函数不是所有 SQL 都该上。

如果你的需求只是简单聚合:

select dept_id, count(*)
from
 emp
group
 by dept_id;

那就没必要硬用窗口函数装高级。

但只要需求开始出现这些关键词:

那你就应该先想窗口函数,而不是本能地往子查询里钻。

它最适合的不是 CRUD 场景,而是分析型场景。


五、为什么很多人学了窗口函数,还是不会用?

因为大多数人记住的是语法,没记住问题类型。

正确姿势不是背:

而是先识别:

这个问题本质上是不是“组内排序”或“顺序比较”或“明细上贴统计值”?

只要你能把业务问题识别成这几类,窗口函数就会自己跳出来。

换句话说:

窗口函数难的不是语法,而是思维切换。

你得从“先聚合,再拼回去”的老套路,切换到“在明细结果上直接计算分析字段”。

这一步一旦跨过去,很多 SQL 会突然从“施工现场”变成“人能读的东西”。


六、再给你一个实战判断标准:什么时候该优先想窗口函数?

以后看到需求,你可以先问自己三个问题:

1)我是不是既要明细,又要统计结果?

如果是,优先想窗口函数。

2)我是不是在比较“当前行”和“上一行/下一行”?

如果是,优先想 lag() / lead()

3)我是不是要做组内排名、Top N、首条末条?

如果是,优先想 row_number() / rank() / dense_rank()

你会发现,很多以前以为必须上子查询的题,其实都可以被窗口函数正面拿下。


七、最后说透:窗口函数为什么值得你现在就补上

不是因为它能让你在面试里显得高级。

而是因为真实工作里,凡是稍微像样一点的数据分析、经营分析、用户行为分析、报表加工,窗口函数出现的频率都很高。

你不用它,很多题也能写。

但代价通常是:

而窗口函数真正厉害的地方在于:

它能让“复杂分析逻辑”写得像正常人说话。

这不是炫技,这是工程质量。

所以别再一上来就拿子查询硬凑了。

很多时候,不是需求难,是你手里那把刀太旧。

窗口函数,差不多该换上了。


你可以顺手记住的窗口函数清单

最常用的其实就这些:

先把这几个吃透,已经能解决 80% 的分析型 SQL 问题。

阅读原文:原文链接


更多精彩文章浏览...
点击右上角图标分享到朋友圈
官方网站:http://www.clicksun.cn
咨询热线:400-186-1886
服务邮箱:service@clicksun.cn