The best way to perform such calculation logic in T-SQL - 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.

Related

Rewrite SQL without cursor for Data Warehouse

I have SQL code (main core) below that works fine on SQL Server. How to rewrite it for T-SQL Azure SQL DW, which doesn't like cursor? I didn't find appropriate example and can't do it myself.
CREATE PROCEDURE calc_balance
CREATE TABLE output_table (Amount FLOAT, ValueStart FLOAT, ValueStop FLOAT);
DECLARE cursor_1 CURSOR FOR
SELECT Criteria, Amount, ValueStart, ValueStop
FROM Input_table
OPEN cursor_1
WHILE #fetchstatus = 0
BEGIN
FETCH NEXT FROM cursor_1 INTO #Criteria, #Amount, #ValueStart, #ValueStop;
#ValueStart1 = #balance;
#ValueStop1 = #ValueStart1 + #Amount;
IF (#Criteria = 1) AND (#balance> -100)
BEGIN
#Amount = 0;
#ValueStop1 = #ValueStart;
END;
#balance = #balance + #Amount + #ValueStart;
INSERT INTO output_table
VALUES (#Amount, #ValueStart1, #ValueStop1);
END;
You may want to try this.
with cte as (
select 0 as ctr, 0 as balance, Criteria, Amount, ValueStart, ValueStop, (select count(1) from Input_table) as ct from Input_table
union all
select ctr +1, balance + case when Criteria = 1 and balance >-100 then 0 else Amount end + ValueStart
, Criteria, Amount, balance
, case when Criteria = 1 and balance >-100 then balance + Amount else ValueStart end
, ct
from cte
where ctr < ct
)
Insert into output_table VALUES
(select Amount,ValueStart, ValueStop from cte where ctr>0)

Churn Rate From Contract Start & End Dates

I am trying to calculate the churn rate of customer and all I have to play with is contract start & end dates.
When the date year is 9999 it just means they are still a customer and I would like to consider account only who have a tenure of more than 3 months (as in have been with the company for 3 or more months).
CREATE TABLE #temp
(
ACCOUNT varchar(20)NOT NULL
,CONTRACT_START_DATE DATETIME NOT NULL
,CONTRACT_END_DATE DATETIME NOT NULL
)
;
INSERT INTO #temp
VALUES('64074558792','20160729','20170805');
INSERT INTO #temp
VALUES('54654654664','20160810','20170110');
INSERT INTO #temp
VALUES('21454654764','20160112','20160812');
INSERT INTO #temp
VALUES('21654765134','20160101','20161231');
INSERT INTO #temp
VALUES('13214868794','20160811','99991231');
INSERT INTO #temp
VALUES ('88321546894','20160427','20160627');
SELECT *,
CASE WHEN CONTRACT_END_DATE <> '99991231' THEN DATEDIFF(DAY, CONTRACT_START_DATE,CONTRACT_END_DATE) ELSE null END AS TENURE
FROM #temp
The below will work for a single reporting year. There's a few things it seems like you're trying to get:
-Number of customers who stay longer than x months
-% of customers that stay for more than x months
-% of customers lost during the year
DECLARE #records TABLE
(
ACCOUNT varchar(20)NOT NULL
,CONTRACT_START_DATE DATETIME NOT NULL
,CONTRACT_END_DATE DATETIME NOT NULL
);
DECLARE #ReportingYear INT = 2016;
DECLARE #MinTenureMonths INT = 3;
INSERT INTO #records
VALUES('64074558792','20160729','20170805'),
('54654654664','20160810','20170110'),
('21454654764','20160112','20160812'),
('21654765134','20151011','20161231'),--I changed this one
('13214868794','20160811','99991231'),
('88321546894','20160427','20160627');
SELECT *,
[CustomerGained] = IIF(YEAR(CONTRACT_START_DATE) = #ReportingYear,1,0),
[CustomerLost] = IIF(YEAR(CONTRACT_END_DATE) = #ReportingYear,1,0) ,
[ExistingCustomer] = IIF(CONTRACT_START_DATE < DATEFROMPARTS(#ReportingYear,1,1)
AND CONTRACT_END_DATE > DATEFROMPARTS(#ReportingYear,12,31), 1, 0),
[CurrentCustomer] = IIF(CONTRACT_END_DATE = '99991231 00:00:00.000',1,0),
[MinTenureExceeded] = IIF(DATEDIFF(MONTH, CONTRACT_START_DATE, CONTRACT_END_DATE) >= #MinTenureMonths,1,0)
FROM #records;
With recordCTE AS
(
SELECT
[CustomersGained] = SUM(IIF(YEAR(CONTRACT_START_DATE) = #ReportingYear,1,0)),
[CustomersLost] = SUM(IIF(YEAR(CONTRACT_END_DATE) = #ReportingYear,1,0)),
[MinTenureExceeded] = SUM(IIF(DATEDIFF(MONTH, CONTRACT_START_DATE, CONTRACT_END_DATE) >= #MinTenureMonths,1,0)),
[ExistingCustomers] = SUM(IIF(CONTRACT_START_DATE < DATEFROMPARTS(#ReportingYear,1,1)
AND CONTRACT_END_DATE > DATEFROMPARTS(#ReportingYear,12,31), 1, 0)),
[CurrentCustomer] = SUM(IIF(CONTRACT_END_DATE = '99991231 00:00:00.000',1,0)),
[TotalCustomers] = COUNT(1)
FROM #records
)
SELECT c.CustomersGained,
c.CustomersLost,
c.MinTenureExceeded,
c.ExistingCustomers,
[% Lost] = CAST(((c.CustomersLost * 1.0) / c.TotalCustomers) * 100 AS DECIMAL(14,2)),
[% Gained] = CAST(((c.CustomersGained * 1.0) / c.TotalCustomers) * 100 AS DECIMAL(14,2))
FROM recordCTE c;
You'll need to decide whether you're just looking for a single reporting year, a reporting period, or in total for everything. If you want everything, just do all the calcs using CurrentCustomers instead.

How to update table based on the values in other columns

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]);

SQL Server - loop through table and update based on count

I have a SQL Server database. I need to loop through a table to get the count of each value in the column 'RevID'. Each value should only be in the table a certain number of times - for example 125 times. If the count of the value is greater than 125 or less than 125, I need to update the column to ensure all values in the RevID (are over 25 different values) is within the same range of 125 (ok to be a few numbers off)
For example, the count of RevID = "A2" is = 45 and the count of RevID = 'B2' is = 165 then I need to update RevID so the 45 count increases and the 165 decreases until they are within the 125 range.
This is what I have so far:
DECLARE #i INT = 1,
#RevCnt INT = SELECT RevId, COUNT(RevId) FROM MyTable group by RevId
WHILE(#RevCnt >= 50)
BEGIN
UPDATE MyTable
SET RevID= (SELECT COUNT(RevID) FROM MyTable)
WHERE RevID < 50)
#i = #i + 1
END
I have also played around with a cursor and instead of trigger. Any idea on how to achieve this? Thanks for any input.
Okay I cam back to this because I found it interesting even though clearly there are some business rules/discussion that you and I and others are not seeing. anyway, if you want to evenly and distribute arbitrarily there are a few ways you could do it by building recursive Common Table Expressions [CTE] or by building temp tables and more. Anyway here is a way that I decided to give it a try, I did utilize 1 temp table because sql was throwing in a little inconsistency with the main logic table as a cte about every 10th time but the temp table seems to have cleared that up. Anyway, this will evenly spread RevId arbitrarily and randomly assigning any remainder (# of Records / # of RevIds) to one of the RevIds. This script also doesn't rely on having a UniqueID or anything it works dynamically over row numbers it creates..... here you go just subtract out test data etc and you have what you more than likely want. Though rebuilding the table/values would probably be easier.
--Build Some Test Data
DECLARE #Table AS TABLE (RevId VARCHAR(10))
DECLARE #C AS INT = 1
WHILE #C <= 400
BEGIN
IF #C <= 200
BEGIN
INSERT INTO #Table (RevId) VALUES ('A1')
END
IF #c <= 170
BEGIN
INSERT INTO #Table (RevId) VALUES ('B2')
END
IF #c <= 100
BEGIN
INSERT INTO #Table (RevId) VALUES ('C3')
END
IF #c <= 400
BEGIN
INSERT INTO #Table (RevId) VALUES ('D4')
END
IF #c <= 1
BEGIN
INSERT INTO #Table (RevId) VALUES ('E5')
END
SET #C = #C+ 1
END
--save starting counts of test data to temp table to compare with later
IF OBJECT_ID('tempdb..#StartingCounts') IS NOT NULL
BEGIN
DROP TABLE #StartingCounts
END
SELECT
RevId
,COUNT(*) as Occurences
INTO #StartingCounts
FROM
#Table
GROUP BY
RevId
ORDER BY
RevId
/************************ This is the main method **********************************/
--clear temp table that is the main processing logic
IF OBJECT_ID('tempdb..#RowNumsToChange') IS NOT NULL
BEGIN
DROP TABLE #RowNumsToChange
END
--figure out how many records there are and how many there should be for each RevId
;WITH cteTargetNumbers AS (
SELECT
RevId
--,COUNT(*) as RevIdCount
--,SUM(COUNT(*)) OVER (PARTITION BY 1) / COUNT(*) OVER (PARTITION BY 1) +
--CASE
--WHEN ROW_NUMBER() OVER (PARTITION BY 1 ORDER BY NEWID()) <=
--SUM(COUNT(*)) OVER (PARTITION BY 1) % COUNT(*) OVER (PARTITION BY 1)
--THEN 1
--ELSE 0
--END as TargetNumOfRecords
,SUM(COUNT(*)) OVER (PARTITION BY 1) / COUNT(*) OVER (PARTITION BY 1) +
CASE
WHEN ROW_NUMBER() OVER (PARTITION BY 1 ORDER BY NEWID()) <=
SUM(COUNT(*)) OVER (PARTITION BY 1) % COUNT(*) OVER (PARTITION BY 1)
THEN 1
ELSE 0
END - COUNT(*) AS NumRecordsToUpdate
FROM
#Table
GROUP BY
RevId
)
, cteEndRowNumsToChange AS (
SELECT *
,SUM(CASE WHEN NumRecordsToUpdate > 1 THEN NumRecordsToUpdate ELSE 0 END)
OVER (PARTITION BY 1 ORDER BY RevId) AS ChangeEndRowNum
FROM
cteTargetNumbers
)
SELECT
*
,LAG(ChangeEndRowNum,1,0) OVER (PARTITION BY 1 ORDER BY RevId) as ChangeStartRowNum
INTO #RowNumsToChange
FROM
cteEndRowNumsToChange
;WITH cteOriginalTableRowNum AS (
SELECT
RevId
,ROW_NUMBER() OVER (PARTITION BY RevId ORDER BY (SELECT 0)) as RowNumByRevId
FROM
#Table t
)
, cteRecordsAllowedToChange AS (
SELECT
o.RevId
,o.RowNumByRevId
,ROW_NUMBER() OVER (PARTITION BY 1 ORDER BY (SELECT 0)) as ChangeRowNum
FROM
cteOriginalTableRowNum o
INNER JOIN #RowNumsToChange t
ON o.RevId = t.RevId
AND t.NumRecordsToUpdate < 0
AND o.RowNumByRevId <= ABS(t.NumRecordsToUpdate)
)
UPDATE o
SET RevId = u.RevId
FROM
cteOriginalTableRowNum o
INNER JOIN cteRecordsAllowedToChange c
ON o.RevId = c.RevId
AND o.RowNumByRevId = c.RowNumByRevId
INNER JOIN #RowNumsToChange u
ON c.ChangeRowNum > u.ChangeStartRowNum
AND c.ChangeRowNum <= u.ChangeEndRowNum
AND u.NumRecordsToUpdate > 0
IF OBJECT_ID('tempdb..#RowNumsToChange') IS NOT NULL
BEGIN
DROP TABLE #RowNumsToChange
END
/***************************** End of Main Method *******************************/
-- Compare the results and clean up
;WITH ctePostUpdateResults AS (
SELECT
RevId
,COUNT(*) as AfterChangeOccurences
FROM
#Table
GROUP BY
RevId
)
SELECT *
FROM
#StartingCounts s
INNER JOIN ctePostUpdateResults r
ON s.RevId = r.RevId
ORDER BY
s.RevId
IF OBJECT_ID('tempdb..#StartingCounts') IS NOT NULL
BEGIN
DROP TABLE #StartingCounts
END
Since you've given no rules for how you'd like the balance to operate we're left to speculate. Here's an approach that would find the most overrepresented value and then find an underrepresented value that can take on the entire overage.
I have no idea how optimal this is and it will probably run in an infinite loop without more logic.
declare #balance int = 125;
declare #cnt_over int;
declare #cnt_under int;
declare #revID_overrepresented varchar(32);
declare #revID_underrepresented varchar(32);
declare #rowcount int = 1;
while #rowcount > 0
begin
select top 1 #revID_overrepresented = RevID, #cnt_over = count(*)
from T
group by RevID
having count(*) > #balance
order by count(*) desc
select top 1 #revID_underrepresented = RevID, #cnt_under = count(*)
from T
group by RevID
having count(*) < #balance - #cnt_over
order by count(*) desc
update top #cnt_over - #balance T
set RevId = #revID_underrepresented
where RevId = #revID_overrepresented;
set #rowcount = ##rowcount;
end
The problem is I don't even know what you mean by balance...You say it needs to be evenly represented but it seems like you want it to be 125. 125 is not "even", it is just 125.
I can't tell what you are trying to do, but I'm guessing this is not really an SQL problem. But you can use SQL to help. Here is some helpful SQL for you. You can use this in your language of choice to solve the problem.
Find the rev values and their counts:
SELECT RevID, COUNT(*)
FROM MyTable
GROUP BY MyTable
Update #X rows (with RevID of value #RevID) to a new value #NewValue
UPDATE TOP #X FROM MyTable
SET RevID = #NewValue
WHERE RevID = #RevID
Using these two queries you should be able to apply your business rules (which you never specified) in a loop or whatever to change the data.

distribute value to all rows while updating table

I have table structure like tblCustData
ID UserID Fee FeePaid
1 12 150 0
2 12 100 0
3 12 50 0
And value to be update in FeePaid Column such that if i have value in #Amt variable in 200 Then it should update any two rows
Output should be like
ID UserID Fee FeePaid
1 12 150 150
2 12 100 50
3 12 50 0
FeePaid should not be grater than Fee Column But if i pass 350 in #Amt variable it should produce output like
ID UserID Fee FeePaid
1 12 150 200
2 12 100 100
3 12 50 50
Only if #Amt is exceeding the total value in Fee column
I can not think beyond this query
Update tblCustData
Set FeePaid=#Amt
Where UserID=12
First with CTE syntax we prepare a table with sums distribution and then using unique field Code update the main table using CASE to handle all possible ways (including first row with remainder).
Declare #Amt int;
SET #Amt=250;
with T as
(
SELECT ROW_NUMBER() OVER (ORDER BY Fee desc) as rn, *
FROM tblCustData WHERE UserId=12
)
,T2 as
(
SELECT *,
ISNULL((SELECT SUM(Fee-FeePaid) FROM T WHERE T1.RN<RN),0) as PrevSum
FROM T as T1
)
UPDATE
A
SET A.FeePaid = A.FeePaid+ CASE WHEN (B.PrevSum+B.Fee-B.FeePaid<=#Amt)
AND (B.RN<>1)
THEN B.Fee-B.FeePaid
WHEN (B.PrevSum+B.Fee-B.FeePaid<=#Amt) AND (B.RN=1)
THEN #Amt-B.PrevSum
WHEN B.PrevSum>=#Amt
THEN 0
WHEN B.PrevSum+B.Fee-B.FeePaid>#Amt
THEN #Amt-B.PrevSum
END
FROM
tblCustData A
JOIN T2 B ON A.Code = B.Code
GO
SQLFiddle demo
Try ..
declare #t table (id int identity, UserId int, Fee money, FeePaid money)
insert into #t (UserID, Fee, FeePaid)
values
(12, 150, 0)
,(12, 100, 0)
,(12, 50 , 0)
declare #amt money = 200; -- change to 400 to test over paid
declare #Fees money;
select #Fees = sum(Fee) from #t;
declare #derivedt table (deid int, id int, UserId int, Fee money, FeePaid money)
insert into #derivedt (deid, id, UserId, Fee, FeePaid)
select row_number() over (order by case when #amt <= #Fees then id else -id end asc), id, UserId, Fee, FeePaid
from #t
-- order by case when #amt <= #Fees then id else -id end asc
; with cte(deid, id, UserId, Fee, FeePaid, Remainder)
as
(
select 0 as deid, 0 as id, 0 as UserId, cast(0.00 as money) as Fee, cast(0.00 as money) as FeePaid , #Amt as Remainder
from #derivedt
where id = 1
union all
select t.deid, t.id, t.UserId, t.Fee, case when cte.Remainder > t.Fee then t.Fee else cte.Remainder end as FeePaid
, case when cte.Remainder > t.Fee then cte.Remainder - t.Fee else 0 end as Remainder
from #derivedt t inner join cte cte on t.deid = (cte.deid + 1)
)
update origt
set FeePaid = det.FeePaid
from #t origt
inner join
(
select cte1.deid, cte1.id, cte1.UserId, cte1.Fee, cte1.FeePaid + ISNULL(cte2.Remainder, 0) as FeePaid
from cte cte1
left outer join (select top 1 deid, Remainder from cte order by deid desc) cte2
on cte1.deid = cte2.deid
where cte1.deid > 0
) det
on origt.id = det.id
select *
from #t
Modified to continuous update of value..
-- Create table once and insert into table once
create table #t (id int identity, UserId int, Fee money, FeePaid money)
insert into #t (UserID, Fee, FeePaid)
values
(12, 150, 0)
,(12, 100, 0)
,(12, 50 , 0)
-- ===============================
-- Run multiple times to populate #t table
declare #amt money = 100; -- change to 400 to test over paid
declare #Fees money;
select #Fees = sum(Fee - FeePaid) from #t;
declare #derivedt table (deid int, id int, UserId int, Fee money, FeePaid money)
insert into #derivedt (deid, id, UserId, Fee, FeePaid)
select row_number() over (order by case when #amt <= #Fees then id else -id end asc), id, UserId, (Fee - FeePaid) as Fee, FeePaid
from #t
-- order by case when #amt <= #Fees then id else -id end asc
; with cte(deid, id, UserId, Fee, FeePaid, Remainder)
as
(
select 0 as deid, 0 as id, 0 as UserId, cast(0.00 as money) as Fee, cast(0.00 as money) as FeePaid , #Amt as Remainder
from #derivedt
where id = 1
union all
select t.deid, t.id, t.UserId, t.Fee, case when cte.Remainder >= t.Fee then t.Fee else cte.Remainder end as FeePaid
, case when cte.Remainder >= t.Fee then cte.Remainder - t.Fee else 0 end as Remainder
from #derivedt t inner join cte cte on t.deid = (cte.deid + 1)
)
update origt
set FeePaid = origt.FeePaid + det.FeePaid
from #t origt
inner join
(
select cte1.deid, cte1.id, cte1.UserId, cte1.Fee, cte1.FeePaid + ISNULL(cte2.Remainder, 0) as FeePaid, cte1.Remainder
from cte cte1
left outer join (select top 1 deid, Remainder from cte order by deid desc) cte2
on cte1.deid = cte2.deid
where cte1.deid > 0
) det
on origt.id = det.id
select *
from #t
-- Drop temp table after
-- drop table #t
Apart from your code, I added an identity column to your table. See the code.
DECLARE #TAB TABLE(ID INT IDENTITY(1,1),USERID INT, FEE INT, FEEPAID INT)
INSERT INTO #TAB VALUES (12,150,0),(12,100,0),(12,50,0)
DECLARE #AMOUNT INT = 230,
#AMOUNTNEW INT = 0,
#B INT = 1,
#S INT = 1,#E INT = (SELECT COUNT(*) FROM #TAB)
WHILE #S <= #E
BEGIN
UPDATE LU
SET LU.FEEPAID = CASE WHEN #AMOUNT >= FEE THEN FEE ELSE #AMOUNT END
FROM #TAB LU
WHERE LU.ID = #S
SET #AMOUNT = #AMOUNT - (SELECT FEE FROM #TAB WHERE ID = #S)
IF #AMOUNT <= 0
SET #S = #E
SET #S = #S + 1
END
SELECT * FROM #TAB
Result:
I hope the idea is clear, we can work from here.