SQL Server calculate running total value with query - sql-server

I Use SQL Server 2012 and have a table like below:
DECLARE #T TABLE(Id INT, [Type] CHAR(1), Quantity INT, Price MONEY, UnitPrice AS (Price/Quantity))
INSERT INTO #T VALUES
(1, 'I', 30, 1500),
(2, 'O', 5, NULL),
(3, 'O', 20, NULL),
(4, 'O', 2, NULL),
(5, 'I', 10, 2500),
(6, 'I', 8, 1000),
(7, 'O', 3, NULL),
(8, 'O', 10, NULL),
(9, 'I', 12, 3600)
In my table I have a Type Column With Values ('I' and 'O') I have unit price for 'I' Type Record and 'O' Type Record used last 'I' Type Record Value I want to calculate RunningTotalPrice (Sum of Quantity*UnitPrice of each rows).
Following code calculate RunningTotalQuantity:
SELECT *,
SUM(CASE WHEN [Type] = 'I' Then Quantity ELSE -Quantity END)OVER (ORDER BY Id) AS QuantityRunningTotal
FROM #T
and Results of this query is:
Id Type Quantity Price UnitPrice QuantityRunningTotal
1 I 30 1500/00 50/00 30
2 O 5 NULL NULL 25
3 O 20 NULL NULL 5
4 O 2 NULL NULL 3
5 I 10 2500/00 250/00 13
6 I 8 1000/00 125/00 21
7 O 3 NULL NULL 18
8 O 10 NULL NULL 8
9 I 12 3600/00 300/00 20
I want to have following Result
Id Type Quantity Price UnitPrice QuantityRunningTotal Price RunningTotalPrice
1 I 30 1500/00 50/00 30 1500/00 1500/00
2 O 5 NULL 50/00 25 250/00 1250/00
3 O 20 NULL 50/00 5 1000/00 250/00
4 O 2 NULL 50/00 3 100/00 150/00
5 I 10 2500/00 250/00 13 2500/00 2650/00
6 I 8 1000/00 125/00 21 1000/00 3650/00
7 O 3 NULL 125/00 18 375/00 3275/00
8 O 10 NULL 125/00 8 1250/00 2025/00
9 I 12 3600/00 300/00 20 3600/00 5625/00
In this result Null Unitprice Column valued with last exists unitprice in before records.
and Calculate Price ( Quantity * UnitPrice) and The Calculate Running Total Of Price.

Unfortunately LEAD and LAG functions can't be used to the last not NULL value, so you would need to use OUTER APPLY to get the previous UnitPrice to use in rows where the type is 'O':
SELECT t.ID,
t.[Type],
t.Quantity,
t.Price,
t.UnitPrice,
SUM(CASE WHEN t.[Type] = 'I' THEN t.Quantity ELSE -t.Quantity END) OVER (ORDER BY t.Id) AS QuantityRunningTotal,
CASE WHEN t.[Type] = 'I' THEN t.Price ELSE t.Quantity * p.UnitPrice END AS Price2,
SUM(CASE WHEN t.[Type] = 'I' THEN t.Price ELSE -t.Quantity * p.UnitPrice END)OVER (ORDER BY t.Id) AS QuantityRunningTotal
FROM #T AS t
OUTER APPLY
( SELECT TOP 1 t2.UnitPrice
FROM #T AS t2
WHERE t2.ID < t.ID
AND t2.UnitPrice IS NOT NULL
ORDER BY t2.ID DESC
) AS p;

Related

Running total in SQL Server based on condition [duplicate]

