SQL窗口函数面试答不上怎么办?3类高频题让面试官相信你会做业务分析
July 5, 2026 · 8:08 AM

SQL窗口函数面试答不上怎么办?3类高频题让面试官相信你会做业务分析

这期把 SQL 窗口函数面试拆成 Top N、前后行对比、累计与移动窗口 3 类题,给出面试官看重的判断逻辑、SQL 模板和现场讲解话术,帮你避免只背函数名却写不出查询。

开场如果遇到 SQL 窗口函数题,不要先背 row_number()rank()lag() 这些函数名。先说一句:「这题的关键是保留明细行,同时在每个业务分组里排序或做前后行计算。」这句话能把你从「会背语法」拉到「知道为什么这样写」。
窗口函数最容易把候选人分成两类:一类知道函数名,但不知道 PARTITION BY 该按用户、部门还是订单分;另一类能先讲清业务口径,再写 SQL。面试官要找的是第二类。

先把窗口函数讲成一句人话

窗口函数可以在一组与当前行相关的记录上做计算,但不会像普通聚合那样把多行合成一行;PostgreSQL 文档也明确说,窗口函数保留每一行自己的身份,只是在背后访问与当前行相关的更多行。1 MySQL 文档同样用 SUM() OVER() 举例说明:窗口计算会给每一行产出结果,而不是把分组压成单行。2
面试里可以这样翻译:
GROUP BY 是「把一组记录合成一个结果」;窗口函数是「每行还在,但多了一列分组内排名、累计值或前后行对比」。
这就是为什么「每个部门薪资最高的人」「每个用户第二次下单」「每天 GMV 的 7 日移动平均」这类题常用窗口函数。结果里通常还要保留人、订单、日期这些明细字段,单纯 GROUP BY 很容易把细节压没。

面试官真正盯的不是函数名

面试官追问他在判断什么你应该先说什么
为什么不用 GROUP BY你是否知道窗口函数会保留明细行,而普通聚合会压缩结果。1「因为最终要返回具体员工/订单/日期,不能只返回一个分组汇总值。」
PARTITION BY 按什么分?你有没有看懂业务对象。「题目问每个用户,就按用户分;问每个品类,就按品类分。」
排序字段怎么定?你是否能处理并列、时间先后和唯一性。MySQL 文档提醒,ROW_NUMBER() 如果没有在窗口定义里写 ORDER BY,分区内编号没有确定顺序。2「先按业务指标降序,再加唯一字段兜底,避免并列时结果漂移。」
要不要写窗口范围?你是否知道累计、移动平均这类题不是只靠 ORDER BY。Microsoft 文档把 ROWS/RANGE 描述为限制分区内窗口起止点的语法,常用于移动平均、累计值、Top N 等场景。3「累计用从分区开头到当前行,移动窗口要明确前 N 行或后 N 行。」
如果你只说「用窗口函数」,面试官还会继续追问;如果你把这四个口径先交代掉,后面的 SQL 即使有小语法差异,也更容易被认为思路靠谱。

高频题型一:Top N per group,别把并列名次说糊

题目长这样:
  • 每个部门工资最高的 2 名员工。
  • 每个品类销售额最高的 3 个商品。
  • 每个用户最近一次登录记录。
这类题先判断三件事:分组对象、排序指标、并列处理。
PostgreSQL 的函数表里把 row_number()rank()dense_rank() 分开定义:row_number() 给分区内每行从 1 开始编号;rank() 遇到并列会留下名次空档;dense_rank() 遇到并列不会留下空档。4 面试时别只写 row_number(),要说清楚为什么选它。
通用模板:
WITH ranked AS (
  SELECT
    department_id,
    employee_id,
    salary,
    ROW_NUMBER() OVER (
      PARTITION BY department_id
      ORDER BY salary DESC, employee_id ASC
    ) AS rn
  FROM employee_salary
)
SELECT department_id, employee_id, salary
FROM ranked
WHERE rn <= 2;
现场解释可以这样说:
「我先按部门分组,在每个部门里按工资倒序编号。这里用 row_number(),因为题目要固定返回 2 个人;如果业务要求并列第一都保留,我会改成 rank()dense_rank(),再筛 rank <= 2。」
这段话比直接写 SQL 更值钱。它告诉面试官你知道结果数量、并列口径和业务规则之间的关系。
常见扣分点是把 PARTITION BY department_id 写成 GROUP BY department_id。这样会把员工明细压没,后面再拿 employee_id 就开始乱补字段。

