小学运动会的入场式上,几百个学生按班级排成队列,一个班接一个班地走进田径场。你站在队列中间,想看看“我们班走到哪儿了”“我在年级里算高个还是矮个”——你只需要扭头看一眼前后左右,队伍不会因为你看了这一眼就重新整编,每一个人都还站在原地。SQL 里的窗口函数就是这样一种“站在队伍里看世界”的本领:它不会像 GROUP BY 那样把一群人“压扁”成一条汇总记录,而是在每一行的旁边额外打开一扇窗,让你看到同一分组、同一排序规则下的其他行,当场算出排名、累计、上一行是谁,算完之后这一行还是原来的这一行。 这扇窗的“尺寸”由 OVER 子句控制:PARTITION BY 决定按哪列分组,ORDER BY 决定组内顺序,可选的框架子句决定窗口覆盖多远。这就是数据分析师做排行榜、同环比、连续登录分析时离不开的利器——一条 SQL,不丢一行,把复杂分析变成“扭头看一眼”。

岗位关联

掌握窗口函数,你就能用一条 SQL 解决排行榜、同环比、连续登录等复杂分析,从“只会聚合”进阶为“分析型 SQL 高手”,这是数据分析师与后端开发的核心竞争力之一。

目录
  1. 可视化演示
  2. 窗口函数概述
  3. OVER子句结构
  4. 分组与排序
  5. 排名函数详解
  6. 偏移分析函数
  7. 聚合窗口统计
  8. 框架子句控制
  9. 实战案例应用
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示窗口函数的排名函数差异、累计聚合框架和分区对比计算,通过模式切换理解OVER子句和窗口框架的核心机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

排名函数对比 ROW_NUMBER连续递增不重复 RANK并列跳号 DENSE_RANK并列不跳号

窗口函数概述

运动会入场式上,你想知道“我在年级里算高个还是矮个”,只需要扭头看看前后的人,队伍不会为了回答你而重新整编。窗口函数就是这种“站在原地环顾四周”的能力:它在不合并行的前提下,给每一行附加一个基于“邻居”计算出的结果。而 GROUP BY 则像老师解散队伍后重新点名,一个班只留下一句“全班到齐”的汇报,明细全部消失。

窗口函数之所以强大,是因为它保留了每一行的完整性,同时又能看到“窗外”的信息。一个典型的窗口函数调用由三部分构成:要计算的函数(SUM、RANK、LAG 等)、OVER 关键字、以及 OVER 括号里的窗口规则。窗口规则又由可选的分组列、排序列和框架子句组成。

一句话记住窗口函数:数据不搬家,队伍不散伙,每行都能站在原地看到全世界。排行榜、累计值、相邻比较,都是这扇窗里的风景。

窗口函数名(参数)  OVER([PARTITION BY 分组列]  [ORDER BY 排序列]  [框架子句])\displaystyle \text{窗口函数名}(\text{参数}) \; \text{OVER} \left( [\text{PARTITION BY 分组列}] \; [\text{ORDER BY 排序列}] \; [\text{框架子句}] \right)
这一行公式是窗口函数的总纲:函数决定“算什么”,OVER 里的三件套决定“在哪个范围算”。
  1. 函数与参数函数决定计算类型,聚合函数传列名,排名函数括号里通常为空
    SUM(amount)RANK()\displaystyle \text{SUM}(amount) \qquad \text{RANK}()
  2. OVER 关键字宣告“我要开一扇窗”的入口,是窗口函数区别于普通聚合的身份证
    OVER\displaystyle \text{OVER}
  3. PARTITION BY 分组列把数据切成互不干扰的片,每片一个窗口
    PARTITION BY dept\displaystyle \text{PARTITION BY dept}
  4. ORDER BY 排序列与框架决定窗口内顺序,框架进一步圈定前后范围
    ORDER BY score DESCROWS BETWEEN 1 PRECEDING AND CURRENT ROW\displaystyle \text{ORDER BY score DESC} \qquad \text{ROWS BETWEEN 1 PRECEDING AND CURRENT ROW}

OVER子句结构

