时间窗口环比是按固定时间粒度(如最近7天vs上一个7天)分组聚合后比较,需用两个独立子查询分别计算当前与上期窗口总和,再JOIN拼接做差值或除法。

什么是时间窗口环比?先搞清计算逻辑
环比不是简单用 LAG() 拉上一行,而是按固定时间粒度(比如“最近7天 vs 上一个7天”)做分组聚合再比较。子查询在这里的作用是:先分别算出两个窗口的汇总值,再把它们拼到同一行做除法或差值。如果直接在 SELECT 里嵌套聚合子查询,会触发“不能在聚合函数中嵌套聚合”的错误,所以必须把窗口计算拆到 FROM 子句里。
用两个独立子查询 + JOIN 实现双窗口对比
核心思路是把“当前窗口”和“上期窗口”各自写成子查询,然后用常量或 dummy key 连接。这样避免了 GROUP BY 冲突,也绕开了窗口函数对排序/分区的强依赖。
- 当前窗口(如最近7天):用
WHERE order_time >= CURRENT_DATE - INTERVAL '6 days' - 上期窗口(往前推7天):用
WHERE order_time BETWEEN CURRENT_DATE - INTERVAL '13 days' AND CURRENT_DATE - INTERVAL '7 days' - 两个子查询都只返回单行单列(如
SUM(amount)),再用SELECT ... FROM (sub1) AS curr, (sub2) AS prev拼接 - PostgreSQL/MySQL 8.0+/SQL Server 都支持这种写法;SQLite 不支持
INTERVAL,得用date('now', '-6 days')
SELECT curr.total AS current_week, prev.total AS last_week, ROUND((curr.total - prev.total) / NULLIF(prev.total, 0), 3) AS week_over_week FROM ( SELECT COALESCE(SUM(amount), 0) AS total FROM orders WHERE order_time >= CURRENT_DATE - INTERVAL '6 days' ) AS curr, ( SELECT COALESCE(SUM(amount), 0) AS total FROM orders WHERE order_time BETWEEN CURRENT_DATE - INTERVAL '13 days' AND CURRENT_DATE - INTERVAL '7 days' ) AS prev;
为什么不用 LAG() 或 CTE?这些场景下它不适用
LAG() 只能按行偏移,无法自动对齐“自然周”或“滚动7天”这类非连续、非对齐的时间段。比如你希望每次跑数都统计「昨天往前推7天」vs「再往前7天」,但订单表里没有预计算的“周序号”字段,LAG() 就没法跨天聚合后取值。
- CTE 如果写成
WITH w AS (SELECT date_trunc('week', order_time) AS wk, SUM(...) FROM ... GROUP BY wk),确实能用LAG(),但它要求数据本身有明确的周期对齐点(比如周一零点起),且无法表达“滚动窗口” - 子查询方式更可控:每个窗口的
WHERE条件可任意定制,比如节假日剔除、工作日过滤、动态起始日等 - 性能上,两个子查询会各自走一次索引扫描(如果
order_time有索引),比全表扫一遍再分窗聚合更快
容易漏掉的细节:NULL 处理、时区、边界日期
-
COALESCE(SUM(...), 0) 必须加,否则任一窗口无数据就整个结果为 NULL
-
NULLIF(prev.total, 0) 是除零保护,不然上期为0时会报错
- 时间条件用
BETWEEN 要小心闭区间语义:PostgreSQL 的 BETWEEN '2024-01-01' AND '2024-01-07' 包含 00:00 和 24:00,但你的数据可能是带时分秒的 TIMESTAMP,建议统一用 >= AND < 写法更安全
- 时区问题常被忽略:数据库服务器时区、应用层传入的时间、用户期望的本地时区可能不一致,最好在子查询里显式转换,例如
WHERE order_time AT TIME ZONE 'Asia/Shanghai' >= ...
COALESCE(SUM(...), 0) 必须加,否则任一窗口无数据就整个结果为 NULL
NULLIF(prev.total, 0) 是除零保护,不然上期为0时会报错BETWEEN 要小心闭区间语义:PostgreSQL 的 BETWEEN '2024-01-01' AND '2024-01-07' 包含 00:00 和 24:00,但你的数据可能是带时分秒的 TIMESTAMP,建议统一用 >= AND < 写法更安全WHERE order_time AT TIME ZONE 'Asia/Shanghai' >= ...
实际跑的时候,如果发现环比值异常大,优先检查两个子查询各自返回的数值是否合理——别急着调公式,先确认窗口没重叠、也没漏掉半天数据。

















