
2026. 7. 5. · 08:08
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;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 步
- 先复述输出粒度:最终一行代表什么,是用户、订单、商品,还是日期。
- 再确定分区:
PARTITION BY对应业务对象,不对应你觉得顺手的字段。 - 再确定排序:时间题要写时间字段,排名题要写指标字段,并补唯一字段兜底。
- 再选函数:编号用
row_number(),保留并列用rank()/dense_rank(),前后行比较用lag()/lead(),累计和移动平均用聚合函数加 frame。 - 最后外层过滤:很多数据库不允许窗口函数直接写在
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,明天刷一个累计值,后天又刷一个完全不同的业务故事。更稳的练法是:
- 连续练 5 道 Top N per group,只改分区对象和排序口径。
- 连续练 5 道第 N 次行为或前后行对比,强迫自己先说「同一条时间线」由哪些字段组成。
- 连续练 5 道累计值、移动平均或近 N 行统计,每题都显式写 frame。
- 每写完一题,用中文复述一遍:输出粒度、分区、排序、函数、边界。
刷到最后,你应该能在看到题目的 10 秒内判断它属于哪一类。写 SQL 只是后半段,前半段是把业务问题翻译成「分区、排序、窗口范围」。
常见追问怎么接
问:窗口函数是不是一定比子查询快?
不要硬说一定。更稳的回答是:窗口函数通常能让表达更直接,少写自连接或多层子查询;但性能要看数据量、分区排序字段、索引和执行计划。Microsoft 文档在性能建议里也提到,窗口函数查询常涉及分区和排序,大数据集上要关注支持索引、统计信息和排序溢出。3
问:
ROW_NUMBER()、RANK()、DENSE_RANK() 怎么选?固定取 N 条,用
ROW_NUMBER();并列要保留且名次可跳号,用 RANK();并列要保留但名次连续,用 DENSE_RANK()。回答时最好补一句「我会先确认业务是否允许并列多返回」。问:
PARTITION BY 和 GROUP BY 有什么区别?GROUP BY 改变结果粒度,窗口函数的 PARTITION BY 只定义计算范围。前者把多行合成一行,后者保留每行,再在行旁边加计算结果。问:窗口函数能不能写在
WHERE 里?多数情况下别这样写。先在 CTE 或子查询里算出
rn、prev_time、cumulative_gmv,外层再筛。这样也更容易让面试官看懂你的步骤。最后给自己一个判断标准:如果你能在写代码前先说清「每行是什么、按谁分组、按什么排序、边界怎么算」,这道 SQL 窗口函数题基本就不会答懵。
관련 콘텐츠
- 로그인하면 댓글을 작성할 수 있습니다.
