LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

还在用子查询硬凑?窗口函数才是 SQL 真正的神器

admin
2026年4月13日 22:33 本文热度 16

很多人写 SQL,一碰到“分组后再取明细”“每组 Top N”“累计求和”“同比环比”“取上一条/下一条记录”这类需求,就开始疯狂套子查询。

结果通常是这样的:

  • • SQL 越写越长
  • • 逻辑一层套一层
  • • 自己第二天都不想看
  • • 性能还未必好

说得难听点,很多子查询不是业务复杂,而是写法原始。

如果你还在靠子查询硬凑这些分析型需求,那窗口函数大概率就是你一直没认真用过的那把刀。

窗口函数的价值,不是“语法高级”,而是它能把“统计结果”和“原始明细”同时留在一张结果集里。

这件事,看起来只是写法变化,实际会直接改变你组织 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:窗口范围到底覆盖哪些行

不是每次都要写全。

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

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() 不是全表排,而是:

  • • 先按 dept_id 分组
  • • 每组内按 salary desc 排序
  • • 然后重新编号

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

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

相关函数顺手记一下

row_number()
rank
()
dense_rank
()

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

  • • 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 &#x27;高于均值&#x27;
    when
 salary < avg(salary) over(partition by dept_id) then &#x27;低于均值&#x27;
    else
 &#x27;等于均值&#x27;
  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;

这里:

  • • 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;

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

窗口函数则是原地解决。


场景 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

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

以前很多人会这么干:

  • • 先 group by user_id 求 min(order_time)
  • • 再 join 回原表拿整行明细

当然能做。

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


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

这里要说句实在话。

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

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

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

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

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

  • • 每组前几名
  • • 和上一条比较
  • • 累计值
  • • 明细 + 统计同时保留
  • • 组内排序
  • • 第一次 / 最后一次
  • • 同比 / 环比

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

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


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

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

正确姿势不是背:

  • • row_number() 怎么写
  • • lag() 有几个参数
  • • sum() over() 的格式是什么

而是先识别:

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

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

换句话说:

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

但代价通常是:

  • • SQL 更长
  • • 可读性更差
  • • 更容易出错
  • • 维护成本更高

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

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

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

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

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

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


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

最常用的其实就这些:

  • • row_number():组内唯一编号
  • • rank():组内排名,允许并列,后续跳号
  • • dense_rank():组内排名,允许并列,但不跳号
  • • sum() over():累计求和 / 窗口求和
  • • avg() over():窗口平均值
  • • max() over() / min() over():窗口最大最小值
  • • lag():取前一条
  • • lead():取后一条

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


阅读原文:原文链接


该文章在 2026/4/14 16:36:04 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-2  粤公网安备44030602007207号