向前滚动累加SQL 实现思路
一、业务背景
在经营分析场景里,我们经常需要回答:
-
“截至今天,过去 N 天/月/周累计发生了多少?”
-
“把维度切到省、市、房型、项目经理、代理商等,结果又是什么?”
本文用两个真实需求做演示:
-
以天为粒度,计算过去 7 天 的放款单量、放款金额;并顺便给出过去 5 天 的口径作为对比。
-
以月为粒度,计算过去 6 个月(含当月)的累计订单数,维度为代理商。
为了便于阅读,下文所有表名、字段名都做了脱敏混淆,但逻辑 100 % 保留。
二、需求 1:过去 7 天放款(天粒度,多维度)
2.1 期望输出
其中:
-
ys_fst_apply_cnt_7sum = 过去 7 天(不含当日)的同维度放款单量之和
-
delivery_go_capacity_sum = 过去 7 天(不含当日)的同维度放款金额之和
2.2 实现思路
采用 自关联(self-join):
-
主表 A:取当日维度切片;
-
从表 B:在时间窗口
[A.data_date-7, A.data_date-1]
且维度完全一致的数据; -
用
SUM()
聚合即可完成累加。
2.3 关键 SQL
SELECT a.data_date, a.dim_province, a.dim_city, a.dim_town, a.dim_roof1, a.dim_roof2, a.dim_pm, a.dim_dealer, a.dim_dealer_team, a.dim_settle, a.cnt_当天单量, a.amt_当天金额, /* 过去 7 天(不含当日)累计 */ SUM(COALESCE(b.cnt_当天单量,0)) AS cnt_7d, SUM(COALESCE(b.amt_当天金额,0)) AS amt_7d, /* 过去 5 天(不含当日)累计,仅作演示 */ SUM(CASE WHEN a.data_date - b.data_date <= 5 THEN COALESCE(b.cnt_当天单量,0) ELSE 0 END) AS cnt_5d, SUM(CASE WHEN a.data_date - b.data_date <= 5 THEN COALESCE(b.amt_当天金额,0) ELSE 0 END) AS amt_5dFROM fact_daily_loan aLEFT JOIN fact_daily_loan b ON ( COALESCE(a.dim_province,\' \') = COALESCE(b.dim_province,\' \') AND COALESCE(a.dim_city,\' \') = COALESCE(b.dim_city,\' \') AND COALESCE(a.dim_town,\' \') = COALESCE(b.dim_town,\' \') AND COALESCE(a.dim_roof1,\' \') = COALESCE(b.dim_roof1,\' \') AND COALESCE(a.dim_roof2,\' \') = COALESCE(b.dim_roof2,\' \') AND COALESCE(a.dim_pm,\' \') = COALESCE(b.dim_pm,\' \') AND COALESCE(a.dim_dealer,-1) = COALESCE(b.dim_dealer,-1) AND COALESCE(a.dim_dealer_team,-1) = COALESCE(b.dim_dealer_team,-1) AND COALESCE(a.dim_settle,\' \') = COALESCE(b.dim_settle,\' \') AND a.data_date - b.data_date BETWEEN 1 AND 7 -- 关键:滑窗 7 天)GROUP BY a.data_date, a.dim_province, a.dim_city, a.dim_town, a.dim_roof1, a.dim_roof2, a.dim_pm, a.dim_dealer, a.dim_dealer_team, a.dim_settle, a.cnt_当天单量, a.amt_当天金额;
2.4 注意点
-
维度对齐:所有维度都要
COALESCE
以防 NULL 匹配不上。 -
时间区间:
a.data_date - b.data_date BETWEEN 1 AND 7
等价于“前 7 天不含当日”。 -
性能:如果数据量大,建议把日期过滤下推、或在从表加索引
(date, 维度组合)
。
三、需求 2:过去 6 月订单(月粒度,仅代理商维度)
3.1 期望输出
3.2 实现思路
采用 生成月份序列 + 预聚合 的经典写法:
-
先把事实表出现的所有月份抽出来(去重)。
-
为每个月生成一个 6 个月窗口(含自己 + 前 5 个月)。
-
预先把订单按月去重,得到
(dealer, month, order_no)
的干净集合。 -
用窗口月把“干净集合”挂上去,再
COUNT(DISTINCT order_no)
即可。
3.3 关键 SQL
-- 1. 提取事实表所有月份WITH months AS ( SELECT DISTINCT date_trunc(\'month\', confirm_dt) AS month_start FROM fact_order_detail),-- 2. 为每个月生成 6 个月窗口windowed AS ( SELECT m.month_start, generate_series( m.month_start - INTERVAL \'5 month\', m.month_start, INTERVAL \'1 month\' )::date AS window_month FROM months m),-- 3. 预聚合:按月去重订单base AS ( SELECT dealer_name, dealer_id, date_trunc(\'month\', confirm_dt) AS month_start, order_no FROM fact_order_detail),-- 4. 把窗口拼到 base 上agg AS ( SELECT w.month_start AS report_month, b.dealer_name, b.dealer_id, COUNT(DISTINCT b.order_no) AS cnt_6m FROM windowed w JOIN base b ON b.month_start = w.window_month GROUP BY w.month_start, b.dealer_name, b.dealer_id)-- 5. 最终输出SELECT cnt_6m, dealer_name, dealer_id, to_char(report_month, \'YYYY-MM-DD\') AS report_month_strFROM aggORDER BY dealer_id, report_month;
3.4 注意点
-
用
generate_series
生成月份序列,天然避开了闰月、大小月问题。 -
预先把订单按月去重,避免后面
COUNT DISTINCT
时扫大表。 -
如果窗口更大(如 12 个月),可把
5
改成11
即可。
四、两种方案对比与选型建议
一句话总结:
-
短周期 + 高维度 → 自关联 + 时间过滤;
-
长周期 + 低维度 → 预聚合 + 生成序列。
五、扩展思考
-
Presto/Trino 可用
RANGE BETWEEN INTERVAL \'7\' DAY PRECEDING AND 1 DAY PRECEDING
的窗口函数,逻辑更简洁,但引擎需要支持。 -
ClickHouse 可用
GROUP BY (date, dim...) WITH ROLLUP
+runningAccumulate
实现实时累加。 -
实时场景 可以把窗口结果写到 Redis / Druid,再通过 API 提供毫秒级查询。