Modify DATE in COLUMN using CASES and LOOP - sql-server

I have One month data.
DATE | AMOUNT
04/01/2019 | 3437824
04/02/2019 | 234834
04/03/2019 | 2343478
.
.
.
04/30/2019 | 343729
I want to change the Date Column to give me something like this
OLDDATE | DATE | AMOUNT
04/01/2019 | 01/01/2019 | 3437824
04/02/2019 | 02/01/2019 | 234834
04/03/2019 | 03/01/2019 | 2343478
.
.
.
04/12/2019 | 12/01/2019 | 328456
04/13/2019 | 01/02/2019 |845754
04/14/2019 | 02/02/2019 |845754
.
.
04/24/2019 | 12/02/2019 |845754
04/25/2019 | 01/03/2019 |845754
04/26/2019 | 02/03/2019 |845754
.
.
.
04/30/2019 | 06/03/2019 | 343729
I tried to achieve the same using a switch case inside a while loop but was not able to get the desired result
BEGIN
DECLARE #Mth INT = 1;
DECLARE #Iteration INT = 1;
WHILE #Iteration != 1000000
BEGIN
WHILE #Mth <= 12
BEGIN
UPDATE TACC
SET BUSINESSDATE =
CASE
WHEN DAY([BUSINESSDATE])<13
THEN datefromparts(year([BUSINESSDATE]), #Mth, 1)
WHEN DAY([BUSINESSDATE]) BETWEEN 13 AND 24
THEN datefromparts(year([BUSINESSDATE]), #Mth, 2)
ELSE datefromparts(year([BUSINESSDATE]), #Mth, 3)
END
SET #Mth = #Mth + 1
IF #Mth=13
BEGIN
SET #Mth=1
END
END
SET #Iteration = #Iteration + 1
END
END
Which will be the better way to do it T-SQL or DAX if i have to do this for say 10 million rows

Basically, we need to divide the rows in groups of 12 and for each group to increase the month part of the new date. The rows in the each groups need to be ordered and we need to increase the day part of each new date with one.
You can use ROW_NUMBER to order the rows and divide them in groups. Then you can use DATEADD to built the new date for each row:
DECLARE #DataSource TABLE
(
[DATE] DATETIME2(0)
,[AMOUNT] INT
);
INSERT INTO #DataSource ([DATE], [AMOUNT])
VALUES ('2019-04-01', 100)
,('2019-04-02', 100)
,('2019-04-03', 100)
,('2019-04-04', 100)
,('2019-04-05', 100)
,('2019-04-06', 100)
,('2019-04-07', 100)
,('2019-04-08', 100)
,('2019-04-09', 100)
,('2019-04-10', 100)
,('2019-04-11', 100)
,('2019-04-12', 100)
,('2019-04-13', 100)
,('2019-04-14', 100)
,('2019-04-15', 100)
,('2019-04-16', 100)
,('2019-04-17', 100)
,('2019-04-18', 100)
,('2019-04-19', 100)
,('2019-04-20', 100)
,('2019-04-21', 100)
,('2019-04-22', 100)
,('2019-04-23', 100)
,('2019-04-24', 100)
,('2019-04-25', 100)
,('2019-04-26', 100)
,('2019-04-27', 100)
,('2019-04-28', 100)
,('2019-04-29', 100);
DECLARE #StartDate DATETIME2 = '2019-01-01'
,#Step TINYINT = 12;
WITH DataSource ([OLDDATE], [AMOUNT], [AddMonth]) AS
(
SELECT *
,(ROW_NUMBER() OVER (ORDER BY [Date] ASC) -1 ) / 12
FROM #DataSource
)
SELECT [OLDDATE]
,[AMOUNT]
,DATEADD(DAY, ROW_NUMBER() OVER (PARTITION BY [AddMonth] ORDER BY [OLDDATE] ASC) - 1, DATEADD(MONTH, [AddMonth], #StartDate))
FROM DataSource;

Related

Denomination distribution calculation

Some background story: Company A gives out vouchers to winners of a challenge. The SQL that I am currently writing needs to decide the required voucher denomination that sums to the value awarded to a person. I have a table that stores the denominations available for vouchers, depending on the country and currency.
In the example below, a particular person is awarded with €80 worth of vouchers.
The query below displays results of a lookup table for voucher denominations available for a particular country.
SELECT * FROM tblDenominationScheme WHERE CountryCode IN ('AT', 'US')
Result:
No. | CountryCode | VoucherName | VoucherValue
-------------------------------------------------
1 | AT | €50 Shop A | 50
2 | AT | €25 Shop A | 25
3 | AT | €15 Shop A | 15
4 | AT | €10 Shop A | 10
5 | US | $50 Store B | 50
6 | US | $10 Store B | 10
7 | US | $5 Store B | 5
My current SQL is as below to determine the required voucher denominations for €80 voucher:
DECLARE #CountryCode1 VARCHAR(2) = 'AT'
DECLARE #ChallengerID INT = 1172
DECLARE #RoundedAmount1 INT = 80
DECLARE #Vouchers INT
DECLARE #AmountAwarded INT = 0
SET #AmountAwarded = #RoundedAmount1
DROP TABLE IF EXISTS #tempVoucher
CREATE TABLE #tempVoucher
(
CountryCode VARCHAR(2),
ChallengerID INT,
AmountAwarded INT,
Vouchers INT,
)
WHILE (#RoundedAmount1 > 0)
BEGIN
SET #Vouchers = 0
SELECT TOP 1 #Vouchers = VoucherValue FROM tblDenominationScheme WHERE CountryCode = #CountryCode1 AND VoucherValue <= #RoundedAmount1 ORDER BY VoucherValue DESC
IF (#Vouchers > 0)
BEGIN
SET #RoundedAmount1 = #RoundedAmount1 - #Vouchers
END
ELSE
BEGIN
SELECT TOP 1 #Vouchers = VoucherValue FROM tblDenominationScheme WHERE CountryCode = #CountryCode1 ORDER BY VoucherValue
SET #RoundedAmount1 = #RoundedAmount1 - #RoundedAmount1
END
INSERT INTO #tempVoucher VALUES (#CountryCode1,#ChallengerID, #AmountAwarded, #Vouchers)
END
SELECT * FROM #tempVoucher
Result from the SQL above:
No. | CountryCode | ChallengerID | AmountAwarded | Vouchers
--------------------------------------------------------------
1 | AT | 1172 | 80 | 50
2 | AT | 1172 | 80 | 25
3 | AT | 1172 | 80 | 10
NOTE: The value in AmountAwarded column will be the same for all 3 rows. The amount in the Vouchers column for the 3 rows should sum up to 80.
The result above is obviously incorrect, because if you sum up the values in the Vouchers column, it gives you 85, which is 5 more than the AmountAwarded
Expected result (or at least closest):
No. | CountryCode | ChallengerID | AmountAwarded | Vouchers
--------------------------------------------------------------
1 | AT | 1172 | 80 | 50
2 | AT | 1172 | 80 | 10
3 | AT | 1172 | 80 | 10
4 | AT | 1172 | 80 | 10
Anyone able to help?
This might be an expensive query, but gets you different options to deliver up to 7 vouchers to get you the expected result. This, however, will generate a huge amount of reads if the rows increase or the amount of vouchers can be greater.
DECLARE #CountryCode1 VARCHAR(2) = 'AT'
DECLARE #RoundedAmount1 INT = 80;
WITH cteDenominations AS(
SELECT No, VoucherValue
FROM tblDenominationScheme
WHERE CountryCode = #CountryCode1
UNION ALL
SELECT 10000, 0
),
ctePermutations AS(
SELECT a.No AS a_No,
a.VoucherValue AS a_Value,
b.No AS b_No,
b.VoucherValue AS b_Value,
c.No AS c_No,
c.VoucherValue AS c_Value,
d.No AS d_No,
d.VoucherValue AS d_Value,
e.No AS e_No,
e.VoucherValue AS e_Value,
f.No AS f_No,
f.VoucherValue AS f_Value,
g.No AS g_No,
g.VoucherValue AS g_Value,
ROW_NUMBER() OVER(ORDER BY a.No, b.No, c.No, d.No) Permutation
FROM cteDenominations a
JOIN cteDenominations b ON a.VoucherValue >= b.VoucherValue
JOIN cteDenominations c ON b.VoucherValue >= c.VoucherValue
JOIN cteDenominations d ON c.VoucherValue >= d.VoucherValue
JOIN cteDenominations e ON d.VoucherValue >= e.VoucherValue
JOIN cteDenominations f ON e.VoucherValue >= f.VoucherValue
JOIN cteDenominations g ON f.VoucherValue >= g.VoucherValue
WHERE #RoundedAmount1 = a.VoucherValue
+ b.VoucherValue
+ c.VoucherValue
+ d.VoucherValue
+ e.VoucherValue
+ f.VoucherValue
+ g.VoucherValue
)
SELECT Permutation,
u.No,
u.VoucherValue
FROM ctePermutations
CROSS APPLY (VALUES(a_No, a_Value),
(b_No, b_Value),
(c_No, c_Value),
(d_No, d_Value),
(e_No, e_Value),
(f_No, f_Value),
(g_No, g_Value))u(No, VoucherValue)
WHERE VoucherValue > 0
AND Permutation = 1 --Remove this to get all possibilities
;
Looks like you need to solve a equation:
80 = n1*v1 + k2*n2...
where v1,v2 ... are values which you store in database
And you need to find n1, n2 ... , which are in {0, N}
There is no way how to implement it in SQL. Except - over all possible values, but it's not the smarter way.
Also, see this info:
https://math.stackexchange.com/questions/431367/solving-a-first-order-diophantine-equation-with-many-terms
Logic
Find the largest amount (that is less than or equal to starting amount) vouchers of 1 denomination can make.
Subtract this value from starting amount to get remainder,
Find the largest amount (that is less than or equal to remainder) a number of vouchers of 1 smaller denomination can make.
Subtract this value from previous remainder.
Go back to step 3
Features:
Handles multiple best combinations.
Small number of combinations are searched.
On my laptop: 100 runs take about 3 seconds
Notes
Performance may be improved by saving output of VoucherCombinations to a table variable and then using it in subsequent CTEs.
Code:
DECLARE #Vouchers TABLE( CountryCode CHAR( 2 ), VoucherValue DECIMAL( 10, 2 ))
INSERT INTO #Vouchers VALUES( 'AT', 50 ), ( 'AT', 40 ), ( 'AT', 25 ), ( 'AT', 20 ), ( 'AT', 15 ), ( 'AT', 10 ), ( 'US', 50 ), ( 'US', 10 ), ( 'US', 5 );
-- Small number table
-- Limits maximum count of Vouchers of a given denomination.
DECLARE #Numbers TABLE( Num INT )
INSERT INTO #Numbers VALUES( 1 ), ( 2 ), ( 3 ), ( 4 ), ( 5 ), ( 6 ), ( 7 ), ( 8 ), ( 9 ), ( 10 )
DECLARE #TargetAmount DECIMAL( 10, 2 ) = 60;
DECLARE #CountryCode CHAR( 2 ) = 'AT';
;WITH VoucherCombinations
AS (
-- Anchor
SELECT ROW_NUMBER() OVER( ORDER BY VoucherValue DESC ) AS ParentGroupID,
ROW_NUMBER() OVER( ORDER BY VoucherValue DESC ) AS SubGroupID,
1 AS IterationID,
VoucherValue, Num AS VoucherCumulativeCount,
CAST( VoucherValue * Num AS DECIMAL( 10, 2 )) AS TotalDenominationValue,
CAST( #TargetAmount - ( VoucherValue * Num ) AS DECIMAL( 10, 2 )) AS Remainder
FROM #Vouchers
-- Find the largest amount a given Voucher denomination can produce that is less than or equal to #TargetAmount
INNER JOIN #Numbers ON ( VoucherValue * Num ) <= #TargetAmount AND #TargetAmount - ( VoucherValue * Num ) < VoucherValue
WHERE CountryCode = #CountryCode
UNION ALL
-- Recursive query
SELECT SubGroupID,
SubGroupID * 10 + ROW_NUMBER() OVER( ORDER BY V.VoucherValue DESC ) AS SubGroupID,
IterationID + 1,
V.VoucherValue, VoucherCumulativeCount + N.Num AS VoucherCount,
CAST( V.VoucherValue * N.Num AS DECIMAL( 10, 2 )) AS TotalDenominationValue,
CAST( Remainder - ( V.VoucherValue * N.Num ) AS DECIMAL( 10, 2 )) AS Remainder
FROM VoucherCombinations AS VP
-- For each denomination look at the smaller denominations
INNER JOIN #Vouchers AS V ON VP.VoucherValue > V.VoucherValue
INNER JOIN #Numbers AS N ON V.VoucherValue * N.Num <= Remainder AND Remainder - ( V.VoucherValue * N.Num ) < V.VoucherValue
WHERE CountryCode = #CountryCode
),
-- Discard invalid combinations i.e. remainder is not 0
VoucherPoolValid AS(
SELECT *, DENSE_RANK() OVER( ORDER BY VoucherCumulativeCount ASC ) AS BestCombos
FROM VoucherCombinations
WHERE Remainder = 0
),
-- Find best combinations i.e. smallest number of Vouchers; Note: logic supports having more than 1 best combination
VoucherPoolBestCombos AS(
SELECT *, ROW_NUMBER() OVER( ORDER BY BestCombos ASC ) AS ComboID
FROM VoucherPoolValid
WHERE BestCombos = 1
),
-- Return all denominations for each combination
VoucherPoolAllDetails AS(
SELECT *
FROM VoucherPoolBestCombos
UNION ALL
SELECT Parent.*, BestCombos, ComboID
FROM VoucherPoolAllDetails AS Child
INNER JOIN VoucherCombinations AS Parent ON Child.ParentGroupID = Parent.SubGroupID
WHERE Child.SubGroupID <> Child.ParentGroupID
)
SELECT * FROM VoucherPoolAllDetails
ORDER BY ComboID

Can't convert time to mm:ss

I have a table with 7 columns:
Start_hour | Start_minute | Start_second | End_hour | End_minute | end_second | date
My task is to sum the differences between start and end times on current day. And my query already does that. Sadly I need the output to be in minutes:seconds only. For Example the total sum of time differences is 2 hour 26 minutes and 52 seconds.
I need my output to like this:
126:52
My query right now looks like this:
SELECT
RIGHT(CONVERT(CHAR(8),
DATEADD(SECOND,
SUM(DATEDIFF(SECOND,
Timefromparts(start_hour, start_minute, start_second, 0, 0),
Timefromparts(end_hour, end_minute, end_second, 0, 0))
), 0), 108), 5)
FROM
opoznienia
WHERE
YEAR(data) = YEAR(GETDATE())
AND MONTH(data) = MONTH(GETDATE())
AND DAY(data) = DAY(GETDATE())
Sample data:
Start_hour | Start_minute | Start_second | End_hour | End_minute | end_second | date
10 15 0 10 30 30 2018-11-27 14:40:53.680
10 15 0 10 30 30 2018-11-30 10:16:20.610
6 10 30 6 23 45 2018-12-02 01:00:27.243
8 10 0 8 53 45 2018-12-02 14:42:48.663
10 5 13 10 55 23 2018-12-02 14:53:03.560
Output of the query above:
47:13 (without RIGHT command it would be 01:47:13)
The wanted output:
107:13
You need to calculate the number of seconds and then format it as you want. From number of seconds you can calculate the minutes as division by 60, where the remainder are the seconds:
declare #NumberOfSeconds int = 7612
-- Returns 126:52
select concat(#NumberOfSeconds / 60, ':', FORMAT(#NumberOfSeconds % 60, 'D2'))
Here is one option:
SELECT
numSeconds,
CASE WHEN numSeconds / 60 <= 100
THEN RIGHT('00' + CONVERT(VARCHAR(20), numSeconds / 60), 2)
ELSE CONVERT(VARCHAR(20), numSeconds / 60) END
+ ':' +
CASE WHEN numSeconds / 60 <= 100
THEN RIGHT('00' + CONVERT(VARCHAR(20), numSeconds % 60), 2)
ELSE CONVERT(VARCHAR(20), numSeconds % 60) END AS output
FROM yourTable;
Demo
The ugliness in the code has to do with that you expect a minimum of two digits for the minute and second components. So, we have to pad each component with zeroes in the case where minutes or seconds happens to be just a single digit.
The other answers are better, but to understand what may be missing in what you're trying to do - you can look at following where DatePart is used.
-->"
..(without RIGHT command it would be 01:47:13)
" This is your clue that you that you need to work with the parts and not the whole...
SELECT Cast
(
Datepart
(
hour,
DATEADD(second,sum(datediff(second,
TIMEFROMPARTS ( Start_hour, Start_minute, Start_second, 0, 0),
TIMEFROMPARTS ( End_hour, End_minute, End_second, 0, 0))),0)
) * 60
+
Datepart
(
minute,
DATEADD(second,sum(datediff(second,
TIMEFROMPARTS ( Start_hour, Start_minute, Start_second, 0, 0),
TIMEFROMPARTS ( End_hour, End_minute, End_second, 0, 0))),0)
) As varchar)
+
':'
+
Cast
(
Datepart
(
second,
DATEADD(second,sum(datediff(second,
TIMEFROMPARTS ( Start_hour, Start_minute, Start_second, 0, 0),
TIMEFROMPARTS ( End_hour, End_minute, End_second, 0, 0))),0)
) As varchar) as result
from minsec
Try the following
CREATE TABLE T(
StartHour INT,
StartMinute INT,
StartSecond INT,
EndHour INT,
EndMinute INT,
EndSecond INT,
[Date] DATE
);
INSERT INTO T VALUES
(10, 15, 0 , 10, 30, 30, '2018-11-27'),
(10, 15, 0 , 10, 30, 30, '2018-11-30'),
(6 , 10, 30, 6 , 23, 45, '2018-12-02'),
(8 , 10, 0 , 8 , 53, 45, '2018-12-02'),
(10, 5 , 13, 10, 55, 23, '2018-12-02');
SELECT *,
CAST( (DATEDIFF(Hour, StartTime, EndTime) * 60) +
(DATEDIFF(Minute, StartTime, EndTime) % 60) AS VARCHAR
) + ':' +
CAST(DATEDIFF(Second, StartTime, EndTime) % 60 AS VARCHAR)
FROM
(
SELECT [Date],
TIMEFROMPARTS(StartHour, StartMinute, StartSecond, 0, 0) StartTime,
TIMEFROMPARTS(EndHour, EndMinute, EndSecond, 0, 0) EndTime
FROM T
) TT
You can also SUM() and GROUP BY [Date] if you want to.
SELECT [Date],
CAST( SUM( (DATEDIFF(Hour, StartTime, EndTime) * 60) +
(DATEDIFF(Minute, StartTime, EndTime) % 60)
) AS VARCHAR
) + ':' +
CAST(SUM(DATEDIFF(Second, StartTime, EndTime) % 60) AS VARCHAR)
FROM
(
SELECT [Date],
TIMEFROMPARTS(StartHour, StartMinute, StartSecond, 0, 0) StartTime,
TIMEFROMPARTS(EndHour, EndMinute, EndSecond, 0, 0) EndTime
FROM T
) TT
GROUP BY [Date]
Demo
UPDATE
It seems like you are looking for
SELECT [Date],
CAST(SUM(DATEDIFF(Second, StartTime, EndTime)) / 60 AS VARCHAR) + ':' +
CAST(SUM(DATEDIFF(Second, StartTime, EndTime)) % 60 AS VARCHAR) [MM:SS]
FROM
(
SELECT [Date],
TIMEFROMPARTS(StartHour, StartMinute, StartSecond, 0, 0) StartTime,
TIMEFROMPARTS(EndHour, EndMinute, EndSecond, 0, 0) EndTime
FROM T
) TT
GROUP BY [Date];
Returns:
+---------------------+--------+
| Date | MM:SS |
+---------------------+--------+
| 27/11/2018 00:00:00 | 15:30 |
| 30/11/2018 00:00:00 | 15:30 |
| 02/12/2018 00:00:00 | 107:10 |
+---------------------+--------+
Demo

SQL Server Group By Help Required

Being a beginner, how can I get results as mentioned in the diagram. I am not getting results by grouping. Please advise.
If I'm reading your example data correctly, it seems that for Query3, you have the data flip-flopped for the dates. I think Query3 elapsed for 10/20/2017 should be 2:03 and for 10/21/2017, the elapsed time should be 1:48.
If that is indeed the case, here is a solution that uses a combination of Common Table Expressions, aggregation and PIVOT. It works for the example data you've provided in your question. You may need to adjust it for other data.
set nocount on
Declare #t Table (ID int, [Date] Date, ExecutedTime varchar(10), Label Varchar(10))
insert into #t values
(1,'2017-10-20','0:01:16','Query1'),
(2,'2017-10-20','0:00:20','Query1'),
(3,'2017-10-20','0:00:14','Query1'),
(4,'2017-10-20','0:01:43','Query2'),
(5,'2017-10-20','0:00:33','Query2'),
(6,'2017-10-20','0:00:34','Query2'),
(7,'2017-10-20','0:01:18','Query3'),
(8,'2017-10-20','0:00:30','Query3'),
(9,'2017-10-20','0:00:15','Query3'),
(10,'2017-10-21','0:01:16','Query1'),
(11,'2017-10-21','0:00:20','Query1'),
(12,'2017-10-21','0:00:14','Query1'),
(13,'2017-10-21','0:01:43','Query2'),
(14,'2017-10-21','0:00:33','Query2'),
(15,'2017-10-21','0:00:34','Query2'),
(16,'2017-10-21','0:01:18','Query3'),
(16,'2017-10-21','0:00:30','Query3')
;
With ExecutedTimeInSeconds as --Convert ExecutedTime to seconds in preparation for aggregation
(
select
[Date],
Label,
datepart(hour,(convert(time,executedTime))) * 3600 +
datepart(minute,(convert(time,executedTime))) * 60 +
datepart(second,(convert(time,executedTime))) as ElapsedSeconds
from #t
)
,AggregatedDataInElapsedSeconds as --Aggregate the seconds by Date and Label
(
SELECT [Date]
,Label
,sum(ElapsedSeconds) AS ElapsedSeconds
FROM ExecutedTimeInSeconds
GROUP BY [Date]
,Label
),
DataReadyForPivot as --Convert the aggregageted seconds back to an elapsed time
(
SELECT [Date], Label,
RIGHT('0' + CAST(ElapsedSeconds / 3600 AS VARCHAR),2) + ':' +
RIGHT('0' + CAST((ElapsedSeconds / 60) % 60 AS VARCHAR),2) + ':' +
RIGHT('0' + CAST(ElapsedSeconds % 60 AS VARCHAR),2) as ExecutedSum
from AggregatedDataInElapsedSeconds
)
,
PivotedData as --Pivot the data
(
SELECT *
FROM DataReadyForPivot
PIVOT(MAX(ExecutedSum) FOR Label IN (
[Query1]
,[Query2]
,[Query3]
)) AS pvt
)
select --add a row number as Id
ROW_NUMBER() over (order by [Date] desc) as Id,
*
from PivotedData
| Id | Date | Query1 | Query2 | Query3 |
|----|------------|----------|----------|----------|
| 1 | 2017-10-21 | 00:01:50 | 00:02:50 | 00:01:48 |
| 2 | 2017-10-20 | 00:01:50 | 00:02:50 | 00:02:03 |

Calculating interest across multiple interest rates

I have a table where I store interest rates, each with a start date where it became applicable. Later-dated entries in the table supersede earlier entries. I have to query this table with a start date, an end date, and an amount. From these values I need to end up with an overall interest amount that takes the different interest rates for the date span into account.
CREATE TABLE [dbo].[Interest_Rates](
[Interest_Rate] [float] NULL,
[Incept_Date] [datetime] NULL
) ON [PRIMARY]
GO
I have four 'bands' of interest rates:
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (10, CAST(N'2001-05-03 11:12:16.000' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (11.5, CAST(N'2014-01-07 10:49:28.433' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (13.5, CAST(N'2016-03-01 00:00:00.000' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (15.5, CAST(N'2016-05-01 00:00:00.000' AS DateTime))
GO
What I'd like to know is whether it's possible to calculate the interest rate for a period of time beginning at a time when the interest rate was, say, 11.5%, and ending at a later time when the interest rate has risen twice to 13.5%, within a single query.
It seems like the interest calculation for each 'band' can be done using the wonderful Suprotim Agarwal's example as follows:
DECLARE #StartDate DateTime
DECLARE #EndDate DateTime
DECLARE #Amount Float
SET #StartDate = '2014-04-22'
SET #EndDate = '2016-04-13'
SET #Amount = 150000.00
SELECT
#Amount*(POWER(1.1550, CONVERT(NUMERIC(8,3),
DATEDIFF(d, #StartDate, #EndDate)/365.25))) - #Amount
as TotalInterest
(Interest rate at 15.5% in above example)
Where I'm getting stuck is at working out how to interrelate the calculation with the Interest Rates table such that the join takes into account which 'band' each subsection of the date span falls into.
Any help or advice would be much appreciated.
tl;dr: the completed query is the last code block at the end of this long explanation.
Let's walk through this step-by-step and then present the final solution as one query. A few steps are needed to solve this problem.
1) Figure out which rates our desired date range covers
2) Devise a clever way to choose those rates
3) Combine those dates and rates in such a way to give us that total interest accrued.
Some Preliminary Notes
Since your example calculation of interest rate considers days as its finest resolution, I just use datatypes date instead of datetime. If you need a finer resolution, let me know and I can update.
I'm using the following declared variables
declare #EndOfTime date = '2049-12-31' -- This is some arbitrary end of time value that I chose
declare #StartDate Date = '2012-04-22' -- I made this earlier to cover more rates
declare #EndDate Date = '2016-04-13'
declare #Amount Float = 100000.00 -- I changed it to a softer number
1) Date Intervals
Right now, your interest_rates table lists dates like this:
+ ------------- + ----------- +
| interest_rate | incept_date |
+ ------------- + ----------- +
| 10 | 2001-05-03 |
| 11.5 | 2014-01-07 |
| 13.5 | 2016-03-01 |
| 15.5 | 2016-05-01 |
+ ------------- + ----------- +
But you want it to list intervals like this:
+ ------------- + ------------ + ------------ +
| interest_rate | inter_begin | inter_end |
+ ------------- + ------------ + ------------ +
| 10 | 2001-05-03 | 2014-01-06 |
| 11.5 | 2014-01-07 | 2016-02-29 |
| 13.5 | 2016-03-01 | 2016-04-30 |
| 15.5 | 2016-05-01 | 2049-12-31 |
+ ------------- + ------------ + ------------ +
The following query can turn your date list into intervals:
select i1.interest_rate
, i1.incept_date as inter_begin
, isnull(min(i2.incept_date) - 1,#EndOfTime) as inter_end
from #interest i1
left join #interest i2 on i2.incept_date > i1.incept_date
group by i1.interest_rate, i1.incept_date
Note: I'm playing a bit loose with the date arithmetic here without using the dateadd() command.
Keeping track of the date intervals like this makes selecting the applicable rates much easier.
2) Choosing the Rates
Now we can select records that sit within our desired range by using the above query as a CTE. This query is a little tricky, so take some time to really understand it.
; with
intervals as (
-- The above query/table
)
select *
from intervals
where inter_begin >= (
select inter_begin -- selects the first rate covered by our desired interval
from intervals
where #StartDate between inter_begin and inter_end
)
and inter_end <= (
select inter_end -- selects the last rate covered by our desired interval
from intervals
where #EndDate between inter_begin and inter_end
)
This effectively filters out any rates we don't care about and leaves us with
+ ------------- + ------------ + ------------ +
| interest_rate | inter_begin | inter_end |
+ ------------- + ------------ + ------------ +
| 10 | 2001-05-03 | 2014-01-06 |
| 11.5 | 2014-01-07 | 2016-02-29 |
| 13.5 | 2016-03-01 | 2016-04-30 |
+ ------------- + ------------ + ------------ +
3) Calculate the Interest
Now we have everything we need, and calculating the interest is just a matter selecting the right things from this table. Most of what you wrote for your calculation remains the same; the main changes are in the datediff() command. Using #StartDate and #EndDate won't give us an accurate count of the days spent at each specific rate. We run into the same problem by using inter_begin and inter_end. Instead, we must use a case statement, something like
datediff(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end
)
Put this in the above Query to get
; with
intervals as (...) -- same as above
select *
, DATEDIFF(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end) as days_active
, #Amount*(POWER((1+interest_rate/100),
convert(float,
DATEDIFF(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end
)
)/365.25)
) - #Amount as Actual_Interest
from ... -- same as above
which gives us this table
+ ------------- + ------------ + ------------ + ----------- + --------------- +
| interest_rate | inter_begin | inter_end | days_active | Actual_interest |
+ ------------- + ------------ + ------------ + ----------- + --------------- +
| 10 | 2001-05-03 | 2014-01-06 | 624 | 17683.63 |
| 11.5 | 2014-01-07 | 2016-02-29 | 786 | 26283.00 |
| 13.5 | 2016-03-01 | 2016-04-30 | 43 | 1501.98 |
+ ------------- + ------------ + ------------ + ----------- + --------------- +
Finally, put this in a CTE and take the sum of the Actual_interest field:
declare #EndOfTime date = '2049-12-31' -- This is some arbitrary end of time value that I chose
declare #StartDate Date = '2012-04-22' -- I made this earlier to cover more rates
declare #EndDate Date = '2016-04-13'
declare #Amount Float = 100000.00 -- I changed it to a softer number
; with
intervals as (
select i1.interest_rate
, i1.incept_date as inter_begin
, isnull(min(i2.incept_date) - 1,#EndOfTime) as inter_end
from #interest i1
left join #interest i2 on i2.incept_date > i1.incept_date
group by i1.interest_rate, i1.incept_date
)
, interest as (
select *
, DATEDIFF(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end) as days_active
, #Amount*(POWER((1+interest_rate/100),
convert(float,
DATEDIFF(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end
)
)/365.25)
) - #Amount as Actual_Interest
from intervals
where inter_begin >= (
select inter_begin -- selects the first rate covered by our desired interval
from intervals
where #StartDate between inter_begin and inter_end
)
and inter_end <= (
select inter_end -- selects the last rate covered by our desired interval
from intervals
where #EndDate between inter_begin and inter_end
)
)
select sum(actual_interest) as total_interest
from interest
Perhaps a little more than you were looking for, but in this example, you can calculate all loans in one query.
You may also notice the last 3 columns which represent Total Number of Days, Total Interest Earned and the Total Weighted Average Interest Rate
Example
Declare #Interest_Rate table (interest_rate money,Incept_Date datetime)
Insert Into #Interest_Rate values
(10 ,'2001-05-03 11:12:16.000'),
(11.5,'2014-01-07 10:49:28.433'),
(13.5,'2016-03-01 00:00:00.000'),
(15.5,'2016-05-01 00:00:00.000')
Declare #Loan table (Id int,StartDate date, EndDate date,Amount money)
Insert Into #Loan values
(1,'2014-01-01','2015-11-17',150000),
(1,'2015-11-18','2016-12-31',175000), -- Notice Balance Change
(2,'2016-01-01','2020-06-15',200000)
Select A.ID
,A.Amount
,DateR1 = min(D)
,DateR2 = max(D)
,Days = count(*)
,B.Interest_Rate
,Interest_Earned = cast(sum(((A.Amount*B.Interest_Rate)/B.DIY)/100.0) as decimal(18,2))
,Total_Days = sum(count(*)) over (Partition By A.ID)
,Total_Int_Earned = sum(cast(sum(((A.Amount*B.Interest_Rate)/B.DIY)/100.0) as decimal(18,2))) over (Partition By A.ID)
,Total_WAIR = sum(A.Amount * count(*) * B.interest_rate) over (Partition By A.ID)/ sum(A.Amount * count(*)) over (Partition By A.ID)
From #Loan A
Join (
Select D
,D1
,interest_rate
,DIY = 365.0 + IIF(Year(D) % 4 = 0 , 1 , 0 )
From ( Select Top (DateDiff(DD,(Select cast(min(Incept_Date) as date) from #Interest_Rate),cast(GetDate() as date))+1) D=DateAdd(DD,-1+Row_Number() Over (Order By (Select NULL)),(Select cast(min(Incept_Date) as date) from #Interest_Rate)) From master..spt_values N1,master..spt_values N2 ) A
Join (
Select interest_rate
,D1 = cast(Incept_Date as Date)
,D2 = cast(DateAdd(DAY,-1,Lead(Incept_Date,1,GetDate()) over (Order by Incept_Date)) as date)
From #Interest_Rate
) B on D between D1 and D2
) B on D Between StartDate and EndDate
Group By A.ID,A.Amount,B.D1,B.Interest_Rate
Returns

SQL Server: update multiple rows with one statement with calculation

I don't know is this possible in one SQL statement to update the refundamt?
I have these three rows initially, using insert statement:
NO | TRANAMT | REFUNDAMT
1 | 100 | 0
2 | 200 | 0
3 | 300 | 0
If refund is 350, the refundamt will be updated as follow, the refundamt cannot be more then the tranamt:
NO | TRANAMT | REFUNDAMT
1 | 100 | 100
2 | 200 | 200
3 | 300 | 50
When refund again with 50, the refundamt will be updated as follow
NO | TRANAMT | REFUNDAMT
1 | 100 | 100
2 | 200 | 200
3 | 300 | 100
I think this is impossible to update refundamt using one sql statement. How about multiple satement? Not hoping to use store procedure. Can I use select update?
I think this works. It's a single statement. Not the prettiest. And I'd imagine the real Transactions table has more columns (e.g. an account number):
declare #RefundAmt int
set #RefundAmt = 350
; with Refunds as (
select
top 1
NO,
TRANAMT,
CASE WHEN TRANAMT-REFUNDAMT < #RefundAmt THEN TRANAMT ELSE REFUNDAMT + #RefundAmt END as REFUNDAMT,
CASE WHEN TRANAMT-REFUNDAMT < #RefundAmt THEN #RefundAmt - (TRANAMT-REFUNDAMT) ELSE 0 END as Remaining
from
dbo.Transactions
where REFUNDAMT < TRANAMT ORDER BY NO
union all
select
t2.NO,
t2.TRANAMT,
CASE WHEN t2.TRANAMT-t2.REFUNDAMT < t1.Remaining THEN t2.TRANAMT ELSE t2.REFUNDAMT + t1.Remaining END as REFUNDAMT,
CASE WHEN t2.TRANAMT-t2.REFUNDAMT < t1.Remaining THEN t1.Remaining - (t2.TRANAMT-t2.REFUNDAMT) ELSE 0 END as Remaining
from
Refunds t1
inner join
dbo.Transactions t2
on
t1.NO = t2.NO - 1 and
t1.Remaining > 0
)
update t set REFUNDAMT = r.REFUNDAMT
from
dbo.Transactions t
inner join
Refunds r
on
t.NO = r.NO
Bit messy, but here's what I came up with:
Code to set up your base tables...
-- setup code
CREATE TABLE #tmp (
[NO] int identity(1,1),
[TRAN] money,
[REFUND] money
)
INSERT #tmp VALUES (100, 0)
INSERT #tmp VALUES (200, 0)
INSERT #tmp VALUES (300, 0)
Code to add your refund amount...
DECLARE #amt money SET #amt = 50
UPDATE u
SET REFUND =
CASE WHEN u.[REFUND] = u.[TRAN] THEN u.[REFUND] --'already full - do nothing'
WHEN cumulativepretran >= aiming_for_ref THEN u.[REFUND] --'after all - do nothing'
WHEN cumulativetran + cumulativeref > ((totalref + #amt) - u.[TRAN]) THEN (totalref + #amt + u.[REFUND]) - (cumulativeref) --'calc'
ELSE u.[TRAN] END -- 'fill-er-up'
FROM #tmp u
INNER JOIN (SELECT *,
ISNULL((select sum([REFUND]) FROM #tmp WHERE [NO] <= t.[NO]), 0) 'cumulativeref',
ISNULL((select sum([TRAN]) FROM #tmp WHERE [NO] <= t.[NO]), 0) 'cumulativetran',
ISNULL((select sum([REFUND]) FROM #tmp WHERE [NO] < t.[NO]), 0) 'cumulativepreref',
ISNULL((select sum([TRAN]) FROM #tmp WHERE [NO] < t.[NO]), 0) 'cumulativepretran',
ISNULL((select sum([REFUND]) FROM #tmp), 0) 'totalref',
ISNULL((select sum([REFUND]) FROM #tmp), 0) + #amt 'aiming_for_ref'
FROM #tmp t) as m ON m.[NO] = u.[NO]
Something like...: (needs work, but gives a conceptual idea)
Declare #pRefund Money
SET #pRefund = 350
WHILE #pRefund > 0
BEGIN
/** SELECT Transaction Amount of min primary key where recorded refundamt is less than tranamt
Apply matching refund
Reduce #pRefund by amount applied
If #pRefund is now less then next highest TransAMT Then apply whats left otherwise loop again
refund has now been all applied and loop will exit */
END

Resources