Date Ranges - Simple, but seemingly impossible

  • Hi all,

    I've done a lot of research and tried so many things for around a week. I'm desperate at this point, so I'm asking for your help.

    I have this ProductDiscounts table:

    --------------------Scripts for creating the table:---------------------
    CREATE TABLE ProductDiscounts (
    Stack VARCHAR (255),
    ProductCode VARCHAR (255),
    ValidFrom DATE,
    ValidTo DATE,
    Discount1 decimal(4,3))
    ;

    INSERT INTO ProductDiscounts
    (Stack, ProductCode, ValidFrom, ValidTo, Discount1)
    VALUES
    ('A','PROD40','20000101','20401231',0.1),
    ('B','PROD40','20090201','20110131',0.2),
    ('C','PROD40','20110201','20411231',0.15),
    ('D','PROD40','20090201','20401231',0.3)
    ;

    -----------------------------------------------------------------------------------

    Here's a look at the timeframe:

    Objective: I need to create a query to slice these timeframes in a correct way to be able to stack them up so that I can later use them to calculate the discounts that will be applied to a certain day.

    In terms of timeframe (for a better understanding) the final result should be like this:

    So the final result should be:

    As you can see, I'm able to Stack the discounts into common timeframes. This is the result I need and I cannot reach no matter what I try. There's always something not correct.

    Here's what I got so far:
    I have a query to slice the dates using ONLY the ValidFrom and ValidTo from the first table:

    But here's the problem: the dates overlap.
    For example, if I have a sale on the 2011-01-31, this date is present in line 2, 3 and 4 but it is also present in the line 5 and 6, and it should only be present in 2, 3 and 4, because I need to apply the discounts from Stack 2, 3 and 4 and not 5 and 6.

    ------------------Here's the query I have so far to achieve the result right above:------------------
    SELECT PD.[Stack]
        ,DR.[ProductCode]
        ,DR.[ValidFrom]
        ,DR.[ValidTo]
        ,PD.[Discount1]
    FROM (
        SELECT [Stack]
            ,[ProductCode]
            ,[ValidFrom]
            ,LEAD([ValidFrom]) OVER (PARTITION BY ProductCode ORDER BY [ValidFrom]) AS [ValidTo]
            ,[Discount1]
        FROM (
            SELECT [Stack]
                ,[ProductCode]
                ,[ValidFrom]
                ,[Discount1]
            FROM [dbo].[ProductDiscounts]
            
            UNION
            
            SELECT [Stack]
                ,[ProductCode]
                ,[ValidTo]
                ,[Discount1]
            FROM [dbo].[ProductDiscounts]
            ) Interceptions
        ) DR
    INNER JOIN [dbo].[ProductDiscounts] PD ON DR.ProductCode = PD.ProductCode
        AND DR.[ValidFrom] BETWEEN PD.[ValidFrom] AND PD.[ValidTo]
        AND DR.[ValidTo] BETWEEN PD.[ValidFrom]    AND PD.[ValidTo]
    WHERE DATEDIFF(DAY, DR.[ValidFrom], DR.[ValidTo]) > 0
    ORDER BY DR.[ValidFrom] ASC,PD.[Stack] ASC

    -----------------------------------------------------------------------------------------------------------------

    Any idea on how to achieve the final result? I can't thank enough if someone tries to help me.

    PS: Many Thanks to Thom A and drew.allen for helping out on a similar problem which helped me get to the query above that I have right now. Although when I applied it to the real database, I found the problems leading to this post and now I'm trying to figure out how to solve them and am asking once again for the help of the experts of this forum.

  • This problem is straight forward if you realize what is missing in the set which is the date range property.
    😎

    Here is a quick solution which generates an "inline calendar" to create the ranges

    USE [TEEST]
    GO

    SET NOCOUNT ON;

    IF OBJECT_ID('dbo.ProductDiscounts') IS NOT NULL DROP TABLE dbo.ProductDiscounts;

    CREATE TABLE dbo.ProductDiscounts
    (
      Stack   VARCHAR (5)
     ,ProductCode VARCHAR (10)
     ,ValidFrom DATE
     ,ValidTo  DATE
     ,Discount1 DECIMAL(4,3)
    )
    ;

    INSERT INTO dbo.ProductDiscounts (Stack, ProductCode, ValidFrom, ValidTo, Discount1)
    VALUES
    ('A','PROD40','20000101','20401231',0.1),
    ('B','PROD40','20090201','20110131',0.2),
    ('C','PROD40','20110201','20411231',0.15),
    ('D','PROD40','20090201','20401231',0.3)

    ;WITH DATE_SET AS
    (
      SELECT DISTINCT
       PRD.TOFROM
      ,PRD.VDATE
      FROM  dbo.ProductDiscounts  PD
      CROSS APPLY
       (
        SELECT 1,PD.ValidFrom UNION ALL
        SELECT 0,PD.ValidTo
       ) PRD(TOFROM,VDATE)
    )
    ,PERIOD_SET AS
    (
      SELECT
       DS.TOFROM
       ,DS.VDATE        AS FROM_DATE      
       ,LEAD(DS.VDATE,1,NULL) OVER
        (
          ORDER BY DS.VDATE ASC
        )         AS TO_DATE
      FROM  DATE_SET  DS
    )
    ,FILTERED_PERIODS AS
    (
      SELECT
       PS.TOFROM
      ,PS.FROM_DATE
      ,PS.TO_DATE
      FROM   PERIOD_SET     PS
      WHERE PS.TO_DATE IS NOT NULL
    )
    ,DATE_RANGE AS
    (
      SELECT
       MIN(FP.FROM_DATE) AS MIN_DATE
       ,MAX(FP.TO_DATE) AS MAX_DATE
       ,DATEDIFF(MONTH,MIN(FP.FROM_DATE),MAX(FP.TO_DATE)) AS NUM_YEAR
      FROM  FILTERED_PERIODS  FP
    )
    ,T(N) AS (SELECT X.N FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) X(N))
    , NUMS(N) AS (SELECT TOP((SELECT DR.NUM_YEAR FROM DATE_RANGE DR)) ROW_NUMBER() OVER (ORDER BY @@VERSION) AS N
         FROM DATE_RANGE DR,T T1,T T2,T T3)
    SELECT DISTINCT
    PS.Stack
    ,PS.ProductCode
    ,FP.FROM_DATE
    ,FP.TO_DATE
    ,PS.Discount1      
    FROM    DATE_RANGE DR
    CROSS APPLY  NUMS  NM
    CROSS APPLY  dbo.ProductDiscounts  PS
    CROSS APPLY  FILTERED_PERIODS   FP
    WHERE DATEADD(MONTH, NM.N, DR.MIN_DATE) > FP.FROM_DATE
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) < FP.TO_DATE
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) > PS.ValidFrom
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) < PS.ValidTo
    ORDER BY FP.FROM_DATE ASC
    ;

    Output

    Stack ProductCode FROM_DATE  TO_DATE    Discount1
    ----- ----------- ---------- ---------- ------------------------
    A     PROD40      2000-01-01 2009-02-01 0.100
    A     PROD40      2009-02-01 2011-01-31 0.100
    B     PROD40      2009-02-01 2011-01-31 0.200
    D     PROD40      2009-02-01 2011-01-31 0.300
    A     PROD40      2011-02-01 2040-12-31 0.100
    C     PROD40      2011-02-01 2040-12-31 0.150
    D     PROD40      2011-02-01 2040-12-31 0.300
    C     PROD40      2040-12-31 2041-12-31 0.150

  • Eirikur Eiriksson - Sunday, November 5, 2017 2:02 AM

    This problem is straight forward if you realize what is missing in the set which is the date range property.
    😎

    Here is a quick solution which generates an "inline calendar" to create the ranges

    USE [TEEST]
    GO

    SET NOCOUNT ON;

    IF OBJECT_ID('dbo.ProductDiscounts') IS NOT NULL DROP TABLE dbo.ProductDiscounts;

    CREATE TABLE dbo.ProductDiscounts
    (
      Stack   VARCHAR (5)
     ,ProductCode VARCHAR (10)
     ,ValidFrom DATE
     ,ValidTo  DATE
     ,Discount1 DECIMAL(4,3)
    )
    ;

    INSERT INTO dbo.ProductDiscounts (Stack, ProductCode, ValidFrom, ValidTo, Discount1)
    VALUES
    ('A','PROD40','20000101','20401231',0.1),
    ('B','PROD40','20090201','20110131',0.2),
    ('C','PROD40','20110201','20411231',0.15),
    ('D','PROD40','20090201','20401231',0.3)

    ;WITH DATE_SET AS
    (
      SELECT DISTINCT
       PRD.TOFROM
      ,PRD.VDATE
      FROM  dbo.ProductDiscounts  PD
      CROSS APPLY
       (
        SELECT 1,PD.ValidFrom UNION ALL
        SELECT 0,PD.ValidTo
       ) PRD(TOFROM,VDATE)
    )
    ,PERIOD_SET AS
    (
      SELECT
       DS.TOFROM
       ,DS.VDATE        AS FROM_DATE      
       ,LEAD(DS.VDATE,1,NULL) OVER
        (
          ORDER BY DS.VDATE ASC
        )         AS TO_DATE
      FROM  DATE_SET  DS
    )
    ,FILTERED_PERIODS AS
    (
      SELECT
       PS.TOFROM
      ,PS.FROM_DATE
      ,PS.TO_DATE
      FROM   PERIOD_SET     PS
      WHERE PS.TO_DATE IS NOT NULL
    )
    ,DATE_RANGE AS
    (
      SELECT
       MIN(FP.FROM_DATE) AS MIN_DATE
       ,MAX(FP.TO_DATE) AS MAX_DATE
       ,DATEDIFF(MONTH,MIN(FP.FROM_DATE),MAX(FP.TO_DATE)) AS NUM_YEAR
      FROM  FILTERED_PERIODS  FP
    )
    ,T(N) AS (SELECT X.N FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) X(N))
    , NUMS(N) AS (SELECT TOP((SELECT DR.NUM_YEAR FROM DATE_RANGE DR)) ROW_NUMBER() OVER (ORDER BY @@VERSION) AS N
         FROM DATE_RANGE DR,T T1,T T2,T T3)
    SELECT DISTINCT
    PS.Stack
    ,PS.ProductCode
    ,FP.FROM_DATE
    ,FP.TO_DATE
    ,PS.Discount1      
    FROM    DATE_RANGE DR
    CROSS APPLY  NUMS  NM
    CROSS APPLY  dbo.ProductDiscounts  PS
    CROSS APPLY  FILTERED_PERIODS   FP
    WHERE DATEADD(MONTH, NM.N, DR.MIN_DATE) > FP.FROM_DATE
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) < FP.TO_DATE
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) > PS.ValidFrom
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) < PS.ValidTo
    ORDER BY FP.FROM_DATE ASC
    ;

    Output

    Stack ProductCode FROM_DATE  TO_DATE    Discount1
    ----- ----------- ---------- ---------- ------------------------
    A     PROD40      2000-01-01 2009-02-01 0.100
    A     PROD40      2009-02-01 2011-01-31 0.100
    B     PROD40      2009-02-01 2011-01-31 0.200
    D     PROD40      2009-02-01 2011-01-31 0.300
    A     PROD40      2011-02-01 2040-12-31 0.100
    C     PROD40      2011-02-01 2040-12-31 0.150
    D     PROD40      2011-02-01 2040-12-31 0.300
    C     PROD40      2040-12-31 2041-12-31 0.150

    Thank you very much for your help 🙂 However, some of the dates are still overlapping 🙁

  • Vegeta7 - Sunday, November 5, 2017 8:17 AM

    Eirikur Eiriksson - Sunday, November 5, 2017 2:02 AM

    This problem is straight forward if you realize what is missing in the set which is the date range property.
    😎

    Here is a quick solution which generates an "inline calendar" to create the ranges

    USE [TEEST]
    GO

    SET NOCOUNT ON;

    IF OBJECT_ID('dbo.ProductDiscounts') IS NOT NULL DROP TABLE dbo.ProductDiscounts;

    CREATE TABLE dbo.ProductDiscounts
    (
      Stack   VARCHAR (5)
     ,ProductCode VARCHAR (10)
     ,ValidFrom DATE
     ,ValidTo  DATE
     ,Discount1 DECIMAL(4,3)
    )
    ;

    INSERT INTO dbo.ProductDiscounts (Stack, ProductCode, ValidFrom, ValidTo, Discount1)
    VALUES
    ('A','PROD40','20000101','20401231',0.1),
    ('B','PROD40','20090201','20110131',0.2),
    ('C','PROD40','20110201','20411231',0.15),
    ('D','PROD40','20090201','20401231',0.3)

    ;WITH DATE_SET AS
    (
      SELECT DISTINCT
       PRD.TOFROM
      ,PRD.VDATE
      FROM  dbo.ProductDiscounts  PD
      CROSS APPLY
       (
        SELECT 1,PD.ValidFrom UNION ALL
        SELECT 0,PD.ValidTo
       ) PRD(TOFROM,VDATE)
    )
    ,PERIOD_SET AS
    (
      SELECT
       DS.TOFROM
       ,DS.VDATE        AS FROM_DATE      
       ,LEAD(DS.VDATE,1,NULL) OVER
        (
          ORDER BY DS.VDATE ASC
        )         AS TO_DATE
      FROM  DATE_SET  DS
    )
    ,FILTERED_PERIODS AS
    (
      SELECT
       PS.TOFROM
      ,PS.FROM_DATE
      ,PS.TO_DATE
      FROM   PERIOD_SET     PS
      WHERE PS.TO_DATE IS NOT NULL
    )
    ,DATE_RANGE AS
    (
      SELECT
       MIN(FP.FROM_DATE) AS MIN_DATE
       ,MAX(FP.TO_DATE) AS MAX_DATE
       ,DATEDIFF(MONTH,MIN(FP.FROM_DATE),MAX(FP.TO_DATE)) AS NUM_YEAR
      FROM  FILTERED_PERIODS  FP
    )
    ,T(N) AS (SELECT X.N FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) X(N))
    , NUMS(N) AS (SELECT TOP((SELECT DR.NUM_YEAR FROM DATE_RANGE DR)) ROW_NUMBER() OVER (ORDER BY @@VERSION) AS N
         FROM DATE_RANGE DR,T T1,T T2,T T3)
    SELECT DISTINCT
    PS.Stack
    ,PS.ProductCode
    ,FP.FROM_DATE
    ,FP.TO_DATE
    ,PS.Discount1      
    FROM    DATE_RANGE DR
    CROSS APPLY  NUMS  NM
    CROSS APPLY  dbo.ProductDiscounts  PS
    CROSS APPLY  FILTERED_PERIODS   FP
    WHERE DATEADD(MONTH, NM.N, DR.MIN_DATE) > FP.FROM_DATE
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) < FP.TO_DATE
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) > PS.ValidFrom
    AND  DATEADD(MONTH, NM.N, DR.MIN_DATE) < PS.ValidTo
    ORDER BY FP.FROM_DATE ASC
    ;

    Output

    Stack ProductCode FROM_DATE  TO_DATE    Discount1
    ----- ----------- ---------- ---------- ------------------------
    A     PROD40      2000-01-01 2009-02-01 0.100
    A     PROD40      2009-02-01 2011-01-31 0.100
    B     PROD40      2009-02-01 2011-01-31 0.200
    D     PROD40      2009-02-01 2011-01-31 0.300
    A     PROD40      2011-02-01 2040-12-31 0.100
    C     PROD40      2011-02-01 2040-12-31 0.150
    D     PROD40      2011-02-01 2040-12-31 0.300
    C     PROD40      2040-12-31 2041-12-31 0.150

    Thank you very much for your help 🙂 However, some of the dates are still overlapping 🙁
    data:image/png;base64,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

    I have to ask... Once corrected, what will this result be used for/to do?  The reason why I ask is because, if you're going to do what I think your going to do, then there's a much easier way to do it.  Let us know.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Jeff Moden - Sunday, November 5, 2017 5:52 PM

    I have to ask... Once corrected, what will this result be used for/to do?  The reason why I ask is because, if you're going to do what I think your going to do, then there's a much easier way to do it.  Let us know.

    --Jeff Moden

    Hi Jeff,

    Thanks for taking the time to look into this 🙂 This is the second transformation I need to do. The two next transformations I already know how to achieve them, but so that you can better understand my objective, the end result will be to have a discount fact table, where I will have all the discounts correctly segmented by period. So, I will have something like this:

    In short, I will know all the stacks and respective discounts, ListPrice and FinalPrice for any given period of time. So if a sale were to occur on the 2009-02-01, I would know that I would have to apply those 3 discounts on the ListPrice to get that FinalPrice.

    I'm pretty confident that I will be able to do the two next steps: add the discounts using a pivot I learned from you Jeff (😀) and add the ListPrice also thanks to the help of this forum's experts 🙂 I only need to achieve the result on the date ranges that I'm asking on this thread to make everything else possible 🙂 But as simple as it seems, I can't seem to get there without forcing the result, which will not work on the real data. I need a clean and correct way to make everything dynamic :pinch:

  • Here's an alternative method. The results match your expected output except for the last row which I think has incorrect startdate:
    SELECT pd.Stack, pd.ProductCode, pd.Discount1,
     x.ValidFrom, x.ValidTo
    FROM #ProductDiscounts pd
    CROSS APPLY (
     SELECT ValidFrom = MIN(Date), ValidTo = MAX(Date)
     FROM (
      SELECT
       x.Date, n = (1+ROW_NUMBER() OVER(ORDER BY x.Date))/2 
      FROM #ProductDiscounts i
      CROSS APPLY (
       SELECT d.Date
       FROM (VALUES
        (DATEADD(DAY,-1,i.ValidFrom)),
        (i.ValidFrom),
        (ValidTo),
        (DATEADD(DAY,1,ValidTo))
        ) d (Date)
       WHERE d.Date BETWEEN pd.ValidFrom AND pd.ValidTo
      ) x
      WHERE i.ProductCode = pd.ProductCode
       AND i.ValidFrom <= pd.ValidTo
       AND i.ValidTo >= pd.ValidFrom
      GROUP BY x.Date
     ) d
     GROUP BY n
    ) x
    ORDER BY pd.ProductCode, x.ValidFrom, pd.Stack

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • ChrisM@Work - Monday, November 6, 2017 6:18 AM

    Here's an alternative method. The results match your expected output except for the last row which I think has incorrect startdate:
    SELECT pd.Stack, pd.ProductCode, pd.Discount1,
     x.ValidFrom, x.ValidTo
    FROM #ProductDiscounts pd
    CROSS APPLY (
     SELECT ValidFrom = MIN(Date), ValidTo = MAX(Date)
     FROM (
      SELECT
       x.Date, n = (1+ROW_NUMBER() OVER(ORDER BY x.Date))/2 
      FROM #ProductDiscounts i
      CROSS APPLY (
       SELECT d.Date
       FROM (VALUES
        (DATEADD(DAY,-1,i.ValidFrom)),
        (i.ValidFrom),
        (ValidTo),
        (DATEADD(DAY,1,ValidTo))
        ) d (Date)
       WHERE d.Date BETWEEN pd.ValidFrom AND pd.ValidTo
      ) x
      WHERE i.ProductCode = pd.ProductCode
       AND i.ValidFrom <= pd.ValidTo
       AND i.ValidTo >= pd.ValidFrom
      GROUP BY x.Date
     ) d
     GROUP BY n
    ) x
    ORDER BY pd.ProductCode, x.ValidFrom, pd.Stack

    Actually, your query matches 100% my expected output! 😀 😀 😀

    My expected output:

    Your output:

    I'm so happy! I will carefully study your query tonight when I get home and adapt it to the real database to test your logic :')

  • Here is an approach that requires one fewer sorts than Chris M.'s method.

    It uses a fairly standard technique for packing intervals developed by Itzik Ben Gan.

    Your data uses a closed interval notation, but this method works best with a half-closed interval notation, so part of the query is used to convert from closed to half-closed and back (the two DATEADD() functions).

    ;
    WITH ValidDates AS
    (
        SELECT ValidDate AS ValidFrom, DATEADD(DAY, -1, LEAD(ValidDate, 1, '2040-12-31') OVER(PARTITION BY ProductCode ORDER BY ValidDate, Stack)) AS ValidTo
        FROM #ProductDiscounts
        CROSS APPLY ( VALUES(ValidFrom), (DATEADD(DAY, 1, ValidTo)) ) dt(ValidDate)
    )
    SELECT pd.Stack, pd.ProductCode, pd.Discount1, vd.ValidFrom, vd.ValidTo
    FROM #ProductDiscounts pd
    INNER JOIN ValidDates vd
        ON pd.ValidFrom < vd.ValidTo
            AND vd.ValidFrom < pd.ValidTo
    WHERE vd.ValidFrom < vd.ValidTo  /*  Exclude invalid date ranges due to "duplicate" dates.  */
    ORDER BY ProductCode, vd.ValidFrom, Stack;

    Drew

    J. Drew Allen
    Business Intelligence Analyst
    Philadelphia, PA

  • What about something like this to avoid creating more rows?

    CREATE TABLE ProductDiscounts (
    Stack VARCHAR (255),
    ProductCode VARCHAR (255),
    ValidFrom DATE,
    ValidTo DATE,
    Discount1 decimal(4,3));

    INSERT INTO ProductDiscounts
    (Stack, ProductCode, ValidFrom, ValidTo, Discount1)
    VALUES
    ('A','PROD40','20000101','20401231',0.1),
    ('B','PROD40','20090201','20110131',0.2),
    ('C','PROD40','20110201','20411231',0.15),
    ('D','PROD40','20090201','20401231',0.3);

    CREATE TABLE Products (
    ProductCode VARCHAR (255),
    ListPrice decimal(10,4));

    INSERT INTO Products VALUES('PROD40', 200);

    WITH cteProductDiscounts AS(
      SELECT *, ROW_NUMBER() OVER(PARTITION BY ProductCode ORDER BY ValidFrom) rn
      FROM ProductDiscounts pd
      WHERE '20090201' BETWEEN ValidFrom AND ValidTo
    ),
    ctePivotedDiscounts AS(
      SELECT MAX( CASE WHEN pd.rn = 1 THEN pd.Stack END) AS Stack1
        ,MAX( CASE WHEN pd.rn = 2 THEN pd.Stack END) AS Stack2
        ,MAX( CASE WHEN pd.rn = 3 THEN pd.Stack END) AS Stack3
        ,p.ProductCode
        ,p.ListPrice
        ,MAX( CASE WHEN pd.rn = 1 THEN pd.Discount1 END) AS Discount1
        ,MAX( CASE WHEN pd.rn = 2 THEN pd.Discount1 END) AS Discount2
        ,MAX( CASE WHEN pd.rn = 3 THEN pd.Discount1 END) AS Discount3
        ,MAX( ValidFrom) AS ValidFrom
        ,MIN( ValidTo) AS ValidTo
      FROM cteProductDiscounts pd
      JOIN Products p ON p.ProductCode = pd.ProductCode
      GROUP BY p.ProductCode
        ,p.ListPrice
    )
    SELECT Stack1
       ,Stack2
       ,Stack3
       ,ProductCode
       ,ListPrice
       ,Discount1
       ,Discount2
       ,Discount3
       ,ListPrice * (1-Discount1) * (1-Discount2) * (1-Discount3) AS FinalPrice --Line edited after Steve's remark.
       ,ValidFrom
       ,ValidTo
    FROM ctePivotedDiscounts;
    GO
    DROP TABLE ProductDiscounts, Products

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • Luis Cazares - Monday, November 6, 2017 1:24 PM

    What about something like this to avoid creating more rows?

    CREATE TABLE ProductDiscounts (
    Stack VARCHAR (255),
    ProductCode VARCHAR (255),
    ValidFrom DATE,
    ValidTo DATE,
    Discount1 decimal(4,3));

    INSERT INTO ProductDiscounts
    (Stack, ProductCode, ValidFrom, ValidTo, Discount1)
    VALUES
    ('A','PROD40','20000101','20401231',0.1),
    ('B','PROD40','20090201','20110131',0.2),
    ('C','PROD40','20110201','20411231',0.15),
    ('D','PROD40','20090201','20401231',0.3);

    CREATE TABLE Products (
    ProductCode VARCHAR (255),
    ListPrice decimal(10,4));

    INSERT INTO Products VALUES('PROD40', 200);

    WITH cteProductDiscounts AS(
      SELECT *, ROW_NUMBER() OVER(PARTITION BY ProductCode ORDER BY ValidFrom) rn
      FROM ProductDiscounts pd
      WHERE '20090201' BETWEEN ValidFrom AND ValidTo
    ),
    ctePivotedDiscounts AS(
      SELECT MAX( CASE WHEN pd.rn = 1 THEN pd.Stack END) AS Stack1
        ,MAX( CASE WHEN pd.rn = 2 THEN pd.Stack END) AS Stack2
        ,MAX( CASE WHEN pd.rn = 3 THEN pd.Stack END) AS Stack3
        ,p.ProductCode
        ,p.ListPrice
        ,MAX( CASE WHEN pd.rn = 1 THEN pd.Discount1 END) AS Discount1
        ,MAX( CASE WHEN pd.rn = 2 THEN pd.Discount1 END) AS Discount2
        ,MAX( CASE WHEN pd.rn = 3 THEN pd.Discount1 END) AS Discount3
        ,MAX( ValidFrom) AS ValidFrom
        ,MIN( ValidTo) AS ValidTo
      FROM cteProductDiscounts pd
      JOIN Products p ON p.ProductCode = pd.ProductCode
      GROUP BY p.ProductCode
        ,p.ListPrice
    )
    SELECT Stack1
       ,Stack2
       ,Stack3
       ,ProductCode
       ,ListPrice
       ,Discount1
       ,Discount2
       ,Discount3
       ,ListPrice * (-1+Discount1) * (-1+Discount2) * (-1+Discount3) AS FinalPrice
       ,ValidFrom
       ,ValidTo
    FROM ctePivotedDiscounts;
    GO
    DROP TABLE ProductDiscounts, Products

    Don't you want (1 - Discount1) instead of (-1 + Discount1) ?   This would be true of the other two discounts as well....

    Steve (aka sgmunson) 🙂 🙂 🙂
    Rent Servers for Income (picks and shovels strategy)

  • sgmunson - Monday, November 6, 2017 2:44 PM

    Don't you want (1 - Discount1) instead of (-1 + Discount1) ?   This would be true of the other two discounts as well....

    You're right. Silly me got, I'm not sure what I was thinking.

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • Vegeta7 - Saturday, November 4, 2017 12:30 PM

    >> I have this ProductDiscounts table: <<

    Apparently you don't know that a table, by definition --Let me repeat that; by definition -- must have a key. Since everything in your non-table can be null, there's no way can ever have a key. I would've kicked you out of the first day of class

    What you posted is basically an old style COBOL punch card file declaration written in SQL. Did you know the products have standardized codes, such as UPC, GTIN, et al? Tables always have keys. By definition, not by option. You also need to put in temporal constraints. These constraints will really, really help the optimizer.

    CREATE TABLE ProductDiscounts
    (stack_name CHAR(5) NOT NULL PRIMARY KEY,
    product_gtin CHAR(15) NOT NULL,
    discount_start_date DATE NOT NULL,
    discount_end_date DATE,
    CHECK(discount_start_date <= discount_end_date) --important!
    foobar_discount_rate DECIMAL(4,3) NOT NULL
    CHECK(foobar_discount_rate > 0.000 )
    );

    INSERT INTO ProductDiscounts
    VALUES
    ('A','PROD40','2000-01-01','2040-1231',0.1),
    ('B','PROD40','2009-02-01','2011-01-31',0.2),
    ('C','PROD40','2011-02-01','2041-12-31',0.15),
    ('D','PROD40','2009-02-01','2040-12-31',0.3);

    >> Here's a look at the timeframe:
    52% of original size(was 974x128) - Click to enlarge <<

    Frankly, I do not feel like printing out and then transcribing the data that you are too lazy to put into SQL. You're asking people to help you for free instead of for a few hundred bucks an hour, and yet you do not have the ability or the willingness to simply transcribe your own data. Why should we have any respect for you?

    You will need a calendar table to locate a date within an interval in which the discount applies. This becomes a simple join.

    Also, also the habit of putting a, in front of the line of of code goes back to the days when wrote code on punch cards. We wrote the code this way (usually Fortran but sometimes COBOL) saluted rearrange the deck of punch cards. You're not thinking in sets or relationally at all.

    Please post DDL and follow ANSI/ISO standards when asking for help. 

  • Vegeta7 - Sunday, November 5, 2017 6:52 PM

    ... the end result will be to have a discount fact table, where I will have all the discounts correctly segmented by period.

    In short, I will know all the stacks and respective discounts, ListPrice and FinalPrice for any given period of time. So if a sale were to occur on the 2009-02-01, I would know that I would have to apply those 3 discounts on the ListPrice to get that FinalPrice.

    That's pretty much what I thought.  I'm thinking there's an easier method to do this, which may be faster and won't require splitting discount periods based on starts and ends of other discount periods.  I'm a bit pressed for time but I'll see if I can put something together for you in the next day.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • jcelko212 32090 - Monday, November 6, 2017 4:16 PM

    Vegeta7 - Saturday, November 4, 2017 12:30 PM

    >> I have this ProductDiscounts table: <<

    Apparently you don't know that a table, by definition --Let me repeat that; by definition -- must have a key. Since everything in your non-table can be null, there's no way can ever have a key. I would've kicked you out of the first day of class

    What you posted is basically an old style COBOL punch card file declaration written in SQL. Did you know the products have standardized codes, such as UPC, GTIN, et al? Tables always have keys. By definition, not by option. You also need to put in temporal constraints. These constraints will really, really help the optimizer.

    CREATE TABLE ProductDiscounts
    (stack_name CHAR(5) NOT NULL PRIMARY KEY,
    product_gtin CHAR(15) NOT NULL,
    discount_start_date DATE NOT NULL,
    discount_end_date DATE,
    CHECK(discount_start_date <= discount_end_date) --important!
    foobar_discount_rate DECIMAL(4,3) NOT NULL
    CHECK(foobar_discount_rate > 0.000 )
    );

    INSERT INTO ProductDiscounts
    VALUES
    ('A','PROD40','2000-01-01','2040-1231',0.1),
    ('B','PROD40','2009-02-01','2011-01-31',0.2),
    ('C','PROD40','2011-02-01','2041-12-31',0.15),
    ('D','PROD40','2009-02-01','2040-12-31',0.3);

    >> Here's a look at the timeframe:
    52% of original size(was 974x128) - Click to enlarge <<

    Frankly, I do not feel like printing out and then transcribing the data that you are too lazy to put into SQL. You're asking people to help you for free instead of for a few hundred bucks an hour, and yet you do not have the ability or the willingness to simply transcribe your own data. Why should we have any respect for you?

    You will need a calendar table to locate a date within an interval in which the discount applies. This becomes a simple join.

    Also, also the habit of putting a, in front of the line of of code goes back to the days when wrote code on punch cards. We wrote the code this way (usually Fortran but sometimes COBOL) saluted rearrange the deck of punch cards. You're not thinking in sets or relationally at all.

    CODE JOE!  Post the code for YOUR Calendar table and the solution for this problem!

    And the good thing about punched cards is that they kept people from making ridiculously wide entities... like Calendar tables.. 😉

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Jeff Moden - Monday, November 6, 2017 5:17 PM

    Vegeta7 - Sunday, November 5, 2017 6:52 PM

    ... the end result will be to have a discount fact table, where I will have all the discounts correctly segmented by period.

    In short, I will know all the stacks and respective discounts, ListPrice and FinalPrice for any given period of time. So if a sale were to occur on the 2009-02-01, I would know that I would have to apply those 3 discounts on the ListPrice to get that FinalPrice.

    That's pretty much what I thought.  I'm thinking there's an easier method to do this, which may be faster and won't require splitting discount periods based on starts and ends of other discount periods.  I'm a bit pressed for time but I'll see if I can put something together for you in the next day.

    Knowing the possible end uses for this problem, you don't need a Calendar table and you don't need to slice and dice the discount table.  You just need to use it.  First, you need to add the following index to it.


     CREATE CLUSTERED INDEX IXC_By_ValidFrom_ValidTo ON dbo.ProductDiscounts
            (ValidFrom, ValidTo)
    ;

    Let's test things out.  I don't know exactly what you want for output for your followup tasks so I'm just showing a possibility.  To test things out and test performance in the process, here's a test data generator that will create a million orders with a date for each order.  I'm also assuming that your discount table will have more than one product in it but made no additional products for this POP code.

    Here's the test data generation code.


    --===== Create and populate a test table
    -- DROP TABLE OrderDetail;
       WITH cteGenDates AS
    (
     SELECT TOP 1000000
            OrderDate = CONVERT(DATE,DATEADD(dd,ABS(CHECKSUM(NEWID())%DATEDIFF(dd,'1999','2013')),'1999'))
       FROM      sys.all_columns ac1
      CROSS JOIN sys.all_columns ac2
    )
     SELECT  OrderID = ROW_NUMBER() OVER (ORDER BY OrderDate)
            ,OrderDate
            ,ProductCode = 'Prod40'
       INTO dbo.OrderDetail
       FROM cteGenDates
      ORDER BY OrderDate
    ;

    Here's the code that does the matching based on ProductCode and the dates, as you indicated.  It returns more than one row per order/product if there's more that one discount for that product based on the order date and the discount from and to dates.  It takes about 16 seconds on my machine.  No optimization was attempted because this is just POP code.  The method could certainly be translated to handle singleton lookups.


     SELECT  OrderID        = od.OrderID
            ,OrderDate      = od.OrderDate
            ,ProductCode    = od.ProductCode
            ,Stack          = ISNULL(pd.Stack,'No Discount')
            ,ValidFrom      = ISNULL(pd.ValidFrom,'9999')
            ,ValidTo        = ISNULL(pd.ValidTo,'9999')
            ,Discount       = ISNULL(pd.Discount1,0)
       FROM      dbo.OrderDetail      od
       LEFT JOIN dbo.ProductDiscounts pd
              ON od.OrderDate  >= pd.ValidFrom
             AND od.OrderDate  <= pd.ValidTo
             AND od.ProductCode = pd.ProductCode
      ORDER BY OrderID
    ;

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

Viewing 15 posts - 1 through 15 (of 29 total)

You must be logged in to reply to this topic. Login to reply