高频题型二:前后行对比,重点是「同一条时间线」

题目长这样:
  • 找出用户第二次下单距注册日相隔几天。
  • 计算每个用户本次消费比上次多了多少。
  • 找出 10 分钟内疑似重复支付。
DataLemur 的窗口函数面试题列表里就包含「第二次打车延迟」「重复支付」「后续购买比例」这类题,它们共同考的不是函数名,而是同一用户或同一交易对象内部的先后关系。5
如果题目要「第 N 次行为」,优先用 row_number() 编号:
WITH ordered_orders AS (
  SELECT
    user_id,
    order_id,
    order_time,
    ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY order_time ASC, order_id ASC
    ) AS order_seq
  FROM orders
)
SELECT user_id, order_id, order_time
FROM ordered_orders
WHERE order_seq = 2;
如果题目要「和上一行比较」,再用 lag():PostgreSQL 文档定义 lag(value) 是取当前分区里当前行之前的某一行,默认偏移量是 1;lead(value) 则取之后的行。4
WITH pay_with_prev AS (
  SELECT
    merchant_id,
    card_id,
    amount,
    pay_time,
    LAG(pay_time) OVER (
      PARTITION BY merchant_id, card_id, amount
      ORDER BY pay_time ASC
    ) AS prev_pay_time
  FROM payments
)
SELECT *
FROM pay_with_prev
WHERE pay_time <= prev_pay_time + INTERVAL '10 minutes';
讲解时不要只说「用 lag() 取上一条」。更好的说法是:
「我把同一商户、同一卡、同一金额看成一条支付时间线,按支付时间排序,再把当前行和上一笔比较。如果两笔间隔小于等于 10 分钟,就标为疑似重复。」
这里的核心不是 lag(),而是你把哪几列放进了同一条时间线。这个口径错了,函数写对也没用。

高频题型三:累计值和移动窗口,别漏掉 frame

题目长这样:
  • 每天 GMV 的累计值。
  • 每个用户最近 3 次购买的平均客单价。
  • 近 7 日活跃用户均值。
