【问题标题】:How to calculate overlapping subscription days from orders with sql-server如何使用 sql-server 从订单中计算重叠订阅天数
【发布时间】:2011-12-13 19:34:20
【问题描述】:

我有一个订单表。我想计算特定日期每个用户的订阅天数(以基于集合的方式优先)。

create table #orders (orderid int, userid int, subscriptiondays int, orderdate date)
insert into #orders
    select 1, 2, 10, '2011-01-01'
    union 
    select 2, 1, 10, '2011-01-10'
    union 
    select 3, 1, 10, '2011-01-15'
    union 
    select 4, 2, 10, '2011-01-15'

declare @currentdate date = '2011-01-20'

--userid 1 is expected to have 10 subscriptiondays left
(since there is 5 left when the seconrd order is placed)
--userid 2 is expected to have 5 subscriptionsdays left 

我确定这已经完成了,我只是不知道要搜索什么。 很像一个运行总数?

所以当我将 @currentdate 设置为 '2011-01-20' 时,我想要这个结果:

userid      subscriptiondays
1           10
2           5

当我将@currentdate 设置为“2011-01-25”时

userid      subscriptiondays
1           5
2           0

当我将@currentdate 设置为“2011-01-11”时

userid      subscriptiondays
1           9
2           0

谢谢!

【问题讨论】:

  • 澄清一下,您希望参数和订单日期之间的天数不同吗?
  • 感谢您提供有效的设置代码!

标签: sql-server tsql overlapping cumulative-sum


【解决方案1】:

我认为您需要使用recursive common table expression

编辑:我还在下面添加了一个程序实现,而不是使用递归公用表表达式。我建议使用这种程序方法,因为我认为我所包含的递归 CTE 查询可能无法处理许多数据场景。

下面的查询为您提供的场景提供了正确答案,但您可能需要想出一些额外的复杂场景并查看是否存在任何错误。

例如,我有一种感觉,如果您有多个先前的订单与后来的订单重叠,则此查询可能会失败。

