被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 —
数据分析 + 职场真相 + 投资洞察
欢迎公众号后台回复”进群”一起交流一起进步

夜雨聆风