Select All combinations of rows for a*b=total <=X - sql-server

I have a situation about writing a query to find and insert into table B all combinations of rows from table A, where the condition is:
a x b=total from row1
c x d=total from row2 ...etc where count(total)<=X
"a" price of item
"b" quantity of item
Idea is to have all combinations like example
For 100$ dollars i can buy:
2 tshirt, 1 jacket, 1 pants
or
1 tshirt, 2 jacket, 1 pants
...etc
Creating a cursor will help me run the query for each row, but how to split the number in col.quantity in the same time ?

I will first write what I understood,
we would have a table of items, each item would have a price,
we have an amount of money and we want to buy as many as possible
items
we want the items to have the same weight as the two examples
provided "2 tshirt, 1 jacket, 1 pants or 1 tshirt, 2 jacket, 1 pants"
did not specify a solution with one item but tried to use all the
items.
So how to determine the Qty for each item to utilize most of the money that we have.
I think this can be described in a different way to be more clear, like for example:- one person goes in a shop and would like to buy each of the items available but if he has some more money left he want to know what other items he can buy with it. if the items are not a lot and the money is not a lot, this can be easy, but if the items are a lot and the money a lot too, I can see that this may be a problem. so lets find a solution.
Declare #Items Table (
Item varchar(250),Price decimal
)
insert into #Items values
('tshirt',30)
,('jacket',30)
,('pants' ,10)
--,('shoe' ,15) ---extra items for testing
--,('socks',5) ---extra items for testing
Declare #total int=100 -- your X
Declare #ItemsCount int
Declare #flag int
Declare #ItemsSum decimal
Declare #AllItmsQty int
select #ItemsCount=count(*),#ItemsSum=sum(price),#flag=POWER(2,count(*)) From #Items
select #AllItmsQty=#total/cast(#ItemsSum as int)
;with Numbers(n) as (
--generat numbers from 1,2,3,... #flag
select 1 union all
select (n+1) n from Numbers where n<#flag
),ItemsWithQty as (
select *,Price*n [LineTotal] from #Items,Numbers
),Combination as (
select items.*,Numbers.n-1 [CombinationId] from #Items items,Numbers
),CombinationWithSeq as (
select *
,ROW_NUMBER() over (Partition by [CombinationId] order by [CombinationId]) [seq]
from Combination
),CombinationWithSeqQty as (
select *,case when (CombinationId & power(2,seq-1))>0 then 1 else 0 end +#AllItmsQty [qty]
from CombinationWithSeq
),CombinationWithSeqQtySubTotal as (
select *,Price*qty [SubTotal] from CombinationWithSeqQty
)
select
--CombinationId,
sum(subtotal) [Total],
replace(
replace(
STRING_AGG(
case when (Qty=0) then 'NA' else (cast(Qty as varchar(5))+' '+Item)
end
,'+')
,'+NA','')
,'NA+','') [Items]
from CombinationWithSeqQtySubTotal
group by CombinationId
having sum(subtotal)<=#total
The result would be as follow:-
Total Items
===== ===========================
100 2 tshirt+1 jacket+1 pants
100 1 tshirt+2 jacket+1 pants
80 1 tshirt+1 jacket+2 pants
70 1 tshirt+1 jacket+1 pants
if I add the other two items we would get
Total Items
===== ===========================
100 1 tshirt+1 jacket+2 pants+1 shoe+1 socks
95 1 tshirt+1 jacket+1 pants+1 shoe+2 socks
90 1 tshirt+1 jacket+1 pants+1 shoe+1 socks
ok so the query is giving the final result not the table B, that you described to have a x b or item price multiplied by qty and sub total , well we can display that one very easily by filtering witch combination we selected, if we are selecting the first one that would be the nearest to the amount we can change the last part of the query to show table B you need.
),CombinationWithSeqQtySubTotal as (
select *,Price*qty [SubTotal] from CombinationWithSeqQty
),Results as (
select
CombinationId,
sum(subtotal) [Total],
replace(
replace(
STRING_AGG(
case when (Qty=0) then 'NA' else (cast(Qty as varchar(5))+' '+Item)
end
,'+')
,'+NA','')
,'NA+','') [Items]
from CombinationWithSeqQtySubTotal
group by CombinationId
having sum(subtotal)<=#total
--order by [Total] desc
)
select item, price, qty, SubTotal from CombinationWithSeqQtySubTotal t where t.CombinationId in
(select top(1) CombinationId from Results order by [Total] desc)
The result would be as below:-
item price qty SubTotal
===== ===== === =======
tshirt 30 1 30
jacket 30 1 30
pants 10 2 20
shoe 15 1 15
socks 5 1 5
or if we run it with only the items you provided the result would be as below:-
item price qty SubTotal
====== === === =======
tshirt 30 2 60
jacket 30 1 30
pants 10 1 10
if we dont want to use 'STRING_AGG' or we dont have it, we can manage its same function by adding some CTE's that will do the same job, as the 'STRING_AGG' was only combining the results in a (qty + item + comma), so the below solution may help.
Declare #Items Table (Item varchar(250),Price decimal)
insert into #Items values
('tshirt',30)
,('jacket',30)
,('pants' ,10)
--,('shoes' ,15) ---extra items for testing
--,('socks',5) ---extra items for testing
Declare #total int=100 -- your X
Declare #ItemsCount int
Declare #flag int
Declare #ItemsSum decimal
Declare #AllItmsQty int
select #ItemsCount=count(*),#ItemsSum=sum(price),#flag=POWER(2,count(*)) From #Items
select #AllItmsQty=#total/cast(#ItemsSum as int)
;with Numbers(n) as (
--generat numbers from 1,2,3,... #flag
select 1 union all
select (n+1) n from Numbers where n<#flag
),ItemsWithQty as (
select *,Price*n [LineTotal] from #Items,Numbers
),Combination as (
select items.*,Numbers.n-1 [CombinationId] from #Items items,Numbers
),CombinationWithSeq as (
select *,ROW_NUMBER() over (Partition by [CombinationId] order by [CombinationId]) [seq] from Combination
),CombinationWithSeqQty as (
select *,case when (CombinationId & power(2,seq-1))>0 then 1 else 0 end +#AllItmsQty [qty] from CombinationWithSeq
),CombinationWithSeqQtySubTotal as (
select *,Price*qty [SubTotal] from CombinationWithSeqQty
),CombinationWithTotal as (
--to find only the combinations that are less or equal to the Total
select
CombinationId,
sum(subtotal) [Total]
from CombinationWithSeqQtySubTotal
group by CombinationId
having sum(subtotal)<=#total
),DetailAnswer as (
select s.*,t.Total,cast(s.qty as varchar(20))+' ' +s.Item QtyItem from CombinationWithTotal t
inner join CombinationWithSeqQtySubTotal s on s.CombinationId=t.CombinationId
),DetailAnswerFirst as (
select *,cast(QtyItem as varchar(max)) ItemList from DetailAnswer t where t.seq=1
union all
select t.*,cast((t.QtyItem+'+'+x.ItemList) as varchar(max)) ItemList from DetailAnswer t
inner join DetailAnswerFirst x on x.CombinationId=t.CombinationId and x.seq+1=t.seq
)
select CombinationId,Total,ItemList from DetailAnswerFirst where seq=#ItemsCount order by Total desc
--select * from DetailAnswer --remark the above line and unremark this one for the details that you want to go in Table B
if any of the assumptions are wrong or if you need some description I would be happy to help.

