目录
[🎯 问题目标](#🎯 问题目标)
[第三步:SQL 怎么表达"某一天的前六天"?](#第三步:SQL 怎么表达“某一天的前六天”?)
[🔍JOIN 比窗口函数更灵活](#🔍JOIN 比窗口函数更灵活)
[第五步:怎么避免不满 7 天的窗口?](#第五步:怎么避免不满 7 天的窗口?)
[最终完整 SQL](#最终完整 SQL)
表: Customer
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| customer_id | int |
| name | varchar |
| visited_on | date |
| amount | int |
+---------------+---------+
在 SQL 中,(customer_id, visited_on) 是该表的主键。
该表包含一家餐馆的顾客交易数据。
visited_on 表示 (customer_id) 的顾客在 visited_on 那天访问了餐馆。
amount 是一个顾客某一天的消费总额。
你是餐馆的老板,现在你想分析一下可能的营业额变化增长(每天至少有一位顾客)。
计算以 7 天(某日期 + 该日期前的 6 天)为一个时间段的顾客消费平均值。average_amount
要 保留两位小数。
结果按 visited_on
升序排序。
返回结果格式的例子如下。
示例 1:
输入:
Customer 表:
+-------------+--------------+--------------+-------------+
| customer_id | name | visited_on | amount |
+-------------+--------------+--------------+-------------+
| 1 | Jhon | 2019-01-01 | 100 |
| 2 | Daniel | 2019-01-02 | 110 |
| 3 | Jade | 2019-01-03 | 120 |
| 4 | Khaled | 2019-01-04 | 130 |
| 5 | Winston | 2019-01-05 | 110 |
| 6 | Elvis | 2019-01-06 | 140 |
| 7 | Anna | 2019-01-07 | 150 |
| 8 | Maria | 2019-01-08 | 80 |
| 9 | Jaze | 2019-01-09 | 110 |
| 1 | Jhon | 2019-01-10 | 130 |
| 3 | Jade | 2019-01-10 | 150 |
+-------------+--------------+--------------+-------------+
输出:
+--------------+--------------+----------------+
| visited_on | amount | average_amount |
+--------------+--------------+----------------+
| 2019-01-07 | 860 | 122.86 |
| 2019-01-08 | 840 | 120 |
| 2019-01-09 | 840 | 120 |
| 2019-01-10 | 1000 | 142.86 |
+--------------+--------------+----------------+
解释:
第一个七天消费平均值从 2019-01-01 到 2019-01-07 是restaurant-growth/restaurant-growth/ (100 + 110 + 120 + 130 + 110 + 140 + 150)/7 = 122.86
第二个七天消费平均值从 2019-01-02 到 2019-01-08 是 (110 + 120 + 130 + 110 + 140 + 150 + 80)/7 = 120
第三个七天消费平均值从 2019-01-03 到 2019-01-09 是 (120 + 130 + 110 + 140 + 150 + 80 + 110)/7 = 120
第四个七天消费平均值从 2019-01-04 到 2019-01-10 是 (130 + 110 + 140 + 150 + 80 + 110 + 130 + 150)/7 = 142.86
来源:Leecode
🎯 问题目标
先问自己最本质的问题:
我想得到的到底是什么?
你想得到:
-
某一天(比如
2019-01-07
)为 窗口最后一天 -
以它为终点往前推 6 天(共 7 天)的所有消费数据
-
求这 7 天的总消费额和平均消费额(平均保留两位小数)
-
然后按日期升序列出每个窗口的情况
第一步:从数据中我们能直接得到什么?
我们原始数据是:
| customer_id | name | visited_on | amount |
|-------------|-------|------------|--------|
| 1 | Jhon | 2019-01-01 | 100 |
| 2 | Daniel| 2019-01-02 | 110 |
| ... | ... | ... | ... |
这是"按顾客"记录的交易数据。
原始数据是"每个顾客某天消费了多少",而我们不关心顾客是谁,只关心 每一天总共有多少消费。
为了达成这个目标,你最小的可操作单位是:
✅ 每一天的"总营业额"
所以,第一步我们应该做的是:
sql
SELECT
visited_on,
SUM(amount) AS total_amount
FROM Customer
GROUP BY visited_on
得到了:
sql
| visited_on | total_amount |
|------------|--------------|
| 2019-01-01 | 100 |
| 2019-01-02 | 110 |
| 2019-01-03 | 120 |
| ... | ... |
第二步:我们想要的"7天窗口"长什么样?
比如你想分析 2019-01-07
这个窗口,它包括:
-
2019-01-01
-
2019-01-02
-
2019-01-03
-
2019-01-04
-
2019-01-05
-
2019-01-06
-
2019-01-07
我们要把这 7 天的金额加总后求平均。
换句话说,对于每一个日期 D
,你要去找所有日期 D'
,满足:
D' >= D - 6 天 AND D' <= D
,然后求 sum(amount)
第三步:SQL 怎么表达"某一天的前六天"?
想象一下,窗口要对比谁和谁?
我们要让每一行(例如日期是 2019-01-10
)"看见"自己之前 6 天的数据。但 SQL 是面向集合的语言,每一行默认不能看见其他行。
怎么让一行"看到"它前面的几天?答案是:自连接(JOIN)!
sql
SELECT
c1.visited_on, -- 作为窗口的当前"右端点"
c2.visited_on, -- 被扫描比较的行
FROM (
SELECT visited_on, SUM(amount) AS daily_total
FROM Customer
GROUP BY visited_on
) c1
JOIN (
SELECT visited_on, SUM(amount) AS daily_total
FROM Customer
GROUP BY visited_on
) c2
ON c2.visited_on BETWEEN DATE_SUB(c1.visited_on, INTERVAL 6 DAY) AND c1.visited_on
这个 JOIN 的意思是:
对于每一行 c1,找出所有 c2,使得 c2.visited_on
落在 c2 之前 6 天之内。
也就是说,每一行 c1 会配对出一个 7 天的"时间窗口"数据集 c2。
就像下面这个例子:
c1.visited_on | c2.visited_on(符合条件) |
---|---|
2019-01-07 | 2019-01-01 ~ 2019-01-07 |
2019-01-08 | 2019-01-02 ~ 2019-01-08 |
2019-01-09 | 2019-01-03 ~ 2019-01-09 |
2019-01-10 | 2019-01-04 ~ 2019-01-10 |
你可以理解为:"c1 的每一天",都配对了"过去七天的 c2",这就模拟出"滑动窗口"的行为了!
🔍JOIN 比窗口函数更灵活
在"时间窗口"这种分析中,数据可能并不是每天都有,或者每天不止一条记录,比如:
sql
| visited_on | amount |
|--------------|--------|
| 2024-01-01 | 100 |
| 2024-01-01 | 80 |
| 2024-01-03 | 200 |
这种不连续、一天多条的情况,用 OVER (ORDER BY visited_on ROWS ...)
是不靠谱的,因为行数 ≠ 时间!
而 JOIN
这种方式,直接按时间范围配对,不依赖数据是否连续,每天有多少条都不影响。
第四步:每个窗口要计算什么?
你想要的就是:
-
c1.visited_on
:当前窗口的最后一天 -
SUM(c2.amount)
:这 7 天的总金额 -
ROUND(SUM(c2.amount) / 7, 2)
:这 7 天的平均值(保留两位小数)
第五步:怎么避免不满 7 天的窗口?
比如当你分析 2019-01-02
时,它前面只有两天的数据(01、02),这是 不满 7 天的窗口,要排除掉。
这时候就要加一条语句:
sql
HAVING COUNT(DISTINCT c2.visited_on) = 7
意思是:只有当这 7 天真的有 7 个不同的日期数据,才纳入最终结果。
最终完整 SQL
把上述分析组合起来,完整 SQL 如下:
sql
SELECT
c1.visited_on,
SUM(c2.daily_total) AS amount,
ROUND(SUM(c2.daily_total)/7, 2) AS average_amount
FROM (
SELECT visited_on, SUM(amount) AS daily_total
FROM Customer
GROUP BY visited_on
) c1
JOIN (
SELECT visited_on, SUM(amount) AS daily_total
FROM Customer
GROUP BY visited_on
) c2
ON c2.visited_on BETWEEN DATE_SUB(c1.visited_on, INTERVAL 6 DAY) AND c1.visited_on
GROUP BY c1.visited_on
HAVING COUNT(c2.visited_on) = 7
ORDER BY c1.visited_on;
问题层级 | 解释 |
---|---|
本质问题 | 想知道某天 + 前六天的消费总和和平均 |
可直接获取的数据 | 每天的顾客消费记录(可汇总) |
怎么形成7天窗口 | 用自连接 + 日期范围:BETWEEN D - 6 AND D |
如何计算 | 汇总 amount,平均除以 7 并 ROUND |
如何过滤不满7天窗口 | HAVING COUNT(DISTINCT c2.visited_on) = 7 |
最终排序 | 按 visited_on 升序展示 |