This question already has an answer here:
SQL Server - Cumulative Sum that resets when 0 is encountered
(1 answer)
Closed 3 years ago.
I am creating a running total for specific group in a sequence. In between a sequence zero value occurs for which I have to start the running total from the zero record
select
Sno,
Group,
Value,
sum(Value) over(partition by Group order by Sno) Cum_Value
from
Table
Output:
Sno Group Value CumValue
-------------------------------
1 A 5 5
2 A 10 15
3 A 25 40
4 A 0 40
5 A 10 50
6 A 5 55
7 A 0 55
7 A 20 75
Sno Group Value CumValue
------------------------------
1 A 5 5
2 A 10 15
3 A 25 40
4 A 0 0--> zero occurs [starts running total again]
5 A 10 10
6 A 5 15
7 A 0 0--> zero occurs [starts running total again]
7 A 20 20
You may try with the following approach:
Input:
CREATE TABLE #Data (
Sno int,
[Group] varchar(1),
[Value] int
)
INSERT INTO #Data
(Sno, [Group], [Value])
VALUES
(1, 'A', 5),
(2, 'A', 10),
(3, 'A', 25),
(4, 'A', 0),
(5, 'A', 10),
(6, 'A', 5),
(7, 'A', 0),
(8, 'A', 20)
Statement:
SELECT
Sno,
[Group],
[Value],
Changed,
SUM([Value]) OVER (PARTITION BY Changed ORDER BY Sno) AS Cum_Value
FROM
(
SELECT
Sno,
[Group],
[Value],
SUM (CASE
WHEN [Value] = 0 THEN 1
ELSE 0
END) OVER (PARTITION BY [Group] ORDER BY Sno) AS Changed
FROM #Data
) t
Output:
Sno Group Value Cum_Value
1 A 5 5
2 A 10 15
3 A 25 40
4 A 0 0
5 A 10 10
6 A 5 15
7 A 0 0
8 A 20 20

Recursive Select using T-SQL CTE

