How to update table based on the values in other columns - sql

Here is a sample below, I want to update AvailableAmt column based on the amount entered on UI.
Requirement
Update the value from the last row to the first row,
If entered 500 on UI, then the table will be like
If entered 1000 on UI, then the table will be like
Thank you for your help in advance !

Can't test it on a Sybase somewhere.
But in theory something like this might work:
DECLARE #Group VARCHAR(8) = 'a';
DECLARE #Amount INT = 1100;
UPDATE t
SET t.AvailableAmt =
CASE
WHEN q.PrevRemain > 0 AND t.AvailableAmt <= q.PrevRemain THEN 0
WHEN q.PrevRemain > 0 THEN t.AvailableAmt - q.PrevRemain
ELSE t.AvailableAmt
END
FROM YourTable t
JOIN
(
select [Group], [Row],
#Amount-(SUM(AvailableAmt) OVER (PARTITION BY [Group] ORDER BY AvailableAmt, [Row] desc) - AvailableAmt) as PrevRemain
from YourTable
where AvailableAmt > 0
and [Group] = #Group
) AS q
ON (q.[Group] = t.[Group] and q.[Row] = t.[Row]);
For a Sybase flavor that doesn't support the window function of SUM, something like this might work.
DECLARE #Group VARCHAR(8) = 'a';
DECLARE #Amount INT = 1200;
UPDATE t
SET t.AvailableAmt =
CASE
WHEN q.PrevRemain > 0 AND t.AvailableAmt <= q.PrevRemain THEN 0
WHEN q.PrevRemain > 0 THEN t.AvailableAmt - q.PrevRemain
ELSE t.AvailableAmt
END
FROM YourTable t
JOIN
(
select t1.[Group], t1.[Row],
#Amount - (SUM(t2.AvailableAmt)-t1.AvailableAmt) as PrevRemain
from YourTable t1
left join YourTable t2 on (t2.[Group] = t1.[Group] and t2.AvailableAmt <= t1.AvailableAmt and t2.[Row] >= t1.[Row])
where t1.AvailableAmt > 0
and t1.[Group] = #Group
group by t1.[Group], t1.[Row], t1.AvailableAmt
) AS q
ON (q.[Group] = t.[Group] and q.[Row] = t.[Row]);

Related

SQL CASE switch for parameter

