
这期把 SQL 窗口函数面试拆成 Top N、前后行对比、累计与移动窗口 3 类题,给出面试官看重的判断逻辑、SQL 模板和现场讲解话术,帮你避免只背函数名却写不出查询。
row_number()、rank()、lag() 这些函数名。先说一句:「这题的关键是保留明细行,同时在每个业务分组里排序或做前后行计算。」这句话能把你从「会背语法」拉到「知道为什么这样写」。PARTITION BY 该按用户、部门还是订单分;另一类能先讲清业务口径,再写 SQL。面试官要找的是第二类。SUM() OVER() 举例说明:窗口计算会给每一行产出结果,而不是把分组压成单行。2GROUP BY是「把一组记录合成一个结果」;窗口函数是「每行还在,但多了一列分组内排名、累计值或前后行对比」。
GROUP BY 很容易把细节压没。| 面试官追问 | 他在判断什么 | 你应该先说什么 |
|---|---|---|
为什么不用 GROUP BY? | 你是否知道窗口函数会保留明细行,而普通聚合会压缩结果。1 | 「因为最终要返回具体员工/订单/日期,不能只返回一个分组汇总值。」 |
PARTITION BY 按什么分? | 你有没有看懂业务对象。 | 「题目问每个用户,就按用户分;问每个品类,就按品类分。」 |
| 排序字段怎么定? | 你是否能处理并列、时间先后和唯一性。MySQL 文档提醒,ROW_NUMBER() 如果没有在窗口定义里写 ORDER BY,分区内编号没有确定顺序。2 | 「先按业务指标降序,再加唯一字段兜底,避免并列时结果漂移。」 |
| 要不要写窗口范围? | 你是否知道累计、移动平均这类题不是只靠 ORDER BY。Microsoft 文档把 ROWS/RANGE 描述为限制分区内窗口起止点的语法,常用于移动平均、累计值、Top N 等场景。3 | 「累计用从分区开头到当前行,移动窗口要明确前 N 行或后 N 行。」 |
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。」
PARTITION BY department_id 写成 GROUP BY department_id。这样会把员工明细压没,后面再拿 employee_id 就开始乱补字段。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(),而是你把哪几列放进了同一条时间线。这个口径错了,函数写对也没用。ORDER BY date 就停了。PostgreSQL 文档提醒:带 ORDER BY 的聚合窗口函数在默认窗口范围下会产生类似 running sum 的行为;如果想覆盖整个分区,应该省略 ORDER BY,或显式写出从分区开头到结尾的窗口范围。4 Microsoft 文档也说明,ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 表示从当前行往前数 2 行到当前行,一共 3 行的窗口。3SELECT
dt,
gmv,
SUM(gmv) OVER (
ORDER BY dt ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_gmv
FROM daily_sales;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 次才输出,我会在外层再筛一遍计数。」
PARTITION BY 对应业务对象,不对应你觉得顺手的字段。row_number(),保留并列用 rank()/dense_rank(),前后行比较用 lag()/lead(),累计和移动平均用聚合函数加 frame。WHERE 里;PostgreSQL 文档也说明窗口函数只允许出现在 SELECT 列表和 ORDER BY 中,需要在窗口计算后过滤时可用子查询。1「我先看最终要保留哪一行明细,所以不用普通GROUP BY直接压缩。接着按业务对象分区,比如每个用户或每个品类;在分区内按题目指标排序,比如时间或销售额。需要第 N 条就用row_number(),需要和上一条比较就用lag(),需要累计值就用SUM() OVER并写清窗口范围。最后用 CTE 包一层再过滤排名或序号。」
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()。SUM(gmv) OVER (ORDER BY dt) 在不少场景能跑,但面试时最好把 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 写出来。它让边界口径清楚,也方便你解释为什么这是累计值。ROW_NUMBER()、RANK()、DENSE_RANK() 怎么选?ROW_NUMBER();并列要保留且名次可跳号,用 RANK();并列要保留但名次连续,用 DENSE_RANK()。回答时最好补一句「我会先确认业务是否允许并列多返回」。PARTITION BY 和 GROUP BY 有什么区别?GROUP BY 改变结果粒度,窗口函数的 PARTITION BY 只定义计算范围。前者把多行合成一行,后者保留每行,再在行旁边加计算结果。WHERE 里?rn、prev_time、cumulative_gmv,外层再筛。这样也更容易让面试官看懂你的步骤。postgresql.org
dev.mysql.com
learn.microsoft.com
postgresql.org
datalemur.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.