SQL Server Dynamic Resetting Running Balance - sql-server
My current issue is that I have a running balance, where one value falls below another the running balance needs to reset. But not only reset, but also use a another value as its starting value and start the balance again.
Below is the table with data in it:
+-------------+--------+---------------------+-------------------+--------------+-------------+
| Tran_DateSK | Amount | Running_AccountFees | Overlimit_Balance | Restart_Calc | Actual_Calc |
+-------------+--------+---------------------+-------------------+--------------+-------------+
| 20200217 | 39 | 39 | 3867.76 | 0 | 39 |
| 20200217 | 50 | 89 | 3867.76 | 0 | 89 |
| 20200316 | 39 | 128 | 4735.52 | 0 | 128 |
| 20200316 | 50 | 178 | 4735.52 | 0 | 178 |
| 20200324 | 50 | 228 | 2685.52 | 0 | 228 |
| 20200330 | 50 | 278 | 49.52 | 1 | 49.52 |
| 20200415 | 39 | 317 | 49.52 | 1 | 49.52 |
| 20200515 | 39 | 356 | 3917.28 | 0 | 88.52 |
| 20200515 | 50 | 406 | 3917.28 | 0 | 138.52 |
| 20200519 | 50 | 456 | 3467.28 | 0 | 188.52 |
| 20200604 | 50 | 506 | 3017.28 | 0 | 238.52 |
| 20200609 | 50 | 556 | 2167.28 | 0 | 288.52 |
| 20200611 | 50 | 606 | 49.28 | 1 | 49.28 |
| 20200615 | 39 | 645 | 3917.04 | 0 | 88.28 |
| 20200615 | 50 | 695 | 3917.04 | 0 | 138.28 |
| 20200616 | 50 | 745 | 3017.04 | 0 | 188.28 |
| 20200616 | 50 | 795 | 3017.04 | 0 | 238.28 |
| 20200619 | 50 | 845 | 2567.04 | 0 | 288.28 |
| 20200624 | 50 | 895 | 47.04 | 1 | 47.04 |
| 20200715 | 39 | 934 | 47.04 | 1 | 47.04 |
+-------------+--------+---------------------+-------------------+--------------+-------------+
Actual Calc is the desired outcome and Running account fees is the issue.
Running account fees is the running balance of "Amount" and overlimit_balance is the test. We need to see that the running_accountfees isn't greater than over limit,
If it is, take overlimits value and start calculating again by adding amount on again.
My query that produced this:
SELECT
[Transaction].ReportDateSK AS 'Tran_DateSK'
,[Transaction].AmountChange/100.00 AS 'Amount'
,SUM([Transaction].AmountChange/100.00)
OVER (PARTITION BY [Transaction].AccountSK
ORDER BY [Transaction].ReportDateSK
ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS 'Running_AccountFees'
,[Summary].Overlimit_Balance AS 'Overlimit_Balance'
,CASE
WHEN SUM([Transaction].AmountChange/100.00)
OVER (PARTITION BY [Transaction].AccountSK
ORDER BY [Transaction].ReportDateSK
ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) > [Summary].Overlimit_Balance
THEN 1
ELSE 0
END AS 'Restart_Calc'
,'' AS 'Actual_Calc'
FROM
Fact.[Transaction] [Transaction]
INNER JOIN Fact.AccountSummary [Summary] ON [Summary].DateSK = [Transaction].ReportDateSK
AND [Summary].AccountSK = [Transaction].AccountSK
AND [Summary].[Current] = 1
WHERE IsFeeTransaction = 1
AND [Transaction].AccountSK = 725
AND [Transaction].ReportDateSK BETWEEN 20200217 AND 20200730
Realised that the data in your question is essentially the source data and have been able to come up with the below. It isn't exactly pretty but it provides the correct output. Explanations on how it works are in the comments:
declare #t table(Tran_DateSK int, Amount decimal(10,2), Running_AccountFees int, Overlimit_Balance decimal(10,2), Restart_Calc bit, Actual_Calc decimal(10,2));
insert into #t values(20200217,39,39,3867.76,0,39),(20200217,50,89,3867.76,0,89),(20200316,39,128,4735.52,0,128),(20200316,50,178,4735.52,0,178),(20200324,50,228,2685.52,0,228),(20200330,50,278,49.52,1,49.52),(20200415,39,317,49.52,1,49.52),(20200515,39,356,3917.28,0,88.52),(20200515,50,406,3917.28,0,138.52),(20200519,50,456,3467.28,0,188.52),(20200604,50,506,3017.28,0,238.52),(20200609,50,556,2167.28,0,288.52),(20200611,50,606,49.28,1,49.28),(20200615,39,645,3917.04,0,88.28),(20200615,50,695,3917.04,0,138.28),(20200616,50,745,3017.04,0,188.28),(20200616,50,795,3017.04,0,238.28),(20200619,50,845,2567.04,0,288.28),(20200624,50,895,47.04,1,47.04),(20200715,39,934,47.04,1,47.04);
with t as
(
select Tran_DateSK
,Amount
-- Check if the Running_AccountFees are over the Overlimit_Balance
,case when sum(Amount) over (order by Tran_DateSK,Amount,Overlimit_Balance rows unbounded preceding) > Overlimit_Balance
-- If so, check if the Running_AccountFees in the previous row were also over the Overlimit_Balance
then case when (sum(Amount) over (order by Tran_DateSK,Amount,Overlimit_Balance rows unbounded preceding) - Amount) > lag(Overlimit_Balance,1,0) over (order by Tran_DateSK,Amount,Overlimit_Balance)
then 0 -- and in those instances this means multiple Restart_Calcs in a row, so set the Amount to zero as we don't want to increase the fees when calculating the Actual_Calc
else Amount
end
else Amount
end as Amount_Adj
,sum(Amount) over (order by Tran_DateSK,Amount,Overlimit_Balance rows unbounded preceding) as Running_AccountFees
,lag(Overlimit_Balance,1,0) over (order by Tran_DateSK,Amount,Overlimit_Balance) as Prev_Overlimit_Balance
,Overlimit_Balance
,case when sum(Amount) over (order by Tran_DateSK,Amount,Overlimit_Balance rows unbounded preceding) > Overlimit_Balance
then 1
else 0
end as Restart_Calc
from #t
)
,b as
(
select *
,case when Running_AccountFees > Overlimit_Balance -- If this row is the first in a possible series of balance resets
and sum(Amount_Adj) over (order by Tran_DateSK,Amount,Overlimit_Balance rows between unbounded preceding and 1 preceding) <= Prev_Overlimit_Balance
then Overlimit_Balance -- Take the Overlimit_Balance and subtract the *Adjusted* Running_AccountFees
- sum(Amount_Adj) over (order by Tran_DateSK,Amount,Overlimit_Balance rows between unbounded preceding and 1 preceding)
- Amount_Adj
else 0
end as Reset_Bal
from t
)
select Tran_DateSK
,Amount
,Running_AccountFees
,Overlimit_Balance
,Restart_Calc
-- For each *Adjusted* Running_AccountFees, apply the most negative Reset_Bal value, as this will contain the entire amount that needs to be reset from the current *Adjusted* Running_AccountFees to get the correct Balance_Calc
,sum(Amount_Adj) over (order by Tran_DateSK,Amount,Overlimit_Balance rows unbounded preceding)
+ min(Reset_Bal) over (order by Tran_DateSK,Amount,Overlimit_Balance rows unbounded preceding)
as Balance_Calc
from b
order by Tran_DateSK;
Output
+-------------+--------+---------------------+-------------------+--------------+--------------+
| Tran_DateSK | Amount | Running_AccountFees | Overlimit_Balance | Restart_Calc | Balance_Calc |
+-------------+--------+---------------------+-------------------+--------------+--------------+
| 20200217 | 39.00 | 39.00 | 3867.76 | 0 | 39.00 |
| 20200217 | 50.00 | 89.00 | 3867.76 | 0 | 89.00 |
| 20200316 | 39.00 | 128.00 | 4735.52 | 0 | 128.00 |
| 20200316 | 50.00 | 178.00 | 4735.52 | 0 | 178.00 |
| 20200324 | 50.00 | 228.00 | 2685.52 | 0 | 228.00 |
| 20200330 | 50.00 | 278.00 | 49.52 | 1 | 49.52 |
| 20200415 | 39.00 | 317.00 | 49.52 | 1 | 49.52 |
| 20200515 | 39.00 | 356.00 | 3917.28 | 0 | 88.52 |
| 20200515 | 50.00 | 406.00 | 3917.28 | 0 | 138.52 |
| 20200519 | 50.00 | 456.00 | 3467.28 | 0 | 188.52 |
| 20200604 | 50.00 | 506.00 | 3017.28 | 0 | 238.52 |
| 20200609 | 50.00 | 556.00 | 2167.28 | 0 | 288.52 |
| 20200611 | 50.00 | 606.00 | 49.28 | 1 | 49.28 |
| 20200615 | 39.00 | 645.00 | 3917.04 | 0 | 88.28 |
| 20200615 | 50.00 | 695.00 | 3917.04 | 0 | 138.28 |
| 20200616 | 50.00 | 745.00 | 3017.04 | 0 | 188.28 |
| 20200616 | 50.00 | 795.00 | 3017.04 | 0 | 238.28 |
| 20200619 | 50.00 | 845.00 | 2567.04 | 0 | 288.28 |
| 20200624 | 50.00 | 895.00 | 47.04 | 1 | 47.04 |
| 20200715 | 39.00 | 934.00 | 47.04 | 1 | 47.04 |
+-------------+--------+---------------------+-------------------+--------------+--------------+
Related
SQL Server Lag by partitioned group
I have a table of data as follows: +----+-------+----------+ | id | value | group_id | +----+-------+----------+ | 1 | -200 | 0 | | 2 | -620 | 0 | | 3 | -310 | 0 | | 4 | 400 | 1 | | 5 | 300 | 1 | | 6 | 100 | 1 | | 7 | -200 | 2 | | 8 | -400 | 2 | | 9 | -500 | 2 | +----+-------+----------+ What I would like to do is produce a 4th column that, for each record, shows the last value of the preceding group_id. So the result I want is as follows: +----+-------+----------+----------------+ | id | value | group_id | LastValByGroup | +----+-------+----------+----------------+ | 1 | -200 | 0 | 0 | | 2 | -620 | 0 | 0 | | 3 | -310 | 0 | 0 | | 4 | 400 | 1 | -310 | | 5 | 300 | 1 | -310 | | 6 | 100 | 1 | -310 | | 7 | -200 | 2 | 100 | | 8 | -400 | 2 | 100 | | 9 | -500 | 2 | 100 | +----+-------+----------+----------------+ What I have done so far is in 2 parts. First I use the LAST_VALUE function to get the last Value in each group. Then I have tried to use the LAG function to get the last value from the previous group. Unfortunately the second part of my code isn't working as desired. Here is my code: CREATE TABLE #temp ( id int identity(1,1), value int, group_id int ) INSERT #temp VALUES(-200,0) INSERT #temp VALUES(-620,0) INSERT #temp VALUES(-310,0) INSERT #temp VALUES(400,1) INSERT #temp VALUES(300,1) INSERT #temp VALUES(100,1) INSERT #temp VALUES(-200,3) INSERT #temp VALUES(-400,3) INSERT #temp VALUES(-500,3) ;WITH cte AS ( SELECT *, LastValByGroup = LAST_VALUE(Value) OVER(Partition By group_id ORDER BY id RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM #temp ), lagged AS ( SELECT *, LaggedLastValByGroup = LAG(LastValByGroup,1,0) OVER(Partition By group_id ORDER BY id) FROM cte ) SELECT * FROM lagged ORDER BY id DROP TABLE #temp And this is the result I get: +----+-------+----------+----------------+----------------------+ | id | value | group_id | LastValByGroup | LaggedLastValByGroup | +----+-------+----------+----------------+----------------------+ | 1 | -200 | 0 | -310 | 0 | | 2 | -620 | 0 | -310 | -310 | | 3 | -310 | 0 | -310 | -310 | | 4 | 400 | 1 | 100 | 0 | | 5 | 300 | 1 | 100 | 100 | | 6 | 100 | 1 | 100 | 100 | | 7 | -200 | 3 | -500 | 0 | | 8 | -400 | 3 | -500 | -500 | | 9 | -500 | 3 | -500 | -500 | +----+-------+----------+----------------+----------------------+ Any help is much appreciated. Thanks
You can use first_value like following to get the desired result. select distinct t2.*, ISNULL(FIRST_VALUE(t1.[value]) over(partition by t1.group_id order by t1.id desc), 0) LastValByGroup from #data t1 right join #data t2 on t1.group_id + 1 = t2.group_id Please find the db<>fiddle here.
Pivoting a table with multiple columns in SQL
My goal here is to take a list of two corresponding store numbers and provide an output similar to: Ultimate goal: produce a list of closest stores by travel time and distance based on source data of 2 rows per zip9 where each row is the travel time in distance, and in time, to a store in question. The result is that each zip code has 2 stores to choose from, and the requirement is being able to return one row with both options. +-----------+---------------+---------------------+-------------------+-------------------------+ | zip | Shortest_time | Shortest_time_store | Shortest_distance | Shortest_distance_store | +-----------+---------------+---------------------+-------------------+-------------------------+ | 70011134 | 38.7035 | 75 | 21.3124 | 115 | | 70011186 | 38.4841 | 75 | 21.4144 | 115 | | 70011207 | 39.1567 | 75 | 21.1826 | 115 | | 100013232 | 22.976 | 145 | 9.5031 | 115 | | 112075140 | 21.888 | 145 | 7.3705 | 115 | +-----------+---------------+---------------------+-------------------+-------------------------+ Original dataset +---------------+--------------------------+-----------------------+------------------+ | CORRECTED_ZIP | SourceOrganizationNumber | Travel Time (Minutes) | Distance (Miles) | +---------------+--------------------------+-----------------------+------------------+ | 70011134 | 75 | 38.7035 | 26.8628 | | 70011134 | 115 | 39.3969 | 21.3124 | | 70011186 | 75 | 38.4841 | 26.7609 | | 70011186 | 115 | 39.6389 | 21.4144 | | 70011207 | 75 | 39.1567 | 31.2771 | | 70011207 | 115 | 39.188 | 21.1826 | | 100013232 | 115 | 28.6561 | 9.50311 | | 100013232 | 145 | 22.976 | 10.0307 | | 112075140 | 115 | 36.1803 | 7.37053 | | 112075140 | 145 | 21.888 | 9.50123 | +---------------+--------------------------+-----------------------+------------------+ Dataset after I've modified it with this query: SELECT TOP 1000 [corrected_zip] , TRY_CONVERT( DECIMAL(18, 4), ROUND([Travel Time (Minutes)], 4)) AS [Unit of Measurement] , [SourceOrganizationNumber] , 'Time' AS [Type] FROM [db].[dbo].[my_table_A] [tt] WHERE [tt].[CORRECTED_ZIP] IN('070011134', '070011186', '070011207', '112075140', '100013232') AND [Travel Time (Minutes)] IN ( SELECT MIN([Travel Time (Minutes)]) FROM [db].[dbo].[my_table_A] WHERE [CORRECTED_ZIP] = [tt].[CORRECTED_ZIP] GROUP BY [CORRECTED_ZIP] ) UNION ALL SELECT TOP 1000 [corrected_zip] , TRY_CONVERT( DECIMAL(18, 4), ROUND([Distance (Miles)], 4)) , [SourceOrganizationNumber] , 'Distance' FROM [db].[dbo].[my_table_A] [tt] WHERE [tt].[CORRECTED_ZIP] IN('070011134', '070011186', '070011207', '112075140', '100013232') AND [Distance (Miles)] IN ( SELECT MIN([Distance (Miles)]) FROM [db].[dbo].[my_table_A] WHERE [CORRECTED_ZIP] = [tt].[CORRECTED_ZIP] GROUP BY [CORRECTED_ZIP] ) ORDER BY [CORRECTED_ZIP]; +---------------+---------------------+--------------------------+----------+ | corrected_zip | Unit of Measurement | SourceOrganizationNumber | Type | +---------------+---------------------+--------------------------+----------+ | 70011134 | 38.7035 | 75 | Time | | 70011134 | 21.3124 | 115 | Distance | | 70011186 | 21.4144 | 115 | Distance | | 70011186 | 38.4841 | 75 | Time | | 70011207 | 39.1567 | 75 | Time | | 70011207 | 21.1826 | 115 | Distance | | 100013232 | 9.5031 | 115 | Distance | | 100013232 | 22.976 | 145 | Time | | 112075140 | 21.888 | 145 | Time | | 112075140 | 7.3705 | 115 | Distance | +---------------+---------------------+--------------------------+----------+ Data after I attempted to pivot it +---------------+--------------------------+----------+---------+ | corrected_zip | SourceOrganizationNumber | Distance | Time | +---------------+--------------------------+----------+---------+ | 070011134 | 115 | 21.3124 | NULL | | 070011134 | 75 | NULL | 38.7035 | | 070011186 | 115 | 21.4144 | NULL | | 070011186 | 75 | NULL | 38.4841 | | 070011207 | 115 | 21.1826 | NULL | | 070011207 | 75 | NULL | 39.1567 | | 100013232 | 115 | 9.5031 | NULL | | 100013232 | 145 | NULL | 22.9760 | | 112075140 | 115 | 7.3705 | NULL | | 112075140 | 145 | NULL | 21.8880 | +---------------+--------------------------+----------+---------+ It seems like my issue is picking the correct store ID as opposed to grouping by store ID?
You can use row_number() twice in a subquery(once to rank by time, another by distance), and then do conditional aggregation in the outer query: select corrected_zip, min(travel_time) shortest_time, min(case when rnt = 1 then source_organization_number end) shortest_time_store, min(distance) shortest_distance, min(case when rnd = 1 then source_organization_number end) shortest_distance_store from ( select t.*, row_number() over(partition by corrected_zip order by travel_time) rnt, row_number() over(partition by corrected_zip order by distance) rnd from mytable t ) t group by corrected_zip
How to find max(sortnumber) on item code in SQL Server?
I have following SQL Server table ITEM: +------------+-----------+------+--------+-----------+------------+ | Date | item_code | name | in/out | total_qty | SortNumber | +------------+-----------+------+--------+-----------+------------+ | 08/07/2019 | 001 | A | -50 | 100 | 8 | | 07/07/2019 | 001 | A | 50 | 100 | 7 | | 06/07/2019 | 003 | C | 25 | 25 | 6 | | 05/07/2019 | 001 | A | 50 | 50 | 5 | | 04/07/2019 | 002 | B | 100 | 200 | 4 | | 03/07/2019 | 003 | C | -25 | 0 | 3 | | 02/07/2019 | 003 | C | 25 | 25 | 2 | | 01/07/2019 | 002 | B | 100 | 100 | 1 | +------------+-----------+------+--------+-----------+------------+ I've tried: select itemcode, max(Sort_Number) from ITEM group by item_code order by item_code asc but I want result: +---------------------+-----------+------------------+ | Distinct(item_code) | Total_qty | Max(Sort_Number) | +---------------------+-----------+------------------+ | 001 | 100 | 8 | | 002 | 200 | 4 | | 003 | 25 | 6 | +---------------------+-----------+------------------+ Can anyone help me?
The below query gives you the desired result - With cteItem as ( select item_code, total_qty, SortNumber, Row_Number() over (partition by item_code order by SortNumber desc) maxSortNumber from ITEM ) select item_code, total_qty, SortNumber from cteItem where maxSortNumber = 1
just need to add max(sort_number) to your query select item_code ,max(total_qty), max(sort_number) from ITEM group by item_code order by item_code asc
T-SQL: Values are grouped by month, if there is no value for a month the month should also appear and display "NULL"
i have a SQL that displays turnover, stock and other values for stores grouped by month. Logically, if there is no value for a month, the month doesn't appear. The target is that the empty month should appear and display "NULL" for the values. The empty months should range from the #FROM to the #TO parameter (201807 to 201907) in this case. Before: +-------+--------+----------+----------+-------+ | Store | Month | Incoming | Turnover | Stock | +-------+--------+----------+----------+-------+ | 123 | 201810 | 5 | 4 | 1 | | 123 | 201811 | 0 | 1 | 0 | | 123 | 201901 | 25 | 5 | 20 | | 123 | 201902 | 5 | 10 | 15 | | 123 | 201903 | 8 | 9 | 14 | | 123 | 201904 | 5 | 4 | 15 | | 123 | 201905 | 10 | 5 | 20 | +-------+--------+----------+----------+-------+ After: +-------+--------+----------+----------+-------+ | Store | Month | Incoming | Turnover | Stock | +-------+--------+----------+----------+-------+ | 123 | 201807 | NULL | NULL | NULL | | 123 | 201808 | NULL | NULL | NULL | | 123 | 201809 | NULL | NULL | NULL | | 123 | 201810 | 5 | 4 | 1 | | 123 | 201811 | 0 | 1 | 0 | | 123 | 201812 | NULL | NULL | NULL | | 123 | 201901 | 25 | 5 | 20 | | 123 | 201902 | 5 | 10 | 15 | | 123 | 201903 | 8 | 9 | 14 | | 123 | 201904 | 5 | 4 | 15 | | 123 | 201905 | 10 | 5 | 20 | | 123 | 201906 | NULL | NULL | NULL | | 123 | 201907 | NULL | NULL | NULL | +-------+--------+----------+----------+-------+ Code Example: db<>fiddle I have absolutely no idea how to solve this and will thank you in advance for your help! :)
You can try to use cte recursive make a calendar table, then do outer-join ;WITH CTE AS ( SELECT CAST(CAST(#FROM AS VARCHAR(10)) + '01' AS DATE) fromDt, CAST(CAST(#TO AS VARCHAR(10)) + '01' AS DATE) toDt, Store FROM (SELECT DISTINCT Store FROM #Test) t1 UNION ALL SELECT DATEADD(MONTH,1,fromDt),toDt,Store FROM CTE WHERE DATEADD(MONTH,1,fromDt) <= toDt ) SELECT FORMAT(fromDt,'yyyyMM') Month, c.Store, t.Incoming, t.Turnover, t.Stock FROM CTE c LEFT JOIN #Test t on c.fromDt = CAST(CAST(t.Month AS VARCHAR(10)) + '01' AS DATE) and c.Store = t.Store sqlfiddle
Cumulative Count of NULL restarting at NOT NULL
I would like to add a column indicating the number invites a person received before they accepted by incrementally counting the number of null columns before a non-null while partitioning over the PERSON_ID and ordering by the INVITED_DATE. My table has the following format: | UNIQUE_ID | PERSON_ID | INVITED_DATE | ACCEPTED_DATE | | 12345 | 567 | 12-01-18 | NULL | | 12346 | 567 | 12-02-18 | NULL | | 12347 | 567 | 12-03-18 | NULL | | 12348 | 567 | 12-04-18 | 12-04-18 | | 12349 | 567 | 12-05-18 | NULL | | 12350 | 568 | 12-01-18 | NULL | | 12351 | 568 | 12-02-18 | 12-02-18 | The output should ideally look like the following: | UNIQUE_ID | PERSON_ID | INVITED_DATE | ACCEPTED_DATE | INVITES_BEFORE_ACCEPT | | 12345 | 567 | 12-01-18 | NULL | 1 | | 12346 | 567 | 12-02-18 | NULL | 2 | | 12347 | 567 | 12-03-18 | NULL | 3 | | 12348 | 567 | 12-04-18 | 12-04-18 | 0 | | 12349 | 567 | 12-05-18 | NULL | 1 | | 12350 | 568 | 12-01-18 | NULL | 1 | | 12351 | 568 | 12-02-18 | 12-02-18 | 0 | So far I've tried a number iterations of ROW NUMBER with OVER and PARTITION but I've found it will need to be an OUTER APPLY. The following OUTER APPLY counts over the data but doesn't restart the count with a successful accept. SELECT t.* , invites.INVITES_BEFORE_ACCEPT FROM table t OUTER APPLY ( SELECT COUNT(*) INVITES_BEFORE_ACCEPT FROM table t2 WHERE t.PERSON_ID = t2.PERSON_ID and t.INVITED_DATE < t2.ACCEPTED_DATE ) invites
One way would be WITH t AS (SELECT *, COUNT(ACCEPTED_DATE) OVER ( PARTITION BY PERSON_ID ORDER BY INVITED_DATE) AS Grp FROM [table]) SELECT *, SUM(CASE WHEN ACCEPTED_DATE IS NULL THEN 1 ELSE 0 END) OVER ( PARTITION BY PERSON_ID, Grp ORDER BY INVITED_DATE) AS INVITES_BEFORE_ACCEPT FROM t Demo