SQL中如何实现按周统计的滚动平均
按周计算滚动平均值,听起来是个常见的需求,但实际动手时,你会发现从日期处理、数据库兼容性到性能优化,处处是“坑”。今天,我们就来把这些关键点逐一拆解清楚。

SQL中DATE_TRUNC('week')在不同数据库的兼容性问题
首先得明白,DATE_TRUNC('week')这个看似标准的函数,在数据库世界里远未统一。PostgreSQL和BigQuery是它的“忠实粉丝”,可以直接用它把日期截取到所在周的周一。但如果你把同样的代码搬到MySQL、SQL Server或SQLite里,大概率会收获一个冰冷的报错:function DATE_TRUNC does not exist。即便是开始部分支持的MariaDB(10.4+版本),其默认的周起始日也可能是周日,这和许多业务的周一为起点设定并不一致。
那么,具体该怎么操作呢?这里有一份速查指南:
- PostgreSQL/BigQuery用户:可以直接使用
DATE_TRUNC('week', order_date)。不过,BigQuery用户需要留个心眼,想确保从周一开始,得写成DATE_TRUNC(order_date, WEEK(MONDAY))。 - MySQL用户:可以换个思路,用
DATE_SUB(order_date, INTERVAL WEEKDAY(order_date) DAY)。这里的WEEKDAY()函数返回0代表周一,所以减去这个天数,就能精准定位到当周的周一。 - SQL Server用户:公式稍复杂一些:
DATEADD(DAY, 2-DATEPART(WEEKDAY, order_date), order_date)。务必记得先用SET DATEFIRST 1设置周一为一周之首,否则计算结果可能会发生偏移。 - 一个通用警告:千万别图省事依赖
YEARWEEK()或WEEK()这类只返回数字的函数。它们在处理跨年周时(比如2024年12月30日属于2025年第1周)极易引发分组混乱,后期排查起来相当头疼。
用窗口函数计算滚动周均值时,ROWS BETWEEN 2 PRECEDING AND CURRENT ROW为什么不对
这是新手最容易踩的“雷区”。按周统计滚动平均,本质上是先聚合,后开窗。如果你直接在原始订单明细表上套用ROWS BETWEEN 2 PRECEDING AND CURRENT ROW,窗口会严格按照物理行序滑动。想象一下,某一周有1000笔订单,下一周只有10笔,这个窗口计算的就只是最近3“行”的平均值,而不是最近3“周”的平均值,完全背离了业务本意。
正确的姿势必须是两步走:
- 第一步:聚合。先用
GROUP BY按周起始日(如上面计算出的周一)将数据汇总,生成一张包含week_start和weekly_amount的周粒度汇总表。 - 第二步:开窗。在这张汇总表上,再使用窗口函数:
A VG(weekly_amount) OVER (ORDER BY week_start ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)。这样,窗口滑动的单位才是“周”。 - 两个细节决定成败:其一,确保
week_start字段是纯粹的DATE类型,而不是TIMESTAMP,避免某些数据库因毫秒级时间戳差异导致排序错乱。其二,如果想计算包含当前周在内的最近3周均值,坚持用ROWS子句,别用RANGE。RANGE BETWEEN INTERVAL '14' DAY PRECEDING AND CURRENT ROW这种基于值域的写法,在跨月、跨年时很容易漏掉整周的数据。
处理跨年周时ISO week与自然周的混淆陷阱
年底的数据分析,常常因为“跨年周”而翻车。以2024年12月30日为例,在ISO标准下,它属于2025年的第1周(因为ISO规定,包含新年至少4天的周,就划归新年)。但你的业务报表很可能希望把它算作2024年的最后一周。一旦用错标准,就会导致2024年莫名少了一周,2025年凭空多出一周,滚动平均曲线会出现一个刺眼的断层。
如何规避?关键在于事先明确:
- 定义先行:和业务方确认,“第1周”到底指什么?是1月1日所在的那一周?还是新年第一个完整的周一到周日?或是严格遵循ISO标准?
- 函数选择:不同数据库的函数含义不同。PostgreSQL中,
TO_CHAR(date, 'IYYY-IW')返回ISO年周,而TO_CHAR(date, 'YYYY-WW')返回日历年周,两者在年初年末可能相差多达2周。MySQL中,YEARWEEK(date, 1)(模式1,周一起始,周数从1开始)通常比YEARWEEK(date, 3)(ISO模式)更符合常规业务认知。 - 维度表设计:构建周维度表时,切忌只存储“年+周数”这样的字符串。务必包含
week_start和week_end这两个明确的DATE类型字段。后续所有的关联、排序和比较,依赖这两个字段远比解析字符串来得可靠和高效。
性能瓶颈常出现在窗口函数前的周分组阶段
当数据量达到千万级甚至更高时,性能瓶颈往往不是窗口函数本身,而是它前面的周分组计算。如果原始表的日期字段上没有合适的索引,像GROUP BY DATE_SUB(order_date, INTERVAL WEEKDAY(order_date) DAY)这样的表达式会迫使数据库进行全表扫描和计算,耗时急剧上升。
如何提速?以下几个思路值得尝试:
- 利用函数索引:在支持函数索引的数据库(如PostgreSQL、MySQL 8.0.13+)中,可以直接为周起始日的计算表达式创建索引:
CREATE INDEX idx_order_week ON orders ((DATE_SUB(order_date, INTERVAL WEEKDAY(order_date) DAY)))。 - 提前物化字段:如果表结构允许,可以增加一个
order_week DATE的字段,并通过UPDATE语句预先计算好每周的起始日。之后,在这个字段上建立普通的B树索引,查询效率会大幅提升。 - 避免窗口内重复计算:不要在窗口函数的
ORDER BY子句中直接写复杂的日期表达式。务必先完成周粒度的聚合,再对聚合后的清晰字段进行窗口排序。 - 限定数据范围:如果业务只关心最近12周(约84天)的滚动均值,那么先在
WHERE子句中过滤数据:WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 84 DAY)。让窗口函数处理少量数据,远比让它扛起全量数据轻松得多。
说到底,最容易被忽略的一点是:周边界的计算必须从一开始就和业务定义对齐。前期多花一行代码过滤,或者建一个合适的索引,远比在后期复杂的窗口逻辑中调试和优化要省力得多,也有效得多。
