SQL窗口函数速查:OVER()、排名与窗口帧避坑
简介:《SQL窗口函数速查表》PDF面向数据库管理员、数据分析师、数据科学家与开发人员,聚焦窗口函数概念零散、语法与适用边界易混的问题,适合具备基础SQL、希望提升复杂查询与报表分析能力的中级读者。整份资源仅含1个PDF文件,压缩包约841KB,轻量易存,便于随查随用;文档按排名、行序号、聚合三类函数组织,系统梳理ROW_NUMBER、RANK、DENSE_RANK、LEAD、LAG及SUM、AVG、COUNT等用法,并给出OVER、PARTITION BY、ORDER BY、frame_clause的语法结构与参数说明。每个函数配合示例代码与返回结果,帮助读者理解分组排序、前后行取值、累计与滑动窗口计算等典型场景,也能为报表生成、异常值排查和教学讲解提供直接参考。目前已有255人学习下载,适合作为案头速查与面试复习材料。
1. 一行 GROUP BY 交不出的报表,才是窗口函数的主场
月度复盘会上,业务方要一张表:每笔订单的明细、它所在大区的累计成交额、它在本大区按金额的排名、以及和上一笔的时间间隔。用 GROUP BY 一聚合,明细就没了;用自连接硬拼,SQL 三层嵌套还慢得离谱。这类“既要明细又要汇总”的查询,正是 SQL 窗口函数的主场——它在一组行上做聚合,却把每一行原样留下。这份《SQL窗口函数速查表.pdf》把日常写报表时最高频的函数、OVER() 的三段结构、帧子句的边界写法压缩成一份可以边写边翻的对照表,适合天天写查询的分析师、要优化慢报表的后端开发,以及被面试题问住想系统补一遍 sql 的人。它不教你数据库原理,只解决一件事:下次遇到排名、累计、环比、去重取最新,能直接翻到那一页照抄。
2. OVER() 骨架拆解:PARTITION BY、ORDER BY、FRAME 三段怎么切
窗口函数之所以让很多人写得心虚,是因为它把“分组、排序、取范围”三件事塞进了一个 OVER() 里,任何一段写错,结果都不会报错,只会静默地算出另一组数。速查表把这三段拆开列,实际动手时按这个顺序对齐,八成的问题在写之前就能避开。
2.1 聚合函数和窗口函数的差别:明细为什么没被折叠
GROUP BY 的本质是“把多行压成一行”,压缩之后原始行的身份就丢了。窗口函数不压缩,它给每一行挂一个“基于它所属那组算出来的值”。两者的差别不是语法糖,而是结果集的行数模型不同。
-- 聚合写法:一个分区只剩一行,明细消失 SELECT region, SUM(amount) AS region_total FROM sales GROUP BY region; -- 窗口写法:行数不变,每行后面挂一个分区汇总 SELECT region, sale_date, amount, SUM(amount) OVER (PARTITION BY region) AS region_total FROM sales;第一段查询返回两行(两个大区),第二段返回六行,每行都带着自己所属大区的总额。逻辑说明:SUM(amount) OVER (PARTITION BY region)里的 SUM 仍然是求和,但 OVER 告诉引擎“别把这组压成一行,算完把结果贴回每一行”。参数说明:PARTITION BY region是切分依据,等价于 GROUP BY 的分组列,但不会减少行数;省略 PARTITION BY 时整个结果集就是一个窗口,所有行共用一个汇总值。
这里有个容易忽略的边界:窗口函数在逻辑执行顺序上位于 GROUP BY 和 HAVING 之后、ORDER BY 之前。也就是说,如果整条 SQL 里同时有 GROUP BY 和窗口函数,窗口函数看到的是聚合之后的结果集。想要“先聚合再窗口”,就把聚合写进子查询;想要“先窗口再聚合”,得反过来包一层。分不清这一点,就会写出SUM(SUM(amount)) OVER ()这种看着别扭但语法合法的嵌套,速查表里专门标了这一类组合写法。
提示:窗口函数不能写在 WHERE 里,也不能直接用它产生的列别名做过滤条件,原因是 WHERE 的执行时机早于窗口计算。这条在后面第 5 章会展开。
2.2 三段式的执行顺序:先切分、再排序、最后取帧
OVER() 内部三段的语义是层层收窄的:PARTITION BY 决定“和谁一起算”,ORDER BY 决定“在组内按什么顺序算”,FRAME 决定“算的时候取哪一段行”。三段都省略时,整个结果集是一个窗口,且没有排序,聚合范围是全量。
SUM(amount) OVER ( PARTITION BY region -- 1. 切分区:每个 region 独立成一个窗口 ORDER BY sale_date -- 2. 组内排序:定义"当前行"的位置 ROWS BETWEEN UNBOUNDED PRECEDING -- 3. 取帧:从分区第一行 AND CURRENT ROW -- 到当前行(累计) ) AS running_total参数说明:UNBOUNDED PRECEDING表示分区起点,CURRENT ROW表示当前行,n PRECEDING/n FOLLOWING表示往前/往后 n 行,UNBOUNDED FOLLOWING表示分区终点。这一串写全之后语义是“截至当前行的累计和”,等价于财务报表里的滚动合计。
关键在于第二段。ORDER BY 一旦写进去,它不仅决定顺序,还隐式决定了一个默认帧。很多人以为“我只写了 ORDER BY,没写帧,那应该还是全量吧”,实际默认帧是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,也就是累计。这个默认值造成的“数值每行都不一样”是窗口函数最经典的翻车现场,第 4 章会拿两组对照数据把它彻底说清。
第三段的写法还受引擎约束:帧边界的起点不能晚于终点,BETWEEN 2 FOLLOWING AND 1 PRECEDING会直接报错;某些引擎在 RANGE 模式下只接受数值或时间类型的边界,写RANGE BETWEEN '2024-01-01' PRECEDING这种字符串是非法的。写之前先确认自己用的是 ROWS(按行数)还是 RANGE(按排序键的值),两者在排序键有重复值时会给出不同结果。
2.3 建一张最小可复现的表,把三段拆开跑
空谈结构不如直接跑一遍。下面这张表只有两个分区、六行数据,足够验证绝大多数窗口行为。
CREATE TABLE sales ( region VARCHAR(8), -- 大区代号 sale_date DATE, -- 成交日期 amount DECIMAL(12,2) -- 成交金额 ); INSERT INTO sales (region, sale_date, amount) VALUES ('R01', '2024-01-05', 1200.00), ('R01', '2024-01-19', 800.00), ('R01', '2024-02-02', 1500.00), ('R02', '2024-01-08', 900.00), ('R02', '2024-01-22', 900.00), ('R02', '2024-02-11', 2100.00);数据里特意让 R02 出现两个 900.00,是为了后面看排名函数的并列行为和 RANGE 帧的 peer 行效应。接下来把三种窗口写法并排跑,一次性看清“全量”“累计”“按行滑动”的区别。
SELECT region, sale_date, amount, -- 不写 ORDER BY:整分区求和,三行值相同 SUM(amount) OVER (PARTITION BY region) AS region_total, -- 只写 ORDER BY:默认帧是累计,值逐行变大 SUM(amount) OVER (PARTITION BY region ORDER BY sale_date) AS running_total, -- 显式写 ROWS 帧:当前行及前一行,窗口大小为 2 SUM(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS last2_sum FROM sales ORDER BY region, sale_date;以 R01 为例,三列的结果分别是:region_total三行都是 3500.00;running_total依次是 1200.00、2000.00、3500.00;last2_sum依次是 1200.00、2000.00、2300.00。第一行没有前一行,1 PRECEDING在分区边界处自动截断,不会补 NULL 也不会报错,这个截断行为是标准规定的,可以放心依赖。
跑完这组对照,再回头看速查表里的语法页,三段各自的效果就有了具体数字锚点。后续换任何函数(AVG、COUNT、MAX),只要 OVER() 这三段不变,窗口范围就完全一致——这也是窗口函数比自连接好维护的地方:换指标不用重写连接条件。
3. 排名与偏移:ROW_NUMBER、RANK、DENSE_RANK 和 LEAD/LAG 的选型边界
速查表把函数按用途分成排名、行偏移、聚合三类,这个分法比按字母序排更贴合实际使用。真正需要判断的只有两件事:并列时该跳号还是不该跳号,以及偏移函数在分区边界上返回什么。
3.1 三个排名函数:并列时到底跳不跳号
ROW_NUMBER、RANK、DENSE_RANK 的差别只在“遇到相同排序键时怎么办”这一个点上,但选错会让下游的筛选条件直接失效。
| 函数 | 并列行的取值 | 下一名取值 | 结果是否连续 | 典型用途 |
|---|---|---|---|---|
| ROW_NUMBER() | 各不相同的连续序号 | 顺延 | 连续 | 去重取最新、分页 |
| RANK() | 相同 | 跳过占用的名次 | 不连续 | 榜单、竞赛名次 |
| DENSE_RANK() | 相同 | 不跳过 | 连续 | 分级、档次划分 |
SELECT region, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn, RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rk, DENSE_RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS drk FROM sales ORDER BY region, amount DESC;在 R02 分区里,2100.00 排第一,两个 900.00 并列:ROW_NUMBER 给出 1、2、3;RANK 给出 1、2、2;DENSE_RANK 给出 1、2、2。注意 RANK 和 DENSE_RANK 在只有两档数据时看不出区别,把 R02 换成 2100、900、900、600 就能看到 RANK 是 1、2、2、4,DENSE_RANK 是 1、2、2、3。
选型理由很直接:如果下游要WHERE rk <= 3取前三名,用 RANK 可能返回四行甚至更多(并列把名额撑开),用 DENSE_RANK 返回的是“三个档次”,用 ROW_NUMBER 则一定是三行但并列行的取舍由引擎决定。做“每户取最新一条”这种必须精确一行的场景,ROW_NUMBER 是唯一正确选择,因为另外两个都会在并列时返回多行,导致下游数据翻倍。速查表在每个排名函数下面都标了返回行数特征,就是防这个。
还有一个隐藏参数:ORDER BY amount DESC NULLS LAST。不同引擎对 NULL 的默认排序位置不一致,有的排最前有的排最后,排名结果会整体错位。涉及可能为空的排序键时,把 NULLS FIRST / NULLS LAST 显式写出来,别赌默认值。
3.2 去重取最新一条:ROW_NUMBER 必须包一层子查询
这是窗口函数最高频的实战用法,也是新手最容易写错的地方——试图在 WHERE 里直接过滤rn = 1。
-- 错误写法:WHERE 里引用窗口列,执行顺序上根本拿不到 SELECT region, sale_date, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date DESC) AS rn FROM sales WHERE rn = 1; -- 报错或语义错误 -- 正确写法:子查询(或 CTE)先算出序号,外层再过滤 WITH ranked AS ( SELECT region, sale_date, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date DESC, amount DESC) AS rn FROM sales ) SELECT region, sale_date, amount FROM ranked WHERE rn = 1;逻辑说明:内层把每个大区的行按日期倒序编号,最新一条拿到 rn = 1;外层只保留 rn = 1 的行,每个大区恰好一行。参数说明:ORDER BY sale_date DESC决定“最新”的定义,后面补的amount DESC是 tiebreaker——如果同一个大区同一天有两笔成交,只按日期排序时 rn = 1 花落谁家由引擎的物理读取顺序决定,今天跑是这个结果,明天统计信息更新后可能换一条。这类不确定性在没有主键兜底的表上尤其常见,加一个唯一性足够的列做次级排序,结果才可复现。
如果表里有自增主键或订单号,最稳的写法是ORDER BY sale_date DESC, id DESC。这一步多写五个字符,能省掉后面反复对数对不上的半小时。
3.3 LEAD / LAG:offset、default 与时间差计算
偏移函数取的是“当前行往前/往后第 n 行”的值,两个参数值得记牢:第二个参数是偏移量(默认 1),第三个参数是越界时的默认值(默认 NULL)。
SELECT region, sale_date, amount, LAG(amount, 1, 0) OVER (PARTITION BY region ORDER BY sale_date) AS prev_amount, LEAD(amount, 1, 0) OVER (PARTITION BY region ORDER BY sale_date) AS next_amount, amount - LAG(amount) OVER (PARTITION BY region ORDER BY sale_date) AS diff_prev FROM sales ORDER BY region, sale_date;LAG(amount, 1, 0)表示取前一行金额,分区第一行没有前行时返回 0 而不是 NULL;LAG(amount)用的是默认偏移和默认 NULL,算差值时第一行会得到 NULL,后续聚合(比如 AVG)会自动忽略 NULL,两种写法的统计口径并不相同。做环比、同比这类需要“第一期没有基数”的指标时,用 NULL 更诚实,用 0 会把第一期算成百分之百增长,这是报表口径上的实质差异,不是风格问题。
把 LAG 和累计求和组合起来,可以做出会话切分(gap-and-islands)这类稍复杂的分析:先用 LAG 算出相邻事件的时间间隔,标记超过阈值的行为“新会话起点”,再用累计求和给每行打上会话编号。
WITH step1 AS ( SELECT user_id, event_time, LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time) AS prev_time FROM user_events ), step2 AS ( SELECT user_id, event_time, CASE WHEN prev_time IS NULL OR event_time > prev_time + INTERVAL '30' MINUTE THEN 1 ELSE 0 END AS is_new_session FROM step1 ) SELECT user_id, event_time, SUM(is_new_session) OVER (PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS session_id FROM step2;逻辑说明:第一步用 LAG 拿到上一条事件时间,第二步判断间隔是否超过 30 分钟(首条事件prev_time IS NULL也判为新会话),第三步对标记做累计求和,同一会话内的行会拿到相同的 session_id。参数说明:INTERVAL '30' MINUTE是标准写法,部分引擎需要用时间加减函数替代;累计求和那一步必须显式写ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,虽然默认帧在这里恰好等价,但显式写出来可以避免以后有人改了 ORDER BY 键之后语义悄悄漂移。
4. 窗口帧 FRAME:默认帧的坑和滑动窗口的正确写法
帧子句是窗口函数里最容易被跳过的一段,也是结果最容易“看着像对的、其实全错”的一段。速查表把 ROWS、RANGE、GROUPS 三种模式并列列出,实际工作中前两种覆盖了绝大多数场景。
4.1 有 ORDER BY 和没 ORDER BY,默认帧完全不同
这条差异值得单独拿出来说,因为它不报错、不告警,只是数不对。
-- 写法 A:没有 ORDER BY,默认帧是整个分区 SELECT region, sale_date, amount, SUM(amount) OVER (PARTITION BY region) AS total_a FROM sales; -- 写法 B:加了 ORDER BY,默认帧变成"起点到当前行" SELECT region, sale_date, amount, SUM(amount) OVER (PARTITION BY region ORDER BY sale_date) AS total_b FROM sales;写法 A 每个分区的三行拿到同一个值(R01 全是 3500.00);写法 B 拿到的是 1200.00、2000.00、3500.00。两段 SQL 只差一个 ORDER BY,语义从“分区总额”变成了“滚动累计”。很多人排查半天以为自己写错了 SUM,实际上是默认帧在起作用。
提示:要“分区总额 + 分区内排序”同时存在,正确写法是
SUM(amount) OVER (PARTITION BY region)单独算总额,ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date)单独算序号,两个窗口互不影响。不要试图在一个 OVER 里既排序又要全量合计。
再看 RANGE 模式下的 peer 行效应。R02 分区有两个 900.00,排序键如果是 amount,RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW会把所有取值相同的行一起算进当前帧,两个 900.00 拿到相同的累计值;换成ROWS模式,则严格按行号切,第二个 900.00 会把第一个也算进去。同样是“累计”,两种模式的数字不一样,选哪个取决于业务语义:按值累计(同额同待遇)用 RANGE,按行累计(逐笔流水)用 ROWS。
4.2 ROWS BETWEEN:累计、移动平均和同期对比
ROWS 模式最直观,边界就是行数。三种常用形态各自对应一类报表需求。
SELECT region, sale_date, amount, -- 1. 累计:分区起点到当前行 SUM(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total, -- 2. 近 3 笔移动平均:当前行及前 2 行 AVG(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS ma_3, -- 3. 前后各 1 行的居中窗口:用于平滑或找局部峰值 AVG(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS centered_avg FROM sales ORDER BY region, sale_date;逻辑说明:第 1 种是标准累计;第 2 种窗口大小固定为 3 行,不足 3 行时按实际行数计算(分区前两行的平均是基于 1 行和 2 行算的,不会补 NULL);第 3 种以当前行为中心,常用于时间序列平滑。参数说明:n PRECEDING和n FOLLOWING的 n 必须是常数或参数,不能是列引用——想按每行不同的窗口宽度计算,标准 SQL 不支持,需要用自连接或条件聚合绕开。
移动平均这里有个业务口径问题:窗口大小固定为 3 行,意味着数据有缺失日期时“近 3 笔”可能跨越很长的时间跨度。要做“近 30 天”这种按时间定义的窗口,得换成 RANGE 模式。
4.3 RANGE 和 GROUPS:按值分组而不是按行
RANGE 的边界是排序键的值,适合时间窗口;GROUPS 的边界是“排序键的取值分组数”,用得少但确实存在。
-- RANGE + 时间间隔:近 30 天窗口(要求排序键是日期类型) SELECT region, sale_date, amount, SUM(amount) OVER (PARTITION BY region ORDER BY sale_date RANGE BETWEEN INTERVAL '30' DAY PRECEDING AND CURRENT ROW) AS sum_30d FROM sales ORDER BY region, sale_date;参数说明:INTERVAL '30' DAY PRECEDING表示“当前行日期减 30 天”作为窗口起点,凡是日期落在这个区间内的行都参与求和。这种写法在各引擎的支持程度不一:有的只支持数值型边界(写RANGE BETWEEN 30 PRECEDING,此时排序键必须是数值),日期间隔需要先转成天数差;有的要求排序键唯一或者至少有序。跑之前先确认自己环境的支持范围,别直接照搬。
EXCLUDE子句是较新标准才补齐的能力,用来把当前行从帧里剔掉,做“不含本行的均值”这类对比很方便:
SELECT region, sale_date, amount, AVG(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING EXCLUDE CURRENT ROW) AS avg_without_self FROM sales;这个特性在老版本上会直接语法报错,速查表在对应函数旁标注了适用版本范围。遇到不支持的引擎,等价写法是把当前行权重扣掉:(SUM(...) - amount) / (COUNT(...) - 1),麻烦但通用。
5. 避坑与排查:窗口函数写错的五条血泪记录
窗口函数的麻烦在于它几乎不报错。语法通过、结果返回、行数正确,但数字可能是错的,而且错得很像对的。下面五条是我在实际对数中真踩过的,每条按“现象 → 原因 → 解决”记。
5.1 五个高频翻车现场
第一条:WHERE 里过滤窗口列,报“列不存在”。
现象:写好ROW_NUMBER() OVER (...) AS rn,外层直接WHERE rn = 1,引擎提示 rn 这个列不存在。原因:逻辑执行顺序是 FROM → WHERE → GROUP BY → HAVING → 窗口计算 → SELECT → ORDER BY,WHERE 执行时窗口列还没算出来。解决:用 CTE 或派生表包一层,外层再过滤;部分数据仓库类引擎支持 QUALIFY 子句,可以把窗口条件直接写在 QUALIFY 里,语义上等价于“窗口计算之后过滤”。
第二条:加了 ORDER BY 忘了写帧,累计值悄悄替换了总额。
现象:本来要每行显示大区总额,结果第一行 1200、第二行 2000、第三行 3500,逐行递增。原因:ORDER BY 一旦出现,默认帧变成RANGE UNBOUNDED PRECEDING AND CURRENT ROW。解决:要全量就删掉 OVER 里的 ORDER BY,或者显式写ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。后者读起来啰嗦,但意图明确,团队协作时更不容易被后人改错。
第三条:用 RANK 取前三,结果返回了五行。
现象:WHERE rk <= 3本该三行,实际返回四到五行。原因:RANK 遇到并列会跳号,两个第 2 名之后直接是第 4 名,但如果并列发生在第 3 名,就会出现多个 3。解决:要精确行数用 ROW_NUMBER,要按档次取用 DENSE_RANK,业务上确实允许并列时用 RANK 并在需求文档里写清楚“可能多于 N 行”。
第四条:同一个查询跑两次,取到的“最新一条”不一样。
现象:去重取最新的结果每次刷新都变,对数时被业务方追着问。原因:ORDER BY 的排序键不唯一(比如只按日期排,同一天有多笔),并列行的先后由执行计划决定,走索引扫描和走全表扫描可能给出不同顺序。解决:排序键补一个唯一性足够的列,通常是主键或订单号,写成ORDER BY sale_date DESC, id DESC。这是我个人认为性价比最高的一条习惯。
第五条:分区键和排序键搞反,排名全乱。
现象:想算“每个大区内部的排名”,结果排名是全局的。原因:把PARTITION BY region写成了ORDER BY region,或者顺序颠倒。解决:写完 OVER 之后回读一遍——PARTITION BY 后面跟的是“分组维度”,ORDER BY 后面跟的是“组内排序维度”,两者语义完全不同,写反了不报错但结果差一个量级。
5.2 窗口结果要过滤,三条改写路径怎么选
除了 QUALIFY,通用的改写方式就三种,选哪种取决于引擎和可读性。
-- 路径一:CTE,可读性最好,适合复杂查询 WITH ranked AS ( SELECT region, sale_date, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date DESC, amount DESC) AS rn FROM sales ) SELECT * FROM ranked WHERE rn <= 2; -- 路径二:派生表,适合一次性小查询,不用起名字 SELECT region, sale_date, amount FROM ( SELECT region, sale_date, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date DESC, amount DESC) AS rn FROM sales ) t WHERE rn <= 2; -- 路径三:QUALIFY,部分数据仓库类引擎支持,少一层嵌套 SELECT region, sale_date, amount FROM sales QUALIFY ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date DESC, amount DESC) <= 2;三种写法的执行结果一致,差别在可移植性和可读性。路径一在几乎所有支持 CTE 的引擎上都能跑,嵌套层次清楚,适合进代码库;路径二适合临时排查;路径三最简洁但换引擎就可能不认。团队里如果混用多种数据库,统一走路径一,别为了少写两行给自己挖坑。
还有个容易忽略的排查手段:把窗口函数单独跑一遍,不加任何外层过滤,肉眼检查 rn、rk 这些序号列是不是从 1 开始连续、有没有重复、分区边界对不对。序号列异常,八成是 PARTITION BY 或 ORDER BY 的问题;序号正常但数值不对,问题就在帧子句。按这个顺序二分,比盯着整条 SQL 猜要快得多。
6. 把速查表用成生产力:命名窗口和一次 EXPLAIN 验证
速查表翻熟之后,真正能提速的是两个动作:把重复的 OVER 抽成命名窗口,以及上线前看一眼执行计划。
6.1 WINDOW 子句:重复的 OVER 抽出来
一条报表 SQL 里经常有四五个指标共用同一个分区和排序,每个都写一遍PARTITION BY region ORDER BY sale_date既长又容易写岔。
SELECT region, sale_date, amount, SUM(amount) OVER w AS running_total, AVG(amount) OVER w AS running_avg, COUNT(*) OVER w AS running_cnt, -- 命名窗口后面可以追加 ORDER BY,覆盖原来的排序 RANK() OVER (w ORDER BY amount DESC) AS amount_rank FROM sales WINDOW w AS (PARTITION BY region ORDER BY sale_date) ORDER BY region, sale_date;逻辑说明:WINDOW w AS (...)定义了一个可复用的窗口,后面所有OVER w都引用同一份定义,改分区键时只需要改一处。参数说明:命名窗口只能“追加”ORDER BY,不能覆盖 PARTITION BY——OVER (w PARTITION BY xxx)是非法写法。这个限制意味着命名窗口适合“同一分区、多种排序或多种函数”的场景,分区维度不同的指标还是得各写各的。
6.2 验证:看排序次数,别只看耗时
窗口函数的性能瓶颈基本都在排序上。PARTITION BY 和 ORDER BY 的组合如果和已有索引顺序一致,引擎可以复用同一次排序;不一致就要额外排一遍,数据量大时这一下就是几秒的差距。
EXPLAIN SELECT region, sale_date, amount, SUM(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total FROM sales WHERE sale_date >= '2024-01-01';看执行计划时关注两点:窗口节点下面挂了几次排序,以及排序键是否和索引列顺序匹配。如果一条 SQL 里有多个窗口、排序键各不相同,排序次数会成倍增加。常见的优化手段是把多个指标的排序键统一(比如都按 sale_date),或者建一个(region, sale_date)的复合索引让排序直接消失。
验证方法也很朴素:把数据量按十倍递增各跑一次,记录耗时曲线。如果耗时随数据量线性增长,说明排序被索引兜住了;如果出现明显跳变,多半是排序落到了临时空间,这时候要么加索引,要么把大查询拆成“先聚合到中间表、再在中间表上开窗”两段。
我现在的习惯是:任何带窗口函数的报表 SQL 进代码库之前,必须跑一次 EXPLAIN,确认排序次数没超过窗口个数,并且排序键都有索引覆盖。这个动作花不了一分钟,但拦下过好几次“开发环境几万行跑得飞快、生产环境几千万行直接超时”的事故。速查表负责让我写对,EXPLAIN 负责让我确认它真的对——两件事都做完,才算把这份 PDF 用到位。希望帮到你。
本文还有配套的精品资源,点击获取