SQL中的窗口函数进阶:滑动窗口与帧子句详解
关键词窗口函数滑动窗口帧子句移动平均ROWSRANGESQL进阶大家好我是小耶写功课只是为了我踩过的坑你们别再踩了上周讲了窗口函数与子查询、CTE的性能对比有读者问窗口函数的帧子句ROWS/RANGE到底怎么用为什么有时候用ROWS有时候用RANGE今天就把这个坑填上专门讲讲窗口函数的进阶能力——滑动窗口与帧子句。先解释两个核心术语什么是“滑动窗口”想象你站在一列数据的长队里眼前有一个固定宽度的“窗口”这个窗口每次向右移动一格每次只统计窗口内的数据。比如计算最近3天的移动平均第一天看第1-3天第二天看第2-4天第三天看第3-5天……窗口在“滑动”。这就是滑动窗口的核心思想窗口位置随着当前行移动每次计算一个范围内的数据。什么是“帧子句”帧子句就是用来定义这个“窗口范围”的规则。它告诉数据库当前行的窗口应该从哪里开始、到哪里结束。比如“从当前行的前2行到当前行的后2行”“从分区第一行到当前行”。帧子句是窗口函数实现滑动窗口的关键语法。窗口函数的核心语法是函数() OVER (PARTITION BY ... ORDER BY ... 帧子句)。帧子句定义了相对于当前行窗口的起止范围。用好帧子句可以实现移动平均、累计求和、同比环比、滑动聚合等复杂逻辑否则窗口函数就只是带排序的分组聚合而已。一、帧子句的基本语法帧子句的完整写法ROWS | RANGE BETWEEN 起点 AND 终点其中起点和终点可以是UNBOUNDED PRECEDING从分区第一行开始n PRECEDING当前行之前的n行CURRENT ROW当前行n FOLLOWING当前行之后的n行UNBOUNDED FOLLOWING直到分区最后一行如果不显式指定帧子句默认行为是有ORDER BY时默认RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW无ORDER BY时默认ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。这一点经常被误解导致计算结果与预期不符。二、ROWS vs RANGE 的核心区别这是最容易踩的坑。用一个比喻帮助你理解ROWS像用“行号”画窗口。窗口按行数严格划分不管ORDER BY列的值是否相同每一行都独立计算。类似于“前5个人、后5个人”。RANGE像用“值”画窗口。窗口按ORDER BY列的值划分相同值的数据必须同时出现在窗口内或被排除在外。类似于“所有年龄相同的人放在一起统计”。用一个具体例子说明。表sales日期和销售额sale_dateamount2026-01-011002026-01-01502026-01-022002026-01-03150执行SELECT sale_date, amount, SUM(amount) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as rows_cum, SUM(amount) OVER (ORDER BY sale_date RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as range_cum FROM sales;结果sale_dateamountrows_cumrange_cum2026-01-011001001502026-01-01501501502026-01-022003503502026-01-03150500500ROWS严格按行顺序累加第一行100第二行10050150每行都变。RANGE按sale_date的值分组。2026-01-01的两行属于同一个值窗口把这两行作为一个整体累计所以两行的累计值都是15010050直到2026-01-02才增加到350。实际业务中需要严格逐行计算如移动平均、每笔交易独立累计→ 用ROWS需要按逻辑分组聚合如按日期统计同一天的数据应同时计入→ 用RANGE三、典型滑动窗口场景场景13日移动平均滑动窗口经典案例计算每个日期前后各1天包含当天的平均销售额。这里的“窗口”就是当前行、前1行、后1行。随着当前行向下移动窗口也跟着“滑动”。SELECT sale_date, amount, AVG(amount) OVER (ORDER BY sale_date ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) as moving_avg_3 FROM sales;注意边界处理第一行没有1 PRECEDING窗口只包含当前行和1 FOLLOWING。这就是滑动窗口最常用的形式。场景2从当前行到分区末尾的累计计算每个部门内按工资从低到高排序从当前员工到工资最高者的工资总和。SELECT dept, salary, SUM(salary) OVER (PARTITION BY dept ORDER BY salary ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) as sum_from_curr FROM emp;这里窗口的起点是“当前行”终点是“分区末尾”随着当前行下移窗口越来越小。适合计算“比我工资高的人的总和”等需求。场景3排除当前行的滑动窗口计算当前行之前2行到当前行之后2行但排除当前行本身。例如分析整体趋势时去掉自身的波动。SELECT sale_date, amount, AVG(amount) OVER (ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW) as moving_avg_exclude_self FROM sales;EXCLUDE CURRENT ROW是SQL标准支持但MySQL尚未实现的语法PostgreSQL等数据库已支持。如果MySQL需要实现类似效果可以自行计算总窗口值再减去当前值。四、ROWS与RANGE在滑动窗口中的选择建议需求场景推荐帧类型原因时间序列移动平均按行严格计算ROWS不关心时间间隔是否连续只关心行数按日期分组统计同一天数据一起算RANGE相同ORDER BY值应属于同一个窗口财务累计按交易顺序ROWS每笔交易独立严格逐行累加滚动窗口最近7天不关心行数RANGE基于日期的范围可能某天有多行或没有行五、实际运用计算同比环比假设有每月销售表monthly_salesyear, month, amount。计算环比与上月比较SELECT year, month, amount, LAG(amount, 1) OVER (ORDER BY year, month) as prev_amount, (amount - LAG(amount, 1) OVER (ORDER BY year, month)) / LAG(amount, 1) OVER (ORDER BY year, month) as growth_rate FROM monthly_sales;LAG/LEAD函数配合帧子句可以更灵活地定义偏移量。计算同比去年同期则需要更复杂的窗口定义或自连接。六、注意事项与性能建议帧子句只对聚合窗口函数SUM、AVG、COUNT、MIN、MAX有意义排名函数ROW_NUMBER、RANK等和偏移函数LAG、LEAD忽略帧子句始终基于整个分区。RANGE模式要求ORDER BY列是数值或日期类型且通常会产生比ROWS更多的内存消耗因为需要识别“相同值”的组边界。超大窗口滑动时如UNBOUNDED PRECEDING相当于全分区扫描性能开销大。可考虑使用索引和物化视图预计算。七、总结窗口函数的高级能力——帧子句是实现复杂滑动分析的关键。区分ROWS与RANGE、正确设置边界能写出更简洁高效的SQL避免使用自连接或游标。掌握这些技巧是SQL从“能写”到“会优化”的重要一步。小耶在手SQL 不愁还有什么想了解的欢迎留言小耶一定知无不言言无不尽……我们下次见~参考文献MySQL官方文档《Window Function Frame Specification》PostgreSQL官方文档《Window Functions: ROWS vs RANGE》《SQL进阶教程》第7章窗口函数