问题
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.
回答1:
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
回答2:
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
来源:https://stackoverflow.com/questions/42576707/calculating-interest-across-multiple-interest-rates