I have a recursive select problem which I can't get to work, it feels like almost there but not necessarily.
I have two tables where:
Table 1 - Product List
List ProductCode SortNumber
------------------------------------
1 A 1
1 B 2
1 F 3
1 G 4
1 K 5
2 C 1
2 A 2
3 B 1
3 K 2
3 G 3
Table 2 - Product Price
ProductType Client ProductCode Price
---------------------------------------------------
Type 1 1 A 100
Type 1 1 A 150
Type 1 1 B 200
Type 1 1 B 120
Type 1 1 F 150
Type 2 1 A 200
Type 2 1 A 300
Type 2 1 B 300
Type 2 1 F 400
Type 2 1 G 125
Type 2 1 G 75
Type 1 2 A 190
Type 1 2 A 130
Type 1 2 A 200
Type 1 2 B 270
Type 1 2 B 180
Type 1 2 F 130
Type 2 2 A 210
Type 2 2 A 100
Type 2 2 B 350
Type 2 2 F 200
Type 2 2 G 175
Type 2 2 G 95
Type 2 2 K 65
What I am trying to achieve is when I request for Product List of 1, it will get all the max prices associated with the product code. If there's no price for Product Type 1, it will check for the max prices for Product Type 2. If there's no corresponding Type 2 price, then value is 0.
So, the desired output is that when I request for price list 1 for all clients (say here for Clients 1 & 2), the desired output should be
Client ProductCode SortNumber PriceToDisplay
---------------------------------------------------
1 A 1 150
1 B 2 200
1 F 3 150
1 G 4 125 (No Type 1, so we get type 2 price)
1 K 5 0 (No Type 1, 2 Price for Product K)
2 A 1 200
2 B 2 270
2 F 3 130
2 G 4 175
2 K 5 65
Tried Solution: CTE Approach
Since I was having the impression that this might be a recursive select, I have thought of using CTE. I have this query -
(note the commented code - -- And pp1.ProductCode = pp2.ProductCode)
Declare #List int = 1
;With ClientCTE As (
Select Distinct Client From ClientsTable
),
RecursiveSelect As (
Select p1.Client
, l.ProductCode
, l.SortOrder
, p1.Price As P1Price
, p2.Price As P2Price
, Case when p1.Price Is Null Then Case When p2.Price Is Null Then 0 Else p2.Price End
Else p1.Price End As PriceToDisplay
From ProductList l
Left Join (
Select Distinct pp.Client, pp.ProductCode, Max(pp.Price) As ItemPrice From ProductPrice pp
Left Join ClientCTE c On c.Client = pp.Client
Where pp.ProductType = 1
Group By pp.Client, pp.ProductCode) p1 On p1.ProductCode = l.ProductCode
Left Join (
Select Distinct pp.Client, pp.ProductCode, Max(pp.Price) As ItemPrice From ProductPrice pp
Left Join ClientCTE c On c.Client = pp.Client
Where pp.ProductType = 2
Group By pp.Client, pp.ProductCode) p2 On p2.Client = p1.Client
Where pp1.Client = pp2.Client
-- And pp1.ProductCode = pp2.ProductCode **this commented code**
And l.List = 1
)
Select Distinct Client, ProductCode, SortOrder, Max(P1Price), Max(P2Price)
From RecursiveSelect
Group By Client, ProductCode, SortOrder
Findings - CTE
If the code is commented, it will:
Get the prices correctly
However, since there is no price for Product Code G and K for Type 1 in Product Price table, it will not display correctly
Results
Client ProductCode SortNumber PriceToDisplay
---------------------------------------------------
1 A 1 150
1 B 2 200
1 F 3 150
(missing G and K product codes)
2 A 1 200
2 B 2 270
2 F 3 130
2 G 4 175
2 K 5 65
If the code is not commented, it will:
All product codes will appear but get the prices wrong
The price that's being displayed is the maximum price in Price1 table, regardless of client hence
Results
Client ProductCode SortNumber PriceToDisplay
---------------------------------------------------
1 A 1 WRONG PRICE
1 B 2 WRONG PRICE
1 F 3 WRONG PRICE
1 G 4 WRONG PRICE
1 K 5 WRONG PRICE
2 A 1 WRONG PRICE
2 B 2 WRONG PRICE
2 F 3 WRONG PRICE
2 G 4 WRONG PRICE
2 K 5 WRONG PRICE
I guess there's just one tweak that I have to do on my query but I can't pinpoint exactly where. The uncommented code returns the right values but why does it not return the missing left table values considering it is a left join?
based on requirements , here's what I would do.
subquery or view e.g. vw_product_max_price:
select producttype, client, productcode, max(price) as maxprice group by producttype, client, productcode
query to request product list
select MP.client, MP.productcode, PL.sortnumber,
case MP.maxprice
when NULL
then 0
else MP.maxprice
end as PriceToDisplay
from productlist PL left outer join vw_product_max_price MP on PL.productcode=MP.productcode
where MP.client = ? and MP.productcode = ?
I havent tested this.. but the idea is to have a list of maximum prices for each product code first.
You can use OUTER APPLY to get the max price, you don't really need a recursive CTE
select c.Client, l.ProductCode, l.SortNumber, Price = isnull(p.Price, 0)
from ProductList l
cross join ClientCTE c
outer apply
(
-- Top 1 to return only one row per client + Pproduct Code
select top 1 p.Price
from ProductPrice p
where p.Client = c.Client
and p.ProductCode = l.ProductCode
-- Type 1 will comes before Type 2, and higher price first
-- If there isn't any rows for Type 1, Type 2 will be selected
-- no rows return if no rows are found
order by p.ProductType, p.Price desc
) p
where l.List = 1
order by Client, ProductCode
You can do this with join and group by
declare #PL table (client int, ProductCode char(1), Sort int);
insert into #pl values
(1, 'A', 1),
(1, 'B', 2),
(1, 'F', 3),
(1, 'G', 4),
(1, 'K', 5),
(2, 'C', 1),
(2, 'A', 2),
(3, 'B', 1),
(3, 'K', 2),
(3, 'G', 3);
--select * from #PL;
declare #PP table (ProductType varchar(10), Client int, ProductCode char(1), Price int);
insert into #PP values
('Type 1', 1, 'A', 100),
('Type 1', 1, 'A', 150),
('Type 1', 1, 'B', 200),
('Type 1', 1, 'B', 120),
('Type 1', 1, 'F', 150),
('Type 2', 1, 'A', 200),
('Type 2', 1, 'A', 300),
('Type 2', 1, 'B', 300),
('Type 2', 1, 'F', 400),
('Type 2', 1, 'G', 125),
('Type 2', 1, 'G', 75),
('Type 1', 2, 'A', 190),
('Type 1', 2, 'A', 130),
('Type 1', 2, 'A', 200),
('Type 1', 2, 'B', 270),
('Type 1', 2, 'B', 180),
('Type 1', 2, 'F', 130),
('Type 2', 2, 'A', 210),
('Type 2', 2, 'A', 100),
('Type 2', 2, 'B', 350),
('Type 2', 2, 'F', 200),
('Type 2', 2, 'G', 175),
('Type 2', 2, 'G', 95),
('Type 2', 2, 'K', 65);
--select * from #pp;
select pl.client, pl.ProductCode
--, pp1.Price as Price1, pp2.Price as Price2
, max(isnull(pp1.Price, isnull(pp2.Price, 0))) as Price12
from #PL pl
left join #PP pp1
on pp1.ProductCode = pl.ProductCode
and pp1.Client = pl.client
and pp1.ProductType = 'Type 1'
left join #PP pp2
on pp2.ProductCode = pl.ProductCode
and pp2.Client = pl.client
and pp2.ProductType = 'Type 2'
where pl.client in (1, 2)
group by pl.client, pl.ProductCode
order by pl.client, pl.ProductCode;
So I sit with a friend and worked on this. The solution that worked (tweaked from original db table name and columns) is as follows:
Declare #List int = 1
;With ClientCTE (BuyGroup, CustomerPriceGroup, ProductListHeaderId) As (
Select Distinct Client From ClientsTable
),
RecursiveSelect As (
Select x.Client, x.ProductCode
, Case When Max(Type1Price) Is Null Then
Case When Max(Type2Price) Is Null Then 0
Else Max(Type2Price) End
Else Max(Type1Price) End
As PriceToDisplay
From
(Select h.Client, h.ProductCode,
Case When h.ProductType = 'Type1'
Then
Max(h.UnitPrice)
Else
Null
End As Type1Price
, Case When h.ProductType = 'Type2'
Then
Max(h.UnitPrice)
Else
Null
End As Type2Price
, h.BuyGroup, h.ConditionScale
From ProductPrice h
Inner Join ClientCTE c On c.Client = h.Client
Where
h.ProductType In ('Type1', 'Type2')
And h.ProductCode In (Select ProductCode From ProductList where List = #List)
Group by h.Client, h.ProductCode
) x
Group by Client, ProductCode
)
select * from RecursiveSelect
We both figured out that on the original query, the two left joins differ only by a single criteria (which is ProductType). Once we got the raw data from the query, then we grouped and selected the price that will be displayed.
The reason for the CTE is that the original intention of the query is to get the prices for N customers. That's why the anchor query gets the list of clients and then passed on the second one. Moreover, if you would be working on X number of lists as well, you can include that in the anchor query and replace "#List" with the column on the first one.

T-SQL - Filling in the gaps in running balance

I am working on a Data Warehouse project and the client provides daily sales data. On-hand quantities are provided in most lines but are often missing. I need help on how to fill those missing values based on prior OH and sales information.
Here's a sample data:
Line# Store Item OnHand SalesUnits DateKey
-----------------------------------------------
1 001 A 100 20 1
2 001 A 80 10 2
3 001 A null 30 3 --[OH updated with 70 (80-10)]
4 001 A null 5 4 --[OH updated with 40 (70-30)]
5 001 A 150 10 5 --[OH untouched]
6 001 B null 4 1 --[OH untouched - new item]
7 001 B 80 12 2
8 001 B null 10 3 --[OH updated with 68 (80-12]
Lines 1 and 2 are not to be updated because OnHand quantities exist.
Lines 3 and 4 are to be updated based on their preceding rows.
Line 5 is to be left untouched because OnHand is provided.
Line 6 is to be left untouched because it is the first row for Item B
Is there a way I can do this in a set operation? I know I can do it easily using a fast_forward cursor but it will take a long time (15M+ rows).
Thanks for your help!
Test data:
declare #t table(
Line# int, Store char(3), Item char, OnHand int, SalesUnits int, DateKey int
)
insert #t values
(1, '001', 'A', 100, 20, 1),
(2, '001', 'A', 80 , 10, 2),
(3, '001', 'A', null, 30, 3),
(4, '001', 'A', null, 5, 4),
(5, '001', 'A', 150, 10, 5),
(6, '001', 'B', null, 4, 1),
(7, '001', 'B', null, 4, 2),
(8, '001', 'B', 80, 12, 3),
(9, '001', 'B', null, 10, 4)
Script to populate not using cursor:
;with a as
(
select Line#, Store, Item, OnHand, SalesUnits, DateKey, 1 correctdata from #t where DateKey = 1
union all
select t.Line#, t.Store, t.Item, coalesce(t.OnHand, a.onhand - a.salesunits), t.SalesUnits, t.DateKey, t.OnHand from #t t
join a on a.DateKey = t.datekey - 1 and a.item = t.item and a.store = t.store
)
update t
set OnHand = a.onhand
from #t t join a on a.line# = t.line#
where a.correctdata is null
Script to populate using cursor:
declare #datekey int, #store int, #item char, #Onhand int,
#calculatedonhand int, #salesunits int, #laststore int, #lastitem char
DECLARE sales_cursor
CURSOR FOR
SELECT datekey+1, store, item, OnHand -SalesUnits, salesunits
FROM #t sales
order by store, item, datekey
OPEN sales_cursor;
FETCH NEXT FROM sales_cursor
INTO #datekey, #store, #item, #Onhand, #salesunits
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #calculatedonhand = case when #laststore = #store and #lastitem = #item
then coalesce(#onhand, #calculatedonhand - #salesunits) else null end
,#laststore = #store, #lastitem = #item
UPDATE s
SET onhand=#calculatedonhand
FROM #t s
WHERE datekey = #datekey and #store = store and #item = item
and onhand is null and #calculatedonhand is not null
FETCH NEXT FROM sales_cursor
INTO #datekey, #store, #item, #Onhand, #salesunits
END
CLOSE sales_cursor;
DEALLOCATE sales_cursor;
I recommand you use the cursor version, I doubt you can get a decent performance using the recursive query. I know people in here hate cursors, but when your table has that size, it can be the only solution.

Tsql query for periods by buy / sell?

An agent buys products from factory and sells them to normal people.
e.g.
he buy apple ( buy) (1/1/2010)
next day he buys again apple ( buy) (2/1/2010)
later he buys orange ( buy) (2/1/2010)
later he sells apple ( sell) (3/1/2010)
later he sells apple ( sell) (20/1/2010)
<=here the cycle of buy/sell for apple has finished.
sells the orange (22/1/2010)
i need Query result that tells me for each cycle - this :>
item | numberOfItems | timeFromBeginingToVanish
------------------------------------------------------
apple 2 19 days // ( 20/1 - 1/1)
orange 1 20 days //(22/1 - 2/1)
notice :
he can buy another apple and not to sell him - this WONT be in the list.
only when the stack is FIRST TO BE FINISHED
Edit: I have added another filter (HAVING clause) to display only those cycles that are completed .
This solution uses recursive CTE:
1) to compute a running total (RunningTotal) for every item and
2) to generate a group id (PseudoDenseRank) for every item cycle.
CREATE TABLE [Transaction]
(
TransactionId INT IDENTITY(10,10) PRIMARY KEY
,Item VARCHAR(100) NOT NULL
,TransactionType CHAR(1) NOT NULL
,Qty INT NOT NULL
,TransactionDate DATE NOT NULL
,CHECK( TransactionType IN ('B', 'S') ) --Buy, Sell
);
INSERT [Transaction]
SELECT 'apple', 'B', 1, '2010-01-01'
UNION ALL
SELECT 'apple', 'B', 1, '2010-01-02'
UNION ALL
SELECT 'orange','B', 1, '2010-01-03'
UNION ALL
SELECT 'apple', 'S', 1, '2010-01-03'
UNION ALL
SELECT 'apple', 'S', 1, '2010-01-20'
UNION ALL
SELECT 'orange','S', 1, '2010-01-22'
UNION ALL
SELECT 'apple', 'B', 2, '2010-02-01'
UNION ALL
SELECT 'orange','B', 3, '2010-02-02'
UNION ALL
SELECT 'apple', 'S', 1, '2010-02-03'
UNION ALL
SELECT 'apple', 'S', 1, '2010-02-10'
UNION ALL
SELECT 'orange','S', 1, '2010-02-10'
UNION ALL
SELECT 'orange','S', 1, '2010-02-11';
DECLARE #Results TABLE
(
TransactionId INT NOT NULL
,Item VARCHAR(100) NOT NULL
,TransactionType CHAR(1) NOT NULL
,Qty INT NOT NULL
,TransactionDate DATE NOT NULL
,RowNum INT NOT NULL
,PRIMARY KEY (Item, RowNum)
);
INSERT #Results
SELECT *
,ROW_NUMBER() OVER(PARTITION BY t.Item ORDER BY t.TransactionDate ASC, t.TransactionType ASC, t.TransactionId ASC) RowNum
FROM [Transaction] t;
WITH CteRecursive
AS
(
SELECT q.Item
,q.RowNum
,CASE WHEN q.TransactionType = 'B' THEN q.Qty END QtyBuy
,CASE WHEN q.TransactionType = 'S' THEN q.Qty END QtySell
,q.TransactionDate
,CASE WHEN q.TransactionType = 'B' THEN q.Qty WHEN q.TransactionType = 'S' THEN -q.Qty END AS RunningTotal
,1 AS PseudoDenseRank
FROM #Results q
WHERE q.RowNum = 1
UNION ALL
SELECT prev.Item
,crt.RowNum
,CASE WHEN crt.TransactionType = 'B' THEN crt.Qty END QtyBuy
,CASE WHEN crt.TransactionType = 'S' THEN crt.Qty END QtySell
,crt.TransactionDate
,prev.RunningTotal + CASE WHEN crt.TransactionType = 'B' THEN crt.Qty WHEN crt.TransactionType = 'S' THEN -crt.Qty END
,CASE WHEN prev.RunningTotal = 0 THEN prev.PseudoDenseRank + 1 ELSE prev.PseudoDenseRank END
FROM CteRecursive prev
INNER JOIN #Results crt ON prev.Item = crt.Item
AND prev.RowNum + 1 = crt.RowNum
)
SELECT q.Item
,q.PseudoDenseRank AS CycleNumber
,SUM(q.QtyBuy) AS QtyBuyTotal
,SUM(q.QtySell) AS QtySellTotal
,CASE WHEN ISNULL(SUM(q.QtyBuy), 0) - ISNULL(SUM(q.QtySell),0) = 0 THEN 'Complete' ELSE 'Incomplete' END AS CycleStatus
,MIN(q.TransactionDate) AS CycleStartDate
,MAX(q.TransactionDate) AS CycleEndDate
,CONVERT(VARCHAR(25), MIN(q.TransactionDate), 112) + ' - ' + CONVERT(VARCHAR(25), MAX(q.TransactionDate), 112) AS CycleInterval
FROM CteRecursive q
GROUP BY q.Item, q.PseudoDenseRank
HAVING ISNULL(SUM(q.QtyBuy), 0) - ISNULL(SUM(q.QtySell),0) = 0
ORDER BY q.Item, q.PseudoDenseRank;
DROP TABLE [Transaction];
Results:
Item CycleNumber QtyBuyTotal QtySellTotal CycleStatus CycleStartDate CycleEndDate CycleInterval
------ ----------- ----------- ------------ ----------- -------------- ------------ -----------------------
apple 1 2 2 Complete 2010-01-01 2010-01-20 2010-01-01 - 2010-01-20
apple 2 2 2 Complete 2010-02-01 2010-02-10 2010-02-01 - 2010-02-10
orange 1 1 1 Complete 2010-01-03 2010-01-22 2010-01-03 - 2010-01-22
--The next row is eliminated by the filter from HAVING
orange 2 3 2 Incomplete 2010-02-02 2010-02-11 2010-02-02 - 2010-02-11