I have a parameter that can be 1 of 3 possible values 0, 2 or NULL (these are values for an enum 0 is Pending, 1 is UnderReview and 2 is Closed). If it is NULL it should retrieve all regardless of the Status but If it is 2 it should retrieve all the closed ones however if the value is 0 it should retrieve the ones that are pending or under review. This last one means that I want to retrieve records that have a status of either 0 or 1. I have the following code so far and tried with a CASE switch but it doesn't work.
SELECT * FROM PurchaseOrder
WHERE (CreationDate >= #StartDate AND CreationDate <= #EndDate)
AND (#POStatus IS NULL OR [Status] = #POStatus)
AND (#POStatus IS NULL OR [Status] = CASE #POStatus WHEN 0 THEN 1 END)
AND (#PurchaseOrderIdSearch IS NULL OR PurchaseOrderId LIKE #PurchaseOrderIdSearch)
AND EmployeeId = #EmployeeId
ORDER BY CreationDate DESC, PurchaseOrderId DESC
I solved it adding an if statement, I was looking for a simpler way to do it to avoid repetition, this is how it looks like now:
IF #POStatus = 0
BEGIN
SELECT * FROM PurchaseOrder
WHERE (CreationDate >= #StartDate AND CreationDate <= #EndDate)
AND (([Status] = #POStatus)
OR ([Status] = 1))
AND (#PurchaseOrderIdSearch IS NULL OR PurchaseOrderId LIKE #PurchaseOrderIdSearch)
AND EmployeeId = #EmployeeId
ORDER BY CreationDate DESC, PurchaseOrderId DESC
END
ELSE
BEGIN
SELECT * FROM PurchaseOrder
WHERE (CreationDate >= #StartDate AND CreationDate <= #EndDate)
AND (#POStatus IS NULL OR [Status] = #POStatus)
AND (#PurchaseOrderIdSearch IS NULL OR PurchaseOrderId LIKE #PurchaseOrderIdSearch)
AND EmployeeId = #EmployeeId
ORDER BY CreationDate DESC, PurchaseOrderId DESC
END
I suppose you're looking for something like
DECLARE #MyParam INT; --Try to set 0, 1 or 2
SELECT *
FROM
(
VALUES
(0),
(1),
(2)
) T(Value)
WHERE (Value = #MyParam) OR (#MyParam IS NULL);
Here is a db<>fiddle to see how it's working.
Update:
It seems like you're looking for
DECLARE #MyParam INT; --Try to set 0, 1 or 2
SELECT *
FROM
(
VALUES
(0),
(1),
(2)
) T(Value)
WHERE
(
CASE WHEN #MyParam = 0 OR #MyParam = 1 THEN 1 ELSE 0 END = 1
AND Value IN(0, 1)
)
OR
(Value = #MyParam AND #MyParam = 2)
OR
#MyParam IS NULL;
Here is a db<>fiddle

Performing a sub query alternatives

I'm looking for advice on how to get around the
"Cannot perform an aggregate function on an expression containing an
aggregate or a subquery".
On the Select PlateID From #Instances code, in the example below. Does anyone have any ideas or suggestions?
DECLARE #Instances AS TABLE(PlateID INT);
INSERT INTO #Instances(PlateID)VALUES(11638),(11637),(11632),(11659)
DECLARE #NumberofPlates INT;
SELECT #NumberofPlates = COUNT(*) FROM #Instances;
SELECT Instance_Plate_Room_Instance_ID_LNK
from dbo.M_Instance_Plate
WHERE Instance_Plate_Deleted = 0
group by Instance_Plate_Room_Instance_ID_LNK
having sum(case
when Instance_Plate_Plate_ID_LNK not in (SELECT PlateID
FROM #Instances)
then 1 else 0 end) = 0 and
SUM(case
when Instance_Plate_Plate_ID_LNK in (SELECT PlateID
FROM #Instances)
then 1 else 0 end) = #NumberofPlates;
In the absence of the structure of the physical table you included in your query I mocked up some random data for this, and put together a query that seems to work?
DECLARE #Instances AS TABLE(PlateID INT);
INSERT INTO #Instances(PlateID) VALUES (11638),(11637),(11632),(11632);
DECLARE #M_Instance_Plate TABLE (Instance_Plate_Plate_ID_LNK INT, Instance_Plate_Deleted INT, Instance_Plate_Room_Instance_ID_LNK INT);
INSERT INTO #M_Instance_Plate SELECT 11638, 0, 100;
INSERT INTO #M_Instance_Plate SELECT 11637, 0, 100;
INSERT INTO #M_Instance_Plate SELECT 11632, 0, 100;
INSERT INTO #M_Instance_Plate SELECT 11632, 0, 200;
INSERT INTO #M_Instance_Plate SELECT 11632, 1, 300;
DECLARE #NumberofPlates INT;
SELECT #NumberofPlates = COUNT(*) FROM #Instances;
WITH x AS (
SELECT
Instance_Plate_Room_Instance_ID_LNK,
SUM(CASE WHEN Instance_Plate_Plate_ID_LNK IS NULL THEN 1 ELSE 0 END) AS test_1, --Any missing
SUM(CASE WHEN Instance_Plate_Plate_ID_LNK IS NOT NULL THEN 1 ELSE 0 END) AS test_2 --Has coverage
FROM
#M_Instance_Plate ip
LEFT JOIN #Instances i ON i.PlateID = ip.Instance_Plate_Plate_ID_LNK
WHERE
Instance_Plate_Deleted = 0
GROUP BY
Instance_Plate_Room_Instance_ID_LNK)
SELECT
Instance_Plate_Room_Instance_ID_LNK
FROM
x
WHERE
test_1 = 0
AND test_2 = #NumberofPlates;
INSERT INTO #Instances(PlateID)VALUES(11638),(11637),(11632),(11659)
--DECLARE #NumberofPlates INT;
--SELECT #NumberofPlates = COUNT(*) FROM #Instances;
SELECT Instance_Plate_Room_Instance_ID_LNK
FROM dbo.M_Instance_Plate p
WHERE Instance_Plate_Deleted = 0
AND NOT EXISTS (
SELECT 1
FROM #Instances i
LEFT JOIN M_Instance_Plate m ON i.PlateID = m.Instance_Plate_Plate_ID_LNK
WHERE m.Instance_Plate_Room_Instance_ID_LNK = p.Instance_Plate_Room_Instance_ID_LNK
AND m.Instance_Plate_Plate_ID_LNK IS NULL
UNION ALL
SELECT 1
FROM #Instances i
JOIN M_Instance_Plate m ON i.PlateID = m.Instance_Plate_Plate_ID_LNK
WHERE m.Instance_Plate_Room_Instance_ID_LNK = p.Instance_Plate_Room_Instance_ID_LNK
GROUP BY i.PlateID
HAVING COUNT(*) != 1
)
Try this instead. Is equivalent expression (checks if plateId exists on your table variable and then matches with your variable #numberofplates)
HAVING #NumberofPlates = (
SELECT COUNT(1) AS cc
FROM #Instances AS a
WHERE a.PlateID = Instance_Plate_Plate_ID_LNK
)

SQL Server: is this a bug or do I have a misunderstanding?

Today I'm found a very sticky problem on SQL Server 2014.
Scenario: I want to pay awards to my customer (some pin code for cell phone operator)
In last cycle of loop T.Used = 0 condition is bypassed and is not working. I know in other conditions in that query (T.Cash < (#myAwards - #paid)) is there a mistake and I must to use T.Cash <= (#myAwards - #paid) instead of this but please focus on main question.
Why it's happened when I update Used flag to 1 (True) then in next loop it's selected while it doesn't have a valid condition (T.Used = 0)?
DECLARE #myAwards INT = 90000,
#paid INT = 0;
DECLARE #Temp TABLE
(
Id INT NOT NULL,
Pin VARCHAR(100) NOT NULL,
Cash INT NOT NULL,
[Weight] INT NULL,
Used BIT NOT NULL
)
INSERT INTO #Temp
SELECT
UPFI.Id, UPFI.PinCode,
PT.Cash, NULL, 0
FROM
dbo.UploadedPinFactorItem UPFI WITH (NOLOCK)
INNER JOIN
dbo.PinType PT WITH (NOLOCK) ON PT.ID = UPFI.PinTypeID
WHERE
PT.Cash <= #myAwards
UPDATE T
SET [Weight] = ISNULL((SELECT COUNT(TT.Id)
FROM #Temp TT
WHERE TT.Cash = T.Cash), 0) * T.Cash
FROM #Temp T
--For debug (first picture)
SELECT * FROM #Temp
DECLARE #i int = 1
DECLARE #count int = 0
SELECT #count = COUNT([Id]) FROM #Temp C WHERE C.Used = 0
WHILE (#i <= #count AND #paid < #myAwards)
BEGIN
DECLARE #nextId INT,
#nextCash INT,
#nextFlag BIT;
-- 'T.Used = 0' condition is by passed
SELECT TOP (1)
#nextId = T.Id, #nextCash = T.Cash, #nextFlag = T.Used
FROM
#Temp T
WHERE
T.Used = 0
AND T.Cash < (#myAwards - #paid)
ORDER BY
T.[Weight] DESC, T.Cash DESC, T.Id DESC
UPDATE #Temp
SET Used = 1
WHERE Id = #nextId
SET #i = #i + 1
SET #paid = #paid + #nextCash
--Show result in second picture
SELECT
#i AS 'i', #paid AS 'paid', #nextFlag AS 'flag', #nextId AS 'marked Id',*
FROM
#temp T
ORDER BY
T.[Weight] DESC, T.Cash DESC, T.Id DESC
END
SELECT 'final', #paid, *
FROM #temp T
ORDER BY T.[Weight] DESC, T.Cash DESC, T.Id DESC
Please let me to understand this is a bug or I have misunderstanding
First screenshot:
Second screenshot (result of loop):
Third screenshot (final result):
As per my comments:
This isn't a problem with the condition, the problem is with the implemented logic. After i = 4, there are no more rows where T.Used = 0 AND T.Cash < (#myAwards - #paid), that makes it so your reassigning variables gets zero rows, so they mantain the previous values.
You can test this behavior by doing:
DECLARE #A INT = 10;
SELECT #A = object_id
FROM sys.all_objects
WHERE name = 'an object that doesn''t exist'
SELECT #A;

Less expensive query?

I have a stored procedure that returns an integer 1 or 0 depending on specific criteria. It currently uses three select statements and it will be used heavily by multiple users across multiple locations. There has to be a more efficient way of doing this.
In short the query checks first to see if all checklist items on an order are completed (a separate table), then it checks to see if a field named BreakOutGuest (a bit field) is a 1 or 0. Depending on that result it checks to see if the total guest count is greater than 0 and the order total is zero. It returns the one or zero on all this criteria. Is there a more efficient way to do this? A temp table so I only have to hit the actual tables once? Below is the code.
#ORDERID INT
AS
BEGIN
DECLARE #AUTO_CLOSE INT
SET NOCOUNT ON;
--If all checklist items are marked complete move on, if not set #AUTO_CLOSE=0
IF NOT EXISTS(SELECT ORDERID FROM dbo.orderchecklistitems WHERE OrderID=#ORDERID AND CompletedON IS NULL)
BEGIN
--if BreakOutGuestFees is 1 only sum Guest_Count_1 + Guest_Count_2
IF EXISTS(SELECT * FROM dbo.Orders WHERE (GuestCount_1 + GuestCount_2)>1 AND OrderTotal=0 AND BreakoutGuestFees=1)
BEGIN
SET #AUTO_CLOSE=1
END
ELSE
SET #AUTO_CLOSE=0
--if BreakOutGuestFees is 0 only consider Guest_Count_1
IF EXISTS(SELECT * FROM dbo.Orders WHERE (GuestCount_1)>1 AND OrderTotal=0 AND BreakoutGuestFees=0)
BEGIN
SET #AUTO_CLOSE=1
END
ELSE
SET #AUTO_CLOSE=0
END
ELSE
SET #AUTO_CLOSE=0
END
If am not wrong you can combine two if clause into single if clause by using AND , OR logic. Try this.
IF NOT EXISTS(SELECT ORDERID
FROM dbo.orderchecklistitems
WHERE OrderID = #ORDERID
AND CompletedON IS NULL)
BEGIN
IF EXISTS(SELECT *
FROM dbo.Orders
WHERE ( ( GuestCount_1 + GuestCount_2 > 1
AND BreakoutGuestFees = 1 )
OR ( BreakoutGuestFees = 0
AND GuestCount_1 > 1 ) )
AND OrderTotal = 0
AND OrderID = #ORDERID)
SET #AUTO_CLOSE=1
ELSE
SET #AUTO_CLOSE=0
END
ELSE
SET #AUTO_CLOSE=0
You can perform your selection check with only one query
SELECT
(SELECT sum(1) FROM dual WHERE EXISTS (SELECT ORDERID FROM dbo.orderchecklistitems WHERE OrderID=#ORDERID AND CompletedON IS NULL)),
(SELECT sum(1) FROM dual WHERE EXISTS (SELECT 1 FROM dbo.Orders WHERE (GuestCount_1 + GuestCount_2)>1 AND OrderTotal=0 AND BreakoutGuestFees=1)),
(SELECT sum(1) FROM dual WHERE EXISTS (SELECT 1 FROM dbo.Orders WHERE (GuestCount_1)>1 AND OrderTotal=0 AND BreakoutGuestFees=0))
INTO
result1, result2, result3
from dual
then check results
DELCARE #AUTO_CLOSE INT = 0
IF NOT EXISTS(SELECT ORDERID
FROM dbo.orderchecklistitems
WHERE OrderID = #ORDERID
AND CompletedON IS NULL)
BEGIN
SET #AUTO_CLOSE =
(
SELECT
CASE
WHEN (GuestCount_1 + GuestCount_2 > 1) AND BreakoutGuestFees = 0 THEN 1
WHEN (GuestCount_1 > 1 ) AND BreakoutGuestFees = 1 THEN 1
ELSE 0 END
FROM dbo.orders
WHERE OrderTotal = 0 AND OrderID = #orderID
)
END

The best way to perform such calculation logic in T-SQL

My program require to pass in an amount into the query to perform such calculation, but in my case it loop through row by row and deduct the correct amount, i know this is not a efficient way to implement. so i am here to seeking a better way.
PS: It is just my draft code, i am sorry about i cannot post the complete source code for some reason. Now i had re-structure my code to make it more complete and reasonable.
--- the amount column is just for reference.
insert into tbl1 (idx,amount,balance) values (1, 50, 50)
insert into tbl1 (idx,amount,balance) values (2, 30, 30)
insert into tbl1 (idx,amount,balance) values (3, 20, 20)
insert into tbl1 (idx,amount,balance) values (4, 50, 50)
insert into tbl1 (idx,amount,balance) values (5, 60, 60)
declare #total_value_to_deduct int
declare #cs_index int, #cs_balance int, #deduct_amount int
set #total_value_to_deduct = 130
declare csDeduct Cursor for select idx, balance from tbl1 where balance > 0
open csDeduct fetch next from csDeduct into #cs_index, #cs_balance
while ##FETCH_STATUS = 0 and #total_value_to_deduct > 0
begin
if #cs_balance >= #total_value_to_deduct
set #deduct_amount = #total_value_to_deduct
else
set #deduct_amount = #cs_balance
-- contine deduct row by row if the total_value_to_deduct is not 0
set #total_value_to_deduct = #total_value_to_deduct - #deduct_amount
update tbl1 set balance = balance - #deduct_amount where idx = #cs_index
fetch next from csDeduct into #cs_index, #cs_balance
end
close csDeduct
deallocate csDeduct
Expected Result :
idx amount balance
1 50 0
2 30 0
3 20 0
4 50 20
5 60 60
Your help is must appreciate. thank
Revision 1: I have added a third solution
First solution (SQL2005+; online query)
DECLARE #tbl1 TABLE
(
idx INT IDENTITY(2,2) PRIMARY KEY,
amount INT NOT NULL,
balance INT NOT NULL
);
INSERT INTO #tbl1 (amount,balance) VALUES (50, 50);
INSERT INTO #tbl1 (amount,balance) VALUES (30, 30);
INSERT INTO #tbl1 (amount,balance) VALUES (20, 20);
INSERT INTO #tbl1 (amount,balance) VALUES (50, 50);
INSERT INTO #tbl1 (amount,balance) VALUES (60, 60);
DECLARE #total_value_to_deduct INT;
SET #total_value_to_deduct = 130;
WITH CteRowNumber
AS
(
SELECT *, ROW_NUMBER() OVER(ORDER BY idx) AS RowNum
FROM #tbl1 a
), CteRecursive
AS
(
SELECT a.idx,
a.amount,
a.amount AS running_total,
CASE
WHEN a.amount <= #total_value_to_deduct THEN 0
ELSE a.amount - #total_value_to_deduct
END AS new_balance,
a.RowNum
FROM CteRowNumber a
WHERE a.RowNum = 1
--AND a.amount < #total_value_to_deduct
UNION ALL
SELECT crt.idx,
crt.amount,
crt.amount + prev.running_total AS running_total,
CASE
WHEN crt.amount + prev.running_total <= #total_value_to_deduct THEN 0
WHEN prev.running_total < #total_value_to_deduct AND crt.amount + prev.running_total > #total_value_to_deduct THEN crt.amount + prev.running_total - #total_value_to_deduct
ELSE crt.amount
END AS new_balance,
crt.RowNum
FROM CteRowNumber crt
INNER JOIN CteRecursive prev ON crt.RowNum = prev.RowNum + 1
--WHERE prev.running_total < #total_value_to_deduct
)
UPDATE #tbl1
SET balance = b.new_balance
FROM #tbl1 a
Second solution (SQL2012)
UPDATE #tbl1
SET balance = b.new_balance
FROM #tbl1 a
INNER JOIN
(
SELECT x.idx,
SUM(x.amount) OVER(ORDER BY x.idx) AS running_total,
CASE
WHEN SUM(x.amount) OVER(ORDER BY x.idx) <= #total_value_to_deduct THEN 0
WHEN SUM(x.amount) OVER(ORDER BY x.idx) - x.amount < #total_value_to_deduct --prev_running_total < #total_value_to_deduct
AND SUM(x.amount) OVER(ORDER BY x.idx) > #total_value_to_deduct THEN SUM(x.amount) OVER(ORDER BY x.idx) - #total_value_to_deduct
ELSE x.amount
END AS new_balance
FROM #tbl1 x
) b ON a.idx = b.idx;
Third solution (SQ2000+) uses triangular join:
UPDATE #tbl1
SET balance = d.new_balance
FROM #tbl1 e
INNER JOIN
(
SELECT c.idx,
CASE
WHEN c.running_total <= #total_value_to_deduct THEN 0
WHEN c.running_total - c.amount < #total_value_to_deduct --prev_running_total < #total_value_to_deduct
AND c.running_total > #total_value_to_deduct THEN c.running_total - #total_value_to_deduct
ELSE c.amount
END AS new_balance
FROM
(
SELECT a.idx,
a.amount,
(SELECT SUM(b.amount) FROM #tbl1 b WHERE b.idx <= a.idx) AS running_total
FROM #tbl1 a
) c
)d ON d.idx = e.idx;
Here is one of the ways to do it. It finds first running sum greater than or equal to requested amount and then updates all records participating in this sum. This should probably be written differently in a sense that a column "toDeduct" should be introduced and would initially have the value of amount. This would allow this update to work over previously used data sets, because toDeduct = 0 would mean that nothing can be deducted from this row. Furthermore, an index on toDeduct, idx will allow for quick toDeduct <> 0 filter you would use to lessen number of meaningless searches/updates.
declare #total_value_to_deduct int
set #total_value_to_deduct = 130
update tbl1
set balance = case when balance.idx = tbl1.idx
then balance.sumamount - #total_value_to_deduct
else 0
end
from tbl1 inner join
(
select top 1 *
from
(
select idx, (select sum (a.amount)
from tbl1 a
where a.idx <= tbl1.idx) sumAmount
from tbl1
) balance
where balance.sumamount >= #total_value_to_deduct
order by sumamount
) balance
on tbl1.idx <= balance.idx
Now on to your cursor. One would gain performance by simply declaring cursor fast_forward:
declare csDeduct Cursor local fast_forward
for select idx, balance
from tbl1
where balance > 0
order by idx
And you might rewrite fetch loop to avoid repeating fetch statement:
open csDeduct
while 1 = 1
begin
fetch next from csDeduct into #cs_index, #cs_balance
if ##fetch_status <> 0
break
if #cs_balance >= #total_value_to_deduct
set #deduct_amount = #total_value_to_deduct
else
set #deduct_amount = #cs_balance
-- contine deduct row by row if the total_value_to_deduct is not 0
set #total_value_to_deduct = #total_value_to_deduct - #deduct_amount
update tbl1 set balance = balance - #deduct_amount where idx = #cs_index
end
close csDeduct
deallocate csDeduct
Makes changing select part of a cursor a bit easier.
I'm pretty sure this query won't work anyway, as "index" is a keyword and so should be wrapped in square brackets to indicate otherwise.
In general it's not a good idea to do anything on a row-by-row basis for performance.
If I'm reading it right, you're setting each balance column to the amount column minus the #total_value_to_deduct variable, or setting it to 0 if the deductions would result in a negative amount. If that's true then why not just do calculations on that directly? Without you posting any expected results I can't double check my logic, but please correct me if I'm wrong and it's more complicated than this.
UPDATE tbl1
SET balance = CASE
WHEN amount < #total_value_to_deduct THEN 0
ELSE amount - #total_value_to_deduct
END
Edit: OK thanks for the edit to the question it's more clear now. You're trying to take the total amount over all the accounts sequentially. I'll see if I can come up with a script to do this and edit my answer further.
Edit #2: OK, I couldn't find a way of doing it without interating through all of the rows (I tried a recursive CTE, but couldn't get it to work) so I've done it with a while loop like you did originally. It's effectively doing 3 data accesses per row though - I tried to knock this down to 2 but again no luck. I'm posting it anyway in case it's faster than what you have now. This should be all the code you need (apart from the table create/populate).
DECLARE #id INT
SELECT #id = Min([index])
FROM tbl1
WHILE #id IS NOT NULL
BEGIN
UPDATE tbl1
SET balance = CASE
WHEN amount < #total_value_to_deduct THEN 0
ELSE amount - #total_value_to_deduct
END
FROM tbl1
WHERE [index] = #id
SELECT #total_value_to_deduct = CASE
WHEN #total_value_to_deduct < amount THEN 0
ELSE #total_value_to_deduct - amount
END
FROM tbl1
WHERE [index] = #id
SELECT #id = Min([index])
FROM tbl1
WHERE [index] > #id
END
If your indexes don't have gaps, the simplest solution would be to
Create a recursive CTE, starting with the value to deduct and decrementing it in the recursive part.
Use the results of the CTE to update your actual table
SQL Statement
;WITH q AS (
SELECT idx, amount, balance, 130 AS Deduct
FROM tbl1
WHERE idx = 1
UNION ALL
SELECT t.idx, t.amount, t.balance, q.Deduct - q.balance
FROM q
INNER JOIN #tbl1 t ON t.idx = q.idx + 1
WHERE q.Deduct - q.balance > 0
)
UPDATE #tbl1
SET Balance = CASE WHEN q.Balance - q.Deduct > 0 THEN q.Balance - q.Deduct ELSE 0 END
FROM q
INNER JOIN tbl1 t ON t.idx = q.idx
Using ROW_NUMBERyou can alleviate the gap problem but it complicates the query a bit.
;WITH r AS (
SELECT idx, amount, balance, rn = ROW_NUMBER() OVER (ORDER BY idx)
FROM tbl1
), q AS (
SELECT rn, amount, balance, 130 AS Deduct, idx
FROM r
WHERE rn = 1
UNION ALL
SELECT r.rn, r.amount, r.balance, q.Deduct - q.balance, r.idx
FROM q
INNER JOIN r ON r.rn = q.rn + 1
WHERE q.Deduct - q.balance > 0
)
UPDATE tbl1
SET Balance = CASE WHEN q.Balance - q.Deduct > 0 THEN q.Balance - q.Deduct ELSE 0 END
FROM q
INNER JOIN #tbl1 t ON t.idx = q.idx
Test script
DECLARE #tbl1 TABLE (idx INTEGER, Amount INTEGER, Balance INTEGER)
INSERT INTO #tbl1 (idx,amount,balance) VALUES (1, 50, 50)
INSERT INTO #tbl1 (idx,amount,balance) VALUES (2, 30, 30)
INSERT INTO #tbl1 (idx,amount,balance) VALUES (3, 20, 20)
INSERT INTO #tbl1 (idx,amount,balance) VALUES (4, 50, 50)
INSERT INTO #tbl1 (idx,amount,balance) VALUES (5, 60, 60)
;WITH q AS (
SELECT idx, amount, balance, 130 AS Deduct
FROM #tbl1
WHERE idx = 1
UNION ALL
SELECT t.idx, t.amount, t.balance, q.Deduct - q.balance
FROM q
INNER JOIN #tbl1 t ON t.idx = q.idx + 1
WHERE q.Deduct - q.balance > 0
)
UPDATE #tbl1
SET Balance = CASE WHEN q.Balance - q.Deduct > 0 THEN q.Balance - q.Deduct ELSE 0 END
FROM q
INNER JOIN #tbl1 t ON t.idx = q.idx
SELECT *
FROM #tbl1
Output
idx Amount Balance
1 50 0
2 30 0
3 20 0
4 50 20
5 60 60
Create a new column in the table with the previous balance for each row, then you could use a trigger on INSERT/UPDATE to create the balance for the newly inserted row.