with CurrentOrders (UserId, SubscriptionDays, StartDate, EndDate) as
(
    select
        userid,
        sum(subscriptiondays),
        min(orderdate),
        dateadd(day, sum(subscriptiondays), min(orderdate))
    from #orders
    where
        #orders.orderdate <= @currentdate
        -- start with the latest order(s)
        and not exists (
            select 1
            from #orders o2
        where
            o2.userid = #orders.userid
            and o2.orderdate <= @currentdate
            and o2.orderdate > #orders.orderdate
        )
    group by
        userid

    union all

    select
        #orders.userid,
        #orders.subscriptiondays,
        #orders.orderdate,
        dateadd(day, #orders.subscriptiondays, #orders.orderdate)
    from #orders
    -- join any overlapping orders
    inner join CurrentOrders on
        #orders.userid = CurrentOrders.UserId
        and #orders.orderdate < CurrentOrders.StartDate
        and dateadd(day, #orders.subscriptiondays, #orders.orderdate) > CurrentOrders.StartDate
)
select
    UserId,
    sum(SubscriptionDays) as TotalSubscriptionDays,
    min(StartDate),
    sum(SubscriptionDays) - datediff(day, min(StartDate), @currentdate) as RemainingSubscriptionDays
from CurrentOrders
group by
    UserId
;

Philip 提到了对公用表表达式的递归限制的担忧。下面是一个使用表变量和 while 循环的程序替代方案,我相信它可以完成同样的事情。

虽然我已验证此替代代码确实有效,但至少对于提供的示例数据而言,我很高兴听到任何人对这种方法的 cmet。好主意?馊主意?有什么需要注意的吗?

declare @ModifiedRows int

declare @CurrentOrders table
(
    UserId int not null,
    SubscriptionDays int not null,
    StartDate date not null,
    EndDate date not null
)

insert into @CurrentOrders
select
    userid,
    sum(subscriptiondays),
    min(orderdate),
    min(dateadd(day, subscriptiondays, orderdate))
from #orders
where
    #orders.orderdate <= @currentdate
    -- start with the latest order(s)
    and not exists (
        select 1
        from #orders o2
        where
            o2.userid = #orders.userid
            and o2.orderdate <= @currentdate
            -- there does not exist any other order that surpasses it
            and dateadd(day, o2.subscriptiondays, o2.orderdate) > dateadd(day, #orders.subscriptiondays, #orders.orderdate)
    )
group by
    userid

set @ModifiedRows = @@ROWCOUNT


-- perform an extra update here in case there are any additional orders that were made after the start date but before the specified @currentdate
update co set
    co.SubscriptionDays = co.SubscriptionDays + #orders.subscriptiondays
from @CurrentOrders co
inner join #orders on
    #orders.userid = co.UserId
    and #orders.orderdate <= @currentdate
    and #orders.orderdate >= co.StartDate
    and dateadd(day, #orders.subscriptiondays, #orders.orderdate) < co.EndDate


-- Keep attempting to update rows as long as rows were updated on the previous attempt
while(@ModifiedRows > 0)
begin
    update co set
        SubscriptionDays = co.SubscriptionDays + overlap.subscriptiondays,
        StartDate = overlap.orderdate
    from @CurrentOrders co
    -- join any overlapping orders
    inner join (
        select
            #orders.userid,
            sum(#orders.subscriptiondays) as subscriptiondays,
            min(orderdate) as orderdate
        from #orders
        inner join @CurrentOrders co2 on
            #orders.userid = co2.UserId
            and #orders.orderdate < co2.StartDate
            and dateadd(day, #orders.subscriptiondays, #orders.orderdate) > co2.StartDate
        group by
            #orders.userid
    ) overlap on
        overlap.userid = co.UserId

    set @ModifiedRows = @@ROWCOUNT
end

select
    UserId,
    sum(SubscriptionDays) as TotalSubscriptionDays,
    min(StartDate),
    sum(SubscriptionDays) - datediff(day, min(StartDate), @currentdate) as RemainingSubscriptionDays
from @CurrentOrders
group by
    UserId

EDIT2:我已经对上面的代码进行了一些调整以解决各种特殊情况,例如,如果恰好有两个用户的订单都在同一日期结束。

例如,将设置数据更改为以下会导致原始代码出现问题,我现在已经更正了:

insert into #orders
    select 1, 2, 10, '2011-01-01'
    union 
    select 2, 1, 10, '2011-01-10'
    union 
    select 3, 1, 10, '2011-01-15'
    union 
    select 4, 2, 6, '2011-01-15'
    union 
    select 5, 2, 4, '2011-01-17'

EDIT3:我做了一些额外的调整来解决其他特殊情况。特别是,之前的代码遇到了以下设置数据的问题,我现在已经更正了:

insert into #orders
    select 1, 2, 10, '2011-01-01'
    union 
    select 2, 1, 6, '2011-01-10'
    union 
    select 3, 1, 10, '2011-01-15'
    union 
    select 4, 2, 10, '2011-01-15'
    union 
    select 5, 1, 4, '2011-01-12'

【讨论】:

    【解决方案2】:

    如果我的澄清评论/问题是正确的,那么您想使用 DATEDIFF:

    DATEDIFF(dd, orderdate,  @currentdate)
    

    【讨论】:

      【解决方案3】:

      我对问题的解释:

      • 在第 X 天,客户购买了“跨度”订阅天数(即有效期为 N 天)
      • 跨度从购买之日开始,适用于 X 到 X + (N - 1) 天...但见下文
      • 如果客户在第一个跨度到期后购买第二个跨度(或所有现有跨度到期后的任何新跨度),请重复此过程。 (30 天前的 10 天单次购买对今天的第二次购买没有影响。)
      • 如果客户在现有跨度仍然有效的情况下购买跨度,则新跨度适用于 day immediately after end of current span(s)that date + (N – 1)
      • 这是迭代的。如果客户在 1 月 1 日、1 月 2 日和 1 月 3 日购买 10 天跨度,则如下所示:

        截至 1 日:1 月 1 日 - 1 月 10 日

        截至 2 日:1 月 1 日至 1 月 10 日,1 月 11 日至 1 月 20 日(有效,1 月 1 日至 1 月 20 日)

        截至 3 日:1 月 1 日至 1 月 10 日、1 月 11 日至 1 月 20 日、1 月 21 日至 1 月 30 日(有效,1 月 1 日至 1 月 30 日)

      如果这确实是问题所在,那么在 T-SQL 中解决它是一个可怕的问题。为了确定给定购买的“有效跨度”,您必须按照购买顺序计算所有先前购买的有效跨度,因为总体累积效应。对于 1 个用户和 3 行来说,这是一个微不足道的问题,但对于拥有数十次购买的数千名用户(这可能是你想要的)来说,这不是微不足道的。

      我会这样解决:

      • 将数据类型为date 的列EffectiveDate 添加到表中
      • 构建一个一次性流程,逐个用户逐个遍历每一行,逐个订单日期逐个订单日期,并按上述方法计算有效日期
      • 修改用于插入数据的过程,以在创建新条目时计算 EffectiveDate。这样一来,您只需参考该用户最近的购买记录。
      • 解决有关删除(取消?)或更新(错误设置?)订单的后续问题

      我可能错了,但我看不出有任何方法可以使用基于集合的策略来解决这个问题。 (递归 CTE 等可以工作,但它们只能递归到这么多级别,而且我们不知道这个问题的极限——更不用说你需要多久运行一次,或者它必须执行得有多好.) 我会关注并支持任何不递归解决此问题的人!

      当然,这仅适用于我对问题的理解正确的情况。如果没有,请忽略。

      【讨论】:

      • 尽管我的描述很糟糕,但你把所有东西都捡了起来。感谢您帮我描述问题!
      【解决方案4】:

      事实上,我们需要计算subscriptiondays 减去第一个订阅日期和@currentdate 的总和,例如:

      select userid, 
             sum(subsribtiondays)-
             DATEDIFF('dd', 
                      (select min(orderdate) 
                       from #orders as a 
                       where a.userid=userid),  @currentdate)
      from #orders
      where orderdate <= @currentdata
      group by userid
      

      【讨论】:

      • 我也考虑过这种方法,但我认为当用户在订阅之间存在差距时,这种方法就会失效。即,如果用户之前的订阅已过期,则该订阅需要被排除在考虑之外。这似乎是使这个问题变得棘手的原因。
      猜你喜欢
      • 2020-10-02
      • 2021-11-28
      • 2019-08-19
      • 1970-01-01
      • 1970-01-01
      • 2016-05-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多