Create "Sets" within the same table based on multi-column criteria

For every unique combination of BoxId and Revision with a single UnitTypeId of 1 and a single UnitTypeId of 2 both having a NULL SetNumber, assign a SetNumber of 1.
Table and data setup:
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[UnitTypes]') AND type in (N'U'))
Drop Table dbo.UnitTypes
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Tracking]') AND type in (N'U'))
DROP TABLE [dbo].[Tracking]
GO
CREATE TABLE dbo.UnitTypes
(
Id int NOT NULL,
Notes varchar(80)
)
GO
CREATE TABLE dbo.Tracking
(
Id int NOT NULL IDENTITY (1, 1),
BoxId int NOT NULL,
Revision int NOT NULL,
UnitValue int NULL,
UnitTypeId int NULL,
SetNumber int NULL
)
GO
ALTER TABLE dbo.Tracking ADD CONSTRAINT
PK_Tracking PRIMARY KEY CLUSTERED
(
Id
)
GO
Insert Into dbo.UnitTypes (Id, Notes) Values (1, 'X Coord'),
(2, 'Y Coord'),
(3, 'Weight'),
(4, 'Length')
Go
Insert Into dbo.Tracking (BoxId, Revision, UnitValue, UnitTypeId, SetNumber)
Values (1165, 1, 150, 1, NULL),
(1165, 1, 1477, 2, NULL),
(1165, 1, 31, 4, NULL),
(1166, 1, 425, 1, 1),
(1166, 1, 1146, 2, 1),
(1166, 1, 438, 1, NULL),
(1166, 1, 1163, 2, NULL),
(1167, 1, 560, 1, NULL),
(1167, 1, 909, 2, NULL),
(1167, 1, 12763, 3, NULL),
(1168, 1, 21, 1, NULL),
(1168, 1, 13109, 3, NULL)
The ideal results would be:
Id BoxId Revision UnitValue UnitTypeId SetNumber
1 1165 1 150 1 1
2 1165 1 1477 2 1
3 1165 1 31 4 1
4 1166 1 425 1 1
5 1166 1 1146 2 1
6 1166 1 438 1 NULL <--NULL Because there is already an existing Set
7 1166 1 1163 2 NULL <--NULL Because there is already an existing Set
8 1167 1 560 1 1
9 1167 1 909 2 1
10 1167 1 12763 3 1
11 1168 1 21 1 NULL <--NULL Because there is not exactly one UnitTypeId of 1 and exactly one UnitTypeId of 2 for this BoxId\Revision combination.
12 1168 1 13109 3 NULL <--NULL Because there is not exactly one UnitTypeId of 1 and exactly one UnitTypeId of 2 for this BoxId\Revision combination.
EDIT:
The question is how can I update the SetNumber, given the constraints above, using pure TSQL?
If I understand your question correctly, you could do this with a subquery that demands all conditions are met:
update t1
set SetNumber = 1
from dbo.Tracking t1
where SetNumber is null
and 1 =
(
select case
when count(case when t2.UnitTypeId = 1 then 1 end) <> 1 then 0
when count(case when t2.UnitTypeId = 2 then 1 end) <> 1 then 0
when count(t2.SetNumber) <> 0 then 0
else 1
end
from dbo.Tracking t2
where t1.BoxId = t2.BoxId
and t1.Revision = t2.Revision
)
The count(t2.SetNumber) is a bit tricky: this will only count rows where SetNumber is not null. So this meets the criterion that no other set with the same (BoxId, Revision) exists.
Try this out, it returns the same results that you gave. The WITH statement sets up a CTE to query from. The ROW_NUMBER() function is partitioning function that does what you want:
;WITH BoxSets AS (
SELECT
ID
,BoxId
,Revision
,UnitValue
,UnitTypeId
,CASE WHEN UnitTypeId IN (1,2) THEN 1 ELSE 0 END ValidUnit
,ROW_NUMBER() OVER (PARTITION BY BoxID,UnitTypeID ORDER BY BoxID,UnitTypeID,UnitValue ) SetNumber
FROM Tracking
)
SELECT
b.ID
,b.BoxId
,b.Revision
,b.UnitValue
,b.UnitTypeId
,CASE ISNULL(b1.ValidUnits,0) WHEN 0 THEN NULL ELSE CASE b.SetNumber WHEN 1 THEN b.SetNumber ELSE NULL END END
FROM BoxSets AS b
LEFT JOIN (SELECT
BoxID
,SUM(ValidUnit) AS ValidUnits
FROM BoxSets
GROUP BY BoxId
HAVING SUM(ValidUnit) > 1) AS b1 ON b.BoxId = b1.BoxId

Resources