Распределите строки равномерно по дням

У меня есть таблица, куда я положил, давайте назовем это ручными значениями, которые позже будут использованы в моем коде. Эта таблица выглядит так:

subId | MonthNo | PackagesNumber | Country | EntryMethod | PaidAmount | Version
1     | 201701  | 223            | NO      | BCD         | 44803      | 2
2     | 201701  | 61             | NO      | GHI         | 11934      | 2
3     | 201701  | 929            | NO      | ABC         | 88714      | 2
4     | 201701  | 470            | NO      | DEF         | 98404      | 2
5     | 201702  | 223            | NO      | BCD         | 28225      | 2

Все, что мне нужно сделать, это разделить эти значения на отдельные строки на уровне одного пакета. Например, в январе 2017 года в стране NO имеется 223 пакета с EntryMethod BCD, поэтому я хочу 223 отдельных строки. PaidAmount также следует разделить на число PackagesNumber.

Проблема в том, что мне нужно связать дату с каждой записью. Записи должны распространяться равномерно в течение всего месяца. У меня есть измерение Date, которое я могу пересечь с моей таблицей, потянув месяц и год отдельно от MontNo. Например, январь 2017 года, EntryMethod BCD У меня есть пакеты, так что это ~7 пакетов в день.

Это то, что я хочу:

subId | Date       | Country | Packages | EntryMethod | PaidAmount       | Version
1     | 01.01.2017 | NO      | 1        | BCD         | 200.910313901345 | 2
2     | 01.01.2017 | NO      | 1        | BCD         | 200.910313901345 | 2
3     | 01.01.2017 | NO      | 1        | BCD         | 200.910313901345 | 2
4     | 01.01.2017 | NO      | 1        | BCD         | 200.910313901345 | 2
5     | 01.01.2017 | NO      | 1        | BCD         | 200.910313901345 | 2
6     | 01.01.2017 | NO      | 1        | BCD         | 200.910313901345 | 2
7     | 01.01.2017 | NO      | 1        | BCD         | 200.910313901345 | 2
8     | 02.01.2017 | NO      | 1        | BCD         | 200.910313901345 | 2

Бонус: я написал код, который разделяет пакеты на отдельные записи, и в качестве даты указывается первый день каждого месяца.

SELECT
       Date =
(
    SELECT TOP 1
           date
    FROM dim_Date dim
    WHERE dim.Month = a.Month
          AND dim.Year = a.Year
)
     , Country
     , EntryMethod
     , Deliveries = 1
     , PaidAmount = NULLIF(PaidAmount, 0) / PackagesNumber
     , SubscriptionId = 90000000 + ROW_NUMBER() OVER(ORDER BY n.number)
     , Version
FROM
(
    SELECT
           [Year] = LEFT(MonthNo, 4)
         , [Month] = RIGHT(MonthNo, 2)
         , Country
         , EntryMethod
         , PackagesNumber
         , PaidAmount
         , Version
    FROM tgm.rep_PredictionsReport_ManualValues tgm

    /*WHERE MonthNo = 201701*/
) a
JOIN master..spt_values n
ON n.type = 'P'
   AND n.number < CAST(PackagesNumber AS INT);

РЕДАКТИРОВАТЬ: я добился определенного прогресса. Я использовал функцию NTILE, чтобы разделить строки на группы. Единственное, что изменилось, это выбор даты с верхнего уровня. Теперь это выглядит так:

Date = concat([Year], '-', [Month], '-', case when ntile(31) over(order by n.number) < 10  then '0' + cast(ntile(31) over(order by n.number) as varchar(2)) else cast(ntile(31) over(order by n.number) as varchar(2)) end)

Объяснение: Я создаю поле Дата, используя поля Год и Месяц, и NTILE в течение количества дней в месяце (теперь это статический номер, но позже он будет изменен). Результаты не так хороши, как я ожидал, они создают группы в два раза больше, чем они должны быть (14 вместо 7 строк в каждой дате).

2 ответа

Решение

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

Вот полный тест: http://rextester.com/TOROA96856

Вот соответствующий запрос:

--recursive query to expand each row.
with expand_rows (subid,monthno,month,packagesnumber,paidamount) as (
    select subid,monthno,month,packagesnumber,(paidamount+0.0000)/packagesnumber 
       from initial_table
    union all
    select subid,monthno,month,packagesnumber-1,paidamount 
       from expand_rows where packagesnumber >1
)
select expand_rows.*,(packagesnumber % numdays)+1 day, paidamount from expand_rows
    join dayspermonth d on
        d.month = expand_rows.month
    order by subid, day
    option (maxrecursion 0)

(packagesnumber % numdays)+1 является операцией по модулю, которая назначает элементы на день.

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

Возможно, вам придется настроить запрос по модулю, если вам важно, где заканчиваются дополнительные элементы, когда они не делятся равномерно (например, если у вас есть 32 элемента в январе, у какого дня есть дополнительный элемент?). В этом примере второй день месяца имеет тенденцию получать наибольшее количество (из-за добавления 1 к учету того факта, что последний день месяца заканчивается 0). Если вы хотите, чтобы дополнительные дни сократились в начале месяца, вместо этого вы можете использовать выписку case, которая преобразует 0 в число дней в месяце.

