I have the following t-sql:
declare #startno int = 1, #finishno int = 365, #AfterO float = 97
declare #daysbet int = 80, #decVal float = 0.10 / 100;
;with ctetest as
(
select #startno as oDay, 1 as oBet, #AfterO as Prod
union all
select oDay +1, case when ((oBet + 1) = #daysbet) then 0 else (oBet + 1) end,
case when (Prod = 0) then #AfterO else
case when ((oBet + 1) = #daysbet) then 0 else (Prod - (#decVal * Prod)) end
end
from ctetest
where oDay+1 <= #finishno
)
select * into #t1
from ctetest option (maxrecursion 0)
Select #daysbet oVal, AVG(Prod) aProd, SUM(Prod) * 24 sProd FROM #t1
So the result should be:
---------------------------------
oVal aProd sProd
---------------------------------
80 92.484 810168.297
My question is, how can I substract some value from oVal on single execute? For instance, If the oVal value is 80 the the 2nd record is 79, 3rd record is 78 and the other columns will follow. Result should be:
---------------------------------
oVal aProd sProd
---------------------------------
80 92.484 810168.297
79 92.xxx 810444.069
78 92.xxx 810675.718
ff.---->
until oVal reach 1
So, the simple way is, if the #daysbet is 80, then the average of Prod is 92.484. If the #daysbet is 79, then the average of Prod is 92 poin something. and so on... In single execute.
Does anyone have an idea for this situation? Please advice.
Thank you.
Maybe this will help you
declare #startno int = 1, #finishno int = 365, #AfterO float = 97
declare #daysbet int = 80, #decVal float = 0.10 / 100;
if object_id(N'tempdb..#t1', N'U') is not null drop table #t1;
;with ctetest as
(
select a2.[n], a1.*
from (select [oDay] = #startno, [oBet] = 1, [prod] = #AfterO) as a1
cross join
(
select top(#daysbet) [n] = row_number() over(order by spt1.[number])
from master..spt_values as spt1
cross join master..spt_values as spt2
) as a2
union all
select [n]
,[oDay] + 1
,case when ((oBet + 1) = [n]) then 0 else (oBet + 1) end
,case when (Prod = 0) then #AfterO else case when ((oBet + 1) = [n]) then 0 else (Prod - (#decVal * Prod)) end
end
from ctetest
where oDay + 1 <= #finishno
)
select * into #t1
from ctetest option (maxrecursion 0)
select [oVal] = t1.[n]
,[aProd] = avg(t1.[prod])
,[sProd] = sum(t1.[prod]) * 24
from #t1 as t1
group by t1.[n]
order by t1.[n] desc
Related
I have following query to split the range of numbers in parts. For ex: i have range of 200 numbers and i have to group the numbers to 95 But Result is coming in reverse order.I Have attached expected result
declare #MinValue bigint = 1
declare #MaxValue bigint = 200;
declare #RowsPerGroup bigint =95
declare #RowsPerGroup1 bigint =(#RowsPerGroup-1)
;with src(val,rm) as (
select #MaxValue, (#MaxValue - #RowsPerGroup1) union all
select rm-1, case when rm-1 > #MinValue + #RowsPerGroup1 then rm-1 - #RowsPerGroup1 else #MinValue end from src where rm-1 >= #MinValue
)
select rm as 'Start', val as 'End',[Difference]=(val-rm)+1 from src order by rm asc
option(maxrecursion 0)
Current Result:
Start End Difference
1 10 10
11 105 95
106 200 95
Expected Result:
Pls let me know where am doing wrong
My variant:
DECLARE
#MinValue bigint = 1,
#MaxValue bigint = 200,
#RowsPerGroup bigint = 95 -- 300
;WITH cte AS(
SELECT #MinValue [Start],IIF(#RowsPerGroup>#MaxValue,#MaxValue,#RowsPerGroup) [End]
UNION ALL
SELECT [End]+1,IIF([End]+#RowsPerGroup>#MaxValue,#MaxValue,[End]+#RowsPerGroup)
FROM cte
WHERE [End]<#MaxValue
)
SELECT [Start],[End],[End]-[Start]+1 [Difference]
FROM cte
You can see the reason in the first line of the common table expression:
select #MaxValue, (#MaxValue - #RowsPerGroup1)
This inserts (200,106) intto src. The second select then counts down from existing rows. To adapt the CTE, exchange minimums with maximums (including for ranges), invert arithmetic, reverse comparisons and any other related exchanges:
select #MinValue, (#MinValue + #RowsPerGroup1) union all
select val+1, case
when val+1 < #MaxValue - #RowsPerGroup1
then val+1 + #RowsPerGroup1
else #MaxValue
end
from src
where val+1 <= #MaxValue
This particular statement can be simplified in parts:
select #MinValue, (#MinValue + #RowsPerGroup - 1) union all
select val + 1, case
when val + #RowsPerGroup < #MaxValue
then val + #RowsPerGroup
else #MaxValue
end
from src
where val < #MaxValue
I am trying to speed up this recursive UNION ALL as shown below, but I cannot think how to do it. Maybe a while loop but I am not sure. The movement data is stored as one long string of encoded movement data and the script recursively calls the select statement to parse/extract this data and then it is all casted.
I would really like to understand more about speeding up recursive union all's or finding another way. I don't believe indexing is a problem so this is not really a possible solution.
"RouteData" is the long string that is parsed by fixed length intervals.
Here is a sample of the encoded data:
ScenarioPID : 3
LegID :1
RoutePart : 0x0000000000000000000100000000000000000000000000
RouteData : 0x40323AAAAAAAAAAB00013FA6FFD663CCA3310000001F00403 ... (goes on)
cnt : 37
sequence : 1
StartTime : 8828
The final output data looks like this for one track.
ScenarioPID LegID sequence TrackID Offset TimeOffset Length StartTime
3 1 1 1 0 0 6300 8828
3 1 2 1 0.0449 31 6300 8828
3 1 3 1 0.8942 325 6300 8828
3 1 4 1 0.9736 356 6300 8828
3 1 5 1 1 369 6300 8828
USE nss_demo;
DECLARE #scenario1 INT;
DECLARE #DAY_START INT;
DECLARE #DAY_END INT;
DECLARE #TRAIN_TYPE VARCHAR(50);
DECLARE #TRACK_TYPE VARCHAR(50);
SET #scenario1 = 3;
SET #DAY_START = 0;
SET #DAY_END = 7;
SET #TRAIN_TYPE = 'Empty Train';
SET #TRACK_TYPE = 'East Track';
DECLARE #KM_START INT;
DECLARE #KM_END INT;
SET #KM_START = 0;
SET #KM_END = 200;
WITH movement
AS (SELECT m.scenariopid,
m.legid,
Substring(routedata, 1, 23) AS RoutePart,
Substring(routedata, 24, Len(routedata) - 23) AS RouteData,
Len(routedata) / 23 - 1 AS cnt,
1 AS sequence,
m.starttime
FROM output.movement m
WHERE scenariopid = #scenario1
AND m.starttime BETWEEN ( #DAY_START * 86400 ) AND
( #DAY_END * 86400 )
UNION ALL
SELECT scenariopid,
legid,
Substring(m1.routedata, 1, 23) AS RoutePart
,
Substring(m1.routedata, 24,
Len(m1.routedata) - 23) AS RouteData,
Len(m1.routedata) / 23 - 1 AS cnt,
sequence + 1 AS sequence,
m1.starttime
FROM movement m1
WHERE m1.cnt > 0),
casttable
AS (SELECT tt.scenariopid,
tt.legid,
tt.sequence,
tt.trackid,
tt.offset,
tt.timeoffset,
tr.[length],
tt.starttime
FROM (SELECT scenariopid,
legid,
sequence,
Cast(trackidbin AS SMALLINT) AS TrackID,
Sign(Cast(offsetbin AS BIGINT)) *
( 1.0 +
( Cast(offsetbin AS BIGINT) & 0x000FFFFFFFFFFFFF ) *
Power(Cast(2 AS FLOAT), -52) )
*
Power(Cast(2 AS FLOAT), ( Cast(offsetbin AS BIGINT) &
0x7ff0000000000000
) /
0x0010000000000000
- 1023) AS Offset,
Cast(timebin AS INT) AS TimeOffset,
starttime AS StartTime
FROM (SELECT legid,
scenariopid,
sequence,
Substring(routepart, 9, 2) AS TrackIDBin,
Substring(routepart, 11, 8) AS OffsetBin,
Substring(routepart, 19, 4) AS TimeBin,
starttime
FROM movement) t) tt
INNER JOIN input.track tr
ON tr.trackid = tt.trackid
AND tr.scenariopid = tt.scenariopid)
SELECT *
FROM casttable
ORDER BY legid,
sequence
OPTION (maxrecursion 20000)
Use a Numbers Table (zero-based assumed below) to create CTE movement like this:
WITH movement
AS (SELECT m.scenariopid,
m.legid,
Substring(routedata, n.N*23 + 1, 23) AS RoutePart,
n.N AS cnt,
-- 1 AS sequence, -- use a row_number function here instead, as per your vendor.
m.starttime
FROM output.movement m
JOIN Numbers n
on n < Len(routedata) / 23
WHERE scenariopid = #scenario1
AND m.starttime BETWEEN ( #DAY_START * 86400 ) AND
( #DAY_END * 86400 )
),
-- etc.
If you don't have a static Numbers Table, my answer here demonstrates how to create one dynamically in a CTE.
I have a query that takes a LINESTRING and converts it to a result set of POINTS.
What I can't figure out is how to find the distance between 2 specific row points in this result set.
This is what I have so far:
DECLARE #GeographyToConvert geography
SET #GeographyToConvert = geography::STGeomFromText('LINESTRING (26.6434033 -81.7097817, 26.6435367 -81.709785, 26.6435783 -81.7098033, 26.6436067 -81.709825, 26.6435883 -81.709875, 26.64356 -81.7100417, 26.6434417 -81.710125, 26.6433167 -81.7101467, 26.643195 -81.7101033, 26.6431533 -81.7099517, 26.643175 -81.7097867, 26.643165 -81.7097917, 26.6431633 -81.7097367, 26.6431583 -81.7097083)',4326);
WITH GeographyPoints(N, Point) AS
(
SELECT 1, #GeographyToConvert.STPointN(1)
UNION ALL
SELECT N + 1, #GeographyToConvert.STPointN(N + 1)
FROM GeographyPoints GP
WHERE N < #GeographyToConvert.STNumPoints()
)
SELECT N,Point.STBuffer(0.25) as point, Point.STAsText() FROM GeographyPoints
For example, how can I compare the distance between N=10 & N=11?
This is what I was trying, but it does not work:
Declare #Point1 geography;
Declare #Point2 geography;
DECLARE #GeographyToConvert geography
--SET #GeometryToConvert = (select top 1 geotrack from dbo.SYNCTESTING2 where geotrack is not null);
SET #GeographyToConvert = geography::STGeomFromText('LINESTRING (26.6434033 -81.7097817, 26.6435367 -81.709785, 26.6435783 -81.7098033, 26.6436067 -81.709825, 26.6435883 -81.709875, 26.64356 -81.7100417, 26.6434417 -81.710125, 26.6433167 -81.7101467, 26.643195 -81.7101033, 26.6431533 -81.7099517, 26.643175 -81.7097867, 26.643165 -81.7097917, 26.6431633 -81.7097367, 26.6431583 -81.7097083)',4326);
WITH GeographyPoints(N, Point) AS
(
SELECT 1, #GeographyToConvert.STPointN(1)
UNION ALL
SELECT N + 1, #GeographyToConvert.STPointN(N + 1)
FROM GeographyPoints GP
WHERE N < #GeographyToConvert.STNumPoints()
)
SELECT N,Point.STBuffer(0.25) as point, Point.STAsText() FROM GeographyPoints
select #Point1 = Point FROM GeometryPoints where N = 10;
select #Point2 = Point FROM GeometryPoints where N = 11
select #Point1.STDistance(#Point2) as [Distance in Meters]
Replace
SELECT N,Point.STBuffer(0.25) as point, Point.STAsText() FROM GeographyPoints
With
SELECT * INTO #GeographyPoints FROM GeographyPoints
DECLARE #N1 INT = 10
DECLARE #N2 INT = 11
SELECT (SELECT Point FROM #GeographyPoints WHERE N=#N1).STDistance(
(SELECT Point FROM #GeographyPoints WHERE N=#N2))
DROP TABLE #GeographyPoints
And just change the values for #N1 & #N2 as neccessary
Is this what you're looking for? Distance to the previous point?
DECLARE #GeographyToConvert geography
SET #GeographyToConvert = geography::STGeomFromText('LINESTRING (26.6434033 -81.7097817, 26.6435367 -81.709785, 26.6435783 -81.7098033, 26.6436067 -81.709825, 26.6435883 -81.709875, 26.64356 -81.7100417, 26.6434417 -81.710125, 26.6433167 -81.7101467, 26.643195 -81.7101033, 26.6431533 -81.7099517, 26.643175 -81.7097867, 26.643165 -81.7097917, 26.6431633 -81.7097367, 26.6431583 -81.7097083)',4326);
WITH GeographyPoints(N, Point, PreviousPoint, DistanceFromPrevious) AS
(
SELECT 1, #GeographyToConvert.STPointN(1), CAST(NULL AS GEOGRAPHY), CAST(0 AS Float)
UNION ALL
SELECT N + 1, #GeographyToConvert.STPointN(N + 1)
, #GeographyToConvert.STPointN(N)
, #GeographyToConvert.STPointN(N).STDistance(#GeographyToConvert.STPointN(N + 1))
FROM GeographyPoints GP
WHERE N < #GeographyToConvert.STNumPoints()
)
SELECT N,Point.STBuffer(0.25) as point, Point.STAsText(), PreviousPoint, DistanceFromPrevious FROM GeographyPoints
I have some SQL code which generates random numbers using the following technique:
DECLARE #Random1 INT, #Random2 INT, #Random3 INT, #Random4 INT, #Random5 INT, #Random6 INT, #Upper INT, #Lower INT
---- This will create a random number between 1 and 49
SET #Lower = 1 ---- The lowest random number
SET #Upper = 49; ---- The highest random number
with nums as (
select #lower as n
union all
select nums.n+1
from nums
where nums.n < #Upper
),
randnums as
(select nums.n, ROW_NUMBER() over (order by newid()) as seqnum
from nums
)
select #Random1 = MAX(case when rn.seqnum = 1 then rn.n end),
#Random2 = MAX(case when rn.seqnum = 2 then rn.n end),
#Random3 = MAX(case when rn.seqnum = 3 then rn.n end),
#Random4 = MAX(case when rn.seqnum = 4 then rn.n end),
#Random5 = MAX(case when rn.seqnum = 5 then rn.n end),
#Random6 = MAX(case when rn.seqnum = 6 then rn.n end)
from randnums rn;
select #Random1, #Random2, #Random3, #Random4, #Random5, #Random6
My question is how random is this number generation? and is there another way to do this which is more "random".
I am using:
Microsoft SQL Server 2008 (SP3) - 10.0.5512.0 (X64) Aug 22 2012 19:25:47 Copyright (c) 1988-2008 Microsoft Corporation Developer Edition (64-bit) on Windows NT 6.1 <X64> (Build 7601: Service Pack 1)
The problem with most solutions is you'll end up with values like this: 14,29,8,14,27,27 I cannot have duplicate numbers!
I guess you could do something like this much simpler and much easier
DECLARE #Upper INT;
DECLARE #Lower INT;
SET #Lower = 1; /* -- The lowest random number */
SET #Upper = 49; /* -- The highest random number */
SELECT #Lower + CONVERT(INT, (#Upper-#Lower+1)*RAND());
For getting a random number without repetition, this will do the job
WITH CTE
AS
(
SELECT randomNumber, COUNT(1) countOfRandomNumber
FROM (
SELECT ABS(CAST(NEWID() AS binary(6)) %49) + 1 randomNumber
FROM sysobjects
) sample
GROUP BY randomNumber
)
SELECT TOP 5 randomNumber
FROM CTE
ORDER BY newid()
To set the highest limit, you can replace 49 with your highest limit number.
For Laravel:
public function generatUniqueId()
{
$rand = rand(10000, 99999);
$itemId = $rand;
while (true) {
if (!BookItem::whereBookItemId($itemId)->exists()) {
break;
}
$itemId = rand(10000, 99999);
}
return $itemId;
}
Create a list of random numbers. For this example I did 100, could be more, could be less (but no less than your limit)
Use row_number() function to detect duplicates
Once you delete the duplicates, select top 6 number in your list
with RandomNumbers as
(
select id = 1, number = round(((56 - 1 -1) * RAND(CHECKSUM(NEWID())) + 1), 0),
orderid = round(((56 - 1 -1) * RAND(CHECKSUM(NEWID())) + 1), 0)
union all
select id + 1, round(((56 - 1 -1) * RAND(CHECKSUM(NEWID())) + 1), 0), round(((56 - 1 -1) * RAND(CHECKSUM(NEWID())) + 1), 0)
from RandomNumbers where id < 100
),
NoDuplicates as
(
select number, id = row_number() over (partition by number orderid by order) from
(
select numeber, order, repeat = row_number() over (partition by numeber orderid by order)
from RandomNumbers
) a
where repeat = 1
)
select * from NoDuplicates where id <= 6
You can use Rand() function .
select CEILING(RAND() *<max of random numbers))
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.