|
Marcos is a support analyst for an ISV company in Joinville, Brazil, where he helps customers properly configure and administer their databases. He is also an enthusiastic .NET and SQL Server developer and holds some Microsoft certifications (MCDBA and MCTS/MCITP for SQL 2005 and 2008). He is currently attending a postgraduate course and helps in local forums whenever time allows.
|
How did Marcos arrive at his solution?
This is how Macros explained his solution to us:
This is a challenge that looks simplistic at first, but quickly becomes complex. I thought I would be able to select from @t and CROSS JOIN with @c twice and then apply the discount calculation.
There are two major problems with these logical steps:
-
The discounts must be cumulative and order does matter for percentage coupons.
-
The maximum discount may not consist of two coupons (perhaps not even one) for a given product. Remember the discount cannot be greater than 30% the original price or 30 dollars.
Instead, I created four CTEs that progressively apply discounts and check business rules.
The Discounts1 CTE crosses join the products with the discounts.
WITH Discounts1 AS
(SELECT t.ID, t.PRICE, c1.ID AS CID1, c1.NAME AS CNAME1,
t.PRICE - CASE c1.IS_PERCENT WHEN 0 THEN c1.VALUE
WHEN 1 THEN t.PRICE * c1.VALUE / 100 END AS PRICE1
FROM @T AS t CROSS JOIN @C AS c1),
The Discounts2 CTE then checks that we are not breaking any rules, with one coupon applied. The Discount2 CTE also applies the second coupon (another cross join); however, this time I am applying filtering. The USE1 column indicates whether or not the discount is in compliance with the business rules. A USE1 value of 1 indicates that the discount is still in compliance and a value of 0 indicates non-compliance.
Discounts2 AS
(SELECT d.*,
CASE WHEN (d.PRICE - d.PRICE1 <= 30) AND
(d.PRICE1 / d.PRICE >= .7) THEN 1 ELSE 0 END AS USE1,
c2.ID AS CID2, c2.NAME AS CNAME2,
d.PRICE1 - CASE c2.IS_PERCENT WHEN 0 THEN c2.VALUE
WHEN 1 THEN d.PRICE1 * c2.VALUE / 100 END AS PRICE2
FROM Discounts1 AS d CROSS JOIN @C AS c2
WHERE d.CID1 <> c2.ID),
The Discounts3 checks that the discount is still in compliance, with two coupons applied. DiscountCTE3 also creates an USE2 column. If USE2 = 1 both coupons are valid to use for a given product. After Discounts3, we have three price columns, PRICE, PRICE1 and PRICE2. Each column corresponds to original price the original proces less one and two discounts respecitvely. We also have the USE1 and USE2 columns that tell us if PRICE1 or PRICE2 columns violate the rules.
Discounts3 AS
(SELECT *,
CASE WHEN (USE1 = 1) AND (PRICE - PRICE2 <= 30) AND
(PRICE2 / PRICE >= .7) THEN 1 ELSE 0 END AS USE2
FROM Discounts2),
So, the Discounts4 CTE picks PRICE2, PRICE1, or PRICE column as appropriate, and generates the COUPON_NAMES column. The Discounts4 generates a list of products with all legal combinations of discounts applied. Once we have them all, we just need to pick the one with the lowest price. This is done in the final select using a cross apply and top 1.
Discounts4 AS
(SELECT ID, PRICE,
CASE WHEN USE2 = 1 THEN PRICE2
WHEN USE1 = 1 THEN PRICE1
ELSE PRICE END AS DISC_PRICE,
CASE WHEN USE2 = 1 THEN CNAME1 + ' + ' + CNAME2
WHEN USE1 = 1 THEN CNAME1
ELSE '' END AS COUPON_NAMES
FROM Discounts3)
Lastly we handle the results format. While this solution is not overly complex, it is very inflexible. Currently the code is limited by a finite number of coupons; however, this process does not scale well with more and more coupons. Consider if we needed to check three or more coupons. To check three or more additonal coupons requires even more CTEs, which increases complexity, reduces speed, and increases maintenance overhead. Because I use common table expressions and the cross apply operator, this solution is limited to SQL Server 2005 or greater. Here is the final solution:
WITH Discounts1 AS
(SELECT t.ID, t.PRICE, c1.ID AS CID1, c1.NAME AS CNAME1,
t.PRICE - CASE c1.IS_PERCENT WHEN 0 THEN c1.VALUE
WHEN 1 THEN t.PRICE * c1.VALUE / 100 END AS PRICE1
FROM @T AS t CROSS JOIN @C AS c1),
Discounts2 AS
(SELECT d.*,
CASE WHEN (d.PRICE - d.PRICE1 <= 30) AND
(d.PRICE1 / d.PRICE >= .7) THEN 1 ELSE 0 END AS USE1,
c2.ID AS CID2, c2.NAME AS CNAME2,
d.PRICE1 - CASE c2.IS_PERCENT WHEN 0 THEN c2.VALUE
WHEN 1 THEN d.PRICE1 * c2.VALUE / 100 END AS PRICE2
FROM Discounts1 AS d CROSS JOIN @C AS c2
WHERE d.CID1 <> c2.ID),
Discounts3 AS
(SELECT *,
CASE WHEN (USE1 = 1) AND (PRICE - PRICE2 <= 30) AND
(PRICE2 / PRICE >= .7) THEN 1 ELSE 0 END AS USE2
FROM Discounts2),
Discounts4 AS
(SELECT ID, PRICE,
CASE WHEN USE2 = 1 THEN PRICE2
WHEN USE1 = 1 THEN PRICE1
ELSE PRICE END AS DISC_PRICE,
CASE WHEN USE2 = 1 THEN CNAME1 + ' + ' + CNAME2
WHEN USE1 = 1 THEN CNAME1
ELSE '' END AS COUPON_NAMES
FROM Discounts3)
SELECT d.ID, t.NAME,
CAST(d.PRICE AS VARCHAR(20)) + '$' AS PRICE,
CAST(d.DISC_PRICE AS VARCHAR(20)) + '$' AS DISC_PRICE,
CAST(d.PRICE - d.DISC_PRICE AS VARCHAR(20)) + '$' AS TOT_DISC,
CAST(ROUND(100 - d.DISC_PRICE / (d.PRICE / 100), 2) AS VARCHAR(20)) + '%' AS RATE,
COUPON_NAMES
FROM @T AS t CROSS APPLY
(SELECT TOP 1* FROM Discounts4 d
WHERE t.ID = d.ID ORDER BY DISC_PRICE) AS d
Thanks Marcos for your explanation!
What can we learn from this?
To resolve this challenge one should sit down and really thing through the methodology that he would like to implement. As with any great challenge, the developer has to break this complex problem into more manageable pieces. This challenge requires organizational skills, computational skills, TSQL skills and most of all patience.
The first two questions one should ask himself are:
• How do I find the correct combination of coupons to maximize discount, while complying with the business rules?
• How do I verify that my solution is compliant with the business rules?
Final Word:
Congratulations Marcos for your solution and in-depth explanation!
All TSQL Challenge 11 winning solutions have been presented, but please remember that we are still accepting solutions. New and better solution are always surfacing, and the next winning solution may be yours.
Stay tuned TSQL Challenge 15, will be online next Monday. Please bookmark our RSS Feed in your favorite RSS reader, so you do not forget!