SQL Query Problem: How to aggregate discount percentages?

  • I have a scenario whereby I need to aggregate n number of discounts to get a total discount %. Each discount must be applied net of the previous discount.

    For example: I buy a hat for £100 and I have two valid vouchers. The first gets me 15% off, 100-(100*(15/100)) = £85. And then we have a second voucher worth 5%, 85-(85*5/100) = £80.75.

    So our total discount is 19.25%.

    Sequence is important, so a further field records the order in which the discounts are applied.

    So in our table we might have:

    SaleID | Discount | Sequence

    1 15% 1

    1 5% 2

    2 5% 1

    3 5% 1

    3 10% 2

    3 25% 3

    Is anyone able to put together an elegant query that will give the total discount for each sale using the above table? (I can think of some less elegant ways to do it, but there must be a cleaner way...?). As in the example above, I expect 19.25% for SaleID 1.

    Many Thanks.

  • This should do what you need, and serve as a starting point

    CREATE Table #disc

    (

    SaleId tinyint

    ,Discount Decimal(5,2)

    ,Seq tinyint

    )

    Insert into #disc

    Values (1,0.15,1)

    Insert into #disc

    Values (1,0.05,2)

    Insert into #disc

    Values (2,0.05,1)

    Insert into #disc

    Values (3,0.05,1)

    Insert into #disc

    Values (3,0.10,2)

    Insert into #disc

    Values (3,0.25,3)

    Select * from #disc;

    WITH CTE_CalcTotalDisc

    AS

    (

    Select SaleId,Discount,Seq, convert(decimal(6,5),1-(1*Discount)) T_Disc

    From #disc

    where Seq=1

    UNION ALL

    Select d.SaleId,d.Discount,d.Seq, convert(decimal(6,5),(1-(1*d.Discount))*T_Disc) T_Disc

    From #disc d

    JOIN CTE_CalcTotalDisc ad on d.SaleId=ad.SaleId

    AND d.Seq=ad.Seq+1

    ),

    CTE_FinalPrice

    AS

    (

    Select SaleId, T_Disc,100-((1-T_Disc)*100) totalPrice

    ,Row_Number() OVER (PARTITION BY SaleId order by Seq Desc) rn

    from CTE_CalcTotalDisc

    )

    Select * FROM CTE_FinalPrice where rn=1

    _________________________________________________________________________
    SSC Guide to Posting and Best Practices

  • Another version:

    declare @t table

    (

    SaleId int

    ,Discount int

    ,Sequence int

    )

    insert @t (SaleId, Discount, Sequence)

    select 1,15,1

    union all

    select 1,5,2

    union all

    select 2,5,1

    union all

    select 3,5,1

    union all

    select 3,10,2

    union all

    select 3,25,3

    select SaleId, (1 - EXP(SUM(LOG(1-Discount/100.0)))) * 100 Discount

    from @t t

    group by SaleId

    The absence of evidence is not evidence of absence.
    Martin Rees

    You can lead a horse to water, but a pencil must be lead.
    Stan Laurel

  • Great, thanks guys.

    Very interested to see the recursive CTE solution as that is what I've been trying to get going most of this morning! I wasn't far off actually I just needed to limit the "Anchor member" to Seq 1. Bah!

    Solution 2 looks interesting, to be honest I don't see how that works immediately but am going to work through it...

    Thanks again.

  • Phils is the best one of the two and it should run a lot quicker.

    If you want to see the totals after each discount then you may have to look at a triangular join/running total type solution.

    _________________________________________________________________________
    SSC Guide to Posting and Best Practices

  • jezza101 (3/26/2013)


    Great, thanks guys.

    Very interested to see the recursive CTE solution as that is what I've been trying to get going most of this morning! I wasn't far off actually I just needed to limit the "Anchor member" to Seq 1. Bah!

    Solution 2 looks interesting, to be honest I don't see how that works immediately but am going to work through it...

    Thanks again.

    How sol 2 works is just a bit of maths.

    When calculating total discount for a sale, order is not important. To calculate the final price of an item after multiple discounts, just do something like this:

    Final Price = (Orig Price)(1 - d1)(1 - d2)(1 - d3) etc etc where d1, d2 are discount percentages as decimals (eg, 0.15 for 15%).

    So calculating the total discount for a sale becomes a sort of aggregation - but instead off summing, we are multiplying across rows. But T-SQL has no 'MULT' aggregation function, so we need to sum the logs & then power up the result - for which there are T-SQL functions.

    The absence of evidence is not evidence of absence.
    Martin Rees

    You can lead a horse to water, but a pencil must be lead.
    Stan Laurel

  • Here's another solution. Entirely set based and no rounding approximations. Returns the current, previous, and cumulative discount for each row in the source table.

    IF OBJECT_ID('tempdb..#TestData') IS NOT NULL

    DROP TABLE #TestData

    ;WITH sampledata AS

    (

    SELECT * FROM

    (VALUES

    (1,0.15,1),

    (1,0.05,2),

    (2,0.05,1),

    (3,0.05,1),

    (3,0.10,2),

    (3,0.25,3)

    ) DATA (SaleID,Discount,Seq))

    SELECT

    SaleID,Discount,Seq

    INTO #TestData

    FROM sampledata

    SELECT

    CurrSaleID AS SaleID

    ,CurrSeq AS Seq

    ,CurrDiscount

    ,PrevDiscount

    ,(CASE

    WHEN PrevDiscount = 0 THEN ((CurrDiscount*100))/100

    ELSE 1-(((100-(PrevDiscount*100))-((100-(PrevDiscount*100))*CurrDiscount))/100)

    END) AS CumulativeDiscount

    FROM

    (

    SELECT DISTINCT

    d.SaleID

    ,d.CurrSeq

    ,d.CurrSaleID

    ,d.NextSaleID

    ,d.Discount AS CurrDiscount

    ,(CASE

    WHEN d.CurrSeq = 1 THEN 0

    WHEN d.CurrSeq > 1

    AND d.CurrSaleID = d.NextSaleID-1

    THEN (SELECT Discount

    FROM #TestData

    WHERE

    SaleID = d.CurrSaleID

    AND Seq = d.CurrSeq-1)

    ELSE 0

    END) AS PrevDiscount

    FROM

    (

    SELECT

    s1.SaleID

    ,s1.Discount

    ,s1.Seq AS CurrSeq

    ,s1.SaleID AS CurrSaleID

    ,s1.SaleID+1 AS NextSaleID

    FROM

    #TestData s1

    ) d

    ) d1

    ORDER BY

    CurrSaleID

    ,CurrSeq

  • Steven Willis (3/26/2013)


    ... no rounding approximations.

    What a misconception.

    Try add this to the final SELECT in your query:

    , SQL_VARIANT_PROPERTY(CurrDiscount, 'basetype'), SQL_VARIANT_PROPERTY(CurrDiscount, 'scale'), SQL_VARIANT_PROPERTY(CurrDiscount, 'precision')

    , SQL_VARIANT_PROPERTY(CurrDiscount*100, 'basetype'), SQL_VARIANT_PROPERTY(CurrDiscount*100, 'scale'), SQL_VARIANT_PROPERTY(CurrDiscount*100, 'precision')

    , SQL_VARIANT_PROPERTY((100-(PrevDiscount*100))*CurrDiscount, 'basetype'), SQL_VARIANT_PROPERTY((100-(PrevDiscount*100))*CurrDiscount, 'scale'), SQL_VARIANT_PROPERTY((100-(PrevDiscount*100))*CurrDiscount, 'precision')

    , SQL_VARIANT_PROPERTY(1-(((100-(PrevDiscount*100))-((100-(PrevDiscount*100))*CurrDiscount))/100), 'basetype')

    , SQL_VARIANT_PROPERTY(1-(((100-(PrevDiscount*100))-((100-(PrevDiscount*100))*CurrDiscount))/100), 'scale'), SQL_VARIANT_PROPERTY(1-(((100-(PrevDiscount*100))-((100-(PrevDiscount*100))*CurrDiscount))/100), 'precision')

    Every step of the way the data type of the monetary values is changed.

    And considering that SQL Server uses floating point data types for multiplications/divisions - there are more implicit conversions and roundings than you can see.

    Sorry, you cannot avoid rounding approximations totally.

    But you can minimise them by using FLOAT data type for such things as "Discount" and converting only final result of calculations to an appropriate decimal data type.

    _____________
    Code for TallyGenerator

Viewing 8 posts - 1 through 7 (of 7 total)

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