1. 为什么窗口函数是SQL进阶路上绕不开的分水岭
大概两年前,我帮业务部门搭销售周报,遇到一个在当时的我看来十分“刁钻”的需求:要按区域统计每个产品的销售额,然后在每个区域内部给产品排名,最后还要保留明细行。我第一反应是直接GROUP BY,但很快发现GROUP BY会把每个区域压缩成一行,压根看不到“每个产品分别排在哪儿”。于是我写了二十多行SQL,又用了自连接加子查询,才勉强算出排名。直到后来我真正搞懂窗口函数,才发现这条SQL核心逻辑其实就是一行代码的事。
窗口函数(Window Function)解决的问题,用一句话说就是:在不改变明细行数的前提下,对每一行做一次“带分组、带排序、带范围”的计算。它让排名、累计值、移动平均、同比环比、组内Top N这些曾经要绕很多弯的需求,变成一种结构化、可读性极高的写法。很多从没认真学过窗口函数的人,写了大半年业务SQL都还在用“GROUP BY + 多个子查询”硬扛,这不是能力问题,而是知识盲区的问题。
这篇文章我把窗口函数从语法到底层逻辑到业务场景一次性讲透。适合这几类人看:刚入门想进阶的SQL学习者,正在准备数据岗面试的同学,以及每天写大量取数SQL但始终没系统整理过窗口函数的朋友。内容不挑数据库,MySQL 8.0、PostgreSQL、SQL Server、Oracle、Hive都适用,个别函数名和写法有小差异,我文中会特意标注。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 窗口函数语法逐层拆解:OVER、PARTITION BY、ORDER BY和窗口边界
2.1 OVER():从一个“函数入口”说起
窗口函数的标准语法形态是:
sql复制窗口函数() OVER (
[PARTITION BY 列...]
[ORDER BY 列...]
[ROWS/RANGE 窗口边界]
)
最容易被忽略的是OVER这个关键字。没有OVER,SUM、AVG、ROW_NUMBER这些函数就是一个普通聚合或普通函数;一旦带上OVER,它的身份立刻切换成“窗口函数”,计算逻辑不再是一整张表压成一行,而是逐行扫描,每一行都能看到一个由分组、排序、边界共同圈定的“窗口”。一个空窗口OVER()代表“整个结果集作为一个窗口”,常用于计算全表的总和、平均值的占比。
为了后面演示方便,我先建一张销售明细表。本文所有案例都基于这张表展开。
sql复制CREATE TABLE sales (
id INT PRIMARY KEY,
region VARCHAR(20),
product VARCHAR(20),
amount DECIMAL(10,2),
sale_date STRING
);
INSERT INTO sales VALUES
(1, '华东', 'A产品', 1200.00, '2024-01-01'),
(2, '华东', 'B产品', 800.00, '2024-01-02'),
(3, '华东', 'A产品', 1500.00, '2024-01-03'),
(4, '华北', 'A产品', 700.00, '2024-01-01'),
(5, '华北', 'C产品', 950.00, '2024-01-02'),
(6, '华东', 'C产品', 600.00, '2024-01-04'),
(7, '华北', 'B产品', 1100.00, '2024-01-03'),
(8, '华东', 'B产品', 900.00, '2024-01-05'),
(9, '华北', 'C产品', 1300.00, '2024-01-04'),
(10, '华北', 'A产品', 500.00, '2024-01-05');
2.2 PARTITION BY:分组但不压行
PARTITION BY负责“开窗户的分区范围”,它的作用类似于GROUP BY,但有一个本质区别:GROUP BY会把同一组的多行合并成一行,PARTITION BY不会,它只是给同一组的行贴上一个“组号”,然后让窗口函数在组内独立计算。
看这个例子:
sql复制SELECT
region,
product,
amount,
SUM(amount) OVER(PARTITION BY region) AS region_total
FROM sales;
执行结果是10行,每一行都带一列region_total,为所在区域的金额总和:
| region | product | amount | region_total |
|---|---|---|---|
| 华东 | A产品 | 1200.00 | 5000.00 |
| 华东 | B产品 | 800.00 | 5000.00 |
| 华东 | A产品 | 1500.00 | 5000.00 |
| 华东 | C产品 | 600.00 | 5000.00 |
| 华东 | B产品 | 900.00 | 5000.00 |
| 华北 | A产品 | 700.00 | 4550.00 |
| 华北 | C产品 | 950.00 | 4550.00 |
| 华北 | B产品 | 1100.00 | 4550.00 |
| 华北 | C产品 | 1300.00 | 4550.00 |
| 华北 | A产品 | 500.00 | 4550.00 |
这种“每行带总数”的写法在计算占比时极其好用,一个SELECT框里就能算出区域贡献率,不需要再额外JOIN一张汇总表。
2.3 ORDER BY:它决定的是计算顺序
窗口函数里的ORDER BY不同于普通查询的ORDER BY,它不直接决定最终结果集的展示顺序,而是决定了窗口函数“按什么顺序计算”。这个区别在累计值和排名类函数里体现得特别明显。
以累计销售额为例:
sql复制SELECT
region,
product,
sale_date,
amount,
SUM(amount) OVER(PARTITION BY region ORDER BY sale_date) AS cum_amount
FROM sales;
这里PARTITION BY region保证计算只在区域内进行,ORDER BY sale_date则让SUM函数逐行累积,date越早的行越先累加。以华东为例,第1行A产品1200,cum_amount=1200;第2行B产品800,cum_amount=2000;第3行A产品1500,cum_amount=3500,以此类推。
但如果你把PARTITION BY region去掉,只写ORDER BY sale_date,那语义就变成“全局按日期递增的累计销售额”,所有区域混在一起算。写窗口函数之前,必须想明白自己想要的到底是“组内累计”还是“全局累计”,这个坑我见过太多次了。
2.4 窗口边界ROWS/RANGE:大多数人忽略的“高级选项”
很多文章讲到OVER(PARTITION BY ... ORDER BY ...)就停了,从不提窗口边界。但边界恰恰是窗口函数最灵活、也最容易踩坑的地方。
窗口边界有两种写法:ROWS和RANGE。它们的区别在于,ROWS按“物理行数”框定窗口范围,RANGE按“排序列的值范围”框定窗口范围。最常用的几个边界是:
| 边界写法 | 含义 |
|---|---|
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | 从分区起点到当前行 |
| ROWS BETWEEN 2 PRECEDING AND CURRENT ROW | 当前行及前两行 |
| ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING | 当前行到分区终点 |
| ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING | 当前行前后各两行 |
| RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW | 按日期值范围,最近7天 |
写不写这个边界,结果可能天差地别。默认情况下,只有当OVER里同时出现ORDER BY时,窗口才会被限定为“从分区起点到当前行”;如果没有ORDER BY,默认窗口是整个分区。
按照默认行为,SUM(amount) OVER(PARTITION BY region ORDER BY sale_date)其实等价于:
sql复制SUM(amount) OVER(
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
做移动平均时,这个边界就必须显式写出来,否则你得到的不是“近3日均值”,而是“从第一天到今天的累计均值”。
2.5 常见的数据库差异
MySQL 8.0开始原生支持窗口函数,MySQL 5.7及以下版本不支持,只能用用户变量模拟,挺痛苦。PostgreSQL是窗口函数支持最完善的开源数据库之一,尤其RANGE的日期/时间区间写得很顺手。Hive和Spark SQL支持完整窗口语法,但底层依赖MapReduce/Spark引擎,对内存和排序的开销更敏感。Oracle从9i就有分析函数,语法基本一致。SQL Server则从2012版本开始引入窗口函数。
业务代码如果要跨数据库迁移,重点检查三类差异:一是框架默认边界是否一致,二是命名细节(比如SQL Server不区分RANK和DENSE_RANK的顺序,但Oracle的PERCENT_RANK返回值格式略有不同),三是某些函数存在性,比如NTH_VALUE在MySQL 8.0和PostgreSQL里都有,但老版本SQL Server不支持。
3. 三大类窗口函数对照详解:聚合、排名、取值
3.1 聚合类窗口函数:SUM、AVG、COUNT、MIN、MAX
聚合窗口函数是普通聚合函数和OVER结合的产物。它和GROUP BY聚合最大的差异是:GROUP BY一行代表一个分组,聚合窗口函数一行代表一条明细,并且可以在同一行看到“组内合计、组内累计、组内平均”等多种口径。
一个被广泛应用的场景是计算占比。比如想知道每个产品在各自区域的销售额占比,写起来非常直接:
sql复制SELECT
region,
product,
amount,
ROUND(
amount / SUM(amount) OVER(PARTITION BY region) * 100,
2
) AS pct
FROM sales;
注意聚合窗口函数里,COUNT的行为值得单独强调。COUNT(column)和COUNT()在窗口函数里语义差异依旧存在:COUNT()统计分区内所有行,COUNT(column)只统计该列非NULL的行。这在某些“用0填充空值”的场景下非常容易造成误判。
3.2 排名类窗口函数:ROW_NUMBER、RANK、DENSE_RANK
三种排名函数是面试和日常使用中出现频率最高的。它们的区别用一个表格说清楚:
| 函数 | 结果特点 | 典型场景 |
|---|---|---|
| ROW_NUMBER() | 严格连续编号1、2、3、4,不关心相同值 | 去重、取第N条 |
| RANK() | 相同值并列,编号跳号,如1、1、3 | 体育比赛排名逻辑 |
| DENSE_RANK() | 相同值并列,编号不跳号,如1、1、2 | 销售榜单不跳名次 |
用同一份数据感受一下区别:
sql复制SELECT
product,
amount,
ROW_NUMBER() OVER(ORDER BY amount DESC) AS rn,
RANK() OVER(ORDER BY amount DESC) AS rank_no,
DENSE_RANK() OVER(ORDER BY amount DESC) AS dense_rn
FROM sales
ORDER BY amount DESC;
结果片段(金额相同的地方会复现并列差异):
| product | amount | rn | rank_no | dense_rn |
|---|---|---|---|---|
| A产品 | 1500.00 | 1 | 1 | 1 |
| C产品 | 1300.00 | 2 | 2 | 2 |
| A产品 | 1200.00 | 3 | 3 | 3 |
| B产品 | 1100.00 | 4 | 4 | 4 |
| C产品 | 950.00 | 5 | 5 | 5 |
| B产品 | 900.00 | 6 | 6 | 6 |
| B产品 | 800.00 | 7 | 7 | 7 |
| A产品 | 700.00 | 8 | 8 | 8 |
| C产品 | 600.00 | 9 | 9 | 9 |
| A产品 | 500.00 | 10 | 10 | 10 |
如果出现相同金额,比如两单都是1200,那么ROW_NUMBER会随机给一个1一个2,RANK会都排1然后下一个跳到3,DENSE_RANK则都排1然后下一个排2。业务上到底选哪个,取决于分析目标:取唯一序号用ROW_NUMBER,要体现并列且允许名次空当用RANK,要保持名次连续用DENSE_RANK。
此外还有两个稍冷门但好用的排名函数:PERCENT_RANK()返回当前行在分区内的百分比排名,取值范围0到1;CUME_DIST()返回累计分布值,表示小于等于当前行值的行数占比。做分位数分析和数据分布探查时很实用。
3.3 取值类窗口函数:LAG、LEAD、FIRST_VALUE、LAST_VALUE、NTH_VALUE
取值类窗口函数的价值在于:它能访问同一分区内其他行的数据,而不是像聚合函数那样把多行压缩成一个值。
LAG(column, n)取当前行之前第n行的值,LEAD(column, n)取之后第n行的值,不指定n时默认取前一行或后一行。这是做同环比、差分分析的核心工具。看这个“每行带前一天销售额”的例子:
sql复制SELECT
region,
sale_date,
amount,
LAG(amount, 1) OVER(PARTITION BY region ORDER BY sale_date) AS prev_amount,
amount - LAG(amount, 1) OVER(PARTITION BY region ORDER BY sale_date) AS diff
FROM sales;
你可能会注意到一个细节:这里写了两遍LAG。这种做法在SQL里合法但不优雅。有些数据库比如PostgreSQL支持在同一个窗口上使用命名窗口简化写法,MySQL 8.0也提供了WINDOW子句,后面讲优化时我会专门展开。
FIRST_VALUE(column)取窗口内第一行的值,LAST_VALUE(column)取最后一行。但注意,LAST_VALUE默认窗口边界是“到当前行”,所以它取的是“截至当前行的最后一个值”,不是整个分区的最后一个值。很多人第一次用LAST_VALUE时都会在这里翻车。要取分区真正的最后一行,需要显式加上ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。
NTH_VALUE(column, n)取窗口内第n行的值,适合取“第二名”“第三名”这类需求。使用时同样要留意边界定义。
3.4 窗口函数速查对照表
| 函数分类 | 函数 | 核心用途 | 注意事项 |
|---|---|---|---|
| 聚合 | SUM/AVG/COUNT/MIN/MAX | 组内统计、累计、移动计算 | COUNT列和COUNT(*)不同 |
| 排名 | ROW_NUMBER | 唯一序号、去重 | 相同值随机编号 |
| 排名 | RANK | 并列排名、允许跳号 | 适合比赛排名 |
| 排名 | DENSE_RANK | 并列排名、不跳号 | 榜单常用 |
| 排名 | PERCENT_RANK / CUME_DIST | 相对位置、分布分析 | 返回值0到1 |
| 取值 | LAG / LEAD | 前后行取值 | 不设偏移量默认1 |
| 取值 | FIRST_VALUE / LAST_VALUE | 窗口首尾值 | LAST_VALUE注意边界 |
| 取值 | NTH_VALUE | 窗口内第N个值 | 注意边界 |
4. 从业务报表出发的五个完整实战案例
4.1 案例一:每个区域销售额Top 3产品
业务需求:销售总监要看每个区域销售额最高的3个产品。
这个需求用GROUP BY自连接会绕,用窗口函数几乎是一套标准动作:先按区域和产品聚合出销售额,再开窗排名,再过滤排名小于等于3。
sql复制WITH product_sales AS (
SELECT
region,
product,
SUM(amount) AS total_amount
FROM sales
GROUP BY region, product
),
ranked AS (
SELECT
region,
product,
total_amount,
ROW_NUMBER() OVER(
PARTITION BY region
ORDER BY total_amount DESC
) AS rn
FROM product_sales
)
SELECT
region,
product,
total_amount
FROM ranked
WHERE rn <= 3;
注意,窗口函数不能直接写在WHERE里。因此我借助CTE(公共表表达式)把排名结果先算出来,再在外层过滤。这里的ORDER BY total_amount DESC决定了“按销售额从高到低排”,PARTITION BY region则确保排名在每个区域内部独立进行。
4.2 案例二:月度销售额的同比和环比
业务需求:看每个月销售额,同时对比上个月(环比)和去年同期(同比)。
环比用LAG取上个月值,同比需要按月份错开12个位置。如果业务表按天存储,需要先聚合到月份,再开窗。
sql复制WITH monthly AS (
SELECT
DATE_FORMAT(sale_date, '%Y-%m') AS ym,
SUM(amount) AS total_amount
FROM sales
GROUP BY DATE_FORMAT(sale_date, '%Y-%m')
)
SELECT
ym,
total_amount,
LAG(total_amount, 1) OVER(ORDER BY ym) AS prev_month,
LAG(total_amount, 12) OVER(ORDER BY ym) AS last_year_same_month,
ROUND(
(total_amount - LAG(total_amount, 1) OVER(ORDER BY ym))
/ LAG(total_amount, 1) OVER(ORDER BY ym) * 100,
2
) AS mom_rate
FROM monthly;
注意LAG(total_amount, 12)要求数据里至少存在12个月之前的历史记录,否则返回NULL。实际业务中,年初1月没有12个月前的数据,所以同比值会为NULL,前端展示时通常需要特殊处理,比如显示为“暂无数据”。
4.3 案例三:连续登录天数
业务需求:找出每个用户连续登录的最大天数。这是面试高频题。
连续性问题有个非常经典的解法:先用ROW_NUMBER给每个用户的登录日期按时间排序,然后用登录日期减去序号得到一个“分组日期”。如果一个用户连续登录,他们减去序号后的日期一定是同一天。最后按用户和分组日期聚合,统计次数。
sql复制WITH login_data AS (
SELECT
user_id,
login_date,
ROW_NUMBER() OVER(
PARTITION BY user_id
ORDER BY login_date
) AS rn
FROM login_log
),
grouped AS (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL rn DAY) AS grp_date
FROM login_data
)
SELECT
user_id,
MIN(login_date) AS start_date,
MAX(login_date) AS end_date,
COUNT(*) AS continuous_days
FROM grouped
GROUP BY user_id, grp_date;
这个解法的精妙之处在于:ROW_NUMBER产生的序号在连续日期上等差递增,所以“登录日期减去序号”在连续区间内保持不变;一旦中断,序号继续递增而日期不连续,差值自然改变,于是自动“切组”。
4.4 案例四:订单日志去重,保留最新一条
业务需求:订单日志表里同一个订单有多条状态变更记录,现在要取每个订单最新的一条状态。
先按订单分组,并按更新时间倒序编号,然后过滤编号为1的行。这是ROW_NUMBER最经典的用法之一。
sql复制WITH ranked AS (
SELECT
order_id,
status,
update_time,
ROW_NUMBER() OVER(
PARTITION BY order_id
ORDER BY update_time DESC
) AS rn
FROM order_log
)
SELECT
order_id,
status,
update_time
FROM ranked
WHERE rn = 1;
这种写法在“表数据有重复,保留最新”“取每个用户最近一次登录记录”“取每个分类最新一条新闻”等场景中通用。如果希望保留一条且删除其他重复记录,可以在这个查询的基础上用DELETE JOIN完成清理。
4.5 案例五:最近3天移动平均
业务需求:看每天的销售额,同时算一个3天移动平均,用来平滑短期波动。
这里必须显式使用ROWS边界,否则默认窗口会把从起始日到当前日全部累计进来,得到的是“累计均值”而不是“滑动均值”。
sql复制SELECT
region,
sale_date,
amount,
AVG(amount) OVER(
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3d
FROM sales;
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW意为窗口包含当前行及之前两行。如果今天是2024-01-03,窗口就是01-01、01-02、01-03这三行,移动平均就是这三天的平均值。日期连续性和缺失值会影响窗口实际包含的行数,这一点做数据校验时要格外留意。
5. 窗口函数与GROUP BY的本质区别,以及最容易踩的坑
5.1 两者输出行数不一样
GROUP BY的语义是“分组汇总”,每组输出一行。窗口函数的语义是“在每行旁边附带窗口计算结果”,输出行数和输入行数一致。这个区别决定了它们的应用场景完全不同:想要一份汇总报告,用GROUP BY;想要在明细基础上加统计列,用窗口函数。
5.2 GROUP BY的聚合结果上还能再开窗口
如果先聚合再排名,窗口函数可以作用于聚合结果。比如案例一里我写的product_sales子查询就是先GROUP BY得到每个区域每个产品的总额,再在外层用ROW_NUMBER() OVER(...)做组内排名。这种“先压平、再开窗”的组合写法非常常见,要记住聚合和开窗不是互斥的,而是可以分步配合。
5.3 窗口函数不能出现在WHERE和HAVING里
这是最容易踩的语法坑。原因是SQL的执行顺序是WHERE → GROUP BY → HAVING → SELECT → ORDER BY,窗口函数在SELECT阶段才计算,WHERE和HAVING根本看不到它的结果。所以想把窗口函数的结果当过滤条件时,必须先子查询或CTE包一层。
sql复制-- 错误写法
SELECT region, product, ROW_NUMBER() OVER(PARTITION BY region ORDER BY amount DESC) AS rn
FROM sales
WHERE rn = 1;
-- 正确写法
SELECT region, product, rn
FROM (
SELECT region, product,
ROW_NUMBER() OVER(PARTITION BY region ORDER BY amount DESC) AS rn
FROM sales
) t
WHERE rn = 1;
5.4 关于别名的“历史遗留问题”
SELECT子句里的别名能不能在同一个SELECT的OVER里使用,要分数据库。MySQL 8.0对某些场景允许,PostgreSQL也允许,但为了兼容性,我建议不要这样写,老老实实把窗口函数写在子查询或WITH里,再在外层引用别名。
为什么很多老手不建议在一个SELECT里写太长的窗口函数?不是因为语法上写不了,而是因为一旦写错,排错成本很高。把复杂逻辑拆成多层CTE,每一层的职责清晰明了,既方便调试又方便后续维护。
6. 窗口函数的性能优化与六个血泪教训
6.1 窗口函数慢,慢在哪里
窗口函数在计算时需要把分区内的数据按ORDER BY排序,然后再逐行扫描计算。数据量大时,排序就是最大的性能瓶颈。尤其是同时开多个不同ORDER BY窗口时,数据库无法复用一个排序结果,可能要对同一批数据排多次。
所以,性能优化的第一原则是:减少不必要的排序。能用同一个窗口粒度复用的,尽量复用;分区间隔大的,尽量用PARTITION BY缩小区间。
MySQL里可以这样做:
sql复制SELECT
region,
sale_date,
amount,
SUM(amount) OVER w AS running_total,
AVG(amount) OVER w AS running_avg
FROM sales
WINDOW w AS (PARTITION BY region ORDER BY sale_date);
WINDOW子句把同一个窗口定义命名成w,多个窗口函数共用一份排序结果,既减少重复排序,代码也清爽很多。
6.2 六个实际开发中踩过的坑
第一个坑:PARTITION BY粒度没想清楚,导致重复统计。比如按区域算占比,却漏了product维度,分区过大,占比算出来的口径不对。写SQL前先问自己:这个统计的“分组单元”到底是哪一个维度?
第二个坑:ORDER BY字段类型不一致,引发隐式排序问题。若排序列是字符串类型,排序结果是字典序而不是数值序,金额排名直接错乱。加上CAST或建表时把列类型定义准确能避免。
第三个坑:默认窗口边界和预想不一致。前面反复提过,不加ROWS/RANGE时,有ORDER BY的窗口默认是“从起点到当前行”,无ORDER BY时是整个分区。尤其做移动平均、LAST_VALUE时,不显式声明边界会得到完全不同的结果。
第四个坑:一个SELECT里重复写多个相同窗口,造成性能浪费。用WINDOW子句或提前算出所需列,不要给数据库额外排序压力。
第五个坑:和GROUP BY混用时搞不清执行顺序。比如先GROUP BY得到每组的聚合值,又想在聚合值上开窗排名,就必须把GROUP BY结果作为子查询再开窗,而不是在一个SELECT里硬写。
第六个坑:忽略了NULL值排序。默认NULL在排序中通常排在最前面,做排名和累计时NULL的行可能干扰结果。可以用ORDER BY column DESC NULLS LAST这样的写法控制NULL位置。
6.3 我的几条优化习惯
实际工作中,我个人的经验是:先在小样本上把窗口函数的计算结果测对,再放开到全量数据。窗口函数表面写法不复杂,但边界、排序、NULL、分组粒度这些细节一旦出错,全量跑完才发现,返工成本极高。我一般会在开发环境先LIMIT 1000行验证,再上生产。
另外,如果数据量千万级以上且窗口函数经常用于报表,优先考虑是否能用“预聚合+窗口”代替“明细+窗口”。比如先按天聚合,再做月度移动平均,比直接在明细表上开窗效率高很多。数据仓库场景里更是如此,能提前聚合尽量提前聚合,别让计算引擎把几亿行明细拉出来排序。
7. 写到最后:几条来自实践的真心建议
说几句比较掏心窝的话。窗口函数刚上手的时候,很多人会被PARTITION BY、ORDER BY、ROWS这三个要素绕晕,其实核心就是三件事:在哪个范围算、按什么顺序算、窗口边界在哪。把一个复杂需求拆成这三个问题,SQL基本就写对了一半。
我自己的学习路径是:先死磕排名三兄弟,因为排名需求最常见,能快速建立信心;然后练LAG/LEAD做环同比,理解“跨行取值”的威力;最后才系统掌握ROWS/RANGE边界,因为这块最抽象,但也是窗口函数真正拉开水平的地方。
如果你正在准备面试,我建议把连续登录、Top N、去重取最新这三道题练到不用想就能写出来。它们的标准解法几乎固定,属于必拿分题。
最后分享一个小技巧:在同一个SQL里,如果多个窗口函数共用一组PARTITION BY和ORDER BY,第一时间用WINDOW子句抽取共同定义。这不仅仅是代码美观问题,而是实打实的性能优化习惯,等遇到千万级数据量时,你会感谢这个习惯的。
