在 SQL 中计算滚动聚合的最有效方法是什么?
What's the most efficient way to calculate a rolling aggregate in SQL?
我有一个数据集,其中包括一堆客户和他们拥有 "stay." 的日期范围,例如:
| ClientID | DateStart | DateEnd |
+----------+-----------+---------+
| 1 | Jan 1 | Jan 31 | (datediff = 30)
| 1 | Apr 4 | May 4 | (datediff = 30)
| 2 | Jan 3 | Feb 27 | (datediff = 55)
| 3 | Jan 1 | Jan 7 | (datediff = 6)
| 3 | Jan 10 | Jan 17 | (datediff = 6)
| 3 | Jan 20 | Jan 27 | (datediff = 6)
| 3 | Feb 1 | Feb 7 | (datediff = 6)
| 3 | Feb 10 | Feb 17 | (datediff = 6)
| 3 | Feb 20 | Feb 27 | (datediff = 6)
我的最终目标是能够确定客户在过去 X
时间超过 N
晚阈值的日期。假设过去 90
天中有 30
天。我还需要知道他们何时超过门槛。用例:酒店住宿和 VIP 身份。
- 在上面的示例中,客户 1 在 1 月 31 日超过了阈值(过去 90 天内有 30 个晚上),并且一直保持到 4 月 2 日达到阈值(现在过去 90 天内只有 29 个晚上),但是5月4日再次突破门槛
- 客户 2 在 2 月 3 日通过了阈值,并一直达到阈值直到 4 月 28 日,此时最早的天数已超过 90 天,它们已过期。
- 客户 3 在 2 月 17 日左右通过了阈值
所以我想生成这样的 table:
| ClientID | VIPStart | VIPEnd |
+----------+-----------+---------+
| 1 | Jan 31 | Apr 2 |
| 1 | May 4 | Jul 5 |
| 2 | Feb 3 | Apr 28 |
| 3 | Feb 17 | Apr 11 |
(Forgive me if the dates are slightly off, I'm doing this in my head)
理想情况下,我想生成一个视图,因为我需要经常引用它。
我想知道生成它的最有效方法是什么?假设我有成千上万的客户和数十万次入住。
到目前为止,我一直采用的方法是使用包含参数的 SQL 语句:截至 {?Date}
,谁拥有 VIP 身份,谁没有。我通过计算 DATEADD(day,-90,{?Date})
,然后排除超出范围的记录,然后截断较早延伸的 DateStart
s 和较晚延伸的 DateEnd
s,然后计算 DATEDIFF(day,DateStart,DateEnd)
用于使用调整后的 DateStart
和 DateEnd
,然后从 {?Date}
开始为每个客户获得结果 DATEDIFF()
的 SUM()
。它有效,但它并不漂亮。它给了我一个时间点快照;我要历史。
生成 table 个日期然后对每个日期都使用上述方法似乎效率有点低。
我考虑的另一个选择是将原始数据转换成分解的 table,每条记录对应一个晚上,这样我就可以更容易地计算它。像这样:
| ClientID | StayDate |
+----------+-----------+
| 1 | Jan 1 |
| 1 | Jan 2 |
| 1 | Jan 3 |
| 1 | Jan 4 |
etc.
然后我可以添加一个列来计算过去 90 天内的天数,这将帮助我完成大部分工作。
但我不确定如何在视图中执行此操作。我有一个执行此操作的代码片段:
WITH DaysTally AS (
SELECT MAX(DATEDIFF(day, DateStart, DateEnd)) - 1 AS Tally
FROM Stays
UNION ALL
SELECT Tally - 1 AS Expr1
FROM DaysTally AS DaysTally_1
WHERE (Tally - 1 >= 0))
SELECT t.ClientID,
DATEADD(day, c.Tally, t.DateStart) AS "StayDate"
FROM Stays AS t
INNER JOIN DaysTally AS c ON
DATEDIFF(day, t.DateStart, t.DateEnd) - 1 >= c.Tally
OPTION (MAXRECURSION 0)
但是没有 MAXRECURSION
我无法让它工作,而且我认为你不能用 MAXRECURSION
保存视图
现在我在胡说八道。所以我正在寻找的帮助是:实现我的目标最有效的方法是什么?如果您有代码示例,那也会很有帮助!谢谢。
这是一个有趣且问得很好的问题。我首先用递归 cte 枚举每个客户从第一次入住开始到最后一次入住结束后 90 天的天数。然后,您可以将 table 与左连接一起使用,并使用 window 函数来标记 "VIP" 天(请注意,这假设给定客户没有重叠停留,这与你的示例数据)。
接下来就是gaps-and-islands:你可以用一个window和把"adjacent"个VIP天数分组,然后合计。
with cte as (
select clientID, min(dateStart) dt, dateadd(day, 90, max(dateEnd)) dateMax
from stays
group by clientID
union all
select clientID, dateadd(day, 1, dt), dateMax
from cte
where dt < dateMax
)
select clientID, min(dt) VIPStart, max(dt) VIPEnd
from (
select t.*, sum(isNotVip) over(partition by clientID order by dt) grp
from (
select
c.clientID,
c.dt,
case when count(s.clientID) over(
partition by c.clientID
order by c.dt
rows between 90 preceding and current row
) >= 30
then 0
else 1
end isNotVip
from cte c
left join stays s
on c.clientID = s.clientID and c.dt between s.dateStart and s.dateEnd
) t
) t
where isNotVip = 0
group by clientID, grp
order by clientID, VIPStart
option (maxrecursion 0)
这个 demo on DB Fiddle 与您的样本数据产生:
clientID | VIPStart | VIPEnd
-------: | :--------- | :---------
1 | 2020-01-30 | 2020-04-01
1 | 2020-05-03 | 2020-07-04
2 | 2020-02-01 | 2020-04-28
3 | 2020-02-07 | 2020-04-20
您可以将其放在视图中,如下所示:
创建视图时必须省略order by
和option(maxrecursion)
子句
在其 from
子句中包含视图的每个查询都必须以 option(max recursion 0)
结尾
您可以通过在视图中创建计数 table 来消除递归。方法如下:
- 对于每个期间,生成从期间之前 90 天到之后 90 天的日期。这些是周期可能影响的所有 "candidate days"。
- 对于每一行,添加一个标志,说明它是否在期间(相对于前后 90 天)。
- 按客户 ID 和日期汇总。
- 使用 运行 总和来计算过去 90 天内超过 30 天的天数。
- 然后筛选出 30 天以上的问题,并将其视为间隙和孤岛问题。
假设 1000 天足够时间段(包括前后 90 天),则查询如下所示:
with n as (
select v.n
from (values (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)) v(n)
),
nums as (
select (n1.n * 100 + n2.n * 10 + n3.n) as n
from n n1 cross join n n2 cross join n n3
),
running90 as (
select clientid, dte, sum(in_period) over (partition by clientid order by dte rows between 89 preceding and current row) as running_90
from (select t.clientid, dateadd(day, n.n - 90, datestart) as dte,
max(case when dateadd(day, n.n - 90, datestart) >= datestart and dateadd(day, n.n - 90, datestart) <= t.dateend then 1 else 0 end) as in_period
from t join
nums n
on dateadd(day, n.n - 90, datestart) <= dateadd(day, 90, dateend)
group by t.clientid, dateadd(day, n.n - 90, datestart)
) t
)
select clientid, min(dte), max(dte)
from (select r.*,
row_number() over (partition by clientid order by dte) as seqnum
from running90 r
where running_90 >= 30
) r
group by clientid, dateadd(day, - seqnum, dte);
没有递归 CTE(尽管可以用于 n
),这不受 maxrecursion
问题的影响。
Here 是一个 db<>fiddle.
结果与您的结果略有不同。这可能是由于定义上的一些细微差别。以上包括作为 "occupied" 天的结束日。上述查询中的 90 天是之前的 89 天加上当天。倒数第二个查询显示 90 天 运行 天,这对我来说似乎是正确的。
我有一个数据集,其中包括一堆客户和他们拥有 "stay." 的日期范围,例如:
| ClientID | DateStart | DateEnd |
+----------+-----------+---------+
| 1 | Jan 1 | Jan 31 | (datediff = 30)
| 1 | Apr 4 | May 4 | (datediff = 30)
| 2 | Jan 3 | Feb 27 | (datediff = 55)
| 3 | Jan 1 | Jan 7 | (datediff = 6)
| 3 | Jan 10 | Jan 17 | (datediff = 6)
| 3 | Jan 20 | Jan 27 | (datediff = 6)
| 3 | Feb 1 | Feb 7 | (datediff = 6)
| 3 | Feb 10 | Feb 17 | (datediff = 6)
| 3 | Feb 20 | Feb 27 | (datediff = 6)
我的最终目标是能够确定客户在过去 X
时间超过 N
晚阈值的日期。假设过去 90
天中有 30
天。我还需要知道他们何时超过门槛。用例:酒店住宿和 VIP 身份。
- 在上面的示例中,客户 1 在 1 月 31 日超过了阈值(过去 90 天内有 30 个晚上),并且一直保持到 4 月 2 日达到阈值(现在过去 90 天内只有 29 个晚上),但是5月4日再次突破门槛
- 客户 2 在 2 月 3 日通过了阈值,并一直达到阈值直到 4 月 28 日,此时最早的天数已超过 90 天,它们已过期。
- 客户 3 在 2 月 17 日左右通过了阈值
所以我想生成这样的 table:
| ClientID | VIPStart | VIPEnd |
+----------+-----------+---------+
| 1 | Jan 31 | Apr 2 |
| 1 | May 4 | Jul 5 |
| 2 | Feb 3 | Apr 28 |
| 3 | Feb 17 | Apr 11 |
(Forgive me if the dates are slightly off, I'm doing this in my head)
理想情况下,我想生成一个视图,因为我需要经常引用它。
我想知道生成它的最有效方法是什么?假设我有成千上万的客户和数十万次入住。
到目前为止,我一直采用的方法是使用包含参数的 SQL 语句:截至 {?Date}
,谁拥有 VIP 身份,谁没有。我通过计算 DATEADD(day,-90,{?Date})
,然后排除超出范围的记录,然后截断较早延伸的 DateStart
s 和较晚延伸的 DateEnd
s,然后计算 DATEDIFF(day,DateStart,DateEnd)
用于使用调整后的 DateStart
和 DateEnd
,然后从 {?Date}
开始为每个客户获得结果 DATEDIFF()
的 SUM()
。它有效,但它并不漂亮。它给了我一个时间点快照;我要历史。
生成 table 个日期然后对每个日期都使用上述方法似乎效率有点低。
我考虑的另一个选择是将原始数据转换成分解的 table,每条记录对应一个晚上,这样我就可以更容易地计算它。像这样:
| ClientID | StayDate |
+----------+-----------+
| 1 | Jan 1 |
| 1 | Jan 2 |
| 1 | Jan 3 |
| 1 | Jan 4 |
etc.
然后我可以添加一个列来计算过去 90 天内的天数,这将帮助我完成大部分工作。
但我不确定如何在视图中执行此操作。我有一个执行此操作的代码片段:
WITH DaysTally AS (
SELECT MAX(DATEDIFF(day, DateStart, DateEnd)) - 1 AS Tally
FROM Stays
UNION ALL
SELECT Tally - 1 AS Expr1
FROM DaysTally AS DaysTally_1
WHERE (Tally - 1 >= 0))
SELECT t.ClientID,
DATEADD(day, c.Tally, t.DateStart) AS "StayDate"
FROM Stays AS t
INNER JOIN DaysTally AS c ON
DATEDIFF(day, t.DateStart, t.DateEnd) - 1 >= c.Tally
OPTION (MAXRECURSION 0)
但是没有 MAXRECURSION
我无法让它工作,而且我认为你不能用 MAXRECURSION
现在我在胡说八道。所以我正在寻找的帮助是:实现我的目标最有效的方法是什么?如果您有代码示例,那也会很有帮助!谢谢。
这是一个有趣且问得很好的问题。我首先用递归 cte 枚举每个客户从第一次入住开始到最后一次入住结束后 90 天的天数。然后,您可以将 table 与左连接一起使用,并使用 window 函数来标记 "VIP" 天(请注意,这假设给定客户没有重叠停留,这与你的示例数据)。
接下来就是gaps-and-islands:你可以用一个window和把"adjacent"个VIP天数分组,然后合计。
with cte as (
select clientID, min(dateStart) dt, dateadd(day, 90, max(dateEnd)) dateMax
from stays
group by clientID
union all
select clientID, dateadd(day, 1, dt), dateMax
from cte
where dt < dateMax
)
select clientID, min(dt) VIPStart, max(dt) VIPEnd
from (
select t.*, sum(isNotVip) over(partition by clientID order by dt) grp
from (
select
c.clientID,
c.dt,
case when count(s.clientID) over(
partition by c.clientID
order by c.dt
rows between 90 preceding and current row
) >= 30
then 0
else 1
end isNotVip
from cte c
left join stays s
on c.clientID = s.clientID and c.dt between s.dateStart and s.dateEnd
) t
) t
where isNotVip = 0
group by clientID, grp
order by clientID, VIPStart
option (maxrecursion 0)
这个 demo on DB Fiddle 与您的样本数据产生:
clientID | VIPStart | VIPEnd -------: | :--------- | :--------- 1 | 2020-01-30 | 2020-04-01 1 | 2020-05-03 | 2020-07-04 2 | 2020-02-01 | 2020-04-28 3 | 2020-02-07 | 2020-04-20
您可以将其放在视图中,如下所示:
创建视图时必须省略
order by
和option(maxrecursion)
子句在其
from
子句中包含视图的每个查询都必须以option(max recursion 0)
结尾
您可以通过在视图中创建计数 table 来消除递归。方法如下:
- 对于每个期间,生成从期间之前 90 天到之后 90 天的日期。这些是周期可能影响的所有 "candidate days"。
- 对于每一行,添加一个标志,说明它是否在期间(相对于前后 90 天)。
- 按客户 ID 和日期汇总。
- 使用 运行 总和来计算过去 90 天内超过 30 天的天数。
- 然后筛选出 30 天以上的问题,并将其视为间隙和孤岛问题。
假设 1000 天足够时间段(包括前后 90 天),则查询如下所示:
with n as (
select v.n
from (values (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)) v(n)
),
nums as (
select (n1.n * 100 + n2.n * 10 + n3.n) as n
from n n1 cross join n n2 cross join n n3
),
running90 as (
select clientid, dte, sum(in_period) over (partition by clientid order by dte rows between 89 preceding and current row) as running_90
from (select t.clientid, dateadd(day, n.n - 90, datestart) as dte,
max(case when dateadd(day, n.n - 90, datestart) >= datestart and dateadd(day, n.n - 90, datestart) <= t.dateend then 1 else 0 end) as in_period
from t join
nums n
on dateadd(day, n.n - 90, datestart) <= dateadd(day, 90, dateend)
group by t.clientid, dateadd(day, n.n - 90, datestart)
) t
)
select clientid, min(dte), max(dte)
from (select r.*,
row_number() over (partition by clientid order by dte) as seqnum
from running90 r
where running_90 >= 30
) r
group by clientid, dateadd(day, - seqnum, dte);
没有递归 CTE(尽管可以用于 n
),这不受 maxrecursion
问题的影响。
Here 是一个 db<>fiddle.
结果与您的结果略有不同。这可能是由于定义上的一些细微差别。以上包括作为 "occupied" 天的结束日。上述查询中的 90 天是之前的 89 天加上当天。倒数第二个查询显示 90 天 运行 天,这对我来说似乎是正确的。