I was trying today to solve a problem that apparently requires recursive common table expressions, though they are not (yet) available in Azure Synapse serverless SQL pool. The problem can be summarized in the below table definition, in which given a set of Products with an initial Sales price, is needed to apply Price Increases successively for each Cycle. The cumulated increase is simulated in the last column for each line.
Unfortunately, there is no SQL Server windowing function that allows multiplying incrementally the values of a column (similar as the running total works). However, there’s a mathematical trick that can be used to transform a product into a sum of elements by applying the Exp (exponential) and Log (logarithm) functions (see Solution 1), and which frankly is more elegant than applying CTEs (see Solution 2).
-- create table with test data SELECT * INTO dbo.ItemPrices FROM (VALUES ('ID001', 1000, 1, 1.02, '1.02') , ('ID001', 1000, 2, 1.03, '1.02*1.03') , ('ID001', 1000, 3, 1.03, '1.02*1.03*1.03') , ('ID001', 1000, 4, 1.04, '1.02*1.03*1.03*1.04') , ('ID002', 100, 1, 1.02, '1.02') , ('ID002', 100, 2, 1.03, '1.02*1.03') , ('ID002', 100, 3, 1.04, '1.02*1.03*1.04') , ('ID002', 100, 4, 1.05, '1.02*1.03*1.04*1.05') ) DAT (ItemId, SalesPrice, Cycle, PriceIncrease, CumulatedIncrease) -- reviewing the data SELECT * FROM dbo.ItemPrices -- Solution 1: new sales prices with log & exp SELECT ItemId , SalesPrice , Cycle , PriceIncrease , EXP(SUM(Log(PriceIncrease)) OVER(PARTITION BY Itemid ORDER BY Cycle)) CumulatedIncrease , SalesPrice * EXP(SUM(Log(PriceIncrease)) OVER(PARTITION BY Itemid ORDER BY Cycle)) NewSalesPrice FROM dbo.ItemPrices -- Solution 2: new sales prices with recursive CTE ;WITH CTE AS ( -- initial record SELECT ITP.ItemId , ITP.SalesPrice , ITP.Cycle , ITP.PriceIncrease , cast(ITP.PriceIncrease as decimal(38,6)) CumulatedIncrease FROM dbo.ItemPrices ITP WHERE ITP.Cycle = 1 UNION ALL -- recursice part SELECT ITP.ItemId , ITP.SalesPrice , ITP.Cycle , ITP.PriceIncrease , Cast(ITP.PriceIncrease * ITO.CumulatedIncrease as decimal(38,6)) CumulatedIncrease FROM dbo.ItemPrices ITP JOIN CTE ITO ON ITP.ItemId = ITO.ItemId AND ITP.Cycle-1 = ITO.Cycle ) -- final result SELECT ItemId , SalesPrice , Cycle , PriceIncrease , CumulatedIncrease , SalesPrice * CumulatedIncrease NewSalesPrice FROM CTE ORDER BY ItemId , Cycle -- validating the cumulated price increases (only last ones) SELECT 1.02*1.03*1.03*1.04 , 1.02*1.03*1.04*1.05 -- cleaning up DROP TABLE IF EXISTS dbo.ItemPrices
Notes:
1. The logarithm in SQL Server’s implementation works only with positive numbers!
2. For simplification I transformed percentages (e.g. 1%) in values that are easier to multitply with (e.g. 1.01). The solution can be easily modified to consider discounts.
3. When CTEs are not available, one is forced to return to the methods used in SQL Server 2000 (I've been there) and thus use temporary tables or table variables with loops. Moreover, the logic can be encapsulated in multi-statement table-valued functions (see example), unfortunately, another feature not (yet) supported by serverless SQL pools.
4. Unfortunately, STRING_AGG, which concatenates values across rows, works only with a GROUP BY clause. Anyway, its result is useless without the availability of a Eval function in SQL (see example), however the Expr function available in data flows could be used as workaround.
4. Even if I thought about the use of logarithms for transforming the product into a sum, I initially ignored the idea, thinking that the solution would be too complex to implement. So, the credit goes to another blogpost. Kudos!
No comments:
Post a Comment