入场式开始前,总指挥手里有一张“入场规则表”:哪个班先走、班内按什么顺序、方队排多宽。OVER 子句就是这张规则表,它决定了窗口函数“看到的世界”长什么样。规则表可以有三种写法:空白、只分组、只排序,也可以三件套全上——每种写法对应一种视野。

空窗口 OVER() 把整张表当作一个窗口,SELECT COUNT(*) OVER() FROM 订单 会给每一行都附上相同的总订单数。只分组的 OVER(PARTITION BY dept) 按部门切窗,窗内所有行共享同一个部门身份。只排序的 OVER(ORDER BY month) 让窗口随当前行逐渐滑动,为累计计算铺路。三者组合则是最常用的形态:先分部门,再在部门内按月推进。

规则表定了,学生就知道自己属于哪个班、按什么顺序进场。OVER 的三种写法,对应分析中的三类经典问题:总量是多少、组内是多少、逐行累计是多少。

COUNT(∗)  OVER()SUM(amount)  OVER(PARTITION BY dept)RANK()  OVER(PARTITION BY dept  ORDER BY score DESC)\displaystyle \text{COUNT}(*) \; \text{OVER} () \qquad \text{SUM}(amount) \; \text{OVER} \left( \text{PARTITION BY dept} \right) \qquad \text{RANK}() \; \text{OVER} \left( \text{PARTITION BY dept} \; \text{ORDER BY score DESC} \right)
三种写法从左到右,视野从“全班都算”到“部门内算”再到“部门内按顺序算”。
  1. 空窗 OVER()不写任何规则,全表就是一个窗口,每行看到的总数都一样
    OVER()\displaystyle \text{OVER}()
  2. 分组窗 PARTITION BY按 dept 列切分,窗口只在部门内部生效
    PARTITION BY dept\displaystyle \text{PARTITION BY dept}
  3. 排序窗 ORDER BYscore 降序排列,排名类函数必须依赖它才能定位“第几名”
    ORDER BY score DESC\displaystyle \text{ORDER BY score DESC}
  4. 组合窗先分部门再排序,是排行榜、组内累计的标准姿势
    PARTITION BY dept  ORDER BY score DESC\displaystyle \text{PARTITION BY dept} \; \text{ORDER BY score DESC}

分组与排序

队列先按班级划好区域,这叫分组;班内再按身高排成一路纵队,这叫排序。分组和排序是两件独立的事,但合在一起威力最大:想找“班里最高的那个人”,你得先确定是哪个班,再在班内比身高。SQL 里,PARTITION BY 负责划区域,ORDER BY 负责排顺序。

只分组不排序时,SUM(amount) OVER (PARTITION BY dept) 会给每行附上“部门总销售额”,明细行一个不少;只排序不分组时,SUM(amount) OVER (ORDER BY month) 得到从第一个月到当前月的逐月累计。两者独立使用都能解决特定问题,但最经典的分析场景是组合使用——求“每个部门从年初到当前月的累计销售额”。

班级区域与身高的双重规则,让每个学生都拥有独一无二的“组内坐标”:分组划界、排序定向,窗口函数沿着坐标轴跑起来,永远不会跨界串组。

SUM(amount)  OVER(PARTITION BY dept  ORDER BY month)\displaystyle \text{SUM}(amount) \; \text{OVER} \left( \text{PARTITION BY dept} \; \text{ORDER BY month} \right)
这个公式是“组内累计”的标准模板:分组划界、排序定向、求和一路累加。
  1. PARTITION BY dept按部门把数据切片,跨部门的行互不可见
    PARTITION BY dept\displaystyle \text{PARTITION BY dept}
  2. ORDER BY month在部门内部按月从小到大排列,窗口随月份推进
    ORDER BY month\displaystyle \text{ORDER BY month}
  3. SUM(amount) 累加对“同部门、月份≤当前行”的所有行求和,得到截至当前的累计值
    ∑同组且 month≤当前 monthamount\displaystyle \sum_{\text{同组且 month} \le \text{当前 month}} 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():1,2,3,4RANK():1,2,2,4DENSE_RANK():1,2,2,3\displaystyle \text{ROW\_NUMBER}(): 1,2,3,4 \qquad \text{RANK}(): 1,2,2,4 \qquad \text{DENSE\_RANK}(): 1,2,2,3
