乐于分享
好东西不私藏

被Excel坑了3年,我用这5个SQL窗口函数把数据分析效率翻了4倍

被Excel坑了3年,我用这5个SQL窗口函数把数据分析效率翻了4倍

 👆 点击上方蓝字关注我们,一个专注职场、投资、AI的公众号 

船长说句扎心的:90%的数据分析师,SQL只用到了不到30%的功力。

剩下那70%,是什么?

窗口函数(Window Function)。

大多数人遇到排名、同比、累计、移动平均这类问题,第一反应是写子查询、用Excel辅助处理,或者直接导出后用Python转一遍。

折腾2小时,写了100行代码,最后得到的结果,一个窗口函数,5行SQL就能搞定。

这篇文章,船长用5个真实业务场景,带你把窗口函数彻底搞懂。

 💬 你现在用SQL做排名/同比/累计的时候,是用窗口函数还是其他方法?评论区扣1:窗口函数 / 扣2:还在用子查询 

 先搞懂:窗口函数是什么? 

窗口函数最难理解的地方在于它的名字——”窗口”是什么意思?

一句话解释:普通聚合函数(SUM、AVG)会把多行压成一行;窗口函数在保留原来每一行的同时,给每行附加一个基于”窗口范围”计算的结果。

基本语法长这样:

函数名() OVER (   PARTITION BY 分组字段   ORDER BY 排序字段   ROWS BETWEEN 范围起点 AND 范围终点 )

PARTITION BY:告诉SQL按哪个维度分组(类似GROUP BY,但不压缩行数)

ORDER BY:窗口内的排序规则

ROWS BETWEEN:窗口的滑动范围(可以省略,默认到当前行)

 📌 内容来源:MySQL 8.0官方文档Window Functions章节;PostgreSQL 14官方文档 

 函数1:ROW_NUMBER() — 排名去重,再也不怕重复值 

业务场景:每个城市销售额最高的3个门店

传统写法:先用子查询算出每城市TOP3门店,再JOIN回来,至少写20行。

窗口函数写法

SELECT city, store_name, sales,   ROW_NUMBER() OVER (     PARTITION BY city     ORDER BY sales DESC   ) AS rn FROM store_sales WHERE rn <= 3

ROW_NUMBER() 会在每个城市分区内,按销售额从高到低给每家门店编一个不重复的序号(即使销售额相同,序号也不同)。

类似的还有 RANK()(相同值并列排名,下一名跳号)和 DENSE_RANK()(相同值并列排名,下一名连续)。

选哪个:

— 要唯一序号 → ROW_NUMBER()

— 要带跳号的名次 → RANK()

— 要连续名次 → DENSE_RANK()

 函数2:LAG() / LEAD() — 同比环比,一行搞定 

业务场景:计算每月销售额环比增长率

LAG(字段, n) 取”往前第n行的值”;LEAD(字段, n) 取”往后第n行的值”。

写法

SELECT   month,   sales,   LAG(sales, 1) OVER (ORDER BY month) AS last_month_sales,   ROUND((sales - LAG(sales,1) OVER (ORDER BY month))         / LAG(sales,1) OVER (ORDER BY month) * 100, 2) AS mom_growth_pct FROM monthly_sales

以前做这个,你得先自JOIN一次,或者在Python里做shift()。现在直接在SQL层完成,减少一次数据传输,也减少一个出错节点。

做同比(year-over-year)的话,把 LAG(sales, 1) 换成 LAG(sales, 12) 就行,往前取12行(12个月前)。

 💬 你做同比环比一般用什么方法?评论区告诉船长,我来看看大家被坑最多的地方 

 函数3:SUM() OVER — 累计求和,不用Python了 

业务场景:计算每个用户的消费累计金额(时序累加)

SELECT   user_id,   order_date,   amount,   SUM(amount) OVER (     PARTITION BY user_id     ORDER BY order_date     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW   ) AS cumulative_amount FROM orders

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 是关键:从该用户第一条记录开始,累加到当前行。

这个写法还能做移动平均(7日均线)

AVG(sales) OVER (   ORDER BY date   ROWS BETWEEN 6 PRECEDING AND CURRENT ROW )

往前取6行+当前行,共7行,算平均——7日移动均线就是这么来的。

 函数4:NTILE() — 用户分层,4行代码搞定 

业务场景:把用户按消费金额分成4层(高/中高/中低/低价值用户)

SELECT   user_id,   total_spend,   NTILE(4) OVER (ORDER BY total_spend DESC) AS value_tier FROM user_spend

NTILE(4) 把所有用户平均切成4份,第1份是最高消费25%的用户。

想做RFM模型分层?把这个用到R/F/M三个维度分别打分,然后组合成用户标签,思路完全一样。

 📌 内容来源:《数据分析实战45讲》(极客时间);MySQL官方文档NTILE Function说明 

 函数5:FIRST_VALUE() / LAST_VALUE() — 找组内极值,不再需要子查询 

业务场景:每个产品类别中,每行都显示该类别的最高销售额产品名称

SELECT   category,   product_name,   sales,   FIRST_VALUE(product_name) OVER (     PARTITION BY category     ORDER BY sales DESC   ) AS top_product_in_category FROM products

FIRST_VALUE() 取窗口内第一行的值(配合 ORDER BY sales DESC,就是销售额最高的那个)。

以前做这种”组内第一名的某个字段”,要写双层子查询或者JOIN,代码量通常超过30行。现在这一段,8行。

 踩坑提醒:这3个地方最容易写错 

❌ 坑1:在WHERE子句里直接过滤窗口函数结果

窗口函数在WHERE之后执行,所以不能直接写 WHERE rn <= 3。要先用子查询或CTE包一层,再在外层过滤。

❌ 坑2:LAST_VALUE() 的默认窗口范围问题

LAST_VALUE() 默认窗口到当前行(不是分区末尾),所以在没有指定 ROWS BETWEEN 的情况下,LAST_VALUE() 始终返回当前行的值。必须加 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING 才能得到真正的最后一个值。

❌ 坑3:PARTITION BY 和 GROUP BY 同时用时的逻辑混乱

窗口函数在GROUP BY之后执行,如果两者的分组维度不一致,会得到意料之外的结果。遇到这种情况,先明确数据粒度,再决定窗口的PARTITION维度。

 📌 内容来源:StackOverflow SQL Window Function常见问题汇总;《SQL必知必会》第5版(Ben Forta,人民邮电出版社) 

会用窗口函数,不只是写SQL更优雅——是真的能让你在数据分析的核心能力上跨一个档次。

船长的话:船长刚开始做数据分析的时候,也是对窗口函数一知半解,遇到复杂需求就绕路用Python。直到某次做一个用户行为分析项目,死活搞不定一个7日留存的移动计算,被同事用一个窗口函数5分钟搞定了,我当场在工位上沉默了3分钟。从那天起,我把窗口函数的每个细节都摸透了。你现在只需要这篇文章,就能省去我当时踩的那些坑。

5个函数里,你现在最想先搞明白哪一个?评论区告诉我,船长来出一期详细的实战练习。

觉得有收获,转给也在学SQL的同事,大家一起提效。


📚 往期精彩推荐


— 船长Talk —

数据分析 + 职场真相 + 投资洞察

欢迎公众号后台回复”进群”一起交流一起进步