sql-server - 如何使用 sql-server 从订单中计算重叠的订阅天数

标签 sql-server tsql overlapping cumulative-sum

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

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

谢谢!

最佳答案

我认为您需要使用 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 循环的过程替代方案,我相信它可以完成同样的事情。

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

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'

关于sql-server - 如何使用 sql-server 从订单中计算重叠的订阅天数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8495053/

相关文章:

mysql - 使用聚合函数过滤列以显示最大记录

sql - SQL Server ROW_NUMBER()在哪些情况下不从1开始?

sql-server - 如何将一列中的值转换为带引号的逗号分隔列表?

sql-server - 将值传递给派生表

sql - 处理存储过程参数中的 NULL

sql - 如何在 SQL 查询中使用 LIKE 转义

sql - MERGE 语句执行时间太长(仅针对 1 条记录)。 (MS-SQL Server 2012)

css - 固定 header 重叠

html - CSS 灵活元素布局 : fixed left position but sink down to avoid overlapping?

android - 支持 Android 中的多个 Activity 重叠表面 View