Как рассчитать перекрывающиеся дни подписки для заказов с sql-сервера - PullRequest
3 голосов
/ 13 декабря 2011

У меня есть таблица заказов с заказами. Я хочу рассчитать количество дней подписки для каждого пользователя (предпочтительным способом на основе набора) для определенного дня.

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

Спасибо!

Ответы [ 4 ]

4 голосов
/ 14 декабря 2011

Я думаю, вам нужно использовать рекурсивное общее табличное выражение .

РЕДАКТИРОВАТЬ: я также добавил ниже процедурную реализацию вместо использования рекурсивного общего табличного выражения. Я рекомендую использовать этот процедурный подход, поскольку я думаю, что может быть несколько сценариев данных, которые рекурсивный запрос 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
;

Филипп упомянул обеспокоенность по поводу предела рекурсии для общих табличных выражений. Ниже приведена процедурная альтернатива с использованием табличной переменной и цикла 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

РЕДАКТИРОВАТЬ2: Я внес некоторые изменения в приведенный выше код, чтобы учесть различные особые случаи, например, если у пользователя просто два заказа, оба заканчиваются в одну и ту же дату.

Например, изменение данных настройки на следующие вызвало проблемы с исходным кодом, которые я сейчас исправил:

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'

РЕДАКТИРОВАТЬ3: я сделал некоторые дополнительные корректировки для решения других особых случаев. В частности, предыдущий код столкнулся с проблемами со следующими данными настройки, которые я сейчас исправил:

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'
0 голосов
/ 13 декабря 2011

Фактически нам нужно вычислить сумму дней подписки минус дни между первой датой подписки и @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
0 голосов
/ 14 декабря 2011

Моя интерпретация проблемы:

  • В день X покупатель покупает «промежуток» дней подписки (т. Е. Подходит для N дней)
  • Промежуток начинается со дня покупки и подходит для X через день X + (N - 1) ... но см. Ниже
  • Если клиент покупает второй интервал после , истекает первый интервал (или любой новый интервал после истечения срока действия всех существующих интервалов), повторите процесс. (Одна 10-дневная покупка, сделанная 30 дней назад, не влияет на второй сделанный сегодня портфель.)
  • Если клиент покупает промежуток, в то время как существующие промежутки еще действуют, новый промежуток применяется к day immediately after end of current span(s) - that date + (N – 1)
  • Это итеративно. Если клиент покупает 10-дневные пролеты 1 января, 2 января и 3 января, это будет выглядеть примерно так:

    По состоянию на 1 января - 10 января

    По состоянию на 2 января: 1 января - 10 января, 11 января - 20 января (в действительности с 1 января по 20 января)

    По состоянию на 3 января: 1 января - 10 января, 11 января - 20 января, 21 января - 30 января (в действительности с 1 января по 30 января)

Если это действительно проблема, то это ужасная проблема в T-SQL. Чтобы определить «эффективный интервал» данной покупки, необходимо рассчитать эффективный интервал всех предыдущих покупок в том порядке, в котором они были приобретены , из-за этого общего кумулятивного эффекта. Это тривиальная проблема с 1 пользователем и 3 строками, но нетривиально с тысячами пользователей с десятками покупок (что, по-видимому, то, что вам нужно).

Я бы решил это так:

  • Добавить столбец EffectiveDate типа данных date в таблицу
  • Создайте одноразовый процесс для обхода каждой строки для каждого пользователя и для даты заказа по дате заказа и рассчитайте EffectiveDate, как описано выше
  • Измените процесс, используемый для вставки данных для вычисления EffectiveDate во время создания новой записи. Для этого вам нужно будет только сослаться на самую последнюю покупку, сделанную этим пользователем.
  • Устранить последующие проблемы, связанные с удалением (отменено?) Или обновлением (не задано?) Заказов

Возможно, я ошибаюсь, но я не вижу способа решить эту проблему, используя основанную на множестве тактику. (Рекурсивные CTE и тому подобное будут работать, но они могут повторяться только на стольких уровнях, и мы не знаем предела этой проблемы - не говоря уже о том, как часто вам нужно ее запускать, или насколько хорошо она должна работать .) Я буду смотреть и оповестить любого, кто решит это без рекурсии!

И, конечно, это применимо, только если я правильно понимаю проблему. Если нет, пожалуйста, не обращайте внимания.

0 голосов
/ 13 декабря 2011

Если мой уточняющий комментарий / вопрос правильный, то вы хотите использовать DATEDIFF:

DATEDIFF(dd, orderdate,  @currentdate)
...