查“最近三个月”必须用 RANGE BETWEEN,不能用 ROWS BETWEEN,因为 ROWS 按物理行数滑动(如前3行),而销售数据常有日期缺失、同日多笔或空档,导致“前三行”实际跨期数月;RANGE 才能基于真实时间值(如 ORDER BY sale_date)动态聚合,确保覆盖准确的日历月范围。

滑动时间窗口必须用 ROWS BETWEEN 还是 RANGE BETWEEN?
直接说结论:查“最近三个月”必须用 RANGE BETWEEN,不能用 ROWS BETWEEN。因为 ROWS 按物理行数滑动(比如前3行),而销售数据常有缺失日期、多笔同日交易或非工作日空档——这时“前三行”可能跨半年,完全不是“三个月”。RANGE 才能按真实时间值(如 ORDER BY sale_date)动态聚合。
常见错误现象:ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 看似取3天,但若某天无销售记录,它会跳到更早的日期,导致窗口实际覆盖5月、8月、11月——结果毫无业务意义。
- 使用场景:月度销售看板、同比环比计算、异常波动检测
- 关键前提:分区字段(如
product_id)+ 排序字段(必须是DATE或TIMESTAMP类型) - PostgreSQL / Snowflake / BigQuery 支持
RANGE时间间隔;MySQL 8.0+ 仅支持RANGE数值类型,需转为天数差绕过
INTERVAL '3 MONTH' 在不同数据库里的写法差异
语法看似统一,实则坑多。核心问题是:三个月 = 90天?还是按日历月滚动?窗口函数要求后者,但各库实现不一致。
PostgreSQL 可直接写:RANGE BETWEEN INTERVAL '3 MONTH' PRECEDING AND CURRENT ROW;Snowflake 必须用 DATEADD('month', -3, sale_date) 配合 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 再加 WHERE 过滤——否则报错“interval not supported in RANGE”。
- BigQuery:用
RANGE BETWEEN 90 PRECEDING AND CURRENT ROW+ORDER BY DATE_DIFF(sale_date, '1970-01-01', DAY)(把日期转为天数整型) - SQL Server:不支持
RANGE时间间隔,只能用自连接或 CTE 先生成日期序列再JOIN - 注意:所有方案中
CURRENT ROW包含当天,若需求是“截至昨日的三个月”,得额外WHERE sale_date
为什么 AVG() 窗口结果常出现 NULL 或精度异常?
不是函数写错了,而是两类隐性问题:
- NULL 值参与计算:若某天销售额为
NULL,AVG()默认忽略该行——但如果你用SUM()/COUNT(*)手动算,COUNT(*)会把NULL行当 1 计,结果偏低。正确做法是显式写AVG(sales_amount) OVER (...),它自动跳过NULL - 整型除法截断:PostgreSQL 中
sales_amount是INTEGER,AVG()结果默认仍为整型(如 12345 → 123)。必须先转sales_amount::DECIMAL或写AVG(CAST(sales_amount AS DECIMAL)) - 性能影响:
RANGE窗口比ROWS慢 2–5 倍(因需索引查找而非顺序扫描),大表务必在(product_id, sale_date)上建复合索引
没有原生时间 RANGE 支持时的降级方案
MySQL 5.7/8.0 和旧版 Redshift 就是硬不支持 RANGE 时间值,这时得用“日期序列填充 + LEFT JOIN”模拟滑动窗口:
WITH date_series AS (
SELECT DISTINCT DATE_SUB(sale_date, INTERVAL (a.a + b.b * 10) DAY) AS dt
FROM sales, (SELECT 0 a UNION SELECT 1 UNION SELECT 2 UNION SELECT 3) AS a,
(SELECT 0 b UNION SELECT 1 UNION SELECT 2 UNION SELECT 3) AS b
WHERE DATE_SUB(sale_date, INTERVAL (a.a + b.b * 10) DAY) >= DATE_SUB(MAX(sale_date) OVER(), INTERVAL 90 DAY)
),
windowed AS (
SELECT ds.dt, s.sales_amount
FROM date_series ds
LEFT JOIN sales s ON DATE(s.sale_date) = ds.dt
)
SELECT dt, AVG(sales_amount) OVER (ORDER BY dt ROWS BETWEEN 89 PRECEDING AND CURRENT ROW) AS avg_90d
FROM windowed;
这个方案本质是把稀疏销售日期补成连续日历,再用 ROWS 安全滑动。代价是临时表膨胀、内存占用高——线上千万级表慎用,优先升级数据库版本。
真正麻烦的从来不是写对一行 AVG() OVER,而是确认你用的数据库到底把 “3 MONTH” 解析成哪个月的最后一天、是否自动处理月末(如1月31日→4月30日)、以及 NULL 值在聚合链路里被吞了几次。










