小学运动会的入场式上,几百个学生按班级排成队列,一个班接一个班地走进田径场。你站在队列中间,想看看“我们班走到哪儿了”“我在年级里算高个还是矮个”——你只需要扭头看一眼前后左右,队伍不会因为你看了这一眼就重新整编,每一个人都还站在原地。SQL 里的窗口函数就是这样一种“站在队伍里看世界”的本领:它不会像 GROUP BY 那样把一群人“压扁”成一条汇总记录,而是在每一行的旁边额外打开一扇窗,让你看到同一分组、同一排序规则下的其他行,当场算出排名、累计、上一行是谁,算完之后这一行还是原来的这一行。 这扇窗的“尺寸”由 OVER 子句控制:PARTITION BY 决定按哪列分组,ORDER BY 决定组内顺序,可选的框架子句决定窗口覆盖多远。这就是数据分析师做排行榜、同环比、连续登录分析时离不开的利器——一条 SQL,不丢一行,把复杂分析变成“扭头看一眼”。
掌握窗口函数,你就能用一条 SQL 解决排行榜、同环比、连续登录等复杂分析,从“只会聚合”进阶为“分析型 SQL 高手”,这是数据分析师与后端开发的核心竞争力之一。
可视化演示
展示窗口函数的排名函数差异、累计聚合框架和分区对比计算,通过模式切换理解OVER子句和窗口框架的核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
排名函数对比 ROW_NUMBER连续递增不重复 RANK并列跳号 DENSE_RANK并列不跳号
窗口函数概述
运动会入场式上,你想知道“我在年级里算高个还是矮个”,只需要扭头看看前后的人,队伍不会为了回答你而重新整编。窗口函数就是这种“站在原地环顾四周”的能力:它在不合并行的前提下,给每一行附加一个基于“邻居”计算出的结果。而 GROUP BY 则像老师解散队伍后重新点名,一个班只留下一句“全班到齐”的汇报,明细全部消失。
窗口函数之所以强大,是因为它保留了每一行的完整性,同时又能看到“窗外”的信息。一个典型的窗口函数调用由三部分构成:要计算的函数(SUM、RANK、LAG 等)、OVER 关键字、以及 OVER 括号里的窗口规则。窗口规则又由可选的分组列、排序列和框架子句组成。
一句话记住窗口函数:数据不搬家,队伍不散伙,每行都能站在原地看到全世界。排行榜、累计值、相邻比较,都是这扇窗里的风景。
- 函数与参数函数决定计算类型,聚合函数传列名,排名函数括号里通常为空
- OVER 关键字宣告“我要开一扇窗”的入口,是窗口函数区别于普通聚合的身份证
- PARTITION BY 分组列把数据切成互不干扰的片,每片一个窗口
- ORDER BY 排序列与框架决定窗口内顺序,框架进一步圈定前后范围
OVER子句结构
入场式开始前,总指挥手里有一张“入场规则表”:哪个班先走、班内按什么顺序、方队排多宽。OVER 子句就是这张规则表,它决定了窗口函数“看到的世界”长什么样。规则表可以有三种写法:空白、只分组、只排序,也可以三件套全上——每种写法对应一种视野。
空窗口 OVER() 把整张表当作一个窗口,SELECT COUNT(*) OVER() FROM 订单 会给每一行都附上相同的总订单数。只分组的 OVER(PARTITION BY dept) 按部门切窗,窗内所有行共享同一个部门身份。只排序的 OVER(ORDER BY month) 让窗口随当前行逐渐滑动,为累计计算铺路。三者组合则是最常用的形态:先分部门,再在部门内按月推进。
规则表定了,学生就知道自己属于哪个班、按什么顺序进场。OVER 的三种写法,对应分析中的三类经典问题:总量是多少、组内是多少、逐行累计是多少。
- 空窗 OVER()不写任何规则,全表就是一个窗口,每行看到的总数都一样
- 分组窗 PARTITION BY按 dept 列切分,窗口只在部门内部生效
- 排序窗 ORDER BYscore 降序排列,排名类函数必须依赖它才能定位“第几名”
- 组合窗先分部门再排序,是排行榜、组内累计的标准姿势
分组与排序
队列先按班级划好区域,这叫分组;班内再按身高排成一路纵队,这叫排序。分组和排序是两件独立的事,但合在一起威力最大:想找“班里最高的那个人”,你得先确定是哪个班,再在班内比身高。SQL 里,PARTITION BY 负责划区域,ORDER BY 负责排顺序。
只分组不排序时,SUM(amount) OVER (PARTITION BY dept) 会给每行附上“部门总销售额”,明细行一个不少;只排序不分组时,SUM(amount) OVER (ORDER BY month) 得到从第一个月到当前月的逐月累计。两者独立使用都能解决特定问题,但最经典的分析场景是组合使用——求“每个部门从年初到当前月的累计销售额”。
班级区域与身高的双重规则,让每个学生都拥有独一无二的“组内坐标”:分组划界、排序定向,窗口函数沿着坐标轴跑起来,永远不会跨界串组。
- PARTITION BY dept按部门把数据切片,跨部门的行互不可见
- ORDER BY month在部门内部按月从小到大排列,窗口随月份推进
- SUM(amount) 累加对“同部门、月份≤当前行”的所有行求和,得到截至当前的累计值
排名函数详解
老师要给每个班发“队列整齐度名次牌”,麻烦在于两个班打分一样,并列第一。名次牌怎么发?老师有三种办法:一是咬死顺序,先看到谁谁第一,绝不并列;二是两人并列第一,下一个直接排第三;三是两人并列第一,下一个依然排第二。三种发牌方式,对应 SQL 里的排名三兄弟。
ROW_NUMBER() 给每一行一个唯一且连续的序号,完全不理会并列;RANK() 遇到相同值并列,但名次会跳号;DENSE_RANK() 遇到相同值也并列,但名次不跳号。以成绩 [95, 90, 90, 85] 按降序排队为例,三者给出的结果截然不同:行号是 1、2、3、4;RANK 是 1、2、2、4;DENSE_RANK 是 1、2、2、3。
名次牌怎么发,取决于老师的性格;排名函数怎么选,取决于业务对并列的定义。写 SQL 之前先问一句:“并列怎么算?”答案定了,函数就定了。
- ROW_NUMBER不管成绩是否并列,序号永不重复,适合“取前 N 行”
- RANK并列者占用两个名次位,后面的名次跳号,适合“竞赛颁奖”
- DENSE_RANK并列后名次不跳号,序列保持紧凑,适合“等级划分”
偏移分析函数
站在队列里,你最常做的动作是“回头看”和“往前看”:回头看看上一个同学是谁(LAG),往前看看下一个是谁(LEAD),再远一点,还能看到队首和队尾(FIRST_VALUE / LAST_VALUE)。偏移函数就是这些“回头”的动作,它们专门用来取相邻行的值。
LAG(列,n) 返回当前行在窗口内按顺序往前数第 n 行的值,LEAD(列,n) 返回往后数第 n 行的值。FIRST_VALUE 和 LAST_VALUE 则分别返回窗口内第一个与最后一个值。这些函数几乎是“环比、同比、相邻差值”的定制工具:算月度环比,只需用 LAG 把上个月的销售额取出来,再用当前值减去它。
回头看向上一个,转身望向下一个,队首队尾尽收眼底。同环比、相邻差这些分析,不过是窗口函数里几个“回头”的动作而已。
- LAG(amount, 1)在当前行往前数 1 行,取那行的 amount 值
- OVER (ORDER BY month)必须先按月排序,“前后”才有明确含义
- 队首边界第一行前方无行,LAG 返回 NULL,需用 COALESCE 处理
聚合窗口统计
队列行进时,体委喜欢边走边数人头:“到目前为止,我们班已经走过去 10 个人了……现在 15 个了。”这种“走到哪数到哪”的计数方式,就是聚合窗口函数的累计求和。SUM、AVG、COUNT 这些平时跟着 GROUP BY 压缩行数的聚合函数,一旦放进 OVER 里就“变了性格”。
放进窗口后,聚合函数不再把多行压成一行,而是给每一行都附上“截至当前窗口”的汇总值。配合 ORDER BY 使用就得到累计值,配合 PARTITION BY 使用就是组内统计。最经典的是月度累计销售额:SUM(amount) OVER (PARTITION BY dept ORDER BY month),每个部门内部按月累加——第一行是当月值,第二行是前两个月之和,以此类推。
一路走一路报数,是体委的累计;站在队伍里问“我们班平均身高多少”,是组内平均。聚合窗口函数让“边走边算”成为一种自然。
- SUM(amount)声明“我要累计求和”,结果附加到每一行
- PARTITION BY dept把部门切成独立跑道,A 部门的累计不会混入 B 部门
- ORDER BY month按月推进,窗口逐步扩大,走过一行多算一行
框架子句控制
ORDER BY 决定了队伍的顺序,但“你能看到多远的范围”由框架子句说了算。有人爱看队首,从第一排一直看到自己;有人只看前后两个,左右邻座就够。看得远算得就多,看得近算得就少——同一个 SUM,范围不同,结果天差地别。
框架子句写在 OVER 内,用 ROWS BETWEEN 或 RANGE BETWEEN 精确划定窗口边界。ROWS BETWEEN 1 PRECEDING AND CURRENT ROW 表示“当前行和它前面一行”,ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 表示“从分区第一行到当前行”,ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING 表示“前后各一行”。ROWS 按物理行数数,RANGE 按值相等算——并列的行会被一起圈进窗口。
看多远由你定——只看前后各一个,窗口里就是三个人;一眼看到队首,窗口里就是整支队伍。框架子句就是那个决定视野远近的“扭头幅度”。
- ROWS按物理行数确定边界,不看值只看位置
- 1 PRECEDING边界起点是当前行往前 1 行
- 1 FOLLOWING边界终点是当前行往后 1 行
- 默认框架只写 ORDER BY 不写框架时,默认 RANGE 从分区首行累计到当前行
实战案例应用
入场式结束,统计老师要交三张表:各班名次榜、本月与去年同月的对比表、以及“哪些同学连续出勤不缺席”的名单。这三张表都不需要把学生叫出队列重新站队——窗口函数让每行站在原地,就把排名、同比、连续天数全部算了出来。
排行榜:按部门取销售额 TOP3,用 RANK 函数分组排序后筛出名次小于等于 3 的行。
同环比:用 LAG 取出上年同月销售额,与本月做差再除以前值,得到同比增幅。
连续登录:先对每个用户按登录日期编号,再用“日期减去编号”得到一个锚点值。连续登录的日子里锚点完全相同,一旦断签锚点就会跳变;最后按锚点分组计数,就是这个用户最长连续登录的天数。
统计老师的三张表,没有一张需要解散队伍。窗口函数让复杂分析沉淀为一条简洁的 SQL,而这正是它与众不同的魅力。
- ROW_NUMBER 编号每个用户按日期顺序编上 1、2、3……,日期与编号相减
- 锚点分组连续日期减去连续编号得到同一个值,断签则锚点改变
- 计数统计每组行数,即为连续登录天数,筛出大于等于 7 的组
AI 应用场景
大促实时销售排行榜
阶段:数据准备与特征工程(AI 建模前的 SQL 取数)
解决的问题:传统排名用自连接或三层子查询,SQL 冗长且性能差;窗口函数一次 OVER 完成分组排名,明细行全部保留,后续拼接用户特征、商品特征都方便。
具体案例:电商大促期间,数据分析师每小时刷新“各品类销售额 TOP10 商品榜”。一条 SELECT 配合 RANK() OVER (PARTITION BY 品类 ORDER BY 销售额 DESC) 直接出结果,榜单再作为热销特征喂给实时推荐模型。
经营同环比自动报表
阶段:指标计算与报表自动化(BI 看板的数据底座)
解决的问题:手工对着 Excel 找“上月数、去年同期数”费时易错;LAG 函数一条 SQL 把三列数据并排取出,指标口径统一,报表每天自动刷新。
具体案例:后端开发给运营部门写月度经营日报:用 LAG(amount, 1) 算环比、LAG(amount, 12) 算同比,一条 SQL 生成“本月 / 上月 / 去年同期”三列,再交给 BI 工具渲染成趋势图。
连续登录用户挖掘
阶段:用户画像与活跃度特征构建
解决的问题:人工核对登录明细无法回答“最长连续登录多少天”;日期减行号的锚点法,把连续性问题转化为分组计数问题,一次查询完成。
具体案例:数据产品经理想找出“连续登录 7 天以上”的种子用户用于推送策略:先对每个用户按日期编号做锚点分组,再 COUNT 组内天数并筛出达标用户,最终圈定高活跃人群。
Python 演示
# -*- coding: utf-8 -*-"""纯 Python 模拟 SQL 窗口函数: 排名、累计与偏移""" def window_rank(rows, partition_key, order_key): """模拟 ROW_NUMBER / RANK / DENSE_RANK 窗口计算。 先按分组列排序, 再按排序列降序, 组内依次编号。""" rows = sorted(rows, key=lambda r: (r[partition_key], -r[order_key])) prev_grp, prev_score = None, None row_pos, rank_pos, dense_count = 0, 0, 0 for row in rows: if row[partition_key] != prev_grp: prev_grp = row[partition_key] prev_score = None row_pos, rank_pos, dense_count = 0, 0, 0 row_pos += 1 if row[order_key] != prev_score: prev_score = row[order_key] rank_pos = row_pos dense_count += 1 row['_row_number'] = row_pos row['_rank'] = rank_pos row['_dense_rank'] = dense_count return rows def running_sum(rows, partition_key, order_key): """模拟 SUM OVER (PARTITION BY ... ORDER BY ...) 组内累计求和。""" rows = sorted(rows, key=lambda r: (r[partition_key], r[order_key])) prev_grp, total = None, 0 for row in rows: if row[partition_key] != prev_grp: prev_grp = row[partition_key] total = 0 total += row[order_key] row['_running'] = total return rows def lag(rows, partition_key, order_key, offset=1): """模拟 LAG: 取同组内前 offset 行的值, 组内第一行返回 None。""" rows = sorted(rows, key=lambda r: (r[partition_key], r[order_key])) for idx, row in enumerate(rows): if idx >= offset and rows[idx - offset][partition_key] == row[partition_key]: row['_lag'] = rows[idx - offset][order_key] else: row['_lag'] = None return rows if __name__ == '__main__': sales = [ {'dept': 'A', 'month': 1, 'amount': 120}, {'dept': 'A', 'month': 2, 'amount': 150}, {'dept': 'A', 'month': 3, 'amount': 90}, {'dept': 'B', 'month': 1, 'amount': 200}, {'dept': 'B', 'month': 2, 'amount': 180}, ] result = [dict(r) for r in sales] result = running_sum(result, 'dept', 'month') result = window_rank(result, 'dept', 'amount') result = lag(result, 'dept', 'amount') print('部门 月份 销售额 累计 行号 排名 稠密排名 上期') for r in result: print(r['dept'], r['month'], r['amount'], r['_running'], r['_row_number'], r['_rank'], r['_dense_rank'], r['_lag'])
rows = sorted(rows, key=lambda r: (r[partition_key], -r[order_key]))模拟 PARTITION BY 分组 + ORDER BY 降序,先排好队再编号if row[partition_key] != prev_grp:检测到新分组,行号、名次、稠密名次全部清零重来if row[order_key] != prev_score:分数发生变化才更新名次,这是 RANK 与 DENSE_RANK 并列逻辑的核心row['_row_number'] = row_pos给每行附加“站在队里第几位”的标记,明细行一个不少total += row[order_key]组内累计求和的核心一行,走到哪算到哪rows[idx - offset][partition_key] == row[partition_key]取上一行前先确认同组,防止 LAG 跨部门取值print(r['dept'], r['month'], ...)一行一条记录输出,直观对比三种排名结果
常见误区
误区窗口函数和 GROUP BY 一样,会把多行数据压缩成一行,所以 SUM 之后就看不到原来的明细了。
正解窗口函数恰恰相反——它不合并行,只是给每一行附加一个计算结果。GROUP BY 像老师解散队伍后点名汇总,窗口函数则像体委边走边数人头,队伍原封不动。判断标准很简单:输出行数是否等于输入行数,等于就是窗口函数。
为何常见:两者都能做 SUM、AVG、COUNT,初学者看到相同函数名就默认输出形态也相同。
误区RANK 和 DENSE_RANK 都是排名,区别不大,随便用一个就行。
正解面对并列时两者行为不同:成绩 [95,90,90,85],RANK 返回 1,2,2,4,名次跳号;DENSE_RANK 返回 1,2,2,3,名次不跳号。做“取前三名”时,RANK 可能只返回两个并列第二加一个第四,凑不满三个人;DENSE_RANK 则可能返回四个人。选错函数,榜单人数就错了。
为何常见:生活中“并列第三,下一个第五”和“并列第三,下一个第四”两种规则都存在,初学者很少意识到数据库默认了其中一种。
误区既然 WHERE 能做条件过滤,那我想筛“排名前 3 的行”,直接在 WHERE 里写 RANK() <= 3 不就行了?
正解窗口函数只能在 SELECT 或 ORDER BY 阶段计算,WHERE 过滤发生在窗口计算之前——WHERE 执行时,窗口结果还不存在。想按窗口结果过滤,必须把查询包一层子查询,在外层 WHERE 里引用排名列,就像“先发完名次牌,再按名次筛人”。
为何常见:聚合函数能配合 HAVING 使用,初学者把这个习惯迁移到窗口函数上,却忽略了执行顺序这道墙。
误区窗口函数必须写 ORDER BY,否则数据库会报错,所以每个 OVER 里都得有排序。
正解PARTITION BY 完全可以单独使用,比如 SUM(amount) OVER (PARTITION BY dept) 返回组内总和,每行都能看到。只有排名、偏移这类依赖“前后顺序”的函数才必须 ORDER BY,否则顺序不确定、结果无意义。
为何常见:教程和示例大多展示“分组加排序”的组合写法,容易让人误以为排序是窗口函数的前置条件。
误区只要写了 ORDER BY,SUM OVER 默认就只算当前这一行,因为“窗口就是眼前这一行”。
正解只要写了 ORDER BY 且没显式写框架,默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,即从分区第一行累计到当前行——这正是累计值能直接算出来的原因。而且 RANGE 模式下,排序列值相同的行会一起被纳入窗口,物理行数可能超出预期。
为何常见:不写框架时结果看起来“理所当然”,直到出现并列值导致累计结果与手算不一致,才让人警觉默认框架的存在。
面试题
Q1. 窗口函数与 GROUP BY 聚合最本质的区别是什么?
- A.窗口函数会把多行合并为一行
- B.窗口函数保留每行并附加计算← 正确答案
- C.窗口函数只处理整数类型数据
- D.窗口函数必须结合索引才生效
答案:B。窗口函数不合并行,它像体委边走边数人头,队伍原封不动;GROUP BY 才像解散队伍后点名汇总。C、D 都是无中生有的限制。
Q2. 成绩降序为 [95, 90, 90, 85],RANK() 返回的结果是?
- A.返回 1, 2, 2, 3 的名次序列
- B.返回 1, 2, 2, 4 的名次序列← 正确答案
- C.返回 1, 1, 3, 4 的名次序列
- D.返回 1, 2, 3, 4 的名次序列
答案:B。两个 90 并列第二,占用了 2、3 两个名次位,下一个 85 直接排第四,即 1,2,2,4。A 是 DENSE_RANK 的结果,C 是并列第一的写法,D 是 ROW_NUMBER。
Q3. 同一组数据 [95, 90, 90, 85],DENSE_RANK() 返回的结果是?
- A.返回 1, 2, 2, 3 的名次序列← 正确答案
- B.返回 1, 2, 2, 4 的名次序列
- C.返回 1, 1, 3, 4 的名次序列
- D.返回 1, 2, 3, 4 的名次序列
答案:A。DENSE_RANK 并列后不跳号,两个并列第二之后直接是第三,即 1,2,2,3。B 是 RANK 的跳号结果,D 是行号。
Q4. LAG(amount, 1) OVER (ORDER BY month) 在第一行返回什么?
- A.返回当前行自身的数值
- B.返回 NULL 表示没有上期← 正确答案
- C.返回 0 作为默认的数值
- D.返回下一行中的数值
答案:B。第一行没有“上个月”,LAG 找不到前一行就返回 NULL,就像队首同学回头看空无一人。NULL 参与运算会传染,记得用 COALESCE 兜底。
Q5. SUM(amount) OVER (ORDER BY day ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) 计算的是什么?
- A.计算从首日到当天的累计值
- B.计算当天与前一天两天的合计← 正确答案
- C.计算当天与后一天两天的合计
- D.计算整个分组分区的总和
答案:B。框架明确圈定“当前行前面 1 行到当前行”,即两天合计。A 是默认框架(UNBOUNDED PRECEDING)的结果,C 方向反了,D 忽略了框架限制。
Q6. 连续登录天数经典解法中,日期减 ROW_NUMBER 的作用是什么?
- A.计算用户总共登录过的次数
- B.让连续登录日得到相同锚点← 正确答案
- C.删除用户重复的登录日期
- D.让登录日期按倒序来排列
答案:B。连续日期减去连续编号,差值恒定,断签则差值跳变,于是“连续”转化为“同锚点分组”,分组计数即连续天数。A 是 COUNT 的职责,C、D 与题意无关。
Q7. 关于窗口函数的执行位置,下列说法正确的是?
- A.可以放在 WHERE 中直接使用
- B.在 SELECT 阶段基于结果集计算← 正确答案
- C.只能配合 GROUP BY 才能使用
- D.只允许写在 ORDER BY 语句中
答案:B。窗口函数在 SELECT 阶段基于已过滤的结果集计算,WHERE 执行时它还不存在;想按窗口结果过滤需包一层子查询。C、D 都是不存在的限制。
Q8. 关于 PARTITION BY 与 ORDER BY 的用法,下列说法正确的有?
- A.只分组不排序可算组内总计← 正确答案
- B.分组加排序可算组内排名← 正确答案
- C.只排序不分组可算全表累计← 正确答案
- D.两者不能同时出现在 OVER 中
答案:ABC。PARTITION BY 与 ORDER BY 是窗口规则的两个独立维度,可单独用、可组合用。D 说反了,两者正是排行榜和组内累计的标准组合。
Q9. 下列哪些函数属于偏移分析函数?
- A.LAG 取同组内上一行的值← 正确答案
- B.LEAD 取同组内下一行的值← 正确答案
- C.ROW_NUMBER 用来计算行号
- D.FIRST_VALUE 取窗口的首值← 正确答案
答案:ABD。LAG、LEAD、FIRST_VALUE、LAST_VALUE 都属于“回头看/向前看”的偏移家族。ROW_NUMBER 是排名函数,对应“站在队里报自己的号”,不算偏移。
Q10. 关于默认框架,下列说法正确的有?
- A.有 ORDER BY 时默认累计到当前行← 正确答案
- B.默认框架按 RANGE 值来界定← 正确答案
- C.相同排序列值的行会一并纳入← 正确答案
- D.有 ORDER BY 时默认只看当前行
答案:ABC。写了 ORDER BY 且不写框架时,默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,从分区首行累计到当前行,且 RANGE 按值界定、并列值一起纳入。D 正好说反,默认并非只看当前行。