2011-12-13 5 views
3

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

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 

Спасибо!

+0

Для claify, вы хотите, чтобы разница в днях между вашим параметром и даты заказа? – Maess

+0

Спасибо за предоставление кода настройки, который работает! –

ответ

4

Думаю, вам нужно будет использовать recursive common table expression.

EDIT: Я также добавил процедуру, описанную ниже, вместо использования рекурсивного общего табличного выражения. Я рекомендую использовать этот процедурный подход, так как я думаю, что может быть несколько сценариев данных, которые, по-видимому, не учитывают рекурсивный запрос 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 

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' 
+0

Отличная работа! Это то, что я искал. – Patrik

0

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

DATEDIFF(dd, orderdate, @currentdate) 
0

На самом деле, нам нужно вычислить Суммы от subscriptiondays минус дни beetwen первой subscrible даты и @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

Я тоже думал об этом подходе, но я думаю, что это ломается, когда у пользователя есть разрыв между подписками. то есть, если у пользователя есть предыдущая подписка, срок действия которой истек, то эта подписка должна быть исключена из рассмотрения. Похоже, это делает эту проблему сложной. –

0

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

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

    По состоянию на 1: Jan 1 - Jan 10

    По 2: 1 января - 10 января , 11 января - 20 января (фактически, с 1 января по 20 января)

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

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

Я бы решить так:

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

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

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

+0

Несмотря на мое плохое описание, вы все собрали. Спасибо, что помогли мне описать проблему! – Patrik

 Смежные вопросы

  • Нет связанных вопросов^_^