两行 90 分并列第二:RANK 让下一位直接变第四,DENSE_RANK 让下一位还是第三——差别就在“跳不跳号”。
  1. ROW_NUMBER不管成绩是否并列,序号永不重复,适合“取前 N 行”
    ROW_NUMBER()⇒1,2,3,4\displaystyle \text{ROW\_NUMBER}() \Rightarrow 1,2,3,4
  2. RANK并列者占用两个名次位,后面的名次跳号,适合“竞赛颁奖”
    RANK()⇒1,2,2,4\displaystyle \text{RANK}() \Rightarrow 1,2,2,4
  3. DENSE_RANK并列后名次不跳号,序列保持紧凑,适合“等级划分”
    DENSE_RANK()⇒1,2,2,3\displaystyle \text{DENSE\_RANK}() \Rightarrow 1,2,2,3

偏移分析函数

站在队列里,你最常做的动作是“回头看”和“往前看”:回头看看上一个同学是谁(LAG),往前看看下一个是谁(LEAD),再远一点,还能看到队首和队尾(FIRST_VALUE / LAST_VALUE)。偏移函数就是这些“回头”的动作,它们专门用来取相邻行的值。

LAG(列,n) 返回当前行在窗口内按顺序往前数第 n 行的值,LEAD(列,n) 返回往后数第 n 行的值。FIRST_VALUE 和 LAST_VALUE 则分别返回窗口内第一个与最后一个值。这些函数几乎是“环比、同比、相邻差值”的定制工具:算月度环比,只需用 LAG 把上个月的销售额取出来,再用当前值减去它。

回头看向上一个,转身望向下一个,队首队尾尽收眼底。同环比、相邻差这些分析,不过是窗口函数里几个“回头”的动作而已。

上月额=LAG(amount,1)  OVER(ORDER BY month)\displaystyle \text{上月额} = \text{LAG}(amount, 1) \; \text{OVER} \left( \text{ORDER BY month} \right)
注意:第一个月没有“上个月”,LAG 返回 NULL,计算前要用 COALESCE 兜底,否则差值全是空。
  1. LAG(amount, 1)在当前行往前数 1 行,取那行的 amount 值
    LAG(amount,1)\displaystyle \text{LAG}(amount, 1)
  2. OVER (ORDER BY month)必须先按月排序,“前后”才有明确含义
    OVER(ORDER BY month)\displaystyle \text{OVER} \left( \text{ORDER BY month} \right)
  3. 队首边界第一行前方无行,LAG 返回 NULL,需用 COALESCE 处理
    COALESCE(LAG(amount,1),0)\displaystyle \text{COALESCE}(\text{LAG}(amount, 1), 0)

聚合窗口统计

队列行进时,体委喜欢边走边数人头:“到目前为止,我们班已经走过去 10 个人了……现在 15 个了。”这种“走到哪数到哪”的计数方式,就是聚合窗口函数的累计求和。SUM、AVG、COUNT 这些平时跟着 GROUP BY 压缩行数的聚合函数,一旦放进 OVER 里就“变了性格”。

放进窗口后,聚合函数不再把多行压成一行,而是给每一行都附上“截至当前窗口”的汇总值。配合 ORDER BY 使用就得到累计值,配合 PARTITION BY 使用就是组内统计。最经典的是月度累计销售额:SUM(amount) OVER (PARTITION BY dept ORDER BY month),每个部门内部按月累加——第一行是当月值,第二行是前两个月之和,以此类推。

一路走一路报数,是体委的累计;站在队伍里问“我们班平均身高多少”,是组内平均。聚合窗口函数让“边走边算”成为一种自然。