Чтобы равномерно распределить 223 номера в дни января, мы делаем следующее: в январе 31 день. Остаток 223/3 - 6 223/31 - 7 (целочисленное деление).

Так вот 7 записей в день, плюс 1 дополнительная запись за 1-6 января.

Я использовал таблицу подсчета для создания дат и некоторых других, но распределение строк по дням может быть определено так:

with 
tally as
(
select row_number() over (order by n)-1 n from
(values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) n(n)
cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) m(m)
cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) l(m)
cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) k(m)
)
,t1 as
(
select
*
from
(values

 (1     , 201701  , 223            , 'NO'      , 'BCD'         , 44803      , 2)
,(2     , 201701  , 61             , 'NO'      , 'GHI'         , 11934      , 2)
,(3     , 201701  , 929            , 'NO'      , 'ABC'         , 88714      , 2)
,(4     , 201701  , 470            , 'NO'      , 'DEF'         , 98404      , 2)
,(5     , 201702  , 223            , 'NO'      , 'BCD'         , 28225      , 2)
) t(subId , MonthNo , PackagesNumber , Country , EntryMethod , PaidAmount , Version)
)
,dates as
(
select dateadd(day,n,'20170101') as dt
    ,convert(varchar(10),dateadd(day,n,'20170101'),112)/100 mnthkey
    ,day(dateadd(day,-1,dateadd(month,1,cast(((convert(varchar(10),dateadd(day,n,'20170101'),112)/100)*100 +1) as varchar(10))))) DaysInMonth
from  
tally
)
select 
    subId
    ,MonthNo
    ,dt
    ,PackagesNumber
    ,case when day(dt)<=PackagesNumber%DaysInMonth then 1 else 0 end remainder
    ,PackagesNumber/DaysInMonth evenlyspread
    ,Country
    ,EntryMethod
    ,PaidAmount
    ,Version
 from t1 a
inner join dates b
on a.MonthNo=b.mnthkey

Я присоединяюсь к месяцу с таблицей данных, и для каждого дня месяца я назначаю равномерно распределенные дни, 7 в нашем примере и для первых дней, 6 в нашем примере, я добавляю 1 как остаток

Теперь у нас есть информация из вашей базовой таблицы, умноженная на каждый день в соответствующие месяцы, теперь нам просто нужно сделать несколько строк в день, здесь мы снова используем таблицу подсчета:

with 
tally as
(
select row_number() over (order by n)-1 n from
(values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) n(n)
cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) m(m)
cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) l(m)
cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) k(m)
)
,t1 as
(
select
*
from
(values

 (1     , 201701  , 223            , 'NO'      , 'BCD'         , 44803      , 2)
,(2     , 201701  , 61             , 'NO'      , 'GHI'         , 11934      , 2)
,(3     , 201701  , 929            , 'NO'      , 'ABC'         , 88714      , 2)
,(4     , 201701  , 470            , 'NO'      , 'DEF'         , 98404      , 2)
,(5     , 201702  , 223            , 'NO'      , 'BCD'         , 28225      , 2)
) t(subId , MonthNo , PackagesNumber , Country , EntryMethod , PaidAmount , Version)
)
,dates as
(
select dateadd(day,n,'20170101') as dt
    ,convert(varchar(10),dateadd(day,n,'20170101'),112)/100 mnthkey
    ,day(dateadd(day,-1,dateadd(month,1,cast(((convert(varchar(10),dateadd(day,n,'20170101'),112)/100)*100 +1) as varchar(10))))) DaysInMonth
from  
tally
)
,forshow as
(
select 
    subId
    ,MonthNo
    ,dt
    ,PackagesNumber
    ,case when day(dt)<=PackagesNumber%DaysInMonth then 1 else 0 end remainder
    ,PackagesNumber/DaysInMonth evenlyspread
    ,Country
    ,EntryMethod
    ,(PaidAmount+0.0000)/(PackagesNumber*1.0000) PaidAmount
    ,Version
    ,PaidAmount TotalPaidAmount
 from t1 a
inner join dates b
on a.MonthNo=b.mnthkey
)
select 
    subId
    ,dt [Date]
    ,Country
    ,1 Packages
    ,EntryMethod
    ,PaidAmount
    ,Version
    -- the following rows are just for control
    ,remainder+evenlyspread toalday
    ,count(*) over (partition by subId,MonthNo,dt) calctotalday
    ,PackagesNumber
    ,count(*) over (partition by subId) calcPackagesNumber
    ,sum(PaidAmount)over (partition by subId) calcPaidAmount
    ,TotalPaidAmount
from forshow
inner join tally on n<(remainder+evenlyspread )

order by subId,MonthNo,dt

Я присоединяюсь к числу дней (равномерно распределенных + остаток) и получаю один ряд PR-пакета.

Я добавил несколько проверочных столбцов, чтобы убедиться, что я получаю 8 строк за первые 6 дней и всего 223 строки для нашего примера.

Другие вопросы по тегам