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
Related
In SQL 2016, I need to create a list using financial periods but only have the from/to available - it's formatted similar to dates but are 0mmyyyy, so the first 3 numbers are the month/period and the last 4 digits the year.
e.g. period_from is '0102017' and period_to '0032018', but trying to bring back a list that includes the ones in between as well?
0102017,
0112017,
0122017,
0012018,
0022018
Also, the first three characters can go to 012 or 013, so need to be able to easily alter the code for other databases.
I am not entirely sure what you are wanting to use this list for, but you can get all your period values with the help of a tally table and some common table expressions.
-- Test data
declare #p table(PeriodFrom nvarchar(10),PeriodTo nvarchar(10));
insert into #p values('0102017','0032018'),('0052018','0112018');
-- Specify the additional periods you want to include, use 31st December for correct sorting
declare #e table(ExtraPeriodDate date
,ExtraPeriodText nvarchar(10)
);
insert into #e values('20171231','0132017');
-- Convert start and end of periods to dates
with m as (select cast(min(right(PeriodFrom,4) + substring(PeriodFrom,2,2)) + '01' as date) as MinPeriod
,cast(max(right(PeriodTo,4) + substring(PeriodTo,2,2)) + '01' as date) as MaxPeriod
from #p
) -- Built a tally table of dates to join from
,t(t) as (select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1)
,d(d) as (select top (select datediff(month,MinPeriod,MaxPeriod)+1 from m) dateadd(m,row_number() over (order by (select null))-1,m.MinPeriod) from m, t t1, t t2, t t3, t t4, t t5)
-- Use the tally table to convert back to your date period text format
,p as (select d.d as PeriodDate
,'0' + right('00' + cast(month(d) as nvarchar(2)),2) + cast(year(d) as nvarchar(4)) as PeriodText
from d
union all -- and add in any of the addition '13th' month periods you specified previously
select ExtraPeriodDate
,ExtraPeriodText
from #e
)
select PeriodText
from p
order by PeriodDate;
Output:
+------------+
| PeriodText |
+------------+
| 0102017 |
| 0112017 |
| 0122017 |
| 0132017 |
| 0012018 |
| 0022018 |
| 0032018 |
| 0042018 |
| 0052018 |
| 0062018 |
| 0072018 |
| 0082018 |
| 0092018 |
| 0102018 |
| 0112018 |
+------------+
If this isn't what you require exactly it should put you on the right path to generating these values either as the result of a function or concatenated together into a list as per your comment by using for xml on the result by changing the final select statement to:
select stuff((select ', ' + PeriodText
from p
order by PeriodDate
for xml path('')
)
,1,2,'') as PeriodTexts;
Which outputs:
+---------------------------------------------------------------------------------------------------------------------------------------+
| PeriodTexts |
+---------------------------------------------------------------------------------------------------------------------------------------+
| 0102017, 0112017, 0122017, 0132017, 0012018, 0022018, 0032018, 0042018, 0052018, 0062018, 0072018, 0082018, 0092018, 0102018, 0112018 |
+---------------------------------------------------------------------------------------------------------------------------------------+
This is going to be a little complicated. To start, I have a user defined table value function that outputs a calendar table based on a start and end date. You'll want to create that first...
CREATE FUNCTION dbo.udf_calendar (#datestart smalldatetime, #dateend smalldatetime)
RETURNS #calendar TABLE (
[day] int,
[date] smalldatetime
)
AS
BEGIN
DECLARE #rows int
DECLARE #i int = 1
SELECT
#rows = DATEDIFF(DAY, #datestart, #dateend)
WHILE (#i <= #rows)
BEGIN
INSERT INTO #calendar ([day])
VALUES (#i)
SET #i = #i + 1
END
UPDATE a
SET [date] = DATEADD(DAY, [day] - 1, #datestart)
--select *, DATEADD(day,id-1,#datestart)
FROM #calendar a
RETURN
END
Then, the following will give you the output that I THINK you are looking for. I've commented to try and explain how I got there, but it still might be a bit difficult to follow...
--Create temp table example with your period from and to.
IF (SELECT
OBJECT_ID('tempdb..#example'))
IS NOT NULL
DROP TABLE #example
SELECT
'0102017' periodfrom,
'0032018' periodto INTO #example
/*
This is the difficult part. Basically you're inner joining the calendar
to the temp table where the dates are between the manipulated period from and to.
I've added an extra column formatted to allow ordering correctly by period.
*/
SELECT DISTINCT
periodfrom,
periodto,
RIGHT('00' + CAST(DATEPART(MONTH, [date]) AS varchar(50)), 3) + CAST(DATEPART(YEAR, [date]) AS varchar(50)) datefill,
CAST(DATEPART(YEAR, [date]) AS varchar(50)) + RIGHT('00' + CAST(DATEPART(MONTH, [date]) AS varchar(50)), 3) datefill2
FROM dbo.udf_calendar('2015-01-01', '2018-12-31') a
INNER JOIN #example b
ON a.[date] BETWEEN SUBSTRING(periodfrom, 2, 2) + '-01-' + SUBSTRING(periodfrom, 4, 4) AND SUBSTRING(periodto, 2, 2) + '-01-' + SUBSTRING(periodto, 4, 4)
ORDER BY datefill2
I have data in a table with dates, and want to count the rows by "Week of" (e.g., "Week of 2017-05-01"), where the result has the week's date (starting on Mondays) and the count of matching rows — even if there are no rows for that week. (This will all be in a date range.)
I can partition things into weeks readily enough by grouping on DATEPART(wk, D) (where D is the date column), but I'm struggling with:
How to get the "Week of" date and fill, and
How to have a row for a week where there are no matching rows in the data
Here's grouping by week:
SET DATEFORMAT ymd;
SET DATEFIRST 1; -- Monday is first day of week
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
SELECT DATEPART(wk, D) AS [Week Number], COUNT(*) AS [Count]
FROM #temp
GROUP BY DATEPART(wk, D)
ORDER BY DATEPART(wk, D);
Which gives me:
+−−−−−−−−−−−−−+−−−−−−−+
| Week Number | Count |
+−−−−−−−−−−−−−+−−−−−−−+
| 19 | 5 |
| 20 | 19 |
| 22 | 8 |
| 23 | 10 |
| 24 | 5 |
| 26 | 4 |
+−−−−−−−−−−−−−+−−−−−−−+
But ideally I want:
+−−−−−−−−−−−−+−−−−−−−+
| Week | Count |
+−−−−−−−−−−−−+−−−−−−−+
| 2017-05-01 | 5 |
| 2017-05-08 | 19 |
| 2017-05-15 | 0 |
| 2017-05-22 | 8 |
| 2017-05-29 | 10 |
| 2017-06-05 | 5 |
| 2017-06-12 | 0 |
| 2017-06-19 | 4 |
| 2017-06-26 | 0 |
+−−−−−−−−−−−−+−−−−−−−+
How can I do that?
Set up information for testing:
SET DATEFIRST 1;
SET DATEFORMAT ymd;
CREATE TABLE #temp (
D DATETIME
);
GO
INSERT INTO #temp (D)
VALUES -- Week of 2017-05-01 (#19)
('2017-05-01'),('2017-05-01'),('2017-05-01'),
('2017-05-06'),('2017-05-06'),
-- Week of 2017-05-08 (#20) - note no data actually on the 8th
('2017-05-10'),
('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),
('2017-05-12'),('2017-05-12'),('2017-05-12'),('2017-05-12'),
('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),
('2017-05-14'),
-- Week of 2017-05-15 (#21)
-- (note we have no data for this week)
-- Week of 2017-05-22 (#22)
('2017-05-22'),('2017-05-22'),('2017-05-22'),
('2017-05-23'),('2017-05-23'),('2017-05-23'),('2017-05-23'),('2017-05-23'),
-- Week of 2017-05-29 (#23)
('2017-05-29'),('2017-05-29'),('2017-05-29'),
('2017-06-02'),('2017-06-02'),
('2017-06-03'),
('2017-06-04'),('2017-06-04'),('2017-06-04'),('2017-06-04'),
-- Week of 2017-06-05 (#24) - note no data actually on the 5th
('2017-06-08'),('2017-06-08'),('2017-06-08'),
('2017-06-11'),('2017-06-11'),
-- Week of 2017-06-12 (#25)
-- (note we have no data for this week)
-- Week of 2017-06-19 (#26)
('2017-06-19'),('2017-06-19'),('2017-06-19'),
('2017-06-20');
GO
To do this, you have to generate a table or CTE with the Monday dates and their week numbers (as shown in this answer, slightly modified for what we need to do below), then LEFT JOIN or OUTER APPLY that with your data grouped by week, using the week numbers:
SET DATEFORMAT ymd;
SET DATEFIRST 1;
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
;WITH Mondays AS (
SELECT #startDate AS D, DATEPART(WK, #startDate) AS W
UNION ALL
SELECT DATEADD(DAY, 7, D), DATEPART(WK, DATEADD(DAY, 7, D))
FROM Mondays m
WHERE DATEADD(DAY, 7, D) < #endDate
)
SELECT LEFT(CONVERT(NVARCHAR(MAX), Mondays.D, 120), 10) AS [Week Of], d.Count
FROM Mondays
OUTER APPLY (
SELECT COUNT(*) AS [Count]
FROM #temp
WHERE DATEPART(WK, D) = W
AND D >= #startDate
AND D < #endDate
) d
ORDER BY Mondays.D;
Two notes on that:
I'm assuming we can ensure that #startDate is a Monday, which is easily done outside the query or could be done with a simple loop in T-SQL if needed (backing up until WEEKPART(WEEKDAY, #startDate) is 1). (Or worst case we could generate all the dates and then filter them with WEEKPART(WEEKDAY, ...).)
I'm assuming the date range is always a year or less; otherwise, we'd have duplicated week numbers. If the date range could be longer than a year, combine the week number with the year everywhere we're just using a week number above (e.g., DATEPART(YEAR, D) * 100 + DATEPART(wk, D)).
You can use this.
SET DATEFORMAT ymd;
SET DATEFIRST 1; -- Monday is first day of week
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
;WITH OrgResult AS ( -- Grouping result with missing week. Answer of the first question
SELECT
DATEADD(DAY, 1 - DATEPART (WEEKDAY, D), D) [Week] -- Fist Day Of the Week
, COUNT(*) [Count]
FROM #temp
WHERE D BETWEEN #startDate AND #endDate
GROUP BY
DATEADD(DAY, 1 - DATEPART (WEEKDAY, D), D)
)
, Result AS -- Adds only missing weeks. Answer of the second question
(
SELECT * FROM OrgResult
UNION ALL
SELECT DATEADD( DAY, 7, R.[Week] ), 0 [Count]
FROM Result R
WHERE NOT EXISTS( SELECT * FROM OrgResult O WHERE [Week] = DATEADD( DAY, 7, R.[Week] ) )
AND DATEADD( DAY, 7, R.[Week] ) <= #endDate
)
SELECT * FROM Result
ORDER BY [Week]
Result:
Week Count
----------- -----------
2017-05-01 5
2017-05-08 19
2017-05-15 0
2017-05-22 8
2017-05-29 10
2017-06-05 5
2017-06-12 0
2017-06-19 4
2017-06-26 0
Here's another approach. I included this as it will generate less reads than the Recursive CTE Solution and will be a lot fast
WITH E(N) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))x(x)),
iTally(N) AS
(
SELECT TOP (((DATEDIFF(day,#startdate, #endDate))/7)+1)
(ROW_NUMBER() OVER (ORDER BY (SELECT 1))-1)
FROM E a, E b, E c
)
SELECT WeekOf = DATEADD(WEEK,N,#startDate), [count] = COUNT(t.D)
FROM iTally i
LEFT JOIN #temp t ON t.D >= DATEADD(WEEK,N,#startDate) AND t.D < DATEADD(WEEK,N+1,#startDate)
GROUP BY DATEADD(WEEK,N,#startDate)
ORDER BY DATEADD(WEEK,N,#startDate); -- not required
Results:
WeekOf count
---------- -----------
2017-05-01 5
2017-05-08 19
2017-05-15 0
2017-05-22 8
2017-05-29 10
2017-06-05 5
2017-06-12 0
2017-06-19 4
2017-06-26 0
I am trying to find out the number of stores that had no sales for the last 4 weeks. Because of the way the grid is structured in DevExpress, I need to show the weeks as Wk1, Wk2, Wk3, Wk4 as columns (the data in these columns would be the store count of stores with zero sales).
Right now my data has the Sale Fiscal Week as a row. What I need it to look like is basically like:
Vendor | Store | City | State | Category | Wk1 | Wk2 | Wk3| Wk4
------------------------------------------------------------------------
Prairie Farms | #16141 | Adrian | MI | 2% Gallon | 1 | 0 | 1 | 1
Prairie Farms | #16141 | Adrian | MI | Whole Gallon | 1 | 1 | 0 | 0
instead of
Vendor | Store | City | State | Category | Sale Cal Wk | Sale Fiscal Wk
--------------------------------------------------------------------------
Prairie Farms | #16141 | Adrian | MI | 2% Gallon | 2015-10-23 | 38
Prairie Farms | #16141 | Adrian | MI | Whole Gallon | 2015-10-23 | 38
I have included my code which works so far. I have a count(store) function as 'ZeroStore' in the final select statement to indicate the store as a Zero Sales store.
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF 1=0
BEGIN
SET FMTONLY OFF
END
DECLARE #4WeeksAgo date
DECLARE #Today date
SET #4WeeksAgo = DATEADD(day,-28,getdate())
SET #Today = cast(GETDATE() as date)
DECLARE #maxweek as tinyint
SET #maxweek = case when (SELECT DGFiscalWeek
FROM scans.dbo.DimDate
WHERE CAST(Actualdate as DATE)=dateadd(dd,0,CAST(getdate() as DATE))) = 1 THEN 52 ELSE
(SELECT DGFiscalWeek FROM scans.dbo.DimDate WHERE CAST(Actualdate as DATE)=dateadd(dd,0,CAST(getdate() as DATE)))-1 END
DECLARE #LYear as int
SET #LYear = cast ((select max(FiscalYr) from Scans.dbo.Scans)-1 as varchar(6))
DECLARE #Year as int
SET #Year = #LYear+1
CREATE TABLE #Initialize
(
[Master Vendor] varchar (100),
[Dairy Vendor] varchar (100),
Store float,
City varchar (50),
State varchar(5),
District int,
Category varchar(20),
[Sale Cal Wk] date,
SaleYear int,
[Sale Fiscal Week] tinyint,
Units int,
Sales money,
Grade int
)
INSERT INTO #Initialize
SELECT DISTINCT
d.[Master Vendor], d.[Dairy Vendor], d.[Store],
City, State, District,
'2% Gallon',
CAST(MAX(ActualDate) AS DATE) AS [Sale Cal Wk],
DGFiscalYear,
DGFiscalWeek,
Units = 0,
Sales = 0,
Grade = 0
FROM
Scans.dbo.DGStores s
FULL JOIN
Scans.dbo.DimDate dd ON s.State <> 'XX'
FULL JOIN
Tableau.dbo.DollarGeneralDairyDistributors d ON d.Store = s.Store
WHERE
((DGFiscalYear = #LYear AND DGFiscalWeek >= #maxweek) OR
(DGFiscalYear = #Year AND DGFiscalWeek BETWEEN 1 AND #maxweek))
AND d.Store IN (SELECT Store
FROM DollarGeneralDairyDistributors)
GROUP BY
d.[Master Vendor], d.[Dairy Vendor], District, d.Store,
DGFiscalYear, DGFiscalWeek, City, State
INSERT INTO #Initialize
SELECT DISTINCT
d.[Master Vendor] ,
d.[Dairy Vendor] ,
d.[Store],
City,
[State],
District,
Category = 'Whole Gallon',
cast(max(ActualDate) as DATE) as [Sale Cal Wk],
DGFiscalYear,
DGFiscalWeek,
Units=0,
Sales=0,
Grade=0
FROM
Scans.dbo.DGStores s
FULL JOIN
Scans.dbo.DimDate dd
ON
s.State <> 'XX'
FULL JOIN
Tableau.dbo.DollarGeneralDairyDistributors d
ON
d.Store = s.Store
WHERE
((DGFiscalYear = #LYear AND DGFiscalWeek >= #maxweek) OR (DGFiscalYear = #Year AND DGFiscalWeek BETWEEN 1 AND #maxweek))
AND
d.Store IN
(SELECT Store
FROM DollarGeneralDairyDistributors)
GROUP BY d.[Master Vendor], d.[Dairy Vendor] ,District, d.Store, DGFiscalYear, DGFiscalWeek, City, State
CREATE TABLE #Update
(Store varchar(15),
Category varchar(25),
FiscalYr int,
FiscalWk tinyint,
Units int,
Sales money)
INSERT #Update
SELECT DISTINCT Store, Category, FiscalYr, FiscalWk,
isnull(sum(isnull(SumUnits,0)),0) as Units,
isnull(sum(isnull(SumSales,0)),0) as Sales
FROM scans.dbo.Scans sc
JOIN [Scans].[dbo].DollarGeneralDairyCategory c
ON sc.ItemSku = c.ItemSku
WHERE
FiscalYr >= #LYear
GROUP BY[Store], FiscalYr, FiscalWk, Category
UPDATE #Initialize
SET Units = u.Units,
Sales = u.Sales,
Grade=100
FROM #Update u
WHERE #Initialize.[Sale Fiscal Week] = u.FiscalWk AND #Initialize.SaleYear = u.FiscalYr
AND #Initialize.Store=u.Store AND #Initialize.Category = u.Category
SELECT *
, COUNT(store) AS 'ZeroStore'
FROM #Initialize
GROUP BY [Sale Cal Wk], [Master Vendor], [Dairy Vendor], Store, City, State, District, Category, SaleYear, [Sale Fiscal Week], Units, Sales, Grade
HAVING SUM( Units) = 0 AND [Sale Cal Wk] BETWEEN #4WeeksAgo AND #Today
drop table #Initialize,#Update
END
Thank you very much for any input or help.
I have a table with account deposits.
+-----------+------------+-----------+
| DepositId | Date | Amount |
+-----------+------------+-----------+
| 1 | 2014-06-12 | 2342,00 |
| 2 | 2014-08-05 | 23423,00 |
| 3 | 2014-09-07 | 7745,00 |
|....................................|
| 12 | 2014-12-05 | 35435,00 |
| 13 | 2014-12-11 | 353453,00 |
| 14 | 2014-12-29 | 53453,00 |
+-----------+------------+-----------+
I want to see weekly balance change like this:
+------------+----------+
| Date | Amount |
+------------+----------+
| 2014-10-07 | 74754,00 |
| 2014-10-14 | 74754,00 |
| 2014-10-21 | 6353,00 |
| 2014-10-28 | 6353,00 |
| ........ | ...... |
| 2014-12-30 | 53453,00 |
+------------+----------+
To see this changes for past 3 month (~ 13 weeks) I can use this queries:
select CONVERT(date, DATEADD(WEEK, -13, GETDATE())) as Date, ad.Amount
from AccountDeposits as ad
inner join
(select Max(Date) as Date
from AccountDeposits
where (Date < DATEADD(WEEK, -13, GETDATE())))
as ad2 on (ad.Date = ad2.Date)
union all
select CONVERT(date, DATEADD(WEEK, -12, GETDATE())) as Date, ad.Amount
from AccountDeposits as ad
inner join
(select Max(Date) as Date
from AccountDeposits
where (Date < DATEADD(WEEK, -12, GETDATE())))
as ad2 on (ad.Date = ad2.Date)
......................................................
select CONVERT(date, DATEADD(WEEK, -1, GETDATE())) as Date, ad.Amount
from AccountDeposits as ad
inner join
(select Max(Date) as Date
from AccountDeposits
where (Date < DATEADD(WEEK, -1, GETDATE())))
as ad2 on (ad.Date = ad2.Date)
I have to do this with recursive Common Table Expressions but in recursive part of CTE I can't use MAX() function. How I should write this query series to on query with CTE?
I may have misinterpreted the question (apologies if I have) but if the question is "for each week when there were deposits, give the sum of the total deposits for that week and the last day of that week" then the T-SQL below would give the correct results.
with myCte1 as
(
select *, datepart(week,d.[Date]) as wk, datepart(year,d.[Date]) as yr,
dateadd(dd, 7-(datepart(dw,d.[Date])), d.[Date]) as weekEndDate
from dbo.AccountDeposits as d
),
myCte2 as
(
select *, sum(m.Amount) over (partition by m.yr, m.wk) as totalWeeklyAmt
from myCte1 as m
)
select distinct m.weekEndDate, m.totalWeeklyAmt
from myCte2 as m
This uses two CTE's, one summarises our source data and the other uses a recursive CTE to generate all the weeks, this allows us to also show weeks where there were no deposits. It also uses two co-related subqueries to get the sumarised data from the first CTE.
I think this satisfies the requirements of your assignment.'
--NOTE: this gets data based on week end date, so all deposits for week of #WeeksHistory ago not just the deposits after the date (today minus #WeeksHistory weeks).
--NOTE: this gets all historical data so that we can start with opening balance of $0 otherwise Closing balance wont take previous deposits into account.
--NOTE: this gets the week starting #WeeksHistory ago and also this week so you will end up with #WeeksHistory +1 records - you might want to adjust this as necessary
-- set up our source data
declare #AccountDeposits table (DepID int, AcctHolderID int, TxnDate date, Amount numeric(10,2))
insert into #AccountDeposits
values
(1, 3,'12-25-2014', 2423.00),
(2, 1,'12-13-2014',4231.00),
(3, 2,'11-01-2014',666.00),
(4, 1,'11-01-2014',4241.34),
(5, 4,'10-23-2014',4221.00),
(6, 2,'10-22-2014',9992.00),
(7, 2,'10-04-2014',3524.00),
(8, 2,'10-14-2014',3524.00),
(9, 2,'10-15-2014',3524.00),
(10, 2,'10-16-2014',3524.00),
(11, 3,'10-14-2014',3524.00),
(12, 3,'10-15-2014',3524.00),
(13, 3,'10-16-2014',3524.00),
(14, 1,'10-01-2014',3524.00),
(15, 2,'10-01-2014',3524.00),
(16, 3,'10-01-2014',3524.00),
(17, 4,'01-01-2015',3524.00)
declare #AcctHolderID as int = 2
declare #WeeksHistory int = -13
select dateadd(week,#WeeksHistory,getdate()) ThirteenWeeksAgo
;with
src (AcctHolderID, WeekEndsOn, Amount)
as (select
AcctHolderID,
DATEADD(DAY, 7-DATEPART(WEEKDAY, TxnDate), TxnDate),
SUM(Amount)
from #AccountDeposits
where AcctHolderID = #AcctHolderID -- we filter up here so that we arent processing data we dont care about.
group by
AcctHolderID,
DATEADD(DAY, 7-DATEPART(WEEKDAY, TxnDate), TxnDate)
),
r_cte (AcctHolderID, WeekEndsOn, TotalDep, ClosingBal)
as (select
AcctHolderID,
dateadd(ww,-1,Min(WeekEndsOn)),
convert(numeric(10,2),0.00),
convert(numeric(10,2),0.00)
from
src
group by
AcctHolderID
union all
select
r_cte.AcctHolderID,
dateadd(WW,1,r_cte.WeekEndsOn),
convert(numeric(10,2),ISNULL((select Amount from src where AcctHolderID = r_cte.AcctHolderID and WeekEndsOn = dateadd(WW,1,r_cte.WeekEndsOn)),0)),
convert(numeric(10,2),ISNULL((select Amount from src where AcctHolderID = r_cte.AcctHolderID and WeekEndsOn = dateadd(WW,1,r_cte.WeekEndsOn)),0) + r_cte.ClosingBal)
from
r_cte
where
AcctHolderID = r_cte.AcctHolderID
and r_cte.WeekEndsOn < DATEADD(DAY, 7-DATEPART(WEEKDAY, Getdate()), DATEADD(WW,-1,Getdate()))
)
select AcctHolderID, DATEDIFF(ww, WeekEndsOn, getdate()) as WeeksAgo, WeekEndsOn, TotalDep, ClosingBal
from r_cte
where r_cte.WeekEndsOn > dateadd(week,#WeeksHistory,getdate())
order by
AcctHolderID,
WeekEndsOn
I have solved it without CTE...
First create table with startdate and enddate for 13 weeks starting from getdate ().
Create table weeklydates
(Startdate date,
Enddate date
)
Declare #startdate date
Declare #enddate date
Set #startdate = cast (dateadd (week,-13,getdate ()) as date)
Set #enddate = dateadd (day,7,#startdate)
While #enddate < = getdate ()
Begin
Insert into weeklydates
Select #startdate, #enddate
Set #startdate = dateadd (day,1,#enddate)
Set #enddate = dateadd (day,7,#startdate)
End
Now use this table to display amount which will be sum of amount whose dates fall between start date and end date
Select a.startdate,a.enddate, (select sum (amount) from yourtablehavingamount as b
Where b.deposit >=a.startdate and b.deposit <=a.enddate)
From weeklydates as a
USE:
SELECT TOP 1 [Date]
FROM AccountDeposits
--Add WHERE Clause
ORDER BY [Date] DESC
:) David
I have been struggling with a problem that should be pretty simple actually but after a full week of reading, googling, experimenting and so on, my colleague and we cannot find the proper solution. :(
The problem: We have a table with two values:
an employeenumber (P_ID, int) <--- identification of employee
a date (starttime, datetime) <--- time employee checked in
We need to know what periods each employee has been working.
When two dates are less then #gap days apart, they belong to the same period
For each employee there can be multiple records for any given day but I just need to know which dates he worked, I am not interested in the time part
As soon as there is a gap > #gap days, the next date is considered the start of a new range
A range is at least 1 day (example: 21-9-2011 | 21-09-2011) but has no maximum length. (An employee checking in every #gap - 1 days should result in a period from the first day he checked in until today)
What we think we need are the islands in this table where the gap in days is greater than #variable (#gap = 30 means 30 days)
So an example:
SOURCETABLE:
P_ID | starttime
------|------------------
12121 | 24-03-2009 7:30
12121 | 24-03-2009 14:25
12345 | 27-06-2011 10:00
99999 | 01-05-2012 4:50
12345 | 27-06-2011 10:30
12345 | 28-06-2011 11:00
98765 | 13-04-2012 10:00
12345 | 21-07-2011 9:00
99999 | 03-05-2012 23:15
12345 | 21-09-2011 12:00
45454 | 12-07-2010 8:00
12345 | 21-09-2011 17:00
99999 | 06-05-2012 11:05
99999 | 20-05-2012 12:45
98765 | 26-04-2012 16:00
12345 | 07-07-2012 14:00
99999 | 01-06-2012 13:55
12345 | 13-08-2012 13:00
Now what I need as a result is:
PERIODS:
P_ID | Start | End
-------------------------------
12121 | 24-03-2009 | 24-03-2009
12345 | 27-06-2012 | 21-07-2012
12345 | 21-09-2012 | 21-09-2012
12345 | 07-07-2012 | (today) OR 13-08-2012 <-- (less than #gap days ago) OR (last date in table)
45454 | 12-07-2010 | 12-07-2010
45454 | 17-06-2012 | 17-06-2012
98765 | 13-04-2012 | 26-04-2012
99999 | 01-05-2012 | 01-06-2012
I hope this is clear this way, I already thank you for reading this far, it would be great if you could contribute!
I've done a rough script that should get you started. Haven't bothered refining the datetimes and the endpoint comparisons might need tweaking.
select
P_ID,
src.starttime,
endtime = case when src.starttime <> lst.starttime or lst.starttime < DATEADD(dd,-1 * #gap,GETDATE()) then lst.starttime else GETDATE() end,
frst.starttime,
lst.starttime
from #SOURCETABLE src
outer apply (select starttime = MIN(starttime) from #SOURCETABLE sub where src.p_id = sub.p_id and sub.starttime > DATEADD(dd,-1 * #gap,src.starttime)) frst
outer apply (select starttime = MAX(starttime) from #SOURCETABLE sub where src.p_id = sub.p_id and src.starttime > DATEADD(dd,-1 * #gap,sub.starttime)) lst
where src.starttime = frst.starttime
order by P_ID, src.starttime
I get the following output, which is a litle different to yours, but I think its ok:
P_ID starttime endtime starttime starttime
----------- ----------------------- ----------------------- ----------------------- -----------------------
12121 2009-03-24 07:30:00.000 2009-03-24 14:25:00.000 2009-03-24 07:30:00.000 2009-03-24 14:25:00.000
12345 2011-06-27 10:00:00.000 2011-07-21 09:00:00.000 2011-06-27 10:00:00.000 2011-07-21 09:00:00.000
12345 2011-09-21 12:00:00.000 2011-09-21 17:00:00.000 2011-09-21 12:00:00.000 2011-09-21 17:00:00.000
12345 2012-07-07 14:00:00.000 2012-07-07 14:00:00.000 2012-07-07 14:00:00.000 2012-07-07 14:00:00.000
12345 2012-08-13 13:00:00.000 2012-08-16 11:23:25.787 2012-08-13 13:00:00.000 2012-08-13 13:00:00.000
45454 2010-07-12 08:00:00.000 2010-07-12 08:00:00.000 2010-07-12 08:00:00.000 2010-07-12 08:00:00.000
98765 2012-04-13 10:00:00.000 2012-04-26 16:00:00.000 2012-04-13 10:00:00.000 2012-04-26 16:00:00.000
The last two output cols are the results of the outer apply sections, and are just there for debugging.
This is based on the following setup:
declare #gap int
set #gap = 30
set dateformat dmy
-----P_ID----|----starttime----
declare #SOURCETABLE table (P_ID int, starttime datetime)
insert #SourceTable values
(12121,'24-03-2009 7:30'),
(12121,'24-03-2009 14:25'),
(12345,'27-06-2011 10:00'),
(12345,'27-06-2011 10:30'),
(12345,'28-06-2011 11:00'),
(98765,'13-04-2012 10:00'),
(12345,'21-07-2011 9:00'),
(12345,'21-09-2011 12:00'),
(45454,'12-07-2010 8:00'),
(12345,'21-09-2011 17:00'),
(98765,'26-04-2012 16:00'),
(12345,'07-07-2012 14:00'),
(12345,'13-08-2012 13:00')
UPDATE: Slight rethink. Now uses a CTE to work out the gaps forwards and backwards from each item, then aggregates those:
--Get the gap between each starttime and the next and prev (use 999 to indicate non-closed intervals)
;WITH CTE_Gaps As (
select
p_id,
src.starttime,
nextgap = coalesce(DATEDIFF(dd,src.starttime,nxt.starttime),999), --Gap to the next entry
prevgap = coalesce(DATEDIFF(dd,prv.starttime,src.starttime),999), --Gap to the previous entry
isold = case when DATEDIFF(dd,src.starttime,getdate()) > #gap then 1 else 0 end --Is starttime more than gap days ago?
from
#SOURCETABLE src
cross apply (select starttime = MIN(starttime) from #SOURCETABLE sub where src.p_id = sub.p_id and sub.starttime > src.starttime) nxt
cross apply (select starttime = max(starttime) from #SOURCETABLE sub where src.p_id = sub.p_id and sub.starttime < src.starttime) prv
)
--select * from CTE_Gaps
select
p_id,
starttime = min(gap.starttime),
endtime = nxt.starttime
from
CTE_Gaps gap
--Find the next starttime where its gap to the next > #gap
cross apply (select starttime = MIN(sub.starttime) from CTE_Gaps sub where gap.p_id = sub.p_id and sub.starttime >= gap.starttime and sub.nextgap > #gap) nxt
group by P_ID, nxt.starttime
order by P_ID, nxt.starttime
Jon most definitively has shown us the right direction. Performance was horrible though (4million+ records in the database). And it looked like we were missing some information. With all that we learned from you we came up with the solution below. It uses elements of all the proposed answers and cycles through 3 temptables before finally spewing results but performance is good enough, as well as the data it generates.
declare #gap int
declare #Employee_id int
set #gap = 30
set dateformat dmy
--------------------------------------------------------------- #temp1 --------------------------------------------------
CREATE TABLE #temp1 ( EmployeeID int, starttime date)
INSERT INTO #temp1 ( EmployeeID, starttime)
select distinct ck.Employee_id,
cast(ck.starttime as date)
from SERVER1.DB1.dbo.checkins pd
inner join SERVER1.DB1.dbo.Team t on ck.team_id = t.id
where t.productive = 1
--------------------------------------------------------------- #temp2 --------------------------------------------------
create table #temp2 (ROWNR int, Employeeid int, ENDOFCHECKIN datetime, FIRSTCHECKIN datetime)
INSERT INTO #temp2
select Row_number() OVER (partition by EmployeeID ORDER BY t.prev) + 1 as ROWNR,
EmployeeID,
DATEADD(DAY, 1, t.Prev) AS start_gap,
DATEADD(DAY, 0, t.next) AS end_gap
from
(
select a.EmployeeID,
a.starttime as Prev,
(
select min(b.starttime)
from #temp1 as b
where starttime > a.starttime and b.EmployeeID = a.EmployeeID
) as Next
from #temp1 as a) as t
where datediff(day, prev, next ) > 30
group by EmployeeID,
t.Prev,
t.next
union -- add first known date for Employee
select 1 as ROWNR,
EmployeeID,
NULL,
min(starttime)
from #temp1 ct
group by ct.EmployeeID
--------------------------------------------------------------- #temp3 --------------------------------------------------
create table #temp3 (ROWNR int, Employeeid int, ENDOFCHECKIN datetime, STARTOFCHECKIN datetime)
INSERT INTO #temp3
select ROWNR,
Employeeid,
ENDOFCHECKIN,
FIRSTCHECKIN
from #temp2
union -- add last known date for Employee
select (select count(*) from #temp2 b where Employeeid = ct.Employeeid)+1 as ROWNR,
ct.Employeeid,
(select dateadd(d,1,max(starttime)) from #temp1 c where Employeeid = ct.Employeeid),
NULL
from #temp2 ct
group by ct.EmployeeID
---------------------------------------finally check our data-------------------------------------------------
select a1.Employeeid,
a1.STARTOFCHECKIN as STARTOFCHECKIN,
ENDOFCHECKIN = CASE WHEN b1.ENDOFCHECKIN <= a1.STARTOFCHECKIN THEN a1.ENDOFCHECKIN ELSE b1.ENDOFCHECKIN END,
year(a1.STARTOFCHECKIN) as JaarSTARTOFCHECKIN,
JaarENDOFCHECKIN = CASE WHEN b1.ENDOFCHECKIN <= a1.STARTOFCHECKIN THEN year(a1.ENDOFCHECKIN) ELSE year(b1.ENDOFCHECKIN) END,
Month(a1.STARTOFCHECKIN) as MaandSTARTOFCHECKIN,
MaandENDOFCHECKIN = CASE WHEN b1.ENDOFCHECKIN <= a1.STARTOFCHECKIN THEN month(a1.ENDOFCHECKIN) ELSE month(b1.ENDOFCHECKIN) END,
(year(a1.STARTOFCHECKIN)*100)+month(a1.STARTOFCHECKIN) as JaarMaandSTARTOFCHECKIN,
JaarMaandENDOFCHECKIN = CASE WHEN b1.ENDOFCHECKIN <= a1.STARTOFCHECKIN THEN (year(a1.ENDOFCHECKIN)*100)+month(a1.STARTOFCHECKIN) ELSE (year(b1.ENDOFCHECKIN)*100)+month(b1.ENDOFCHECKIN) END,
datediff(M,a1.STARTOFCHECKIN,b1.ENDOFCHECKIN) as MONTHSCHECKEDIN
from #temp3 a1
full outer join #temp3 b1 on a1.ROWNR = b1.ROWNR -1 and a1.Employeeid = b1.Employeeid
where not (a1.STARTOFCHECKIN is null AND b1.ENDOFCHECKIN is null)
order by a1.Employeeid, a1.STARTOFCHECKIN