Maybe the easiest way to get the possible combinations is via self-joins and joins to numbers.
If you want combinations of 3, then use 3 self-joins.
And 3 joins to a number table or CTE for each joined "Items" table.
The way the ON criteria are used, is to minimize the impact of all that joining.
You could also take the SQL from the COMBOS CTE, and use it to first insert it into a temporary table.
For example:
declare #PriceLimit decimal(10,2) = 100;
WITH COMBOS AS
(
SELECT
i1.id as id1, i2.id as id2, i3.id as id3,
n1.n as n1, n2.n as n2, n3.n as n3,
(n1.n + n2.n + n3.n) AS TotalItems,
(i1.Price * n1.n + i2.Price * n2.n + i3.Price * n3.n) as TotalCost
FROM Items i1
JOIN Items i2 ON i2.id > i1.id AND i2.Price < #PriceLimit
JOIN Items i3 ON i3.id > i2.id AND i3.Price < #PriceLimit
JOIN Nums n1
ON n1.n between 1 and FLOOR(#PriceLimit/i1.Price)
AND (i1.Price * n1.n) < #PriceLimit
JOIN Nums n2
ON n2.n between 1 and FLOOR(#PriceLimit/i2.Price)
AND (i1.Price * n1.n + i2.Price * n2.n) < #PriceLimit
JOIN Nums n3
ON n3.n between 1 and FLOOR(#PriceLimit/i3.Price)
AND (i1.Price * n1.n + i2.Price * n2.n + i3.Price * n3.n) <= #PriceLimit
AND (i1.Price * n1.n + i2.Price * n2.n + i3.Price * (n3.n+1)) > #PriceLimit
WHERE i1.Price < #PriceLimit
)
SELECT
c.TotalItems, c.TotalCost,
CONCAT (c.n1,' ',item1.Name,', ',c.n2,' ',item2.Name,', ',c.n3,' ',item3.Name) AS ItemList
FROM COMBOS c
LEFT JOIN Items item1 ON item1.id = c.id1
LEFT JOIN Items item2 ON item2.id = c.id2
LEFT JOIN Items item3 ON item3.id = c.id3
ORDER BY c.TotalCost desc, c.TotalItems desc, c.id1, c.id2, c.id3;
A test on db<>fiddle here
Test result:
TotalItems | TotalCost | ItemList
---------- | --------- | ---------------------------
7 | 100.00 | 1 pants, 1 tshirt, 5 socks
6 | 100.00 | 1 jacket, 1 tshirt, 4 socks
6 | 100.00 | 1 pants, 2 tshirt, 3 socks
5 | 100.00 | 1 jacket, 1 pants, 3 socks
5 | 100.00 | 1 jacket, 2 tshirt, 2 socks
5 | 100.00 | 1 pants, 3 tshirt, 1 socks
5 | 100.00 | 2 pants, 1 tshirt, 2 socks
3 | 90.00 | 1 jacket, 1 pants, 1 tshirt

Related

Denomination distribution calculation

Some background story: Company A gives out vouchers to winners of a challenge. The SQL that I am currently writing needs to decide the required voucher denomination that sums to the value awarded to a person. I have a table that stores the denominations available for vouchers, depending on the country and currency.
In the example below, a particular person is awarded with €80 worth of vouchers.
The query below displays results of a lookup table for voucher denominations available for a particular country.
SELECT * FROM tblDenominationScheme WHERE CountryCode IN ('AT', 'US')
Result:
No. | CountryCode | VoucherName | VoucherValue
-------------------------------------------------
1 | AT | €50 Shop A | 50
2 | AT | €25 Shop A | 25
3 | AT | €15 Shop A | 15
4 | AT | €10 Shop A | 10
5 | US | $50 Store B | 50
6 | US | $10 Store B | 10
7 | US | $5 Store B | 5
My current SQL is as below to determine the required voucher denominations for €80 voucher:
DECLARE #CountryCode1 VARCHAR(2) = 'AT'
DECLARE #ChallengerID INT = 1172
DECLARE #RoundedAmount1 INT = 80
DECLARE #Vouchers INT
DECLARE #AmountAwarded INT = 0
SET #AmountAwarded = #RoundedAmount1
DROP TABLE IF EXISTS #tempVoucher
CREATE TABLE #tempVoucher
(
CountryCode VARCHAR(2),
ChallengerID INT,
AmountAwarded INT,
Vouchers INT,
)
WHILE (#RoundedAmount1 > 0)
BEGIN
SET #Vouchers = 0
SELECT TOP 1 #Vouchers = VoucherValue FROM tblDenominationScheme WHERE CountryCode = #CountryCode1 AND VoucherValue <= #RoundedAmount1 ORDER BY VoucherValue DESC
IF (#Vouchers > 0)
BEGIN
SET #RoundedAmount1 = #RoundedAmount1 - #Vouchers
END
ELSE
BEGIN
SELECT TOP 1 #Vouchers = VoucherValue FROM tblDenominationScheme WHERE CountryCode = #CountryCode1 ORDER BY VoucherValue
SET #RoundedAmount1 = #RoundedAmount1 - #RoundedAmount1
END
INSERT INTO #tempVoucher VALUES (#CountryCode1,#ChallengerID, #AmountAwarded, #Vouchers)
END
SELECT * FROM #tempVoucher
Result from the SQL above:
No. | CountryCode | ChallengerID | AmountAwarded | Vouchers
--------------------------------------------------------------
1 | AT | 1172 | 80 | 50
2 | AT | 1172 | 80 | 25
3 | AT | 1172 | 80 | 10
NOTE: The value in AmountAwarded column will be the same for all 3 rows. The amount in the Vouchers column for the 3 rows should sum up to 80.
The result above is obviously incorrect, because if you sum up the values in the Vouchers column, it gives you 85, which is 5 more than the AmountAwarded
Expected result (or at least closest):
No. | CountryCode | ChallengerID | AmountAwarded | Vouchers
--------------------------------------------------------------
1 | AT | 1172 | 80 | 50
2 | AT | 1172 | 80 | 10
3 | AT | 1172 | 80 | 10
4 | AT | 1172 | 80 | 10
Anyone able to help?
This might be an expensive query, but gets you different options to deliver up to 7 vouchers to get you the expected result. This, however, will generate a huge amount of reads if the rows increase or the amount of vouchers can be greater.
DECLARE #CountryCode1 VARCHAR(2) = 'AT'
DECLARE #RoundedAmount1 INT = 80;
WITH cteDenominations AS(
SELECT No, VoucherValue
FROM tblDenominationScheme
WHERE CountryCode = #CountryCode1
UNION ALL
SELECT 10000, 0
),
ctePermutations AS(
SELECT a.No AS a_No,
a.VoucherValue AS a_Value,
b.No AS b_No,
b.VoucherValue AS b_Value,
c.No AS c_No,
c.VoucherValue AS c_Value,
d.No AS d_No,
d.VoucherValue AS d_Value,
e.No AS e_No,
e.VoucherValue AS e_Value,
f.No AS f_No,
f.VoucherValue AS f_Value,
g.No AS g_No,
g.VoucherValue AS g_Value,
ROW_NUMBER() OVER(ORDER BY a.No, b.No, c.No, d.No) Permutation
FROM cteDenominations a
JOIN cteDenominations b ON a.VoucherValue >= b.VoucherValue
JOIN cteDenominations c ON b.VoucherValue >= c.VoucherValue
JOIN cteDenominations d ON c.VoucherValue >= d.VoucherValue
JOIN cteDenominations e ON d.VoucherValue >= e.VoucherValue
JOIN cteDenominations f ON e.VoucherValue >= f.VoucherValue
JOIN cteDenominations g ON f.VoucherValue >= g.VoucherValue
WHERE #RoundedAmount1 = a.VoucherValue
+ b.VoucherValue
+ c.VoucherValue
+ d.VoucherValue
+ e.VoucherValue
+ f.VoucherValue
+ g.VoucherValue
)
SELECT Permutation,
u.No,
u.VoucherValue
FROM ctePermutations
CROSS APPLY (VALUES(a_No, a_Value),
(b_No, b_Value),
(c_No, c_Value),
(d_No, d_Value),
(e_No, e_Value),
(f_No, f_Value),
(g_No, g_Value))u(No, VoucherValue)
WHERE VoucherValue > 0
AND Permutation = 1 --Remove this to get all possibilities
;
Looks like you need to solve a equation:
80 = n1*v1 + k2*n2...
where v1,v2 ... are values which you store in database
And you need to find n1, n2 ... , which are in {0, N}
There is no way how to implement it in SQL. Except - over all possible values, but it's not the smarter way.
Also, see this info:
https://math.stackexchange.com/questions/431367/solving-a-first-order-diophantine-equation-with-many-terms
Logic
Find the largest amount (that is less than or equal to starting amount) vouchers of 1 denomination can make.
Subtract this value from starting amount to get remainder,
Find the largest amount (that is less than or equal to remainder) a number of vouchers of 1 smaller denomination can make.
Subtract this value from previous remainder.
Go back to step 3
Features:
Handles multiple best combinations.
Small number of combinations are searched.
On my laptop: 100 runs take about 3 seconds
Notes
Performance may be improved by saving output of VoucherCombinations to a table variable and then using it in subsequent CTEs.
Code:
DECLARE #Vouchers TABLE( CountryCode CHAR( 2 ), VoucherValue DECIMAL( 10, 2 ))
INSERT INTO #Vouchers VALUES( 'AT', 50 ), ( 'AT', 40 ), ( 'AT', 25 ), ( 'AT', 20 ), ( 'AT', 15 ), ( 'AT', 10 ), ( 'US', 50 ), ( 'US', 10 ), ( 'US', 5 );
-- Small number table
-- Limits maximum count of Vouchers of a given denomination.
DECLARE #Numbers TABLE( Num INT )
INSERT INTO #Numbers VALUES( 1 ), ( 2 ), ( 3 ), ( 4 ), ( 5 ), ( 6 ), ( 7 ), ( 8 ), ( 9 ), ( 10 )
DECLARE #TargetAmount DECIMAL( 10, 2 ) = 60;
DECLARE #CountryCode CHAR( 2 ) = 'AT';
;WITH VoucherCombinations
AS (
-- Anchor
SELECT ROW_NUMBER() OVER( ORDER BY VoucherValue DESC ) AS ParentGroupID,
ROW_NUMBER() OVER( ORDER BY VoucherValue DESC ) AS SubGroupID,
1 AS IterationID,
VoucherValue, Num AS VoucherCumulativeCount,
CAST( VoucherValue * Num AS DECIMAL( 10, 2 )) AS TotalDenominationValue,
CAST( #TargetAmount - ( VoucherValue * Num ) AS DECIMAL( 10, 2 )) AS Remainder
FROM #Vouchers
-- Find the largest amount a given Voucher denomination can produce that is less than or equal to #TargetAmount
INNER JOIN #Numbers ON ( VoucherValue * Num ) <= #TargetAmount AND #TargetAmount - ( VoucherValue * Num ) < VoucherValue
WHERE CountryCode = #CountryCode
UNION ALL
-- Recursive query
SELECT SubGroupID,
SubGroupID * 10 + ROW_NUMBER() OVER( ORDER BY V.VoucherValue DESC ) AS SubGroupID,
IterationID + 1,
V.VoucherValue, VoucherCumulativeCount + N.Num AS VoucherCount,
CAST( V.VoucherValue * N.Num AS DECIMAL( 10, 2 )) AS TotalDenominationValue,
CAST( Remainder - ( V.VoucherValue * N.Num ) AS DECIMAL( 10, 2 )) AS Remainder
FROM VoucherCombinations AS VP
-- For each denomination look at the smaller denominations
INNER JOIN #Vouchers AS V ON VP.VoucherValue > V.VoucherValue
INNER JOIN #Numbers AS N ON V.VoucherValue * N.Num <= Remainder AND Remainder - ( V.VoucherValue * N.Num ) < V.VoucherValue
WHERE CountryCode = #CountryCode
),
-- Discard invalid combinations i.e. remainder is not 0
VoucherPoolValid AS(
SELECT *, DENSE_RANK() OVER( ORDER BY VoucherCumulativeCount ASC ) AS BestCombos
FROM VoucherCombinations
WHERE Remainder = 0
),
-- Find best combinations i.e. smallest number of Vouchers; Note: logic supports having more than 1 best combination
VoucherPoolBestCombos AS(
SELECT *, ROW_NUMBER() OVER( ORDER BY BestCombos ASC ) AS ComboID
FROM VoucherPoolValid
WHERE BestCombos = 1
),
-- Return all denominations for each combination
VoucherPoolAllDetails AS(
SELECT *
FROM VoucherPoolBestCombos
UNION ALL
SELECT Parent.*, BestCombos, ComboID
FROM VoucherPoolAllDetails AS Child
INNER JOIN VoucherCombinations AS Parent ON Child.ParentGroupID = Parent.SubGroupID
WHERE Child.SubGroupID <> Child.ParentGroupID
)
SELECT * FROM VoucherPoolAllDetails
ORDER BY ComboID

How to insert "empty" row extracting a month list?

I've this sp, which return a list of data, for each "month" (i.e. each row is a month). Somethings like that:
SELECT
*,
(CAST(t1.NumActivities AS DECIMAL) / t1.NumVisits) * 100 AS PercAccepted,
(CAST(t1.Accepted AS DECIMAL) / t1.Estimated) * 100 AS PercValue
FROM
(SELECT
MONTH(DateVisit) AS Month,
COUNT(*) AS NumVisits,
SUM(CASE WHEN DateActivity is not null THEN 1 ELSE 0 END) AS NumActivities,
SUM(Estimate) AS Estimated,
SUM(CASE WHEN DateActivity is not null THEN Estimate ELSE 0 END) AS Accepted
FROM [dbo].[Activities]
WHERE
DateVisit IS NOT NULL
AND (#year IS NULL OR YEAR(DateVisit) = #year)
AND (#clinicID IS NULL OR ClinicID = #clinicID)
GROUP BY MONTH(DateVisit)) t1
This is a result:
Month NumVisits NumActivities Estimated Accepted PercAccepted PercValue
1 5 1 13770.00 2520.00 20.00000000000 18.30065359477124
2 2 2 7900.00 7900.00 100.00000000000 100.00000000000000
3 1 0 2730.00 0.00 0.00000000000 0.00000000000000
8 1 1 3000.00 3000.00 100.00000000000 100.00000000000000
But as you can see, I could "miss" some Month (for example, here April "4" is missed).
Is it possible to insert, for the missing month/row, an empty (0) record? Such as:
Month NumVisits NumActivities Estimated Accepted PercAccepted PercValue
1 5 1 13770.00 2520.00 20.00000000000 18.30065359477124
2 2 2 7900.00 7900.00 100.00000000000 100.00000000000000
3 1 0 2730.00 0.00 0.00000000000 0.00000000000000
4 0 0 0 0 0 0
...
Here is a example with sample data:
CREATE TABLE #Report
(
Id INT,
Name nvarchar(max),
Percentage float
)
INSERT INTO #Report VALUES (1,'ONE',2.01)
INSERT INTO #Report VALUES (2,'TWO',3.01)
INSERT INTO #Report VALUES (5,'Five',5.01)
;WITH months(Month) AS
(
SELECT 1
UNION ALL
SELECT Month+1
FROM months
WHERE Month < 12
)
SELECT *
INTO #AllMonthsNumber
from months;
Your select query:
The left join will gives you the NULL for other months so just use ISNULL('ColumnName','String_to_replace')
\/\/\/\/
SELECT Month, ISNULL(Name,0), ISNULL(Percentage,0)
FROM AllMonthsNumber A
LEFT JOIN #Report B
ON A.Month = B.Id
EDIT:
Yes you can do it without creating AllMonthNumber Table:
You can use master..spt_values (found here) system table which contains the numbers so just with some where condition.
SELECT Number as Month, ISNULL(B.Name,0), ISNULL(Percentage,0)
FROM master..spt_values A
LEFT JOIN #Report B ON A.Number = B.Id
WHERE Type = 'P' AND number BETWEEN 1 AND 12

Performance issue with CTE SQL Server query

We have a table with a parent child relationship, that represents a deep tree structure.
We are using a view with a CTE to query the data but the performance is poor (see code and execution plan below).
Is there any way we can improve the performance?
WITH cte (ParentJobTypeId, Id) AS
(
SELECT
Id, Id
FROM
dbo.JobTypes
UNION ALL
SELECT
e.Id, cte.Id
FROM
cte
INNER JOIN
dbo.JobTypes AS e ON e.ParentJobTypeId = cte.ParentJobTypeId
)
SELECT
ISNULL(Id, 0) AS ParentJobTypeId,
ISNULL(ParentJobTypeId, 0) AS Id
FROM
cte
A quick example of using the range keys. As I mentioned before, hierarchies were 127K points and some sections where 15 levels deep
The cte Builds, let's assume the hier results will be will be stored in a table (indexed as well)
Declare #Table table(ID int,ParentID int,[Status] varchar(50))
Insert #Table values
(1,101,'Pending'),
(2,101,'Complete'),
(3,101,'Complete'),
(4,102,'Complete'),
(101,null,null),
(102,null,null)
;With cteOH (ID,ParentID,Lvl,Seq)
as (
Select ID,ParentID,Lvl=1,cast(Format(ID,'000000') + '/' as varchar(500)) from #Table where ParentID is null
Union All
Select h.ID,h.ParentID,cteOH.Lvl+1,Seq=cast(cteOH.Seq + Format(h.ID,'000000') + '/' as varchar(500)) From #Table h INNER JOIN cteOH ON h.ParentID = cteOH.ID
),
cteR1 as (Select ID,Seq,R1=Row_Number() over (Order by Seq) From cteOH),
cteR2 as (Select A.ID,R2 = max(B.R1) From cteOH A Join cteR1 B on (B.Seq Like A.Seq+'%') Group By A.ID)
Select B.R1
,C.R2
,A.Lvl
,A.ID
,A.ParentID
Into #TempHier
From cteOH A
Join cteR1 B on (A.ID=B.ID)
Join cteR2 C on (A.ID=C.ID)
Select * from #TempHier
Select H.R1
,H.R2
,H.Lvl
,H.ID
,H.ParentID
,Total = count(*)
,Complete = sum(case when D.Status = 'Complete' then 1 else 0 end)
,Pending = sum(case when D.Status = 'Pending' then 1 else 0 end)
,PctCmpl = format(sum(case when D.Status = 'Complete' then 1.0 else 0.0 end)/count(*),'##0.00%')
From #TempHier H
Join (Select _R1=B.R1,A.* From #Table A Join #TempHier B on A.ID=B.ID) D on D._R1 between H.R1 and H.R2
Group By H.R1
,H.R2
,H.Lvl
,H.ID
,H.ParentID
Order By 1
Returns the hier in a #Temp table for now. Notice the R1 and R2, I call these the range keys. Data (without recursion) can be selected and aggregated via these keys
R1 R2 Lvl ID ParentID
1 4 1 101 NULL
2 2 2 1 101
3 3 2 2 101
4 4 2 3 101
5 6 1 102 NULL
6 6 2 4 102
VERY SIMPLE EXAMPLE: Illustrates the rolling the data up the hier.
R1 R2 Lvl ID ParentID Total Complete Pending PctCmpl
1 4 1 101 NULL 4 2 1 50.00%
2 2 2 1 101 1 0 1 0.00%
3 3 2 2 101 1 1 0 100.00%
4 4 2 3 101 1 1 0 100.00%
5 6 1 102 NULL 2 1 0 50.00%
6 6 2 4 102 1 1 0 100.00%
The real beauty of the the range keys, is if you know an ID, you know where it exists (all descendants and ancestors).

Get top X percentage based on cumulative sum

My table looks like this:
ID | ItemID | ItemQualityID | Amount | UnitPrice
My goal is to find the top x% rows for each ItemID + ItemQualityID pair based on Amount cumulative sum and ordered by UnitPrice.
For example:
ID | ItemID | ItemQualityID | Amount | UnitPrice
1 1 1 18 2
2 1 1 1 1
3 1 1 1 1
4 2 1 18 2
5 2 1 1 1
6 2 1 1 1
7 1 1 1 3
and I want the top 10%, then the resulting table should contain row #2, 3, 5, 6. Since the total amount for ItemID 1 and 2 are 21 and 20 respectively, thus 10% would be 2 items each. If I want the top 20%, the resulting table should still be the same since if I include row 1 and 4 it would make it 100%. Row #7 has unit price > row #1 so if row #1 is not included then row #7 shouldn't be included as well.
Ideally I want the table with all the filtered rows for some other calculations but I will be happy even if I can only get the sum of Amount * UnitPrice of the filtered table. Something like
ItemID | ItemQualityID | Sum
1 1 2
2 1 2
for the above example.
You can use SUM OVER :
DECLARE #percent DECIMAL(5, 2) = .1
;WITH CteSum AS(
SELECT *,
TotalSum = SUM(Amount) OVER(PARTITION BY ItemID, ItemQualityID),
CumSum = SUM(Amount) OVER(PARTITION BY ItemID, ItemQualityID ORDER BY UnitPrice, ID)
FROM tbl
)
SELECT
ItemID,
ItemQualityID,
[Sum] = SUM(Amount * UnitPrice)
FROM CteSum
WHERE CumSum <= #percent * TotalSum
GROUP BY ItemID, ItemQualityID
ONLINE DEMO

SQL How to create output with sub totals

I'm new to T-SQL and need help converting an excel report to a run on SQL. I have a SQL table that records all the daily inventory transactions (in/out) from each stockroom. I need to create a report that list the current inventory levels for each product in each location and the qty in each place as follows. In other words, the current inventory levels of each place.
I also need help on how to insert the Preferred Out Report (below) into SQL Server as a view so I can run this each month over and over again.
Thanks in Advance!
Inventory Log table:
PubID QTY LocationID Transaction
1 10 1 Add
1 20 2 Add
1 30 3 Add
1 5 1 Sold
1 10 2 Sold
1 5 3 Sold
2 10 1 Add
2 10 2 Add
2 5 2 Sold
2 8 2 Sold
1 20 1 Add
1 20 2 Add
2 2 2 Sold
Preferred Output Table:
PubID Local_1 Local_2 Local_3 Total
1 25 30 25 80
2 5 0 0 5
Total 30 30 25 85
I see a lot of close examples here but most just add the value while I need to subtract the Sold inventory from the Added stock to get my totals in each column.
The row totals and column totals on the right and bottom are pluses but not needed if it's easier without.
THANKS!
If this was about aggregation without pivoting, you could use a CASE expression, like this:
SELECT
...
Local_1 = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END),
...
FROM ...
GROUP BY ...
However, in the PIVOT clause, the argument of the aggregate function must be just a column reference, not an expression. You can work around that by transforming the original dataset so that QTY is either positive or negative, depending on Transaction:
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
The above query will give you a result set like this:
PubID QTY LocationID
----- --- ----------
1 10 1
1 20 2
1 30 3
1 -5 1
1 -10 2
1 -5 3
2 10 1
2 10 2
2 -5 2
2 -8 2
1 20 1
1 20 2
2 -2 2
which is now easy to pivot:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1 = [1],
Local_2 = [2],
Local_3 = [3]
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
Note that you could actually prepare the names Local_1, Local_2, Local_3 beforehand and avoid renaming them in the main SELECT. Assuming they are formed by appending the LocationID value to the string Local_, here's an example of what I mean:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
Name = 'Local_' + CAST(LocationID AS varchar(10))
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1,
Local_2,
Local_3
FROM prepared
PIVOT
(
SUM(QTY)
FOR Name IN (Local_1, Local_2, Local_3)
) AS p
;
You will see, however, that in this solution renaming will be needed at some point anyway, so I'll use the previous version in my further explanation.
Now, adding the totals to the pivot results as in your desired output may seem a little tricky. Obviously, the column could be calculated simply as the sum of all the Local_* columns, which might actually not be too bad with a small number of locations:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1 = [1],
Local_2 = [2],
Local_3 = [3]
Total = COALESCE([1], 0)
+ COALESCE([2], 0)
+ COALESCE([3], 0)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
(COALESCE is needed because some results may be NULL.)
But there's an alternative to that, where you don't have to list all the locations explicitly one extra time. You could return the totals per PubID alongside the details in the prepared dataset using SUM() OVER (...), like this:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID,
Total = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
OVER (PARTITION BY PubID)
FROM dbo.InventoryLog
)
…
or like this, if you wish to avoid repetition of the CASE expression:
WITH prepared AS (
SELECT
t.PubID,
QTY = x.AdjustedQTY,
t.LocationID,
Total = SUM(x.AdjustedQTY) OVER (PARTITION BY t.PubID)
FROM dbo.InventoryLog AS t
CROSS APPLY (
SELECT CASE t.[Transaction] WHEN 'Add' THEN t.QTY ELSE -t.QTY END
) AS x (AdjustedQTY)
)
…
Then you would just include the Total column into the main SELECT clause along with the pivoted results and PubID:
…
SELECT
PubID,
Local_1,
Local_2,
Local_3,
Total
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
That would be the total column for you. As for the row, it is actually easy to add it when you are acquainted with the ROLLUP() grouping function:
…
SELECT
PubID,
Local_1 = SUM([1]),
Local_2 = SUM([2]),
Local_3 = SUM([3]),
Total = SUM(Total)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
GROUP BY ROLLUP(PubID)
;
The total row will have NULL in the PubID column, so you'll again need COALESCE to put the word Total instead (only if you want to return it in SQL; alternatively you could substitute it in the calling application):
…
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
…
And that would be all. To sum it up, here is a complete query:
WITH prepared AS (
SELECT
PubID,
QTY = x.AdjustedQTY,
t.LocationID,
Total = SUM(x.AdjustedQTY) OVER (PARTITION BY t.PubID)
FROM dbo.InventoryLog AS t
CROSS APPLY (
SELECT CASE t.[Transaction] WHEN 'Add' THEN t.QTY ELSE -t.QTY END
) AS x (AdjustedQTY)
)
SELECT
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
Local_1 = SUM([1]),
Local_2 = SUM([2]),
Local_3 = SUM([3]),
Total = SUM(Total)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
GROUP BY ROLLUP(PubID)
;
As a final touch to it, you may want to apply COALESCE to the SUMs as well, to avoid returning NULLs in your data (if that is necessary).
The query below does what you need. I might have had one extra group by that could be combined into 1 but you get the idea.
DECLARE #InventoryLog TABLE
(
PubId INT,
Qty INT,
LocationId INT,
[Transaction] Varchar(4)
)
DECLARE #LocationTable TABLE
(
Id INT,
Name VarChar(10)
)
INSERT INTO #LocationTable
VALUES
(1, 'LOC_1'),
(2, 'LOC_2'),
(3, 'LOC_3')
INSERT INTO #InventoryLog
VALUES
(1 , 10, 1 , 'Add'),
(1 , 20, 2 , 'Add'),
(1 , 30, 3 , 'Add'),
(1 , 5 , 1 , 'Sold'),
(1 , 10, 2 , 'Sold'),
(1 , 5 , 3 , 'Sold'),
(2 , 10, 1 , 'Add'),
(2 , 10, 2 , 'Add'),
(2 , 5 , 2 , 'Sold'),
(2 , 8 , 2 , 'Sold'),
(1 , 20, 1 , 'Add'),
(1 , 20, 2 , 'Add'),
(2 , 2 , 2 , 'Sold')
SELECT PubId,
lT.Name LocationName,
CASE
WHEN [Transaction] ='Add' Then Qty
WHEN [Transaction] ='Sold' Then -Qty
END as Quantity
INTO #TempInventoryTable
FROM #InventoryLog iL
INNER JOIN #LocationTable lT on iL.LocationId = lT.Id
SELECT * INTO #AlmostThere
FROM
(
SELECT PubId,
ISNULL(LOC_1,0) LOC_1,
ISNULL(LOC_2,0) LOC_2,
ISNULL(LOC_3,0) LOC_3,
SUM(ISNULL(LOC_1,0) + ISNULL(LOC_2,0) + ISNULL(LOC_3,0)) AS TOTAL
FROM #TempInventoryTable s
PIVOT
(
SUM(Quantity)
FOR LocationName in (LOC_1,LOC_2,LOC_3)
) as b
GROUP BY PubId, LOC_1, LOC_2, LOC_3
) b
SELECT CAST(PubId as VARCHAR(10))PubId,
LOC_1,
LOC_2,
LOC_3,
TOTAL
FROM #AlmostThere
UNION
SELECT ISNULL(CAST(PubId AS VARCHAR(10)),'TOTAL') PubId,
[LOC_1]= SUM(LOC_1),
[LOC_2]= SUM(LOC_2),
[LOC_3]= SUM(LOC_3),
[TOTAL]= SUM(TOTAL)
FROM #AlmostThere
GROUP BY ROLLUP(PubId)
DROP TABLE #TempInventoryTable
DROP TABLE #AlmostThere
PubId LOC_1 LOC_2 LOC_3 TOTAL
1 25 30 25 80
2 10 -5 0 5
TOTAL 35 25 25 85
Sql Fiddle
Here is another approach: aggregate the data before pivoting, then pivot the aggregated results.
Compared to my other suggestion, this method is much simpler syntactically, which may also make it easier to understand and maintain.
All the aggregation is done with the help of the CUBE() grouping function. The basic query would be this:
SELECT
PubID,
LocationID,
QTY = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
FROM dbo.InventoryLog
GROUP BY CUBE(PubID, LocationID)
You can see the same CASE expression as in my other answer, only this time it can be directly used as the argument of SUM.
Using aggregation by CUBE gives us not only the totals by (PubID, LocationID), but also by PubID and LocationID separately, as well as the grand total. This is the result of the query for the example in your question:
PubID LocationID QTY
----- ---------- ---
1 1 35
2 1 10
NULL 1 45
1 2 50
2 2 25
NULL 2 75
1 3 35
NULL 3 35
NULL NULL 155
1 NULL 120
2 NULL 35
Rows with NULLs in LocationID are row totals in the final result set, and those with NULLs in PubID are column totals. The row with NULLs in both columns is the grand total.
Before we can proceed with the pivoting, we need to prepare column names for the pivoted results. If the names are supposed to be derived from the values of LocationID, the following declaration will replace LocationID in the original query's SELECT clause:
Location = COALESCE('Local_' + CAST(LocationID AS varchar(10)), 'Total')
We can also substitute 'Total' for the NULLs in PubID at this same stage, so this will replace PubID in the SELECT clause:
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total')
Now the results will look like this:
PubID LocationID QTY
----- ---------- ---
1 Local_1 35
2 Local_1 10
Total Local_1 45
1 Local_2 50
2 Local_2 25
Total Local_2 75
1 Local_3 35
Total Local_3 35
Total Total 155
1 Total 120
2 Total 35
and at this point everything is ready to apply PIVOT. This query transforms the above result set according to the desired format:
WITH aggregated AS (
SELECT
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
Location = COALESCE('Local_' + CAST(LocationID AS varchar(10)), 'Total'),
QTY = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
FROM dbo.InventoryLog
GROUP BY CUBE(PubID, LocationID)
)
SELECT
PubID,
Local_1,
Local_2,
Local_3,
Total
FROM aggregated
PIVOT (
MAX(QTY)
FOR Location IN (Local_1, Local_2, Local_3, Total)
) AS p
;
This query will return NULLs for missing combinations of (PubID, LocationID). If you want to return 0 instead, apply COALESCE to the result of SUM in the definition of aggregated.

Resources