sqlsql-serverdatetimegaps-and-islandstimeline

Build a timeline based on separate tables with multiple dateFrom and DateTo columns?


I have two tables, tblPrice and tblDiscount, each have their value with a liveFrom and liveUntil column (plus some other columns but I'm leaving those out to try and make this more straightforward).

What I'm trying to do is display a list of prices and discounts as an ordered list which will show how prices have changed based on the price changing or the discount changing.

tblPrice

row priceID retailPrice liveFrom LiveUntil
1 446413 1666.33 2022-01-31 11:36:21.490 2022-04-08 15:13:41.230
2 1338193 1666.33 2022-04-09 09:30:14.043 2023-04-05 09:37:21.767
3 2707357 1749.65 2023-04-05 09:37:21.767 NULL

tblDiscount

row logID discount liveFrom LiveUntil
1 192 0.3700 2022-01-31 11:27:45.060 2023-01-09 14:32:24.413
2 498 0.3200 2023-01-09 14:32:24.413 2023-04-11 15:40:06.460
3 639 0.3100 2023-04-11 15:40:06.460 NULL

expected result

row retailPrice discount liveFrom liveUntil
1 1666.33 0.37 31/01/2022 11:36 08/04/2022 15:13
2 1666.33 0.37 09/04/2022 09:30 09/01/2023 14:32
3 1666.33 0.32 09/01/2023 14:32 05/04/2023 09:37
4 1749.65 0.32 05/04/2023 09:37 11/04/2023 15:40
5 1749.65 0.317 11/04/2023 15:40 NULL

Things to note are:

I've tried joining the tables on each other based on a date range and then using a case statement to show the live from and live until dates but it just doesn't seem right and I just can't quite get the logic. The results aren't coming up how I'd hope and I feel I'm going down a rabbit hole in the wrong direction.

My query so far (which as said I'm not sure is the right way to go) is:

SELECT
    P.[priceID],
    P.[retailPrice],
    P.[liveFrom],
    P.[liveUntil],
    D.[logID],
    D.[discount],
    D.[liveFrom],
    D.[liveUntil],
    ------------
    CASE
        WHEN SDV.[liveFrom] <= P.[liveFrom] AND (SDV.[liveUntil] >= P.[liveFrom] OR SDV.[liveUntil] IS NULL) THEN
            SDV.[liveFrom]
        WHEN SDV.[liveFrom] >= P.[liveFrom] AND (SDV.[liveUntil] <= P.[liveUntil] OR P.[liveUntil] IS NULL) THEN
            P.[liveFrom]
        ELSE
            '1900-01-01 00:00:00'
    END AS [EFFECTIVE_FROM],
    CASE
        WHEN ISNULL(P.[liveUntil],@date) < ISNULL(SDV.[liveUntil],@date) THEN
            P.[liveUntil]
        ELSE
            SDV.[liveUntil]
    END AS [EFFECTIVE_UNTIL]
FROM 
    tblPrice P
    INNER JOIN tblDiscount D ON P.[supplierDiscountID] = D.[supplierDiscountID]
        AND ((
                D.[liveFrom] <= P.[liveFrom] 
                AND (D.[liveUntil] >= P.[liveFrom] OR D.[liveUntil] IS NULL) 
            ) OR
            (
                D.[liveFrom] >= P.[liveFrom]
                AND (D.[liveFrom] <= P.[liveUntil] OR P.[liveUntil] IS NULL)
            ))
ORDER BY
    P.[liveFrom],
    D.[liveFrom];

I'm not getting the results I'm wanting and the route I'm going down just doesn't seem right. Can anybody give me any pointers or ideas please?


Solution

  • You have to look for discount rows that begins before the price ends and ends after the price begins, so this could be a way to do it:

    -- Sample data
    declare @tblPrice table
        ( priceID int not null, retailPrice money not null
        , liveFrom datetime not null, liveUntil datetime)
    declare @tblDiscount table
        ( logID int not null, discount decimal(5,4) not null
        , liveFrom datetime not null, liveUntil datetime)
    insert into @tblPrice values
         ( 446413,1666.33,'2022-01-31T11:36:21.490','2022-04-08T15:13:41.230')
        ,(1338193,1666.33,'2022-04-09T09:30:14.043','2023-04-05T09:37:21.767')
        ,(2707357,1749.65,'2023-04-05T09:37:21.767', null)
    insert into @tblDiscount values
         (192, 0.37, '2022-01-31T11:27:45.060','2023-01-09T14:32:24.413')
        ,(498, 0.32, '2023-01-09T14:32:24.413','2023-04-11T15:40:06.460')
        ,(639, 0.317,'2023-04-11T15:40:06.460', null)
    
    -- Prices with discounts available or without any discount
    select P.retailPrice
        , isNull(D.discount, 0) as discount
        , case when P.liveFrom  >= isNull(D.liveFrom, P.liveFrom)
            then P.liveFrom
            else D.liveFrom
          end as liveFrom
        , case when P.liveUntil <= isNull(D.liveUntil, P.liveUntil)
            then P.liveUntil
            else D.liveUntil
          end as liveUntil
    from @tblPrice P
    left join @tblDiscount D on
         -- P.supplierDiscountID = D.supplierDiscountID and
         D.liveFrom <= isNull(P.liveUntil, getDate())
     and P.liveFrom <= isNull(D.liveUntil, getDate())
    
    union all
    -- Prices that begins before 1st discount available
    select x.retailPrice
        , 0 as discount
        , x.liveFrom
        , x.lf as liveUntil
    from (
    select P.retailPrice, P.liveFrom
        , min(D.liveFrom) lf
    from @tblPrice P
    left join @tblDiscount D on
         -- P.supplierDiscountID = D.supplierDiscountID and
         D.liveFrom <= isNull(P.liveUntil, getDate())
     and P.liveFrom <= isNull(D.liveUntil, getDate())
    group by P.priceID, P.retailPrice, P.liveFrom
    having min(D.liveFrom)
         > P.liveFrom
    ) x
    
    union all
    -- Prices that ends after last discount available
    select x.retailPrice
        , 0 as discount
        , x.lu as liveFrom
        , x.liveUntil
    from (
    select P.retailPrice, P.liveUntil
        , max(isNull(D.liveUntil, getDate())) lu
    from @tblPrice P
    left join @tblDiscount D on
         -- P.supplierDiscountID = D.supplierDiscountID and
         D.liveFrom <= isNull(P.liveUntil, getDate())
     and P.liveFrom <= isNull(D.liveUntil, getDate())
    group by P.priceID, P.retailPrice, P.liveUntil
    having max(isNull(D.liveUntil, getDate()))
         < isNull(P.liveUntil, getDate())
    ) x
    
    order by liveFrom