SUM(amount)  OVER(PARTITION BY dept  ORDER BY month)\displaystyle \text{SUM}(amount) \; \text{OVER} \left( \text{PARTITION BY dept} \; \text{ORDER BY month} \right)
同样的模板换函数名就能得到组内平均(AVG)和组内计数(COUNT),逻辑完全一致。
  1. SUM(amount)声明“我要累计求和”,结果附加到每一行
    SUM(amount)\displaystyle \text{SUM}(amount)
  2. PARTITION BY dept把部门切成独立跑道,A 部门的累计不会混入 B 部门
    PARTITION BY dept\displaystyle \text{PARTITION BY dept}
  3. ORDER BY month按月推进,窗口逐步扩大,走过一行多算一行
    ORDER BY month\displaystyle \text{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 按值相等算——并列的行会被一起圈进窗口。

看多远由你定——只看前后各一个,窗口里就是三个人;一眼看到队首,窗口里就是整支队伍。框架子句就是那个决定视野远近的“扭头幅度”。

SUM(amount)  OVER(ORDER BY day  ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)\displaystyle \text{SUM}(amount) \; \text{OVER} \left( \text{ORDER BY day} \; \text{ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING} \right)
这就是三天的移动合计:前一天、当天、后一天。移动平均只需把 SUM 换成 AVG。
  1. ROWS按物理行数确定边界,不看值只看位置
    ROWS\displaystyle \text{ROWS}
  2. 1 PRECEDING边界起点是当前行往前 1 行
    1 PRECEDING\displaystyle \text{1 PRECEDING}
  3. 1 FOLLOWING边界终点是当前行往后 1 行
    1 FOLLOWING\displaystyle \text{1 FOLLOWING}
  4. 默认框架只写 ORDER BY 不写框架时,默认 RANGE 从分区首行累计到当前行
    RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\displaystyle \text{RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW}

实战案例应用

入场式结束,统计老师要交三张表:各班名次榜、本月与去年同月的对比表、以及“哪些同学连续出勤不缺席”的名单。这三张表都不需要把学生叫出队列重新站队——窗口函数让每行站在原地,就把排名、同比、连续天数全部算了出来。

排行榜:按部门取销售额 TOP3,用 RANK 函数分组排序后筛出名次小于等于 3 的行。

同环比:用 LAG 取出上年同月销售额,与本月做差再除以前值,得到同比增幅。

连续登录:先对每个用户按登录日期编号,再用“日期减去编号”得到一个锚点值。连续登录的日子里锚点完全相同,一旦断签锚点就会跳变;最后按锚点分组计数,就是这个用户最长连续登录的天数。

统计老师的三张表,没有一张需要解散队伍。窗口函数让复杂分析沉淀为一条简洁的 SQL,而这正是它与众不同的魅力。

RANK()  OVER(PARTITION BY dept  ORDER BY amount DESC)≤3\displaystyle \text{RANK}() \; \text{OVER} \left( \text{PARTITION BY dept} \; \text{ORDER BY amount DESC} \right) \le 3
  1. ROW_NUMBER 编号每个用户按日期顺序编上 1、2、3……,日期与编号相减
    login_date−ROW_NUMBER()\displaystyle \text{login\_date} - \text{ROW\_NUMBER}()
  2. 锚点分组连续日期减去连续编号得到同一个值,断签则锚点改变
    GROUP BY user, anchor\displaystyle \text{GROUP BY user, anchor}
  3. 计数统计每组行数,即为连续登录天数,筛出大于等于 7 的组
    COUNT(∗)≥7\displaystyle \text{COUNT}(*) \ge 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 演示

python66 行
# -*- 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'])
目的:用纯 Python 演示窗口函数三大核心能力——组内排名、累计求和、偏移取值,帮助理解 SQL 窗口函数在数据库内部的执行逻辑。
  1. rows = sorted(rows, key=lambda r: (r[partition_key], -r[order_key]))模拟 PARTITION BY 分组 + ORDER BY 降序,先排好队再编号
  2. if row[partition_key] != prev_grp:检测到新分组,行号、名次、稠密名次全部清零重来
  3. if row[order_key] != prev_score:分数发生变化才更新名次,这是 RANK 与 DENSE_RANK 并列逻辑的核心
  4. row['_row_number'] = row_pos给每行附加“站在队里第几位”的标记,明细行一个不少
  5. total += row[order_key]组内累计求和的核心一行,走到哪算到哪
  6. rows[idx - offset][partition_key] == row[partition_key]取上一行前先确认同组,防止 LAG 跨部门取值
  7. 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 聚合最本质的区别是什么?

单选题 · easy

  • A.窗口函数会把多行合并为一行
  • B.窗口函数保留每行并附加计算← 正确答案
  • C.窗口函数只处理整数类型数据
  • D.窗口函数必须结合索引才生效

答案:B。窗口函数不合并行,它像体委边走边数人头,队伍原封不动;GROUP BY 才像解散队伍后点名汇总。C、D 都是无中生有的限制。

Q2. 成绩降序为 [95, 90, 90, 85],RANK() 返回的结果是?

单选题 · easy

  • 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() 返回的结果是?

单选题 · mid

  • 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) 在第一行返回什么?