这类题容易翻车,因为很多人写到 ORDER BY date 就停了。PostgreSQL 文档提醒:带 ORDER BY 的聚合窗口函数在默认窗口范围下会产生类似 running sum 的行为;如果想覆盖整个分区,应该省略 ORDER BY,或显式写出从分区开头到结尾的窗口范围。4 Microsoft 文档也说明,ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 表示从当前行往前数 2 行到当前行,一共 3 行的窗口。3
累计值模板:
SELECT
  dt,
  gmv,
  SUM(gmv) OVER (
    ORDER BY dt ASC
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS cumulative_gmv
FROM daily_sales;
最近 3 次平均值模板:
SELECT
  user_id,
  order_id,
  order_time,
  amount,
  AVG(amount) OVER (
    PARTITION BY user_id
    ORDER BY order_time ASC, order_id ASC
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) AS avg_last_3_orders
FROM orders;
面试现场可以补一句边界说明:
「前三条记录不够 3 次时,就按已有记录算平均值;如果业务要求必须满 3 次才输出,我会在外层再筛一遍计数。」
这句话很实用。窗口题的追问经常卡在边界:第一天怎么算、并列日期怎么算、用户只有一单怎么算。你提前讲出来,就少掉一轮被动追问。

现场答题用这 5 步

  1. 先复述输出粒度:最终一行代表什么,是用户、订单、商品,还是日期。
  2. 再确定分区:PARTITION BY 对应业务对象,不对应你觉得顺手的字段。
  3. 再确定排序:时间题要写时间字段,排名题要写指标字段,并补唯一字段兜底。
  4. 再选函数:编号用 row_number(),保留并列用 rank()/dense_rank(),前后行比较用 lag()/lead(),累计和移动平均用聚合函数加 frame。
  5. 最后外层过滤:很多数据库不允许窗口函数直接写在 WHERE 里;PostgreSQL 文档也说明窗口函数只允许出现在 SELECT 列表和 ORDER BY 中,需要在窗口计算后过滤时可用子查询。1
可以把这段话练成 30 秒版本:
「我先看最终要保留哪一行明细,所以不用普通 GROUP BY 直接压缩。接着按业务对象分区,比如每个用户或每个品类;在分区内按题目指标排序,比如时间或销售额。需要第 N 条就用 row_number(),需要和上一条比较就用 lag(),需要累计值就用 SUM() OVER 并写清窗口范围。最后用 CTE 包一层再过滤排名或序号。」
这段话不用背得一字不差,但顺序要固定。先业务,再语法。

4 个最容易扣分的细节

第一,PARTITION BY 写成了题目里的所有字段。比如「每个用户最近一单」只按 user_id 分区,不要把 order_id 也放进去;每个订单都是独立分区,就没有「最近」可言。
第二,排序字段没有兜底。只按 order_time 排,如果同一秒有两单,row_number() 的结果可能不稳定。加上 order_id 这类唯一字段,面试官会觉得你写过真实数据。
第三,混淆 rank()dense_rank()。如果分数是 100、100、90,rank() 的名次是 1、1、3,dense_rank() 是 1、1、2。题目说「前 2 名」时,要先问是否保留并列;题目说「固定返回 2 条」时,通常用 row_number()
第四,累计和移动平均不写 frame。只写 SUM(gmv) OVER (ORDER BY dt) 在不少场景能跑,但面试时最好把 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 写出来。它让边界口径清楚,也方便你解释为什么这是累计值。

自己练题时,别只刷答案

窗口函数题要按题型练,不要今天刷一个 Top N,明天刷一个累计值,后天又刷一个完全不同的业务故事。更稳的练法是:
  1. 连续练 5 道 Top N per group,只改分区对象和排序口径。
  2. 连续练 5 道第 N 次行为或前后行对比,强迫自己先说「同一条时间线」由哪些字段组成。
  3. 连续练 5 道累计值、移动平均或近 N 行统计,每题都显式写 frame。
  4. 每写完一题,用中文复述一遍:输出粒度、分区、排序、函数、边界。
刷到最后,你应该能在看到题目的 10 秒内判断它属于哪一类。写 SQL 只是后半段,前半段是把业务问题翻译成「分区、排序、窗口范围」。

常见追问怎么接

问:窗口函数是不是一定比子查询快?
不要硬说一定。更稳的回答是:窗口函数通常能让表达更直接,少写自连接或多层子查询;但性能要看数据量、分区排序字段、索引和执行计划。Microsoft 文档在性能建议里也提到,窗口函数查询常涉及分区和排序,大数据集上要关注支持索引、统计信息和排序溢出。3
问:ROW_NUMBER()RANK()DENSE_RANK() 怎么选?
固定取 N 条,用 ROW_NUMBER();并列要保留且名次可跳号,用 RANK();并列要保留但名次连续,用 DENSE_RANK()。回答时最好补一句「我会先确认业务是否允许并列多返回」。
问:PARTITION BYGROUP BY 有什么区别?
GROUP BY 改变结果粒度,窗口函数的 PARTITION BY 只定义计算范围。前者把多行合成一行,后者保留每行,再在行旁边加计算结果。
问:窗口函数能不能写在 WHERE 里?
多数情况下别这样写。先在 CTE 或子查询里算出 rnprev_timecumulative_gmv,外层再筛。这样也更容易让面试官看懂你的步骤。
最后给自己一个判断标准:如果你能在写代码前先说清「每行是什么、按谁分组、按什么排序、边界怎么算」,这道 SQL 窗口函数题基本就不会答懵。

Related content

  • Sign in to comment.
More from this channel