I decided to come up with a slightly different approach. This set is installed, and the general idea is to find all possible combinations of amounts that meet the desired condition, and then choose the cheapest one.
Steps:
- taking into account
@ReqQty , for each type of bag, find how many of these amounts it makes sense to include in the expression (that is, if the bag contains 5 pieces, and we want to purchase 12 pieces, it makes sense to consider 1, 2 or 3 bags, but 4 bags obviously too much). - find all possible combination of all the sums and their sums (i.e. for the type Bag
A with the sums 1, 2 and 3 and the type Bag B with the sums 1 and 2 you can try: A * 1 + B * 1 , A * 2 + B * 1 , A * 3 + B * 1 , A * 1 + B * 2 , A * 2 + B * 2 , A * 3 + B * 2 ) - calculate all combinations (this is actually done on the fly), that is, find the total quantity and total price.
- Get the row with the lowest price that is higher or equal to the required value.
This is the whole solution with the provided OP data examples:
(the decision has been changed, a new version is available below!)
-- sample data declare @ReqQty int = 21 declare @Bags table (Code nvarchar(1), Quantity int, Price decimal(10,2)) insert into @Bags select 'X', 16, 25.00 union select 'Y', 8, 18.00 union select 'Z', 4, 7 ; with -- helper table: all possible integer numbers <= @ReqQty Nums (I) as ( select 1 union all select I + 1 from Nums where I < @ReqQty ), -- possible amounts of each kind bag that make sense -- ie with 3-piece bag and 5-piece requirement it -- is worth checking 1 (x3 = 3) or 2 (x2 = 6) bags, but -- 3, 4... would be definitely too much Vars (Code, Amount) as ( select B.Code, Nums.I from @Bags as B inner join Nums on B.Quantity * I - @ReqQty < B.Quantity ), Sums (Expr, Amount, TotalQuantity, TotalPrice) as ( -- take each kind of bag with every amount as recursion root select convert(nvarchar(100), V.Code + '(' + convert(nvarchar(100), Amount) + ')'), Amount, B.Quantity * Amount, convert(decimal(10, 2), B.Price * Amount) from Vars as V inner join @Bags as B on V.Code = B.Code union all -- add different kind of bag to the summary -- 'Sums.Amount >= V.Amount' is to eliminate at least some duplicates select convert(nvarchar(100), Expr + ' + ' + V.Code + '(' + convert(nvarchar(100), V.Amount) + ')'), V.Amount, Sums.TotalQuantity + B.Quantity * V.Amount, convert(decimal(10, 2), Sums.TotalPrice + B.Price * V.Amount) from Vars as V inner join @Bags as B on V.Code = B.Code inner join Sums on (charindex(V.Code, Expr) = 0) and Sums.Amount >= V.Amount ) -- now find lowest price that matches required quantity -- remove 'top 1' to see all combinations select top 1 Expr, TotalQuantity, TotalPrice from Sums where TotalQuantity >= @ReqQty order by TotalPrice asc
For the data of this sample, this is the result:
Expr TotalQuantity TotalPrice Z(2) + X(1) 24 39.00
The solution is definitely not perfect:
- I do not like to use
charindex to eliminate the same packages. - all duplicate combinations should be removed.
- I'm not sure about the effectiveness
but I just didn’t have enough time or skills to come up with smarter ideas. I think it's nice that this is a purely announcement-based solution.
EDIT
I changed the solution a bit to get rid of charindex (and thus get rid of text-based packet identifier dependencies). Unfortunately, I had to add a sum of 0 for each kind of bag that made even more combinations, but didn't seem to have a noticeable effect on performance. Also at the same price a combination with a large number of items is shown. :-)
-- sample data declare @ReqQty int = 21 declare @Bags table (Code nvarchar(1), Quantity int, Price decimal(10,2)) insert into @Bags select 'X', 16, 25.00 union select 'Y', 8, 18.00 union select 'Z', 4, 7.00 ; with -- helper table to apply order to bag types Bags (Code, Quantity, Price, BI) as ( select Code, Quantity, Price, ROW_NUMBER() over (order by Code) from @Bags ), -- helper table: all possible integer numbers <= @ReqQty Nums (I) as ( select 0 union all select I + 1 from Nums where I < @ReqQty ), -- possible amounts of each kind bag that make sense -- ie with 3-piece bag and 5-piece requirement it -- is worth checking 1 (x3 = 3) or 2 (x2 = 6) bags, but -- 3, 4... would be definitely too much Vars (Code, Amount) as ( select B.Code, Nums.I from Bags as B inner join Nums on B.Quantity * I - @ReqQty < B.Quantity ), Sums (Expr, Amount, BI, TotalQuantity, TotalPrice) as ( -- take first kind of bag with every amount as recursion root select convert(nvarchar(100), V.Code + '(' + convert(nvarchar(100), Amount) + ')'), Amount, B.BI, B.Quantity * Amount, convert(decimal(10, 2), B.Price * Amount) from Vars as V inner join Bags as B on V.Code = B.Code where B.BI = 1 union all -- add different kind of bag to the summary select convert(nvarchar(100), Expr + ' + ' + V.Code + '(' + convert(nvarchar(100), V.Amount) + ')'), V.Amount, B.BI, Sums.TotalQuantity + B.Quantity * V.Amount, convert(decimal(10, 2), Sums.TotalPrice + B.Price * V.Amount) from Vars as V inner join Bags as B on V.Code = B.Code -- take next bag kind according to order inner join Sums on B.BI = Sums.BI + 1 and Sums.TotalQuantity + B.Quantity * V.Amount - @ReqQty < B.Quantity ) -- now find lowest price that matches required quantity -- remove 'top 1' to see all combinations select top 1 Expr, TotalQuantity, TotalPrice from Sums where TotalQuantity >= @ReqQty order by TotalPrice asc, TotalQuantity desc, Expr asc