单选题 · mid

  • 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) 计算的是什么?

单选题 · hard

  • A.计算从首日到当天的累计值
  • B.计算当天与前一天两天的合计← 正确答案
  • C.计算当天与后一天两天的合计
  • D.计算整个分组分区的总和

答案:B。框架明确圈定“当前行前面 1 行到当前行”,即两天合计。A 是默认框架(UNBOUNDED PRECEDING)的结果,C 方向反了,D 忽略了框架限制。

Q6. 连续登录天数经典解法中,日期减 ROW_NUMBER 的作用是什么?

单选题 · hard

  • A.计算用户总共登录过的次数
  • B.让连续登录日得到相同锚点← 正确答案
  • C.删除用户重复的登录日期
  • D.让登录日期按倒序来排列

答案:B。连续日期减去连续编号,差值恒定,断签则差值跳变,于是“连续”转化为“同锚点分组”,分组计数即连续天数。A 是 COUNT 的职责,C、D 与题意无关。

Q7. 关于窗口函数的执行位置,下列说法正确的是?

单选题 · easy

  • A.可以放在 WHERE 中直接使用
  • B.在 SELECT 阶段基于结果集计算← 正确答案
  • C.只能配合 GROUP BY 才能使用
  • D.只允许写在 ORDER BY 语句中

答案:B。窗口函数在 SELECT 阶段基于已过滤的结果集计算,WHERE 执行时它还不存在;想按窗口结果过滤需包一层子查询。C、D 都是不存在的限制。

Q8. 关于 PARTITION BY 与 ORDER BY 的用法,下列说法正确的有?

多选题 · mid

  • A.只分组不排序可算组内总计← 正确答案
  • B.分组加排序可算组内排名← 正确答案
  • C.只排序不分组可算全表累计← 正确答案
  • D.两者不能同时出现在 OVER 中

答案:ABC。PARTITION BY 与 ORDER BY 是窗口规则的两个独立维度,可单独用、可组合用。D 说反了,两者正是排行榜和组内累计的标准组合。

Q9. 下列哪些函数属于偏移分析函数?

多选题 · mid

  • A.LAG 取同组内上一行的值← 正确答案
  • B.LEAD 取同组内下一行的值← 正确答案
  • C.ROW_NUMBER 用来计算行号
  • D.FIRST_VALUE 取窗口的首值← 正确答案

答案:ABD。LAG、LEAD、FIRST_VALUE、LAST_VALUE 都属于“回头看/向前看”的偏移家族。ROW_NUMBER 是排名函数,对应“站在队里报自己的号”,不算偏移。

Q10. 关于默认框架,下列说法正确的有?

多选题 · hard

  • A.有 ORDER BY 时默认累计到当前行← 正确答案
  • B.默认框架按 RANGE 值来界定← 正确答案
  • C.相同排序列值的行会一并纳入← 正确答案
  • D.有 ORDER BY 时默认只看当前行

答案:ABC。写了 ORDER BY 且不写框架时,默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,从分区首行累计到当前行,且 RANGE 按值界定、并列值一起纳入。D 正好说反,默认并非只看当前行。