How to find overlapping periods recursively in SQL Server - sql

I have this example dataset
ID StartDate EndDate
------------------------------
1 2014-01-05 2014-01-10
2 2014-01-06 2014-01-11
3 2014-01-07 2014-01-12
4 2014-01-08 2014-01-13
5 2014-01-09 2014-01-14
6 2014-01-26 2014-01-31
7 2014-01-27 2014-02-01
8 2014-01-28 2014-02-02
9 2014-01-29 2014-02-03
10 2014-01-30 2014-02-04
I want to select any rows that overlap a supplied period, and any rows those overlap with and so on.
So If I want to select any rows that over lap period 2014-01-06 to 2014-01-07
The following are directly overlapping (immediate overlaps)
1 2014-01-05 2014-01-10
2 2014-01-06 2014-01-11
But I also need rows that overlap with 1 and 2 (child overlap)
3 2014-01-07 2014-01-12
4 2014-01-08 2014-01-13
5 2014-01-09 2014-01-14
And if 3 to 5 had overlaps, return them too. but in this case, there are none.
This is my attempt so far but it has two problems I see that I don't know how to fix.
;WITH cte
AS
(
SELECT t.ID,
t.StartTime,
t.EndTime
FROM
dbo.Tasks AS t
UNION ALL
SELECT t.ID,
t.StartTime,
t.EndTime
FROM
dbo.Tasks AS t INNER JOIN
cte AS c ON t.StartTime < c.EndTime
AND t.EndTime > c.StartTime
)
SELECT * FROM cte AS a WHERE a.StartTime < #NewEnd
AND a.EndTime > #NewStart
When it goes to get overlapping periods of child overlap, the immediate overlap is re-included and causes an infinite recursion. And Secondly,
SELECT * FROM cte AS a WHERE a.StartTime < #NewEnd
AND a.EndTime > #NewStart
The where clause will filter out any recursively found overlaps.

I would first work out where the islands are in your data set, and only after that, work out which ones are overlapped by your query ranges:
declare #t table (ID int,StartDate date,EndDate date)
insert into #t(ID,StartDate,EndDate) values
(1 ,'20140105','20140110'),
(2 ,'20140106','20140111'),
(3 ,'20140107','20140112'),
(4 ,'20140108','20140113'),
(5 ,'20140109','20140114'),
(6 ,'20140126','20140131'),
(7 ,'20140127','20140201'),
(8 ,'20140128','20140202'),
(9 ,'20140129','20140203'),
(10 ,'20140130','20140204')
declare #Start date
declare #End date
select #Start='20140106',#End='20140107'
;With PotIslands as (
--Find ranges which aren't overlapped at their start
select StartDate,EndDate from #t t where
not exists (select * from #t t2 where
t2.StartDate < t.StartDate and
t2.EndDate >= t.StartDate)
union all
--Extend the ranges by any other ranges which overlap on the end
select pi.StartDate,t.EndDate
from PotIslands pi
inner join
#t t
on
pi.EndDate >= t.StartDate and pi.EndDate < t.EndDate
), Islands as (
select StartDate,MAX(EndDate) as EndDate from PotIslands group by StartDate
)
select * from Islands i where #Start <= i.EndDate and #End >= i.StartDate
Result:
StartDate EndDate
---------- ----------
2014-01-05 2014-01-14
If you need the individual rows, you can now join the selected islands back to the #t table for a simple range query.
This works because, for example, if any row within an island is ever included in a range, the entire remaining rows on an island will always also be included. So we find the islands first.

This should do it, you could put it into a function if needed and use a cross apply to join to another table. I did not test it but it should work with minimal (if any) errors.
declare #rt table
(
ID int not null,
StartTime date not null,
EndTime date not null
)
insert into #rt (ID, StartTime, EndTime)
select t.*
from Tasks t
where (#StartTime <= t.StartTime and #EndTime > t.StartTime)
or (#StartTime < t.EndTime and #EndTime >= t.EndTime)
declare #found int = ##rowcount
while #found > 0
begin
insert into #rt (ID, StartTime, EndTime)
select t.*
from Tasks t
left join #rt rt
on (rt.StartTime <= t.StartTime and rt.EndTime > t.StartTime)
or (rt.StartTime < t.EndTime and rt.EndTime >= t.EndTime)
where t.ID not in (select ID from #rt)
set #found = ##rowcount
end
select * from #rt

Related

Finding all dates after a date for a variable number of days

I have a list of dates in a table. For this examples the 1st day of each month. Let's call it table timeperiod with column endTime
endTime
1-1-2019
2-1-2019
3-1-2019
4-1-2019
I want to find all dates x number of days after each date in a list. Lets say x = 4. Then the list should be:
1-1-2019
1-2-2019
1-3-2019
1-4-2019
2-1-2019
2-2-2019
2-3-2019
2-4-2019
3-1-2019
3-2-2019
3-3-2019
3-4-2019
4-1-2019
4-2-2019
4-3-2019
4-4-2019
I have found solutions to find all dates between dates but I keep getting "Subquery returned more than 1 value" error when I try to use it with a list of dates.
Here is an example of something I tried but doesn't work
declare #days DECIMAL = 4
declare #StartDate date = (select convert(varchar, DATEADD(Day, +0, endTime),101) from timeperiod
declare #EndDate date = (select convert(varchar, DATEADD(Day, +#days, endTime),101) from timeperiod;
;WITH cte AS (
SELECT #StartDate AS myDate
UNION ALL
SELECT DATEADD(day,1,myDate) as myDate
FROM cte
WHERE DATEADD(day,1,myDate) <= #EndDate
)
SELECT myDate
FROM cte
OPTION (MAXRECURSION 0)
Here is a row generator that generates 5 rows, 0 to 4:
WITH rg AS (
SELECT 0 AS rn
UNION ALL
SELECT rg.rn + 1
FROM rg
WHERE rn < 4
)
Here we join it with your existing table that has firsts of the month and use DATEADD to add rn numbers of days (between 0 and 4) to the endPeriod. CROSS JOINing it caused the rows in timePeriod to repeat 5 times each:
SELECT
DATEADD(DAY, rg.rn, timePeriod.endTime) as fakeEndTime
FROM
rg CROSS JOIN timePeriod
I wasn't really clear when you say "days X days after the date, say x = 4" - to me if there is a day that is 1-Jan-2000, then the date 4 days after this is 5-Jan-2000
If you only want the 1,2,3 and 4 of Jan make the row generator < 3 instead of < 4
Already +1'd on Caius Jard's recursive cte.
Here is yet another option using an ad-hoc tally table in concert with a CROSS JOIN
Example
Declare #YourTable Table ([endTime] date)
Insert Into #YourTable Values
('1-1-2019')
,('2-1-2019')
,('3-1-2019')
,('4-1-2019')
Select NewDate = dateadd(DAY,N-1,EndTime)
From #YourTable A
Cross Join (
Select Top (4) N=row_number() over (order by (select null))
From master..spt_values N1
) B
Returns
NewDate
2019-01-01
2019-01-02
2019-01-03
2019-01-04
2019-02-01
2019-02-02
2019-02-03
2019-02-04
2019-03-01
2019-03-02
2019-03-03
2019-03-04
2019-04-01
2019-04-02
2019-04-03
2019-04-04

Generate a list of dates between 2 dates for more than one record

I am trying to write SQL to generate the following data
Date Count
2018-09-24 2
2018-09-25 2
2018-09-26 2
2018-09-27 2
2018-09-28 2
2018-09-29 1
A sample of the base table I am using is
ID StartDate EndDate
187267 2018-09-24 2018-10-01
187270 2018-09-24 2018-09-30
So I'm trying to get a list of dates between 2 dates and then count how many base data records there are in each date.
I started using a temporary table and attempting to loop through the records to get the results but I'm not sure if this is the right approach.
I have this code so far
WITH ctedaterange
AS (SELECT [Dates] = (select ea.StartWork from EngagementAssignment ea where ea.EngagementAssignmentId IN(SELECT ea.EngagementAssignmentId
FROM EngagementLevel el INNER JOIN
EngagementAssignment ea ON el.EngagementLevelID = ea.EngagementLevelId
WHERE el.JobID = 15072 and ea.AssetId IS NOT NULL))
UNION ALL
SELECT [dates] + 1
FROM ctedaterange
WHERE [dates] + 1 < = (select ea.EndWork from EngagementAssignment ea where ea.EngagementAssignmentId IN(SELECT ea.EngagementAssignmentId
FROM EngagementLevel el INNER JOIN
EngagementAssignment ea ON el.EngagementLevelID = ea.EngagementLevelId
WHERE el.JobID = 15072 and ea.AssetId IS NOT NULL)))
SELECT [Dates], Count([Dates])
FROM ctedaterange
GROUP BY [Dates]
But I get this error
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
I get correct results when the job I use only generates one record in the subselect in the where clause, ie:
SELECT ea.EngagementAssignmentId
FROM EngagementLevel el INNER JOIN
EngagementAssignment ea ON el.EngagementLevelID = ea.EngagementLevelId
WHERE el.JobID = 15047 and ea.AssetId IS NOT NULL
generates one record.
The results look like this:
Dates (No column name)
2018-09-24 02:00:00.000 1
2018-09-25 02:00:00.000 1
2018-09-26 02:00:00.000 1
2018-09-27 02:00:00.000 1
2018-09-28 02:00:00.000 1
2018-09-29 02:00:00.000 1
2018-09-30 02:00:00.000 1
2018-10-01 02:00:00.000 1
you can generate according to your range by changing from and to date
DECLARE
#DateFrom DATETIME = GETDATE(),
#DateTo DATETIME = '2018-10-30';
WITH DateGenerate
AS (
SELECT #DateFrom as MyDate
UNION ALL
SELECT DATEADD(DAY, 1, MyDate)
FROM DateGenerate
WHERE MyDate < #DateTo
)
SELECT
MyDate
FROM
DateGenerate;
Well, if you only have a low date range, you can use a recursive CTE as demonstrated in the other answers. The problem with a recursive CTE is with large ranges, where it starts to be ineffective - So I wanted to show you a different approach, that builds the calendar CTE without using recursion.
First, Create and populate sample table (Please save us this step in your future questions):
DECLARE #T AS TABLE
(
ID int,
StartDate date,
EndDate date
)
INSERT INTO #T (ID, StartDate, EndDate) VALUES
(187267, '2018-09-24', '2018-10-01'),
(187270, '2018-09-24', '2018-09-30')
Then, get the first start date and the number of dates you need in the calendar cte:
DECLARE #DateDiff int, #StartDate Date
SELECT #DateDiff = DATEDIFF(DAY, MIN(StartDate), Max(EndDate)),
#StartDate = MIN(StartDate)
FROM #T
Now, construct the calendar cte based on row_number (that is, unless you already have a numbers (tally) table you can use):
;WITH Calendar(TheDate)
AS
(
SELECT TOP(#DateDiff + 1) DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY ##SPID)-1, #StartDate)
FROM sys.objects t0
-- unremark the next row if you don't get enough records...
-- CROSS JOIN sys.objects t1
)
Note that I'm using row_number() - 1 and therefor have to select top(#DateDiff + 1)
Finally - the query:
SELECT TheDate, COUNT(ID) As NumberOfRecords
FROM Calendar
JOIN #T AS T
ON Calendar.TheDate >= T.StartDate
AND Calendar.TheDate <= T.EndDate
GROUP BY TheDate
Results:
TheDate | NumberOfRecords
2018-09-24 | 2
2018-09-25 | 2
2018-09-26 | 2
2018-09-27 | 2
2018-09-28 | 2
2018-09-29 | 2
2018-09-30 | 2
2018-10-01 | 1
You can see a live demo on rextester.
Can you please try following SQL CTE query where I have used a SQL dates table function [dbo].[DatesTable] which produces a list of dates between min date and max date in the source table
;with boundaries as (
select
min(StartDate) minD, max(EndDate) maxD
from DateRanges
), dates as (
select
dates.[date]
from boundaries
cross apply [dbo].[DatesTable](minD, maxD) as dates
)
select dates.[date], count(*) as [count]
from dates
inner join DateRanges
on dates.date between DateRanges.StartDate and DateRanges.EndDate
group by dates.[date]
order by dates.[date]
The output is as expected
Try this: demo
WITH cte1
AS (SELECT id,sdate,edate from t
union all
select c.id,DATEADD(DAY, 1, c.sdate),c.edate from cte1 c where DATEADD(DAY, 1, c.sdate)<=c.edate
)
SELECT sdate,count(id) as total FROM cte1
group by sdate
OPTION (MAXRECURSION 0)
Output:
sdate total
2018-09-24 2
2018-09-25 2
2018-09-26 2
2018-09-27 2
2018-09-28 2
2018-09-29 2
2018-09-30 1

SQL how to count census points occurring between date records

I’m using MS-SQL-2008 R2 trying to write a script that calculates the Number of Hospital Beds occupied on any given day, at 2 census points: midnight, and 09:00.
I’m working from a data set of patient Ward Stays. Basically, each row in the table is a record of an individual patient's stay on a single ward, and records the date/time the patient is admitted onto the ward, and the date/time the patient leaves the ward.
A sample of this table is below:
Ward_Stay_Primary_Key | Ward_Start_Date_Time | Ward_End_Date_Time
1 | 2017-09-03 15:04:00.000 | 2017-09-27 16:55:00.000
2 | 2017-09-04 18:08:00.000 | 2017-09-06 18:00:00.000
3 | 2017-09-04 13:00:00.000 | 2017-09-04 22:00:00.000
4 | 2017-09-04 20:54:00.000 | 2017-09-08 14:30:00.000
5 | 2017-09-04 20:52:00.000 | 2017-09-13 11:50:00.000
6 | 2017-09-05 13:32:00.000 | 2017-09-11 14:49:00.000
7 | 2017-09-05 13:17:00.000 | 2017-09-12 21:00:00.000
8 | 2017-09-05 23:11:00.000 | 2017-09-06 17:38:00.000
9 | 2017-09-05 11:35:00.000 | 2017-09-14 16:12:00.000
10 | 2017-09-05 14:05:00.000 | 2017-09-11 16:30:00.000
The key thing to note here is that a patient’s Ward Stay can span any length of time, from a few hours to many days.
The following code enables me to calculate the number of beds at both census points for any given day, by specifying the date in the case statement:
SELECT
'05/09/2017' [Date]
,SUM(case when Ward_Start_Date_Time <= '05/09/2017 00:00:00.000' AND (Ward_End_Date_Time >= '05/09/2017 00:00:00.000' OR Ward_End_Date_Time IS NULL)then 1 else 0 end)[No. Beds Occupied at 00:00]
,SUM(case when Ward_Start_Date_Time <= '05/09/2017 09:00:00.000' AND (Ward_End_Date_Time >= '05/09/2017 09:00:00.000' OR Ward_End_Date_Time IS NULL)then 1 else 0 end)[No. Beds Occupied at 09:00]
FROM
WardStaysTable
And, based on the sample 10 records above, generates this output:
Date | No. Beds Occupied at 00:00 | No. Beds Occupied at 09:00
05/09/2017 | 4 | 4
To perform this for any number of days is obviously onerous, so what I’m looking to create is a query where I can specify a start/end date parameter (e.g. 1st-5th Sept), and for the query to then evaluate the Ward_Start_Date_Time and Ward_End_Date_Time variables for each record, and – grouping by the dates defined in the date parameter – count each time the 00:00:00.000 and 09:00:00.000 census points fall between these 2 variables, to give an output something along these lines (based on the above 10 records):
Date | No. Beds Occupied at 00:00 | No. Beds Occupied at 09:00
01/09/2017 | 0 | 0
02/09/2017 | 0 | 0
03/09/2017 | 0 | 0
04/09/2017 | 1 | 1
05/09/2017 | 4 | 4
I’ve approached this (perhaps naively) thinking that if I use a cte to create a table of dates (defined by the input parameters), along with associated midnight and 9am census date/time points, then I could use these variables to group and evaluate the dataset.
So, this code generates the grouping dates and census date/time points:
DECLARE
#StartDate DATE = '01/09/2017'
,#EndDate DATE = '05/09/2017'
,#0900 INT = 540
SELECT
DATEADD(DAY, nbr - 1, #StartDate) [Date]
,CONVERT(DATETIME,(DATEADD(DAY, nbr - 1, #StartDate))) [MidnightDate]
,DATEADD(mi, #0900,(CONVERT(DATETIME,(DATEADD(DAY, nbr - 1, #StartDate))))) [0900Date]
FROM
(
SELECT
ROW_NUMBER() OVER ( ORDER BY c.object_id ) AS nbr
FROM sys.columns c
) nbrs
WHERE nbr - 1 <= DATEDIFF(DAY, #StartDate, #EndDate)
The stumbling block I’ve hit is how to join the cte to the WardStays dataset, because there’s no appropriate key… I’ve tried a few iterations of using a subquery to make this work, but either I’m taking the wrong approach or I’m getting my syntax in a mess.
In simple terms, the logic I’m trying to create to get the output is something like:
SELECT
[Date]
,SUM (case when WST.Ward_Start_Date_Time <= [MidnightDate] AND (WST.Ward_End_Date_Time >= [MidnightDate] OR WST.Ward_End_Date_Time IS NULL then 1 else 0 end) [No. Beds Occupied at 00:00]
,SUM (case when WST.Ward_Start_Date_Time <= [0900Date] AND (WST.Ward_End_Date_Time >= [0900Date] OR WST.Ward_End_Date_Time IS NULL then 1 else 0 end) [No. Beds Occupied at 09:00]
FROM WardStaysTable WST
GROUP BY [Date]
Is the above somehow possible, or am I barking up the wrong tree and need to take a different approach altogether? Appreciate any advice.
I would expect something like this:
WITH dates as (
SELECT CAST(#StartDate as DATETIME) as dte
UNION ALL
SELECT DATEADD(DAY, 1, dte)
FROM dates
WHERE dte < #EndDate
)
SELECT dates.dte [Date],
SUM(CASE WHEN Ward_Start_Date_Time <= dte AND
Ward_END_Date_Time >= dte
THEN 1 ELSE 0
END) as num_beds_0000,
SUM(CASE WHEN Ward_Start_Date_Time <= dte + CAST('09:00' as DATETIME) AND
Ward_END_Date_Time >= dte + CAST('09:00' as DATETIME)
THEN 1 ELSE 0
END) as num_beds_0900
FROM dates LEFT JOIN
WardStaysTable wt
ON wt.Ward_Start_Date_Time <= DATEADD(day, 1, dates.dte) AND
wt.Ward_END_Date_Time >= dates.dte
GROUP BY dates.dte
ORDER BY dates.dte;
The cte is just creating the list of dates.
What a cool exercise. Here is what I came up with:
CREATE TABLE #tmp (ID int, StartDte datetime, EndDte datetime)
INSERT INTO #tmp values(1,'2017-09-03 15:04:00.000','2017-09-27 06:55:00.000')
INSERT INTO #tmp values(2,'2017-09-04 08:08:00.000','2017-09-06 18:00:00.000')
INSERT INTO #tmp values(3,'2017-09-04 13:00:00.000','2017-09-04 22:00:00.000')
INSERT INTO #tmp values(4,'2017-09-04 20:54:00.000','2017-09-08 14:30:00.000')
INSERT INTO #tmp values(5,'2017-09-04 20:52:00.000','2017-09-13 11:50:00.000')
INSERT INTO #tmp values(6,'2017-09-05 13:32:00.000','2017-09-11 14:49:00.000')
INSERT INTO #tmp values(7,'2017-09-05 13:17:00.000','2017-09-12 21:00:00.000')
INSERT INTO #tmp values(8,'2017-09-05 23:11:00.000','2017-09-06 07:38:00.000')
INSERT INTO #tmp values(9,'2017-09-05 11:35:00.000','2017-09-14 16:12:00.000')
INSERT INTO #tmp values(10,'2017-09-05 14:05:00.000','2017-09-11 16:30:00.000')
DECLARE
#StartDate DATE = '09/01/2017'
,#EndDate DATE = '10/01/2017'
, #nHours INT = 9
;WITH d(OrderDate) AS
(
SELECT DATEADD(DAY, n-1, #StartDate)
FROM (SELECT TOP (DATEDIFF(DAY, #StartDate, #EndDate) + 1)
ROW_NUMBER() OVER (ORDER BY [object_id]) FROM sys.all_objects) AS x(n)
)
, CTE AS(
select OrderDate, t2.*
from #tmp t2
cross apply(select orderdate from d ) d
where StartDte >= #StartDate and EndDte <= #EndDate)
select OrderDate,
SUM(CASE WHEN OrderDate >= StartDte and OrderDate <= EndDte THEN 1 ELSE 0 END) [No. Beds Occupied at 00:00],
SUM(CASE WHEN StartDTE <= DateAdd(hour,#nHours,CAST(OrderDate as datetime)) and DateAdd(hour,#nHours,CAST(OrderDate as datetime)) <= EndDte THEN 1 ELSE 0 END) [No. Beds Occupied at 09:00]
from CTE
GROUP BY OrderDate
This should allow you to check for any hour of the day using the #nHours parameter if you so choose. If you only want to see records that actually fall within your date range then you can filter the cross apply on start and end dates.

SQL - creating a list of custom dates between two dates

I am having trouble compiling a query than can do the following:
I have a table which has a startDate and endDate [tblPayments]
I have a column which stores a specific paymentDay [tblPayments]
Data
paymentID startDate endDate paymentDay
1 2016-01-01 2016-12-31 25
2 2015-06-01 2016-06-30 16
I am trying to generate a SELECT query which will split this specific table into separate lines based on the amount of months between these two dates, and set the paymentDay as the day for these queries
Example Output
paymentID expectedDate
1 2016-01-25
1 2016-02-25
1 2016-03-25
1 2016-04-25
1 2016-05-25
1 2016-06-25
1 2016-07-25
1 2016-08-25
1 2016-09-25
1 2016-10-25
1 2016-11-25
1 2016-12-25
2 2015-06-16
2 2015-07-16
2 2015-08-16
2 2015-09-16
2 2015-10-16
2 2015-11-16
2 2015-12-16
2 2016-01-16
2 2016-02-16
2 2016-03-16
2 2016-04-16
2 2016-05-16
I have found a query which will select the months between these dates but its adapting it to my table above, and multiple startDates and endDates I am struggling with
spliting the months
declare #start DATE = '2015-01-01'
declare #end DATE = '2015-12-31'
;with months (date)
AS
(
SELECT #start
UNION ALL
SELECT DATEADD(MM,1,date)
from months
where DATEADD(MM,1,date)<=#end
)
select Datename(MM,date) from months
This query is limited to just one startDate and endDate, so I haven't expanded it to change the DAY of the date.
Use a date table and a simple inner join
DECLARE #tblPayments table (paymentID int identity(1,1), startDate date, endDate date, paymentDay int)
INSERT #tblPayments VALUES
('2016-01-01', '2016-12-31', 25),
('2015-06-01', '2016-06-30', 16)
;WITH dates AS -- Build date within the range of startDate and endDate
(
SELECT MIN(startDate) AS Value, MAX(endDate) AS MaxDate FROM #tblPayments
UNION ALL
SELECT DATEADD(DAY, 1, Value), MaxDate
FROM dates WHERE DATEADD(DAY, 1, Value) <= MaxDate
)
SELECT pay.paymentID, dates.Value AS expectedDate
FROM
#tblPayments pay
INNER JOIN dates ON
dates.Value BETWEEN pay.startDate AND pay.endDate
AND DAY(dates.Value) = paymentDay
OPTION (maxrecursion 0)
I would create an in memory calendar table and then perform a simple query by joining to that:
-- Create a table with all the dates between the min and max dates in the
-- data table
DECLARE #Calendar TABLE
(
[CalendarDate] DATETIME
)
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SELECT #StartDate = MIN(startdate), #EndDate = MAX(enddate) FROM YourDataTable
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO #Calendar (CalendarDate)
SELECT #StartDate
SET #StartDate = DATEADD(dd, 1, #StartDate)
END
-- Join to return only dates between the start and end date that match the Payment Day
SELECT D.PaymentId, C.CalendarDate FROM YourDataTable D
INNER JOIN #Calendar C ON C.CalendarDate BETWEEN D.StartDate AND D.EndDate
AND DATEPART(day, C.CalendarDate) = D.PaymentDay

Convert Date Range to Individual Days

A table called VolumeRequest stores the volume requests by accounts for a date range.
AccountId StartDate EndDate DailyVolume
670 2013-07-01 00:00:00.000 2013-07-31 00:00:00.000 10
670 2013-07-01 00:00:00.000 2013-07-31 00:00:00.000 1050
670 2013-07-10 00:00:00.000 2013-07-10 00:00:00.000 -350
670 2013-07-24 00:00:00.000 2013-07-26 00:00:00.000 -350
673 2013-06-01 00:00:00.000 2013-07-31 00:00:00.000 233
I need to display the requests on daily basis where volume is summed by day by account for a given date range like for month of July the report is like below. The date start and end dates of the volume requests need to be trimmed for the given report dates
AccountId Date Volume
670 2013-07-01 00:00:00.000 1060
670 2013-07-02 00:00:00.000 1060
.
.
670 2013-07-10 00:00:00.000 710
.
.
670 2013-07-24 00:00:00.000 710
670 2013-07-25 00:00:00.000 710
670 2013-07-26 00:00:00.000 710
.
.
670 2013-07-31 00:00:00.000 1060
673 2013-07-01 00:00:00.000 233
.
.
673 2013-07-31 00:00:00.000 233
Right now I am using table Variables and loops to achieve it which I know is not a good way to code.
DECLARE #sDate DATETIME, #eDate DATETIME , #volume DECIMAL (10, 4), rstartdate DATETIME, #renddate DATETIME , #loopcount INT
SET #sdate = '4/1/2013'
SET #edate = '4/30/2013'
DECLARE #VolumeRequest TABLE
(
ID INT IDENTITY (1, 1) PRIMARY KEY,
Aid INT,
Startdate DATETIME,
Enddate DATETIME,
volume DECIMAL (14, 4)
)
DECLARE #DailyRequest TABLE
(
ID INT IDENTITY (1, 1) PRIMARY KEY,
Accountid INT,
ReadDate DATETIME,
Volume DECIMAL (14, 4)
)
INSERT INTO #VolumeRequest
SELECT Accountid,
( CASE
WHEN #sdate > startdate THEN #sdate
ELSE startdate
END ),
( CASE
WHEN #edate < enddate THEN #edate
ELSE enddate
END ),
dailyvolume
FROM VolumeRequest
WHERE Startdate <= #edate
AND Enddate >= #sdate
AND isnull (deprecated, 0) != 1
--loop to breakdown the volume requests into daily requests
SET #loopcount = 1
WHILE #loopcount <= (SELECT MAX(ID)
FROM #VolumeRequest)
BEGIN
SELECT #volume = volume,
#rstartdate = Startdate,
#renddate = Enddate
FROM #VolumeRequest
WHERE ID = #loopcount
WHILE #rstartdate <= #renddate
BEGIN
INSERT INTO #DailyRequest
SELECT #currentaid,
#rstartdate,
#volume
SET #rstartdate = DATEADD(day, 1, #rstartdate)
END
SET #LoopCount = #LoopCount + 1
END
I am looking for ways which don't involve loops or cursors. I found a Similar Question. The answers there didn't help me.
I like to use a Dates table such as
CREATE TABLE #Dates(
DateId INT,
CalendarDate DATETIME)
filled with dates for whatever range you need. I use this table to join to tables such as VolumeRequest to retrieve the output you requested.
SELECT
v.AccountId,
d.CalendarDate,
SUM(v.DailyVolume)
FROM
#Dates d INNER JOIN
VolumeRequest v ON
d.CalendarDate >= v.StartDate AND
d.CalendarDate <= v.EndDate
group by
d.CalendarDate,
v.AccountId
to fill the #Dates table, I use something like this:
declare #startdate datetime = '6/1/13', #enddate datetime = '7/31/13'
create table #Dates(CalendarDate datetime)
insert into #Dates(CalendarDate)
select
dateadd(dd, rid-1, #startdate) as calendardate
from (
select
ROW_NUMBER() over(order by o.object_id) as rid
From
sys.objects o cross apply
sys.objects o2
) dates
where
dateadd(dd, rid-1, #startdate) >= #startdate and dateadd(dd, rid-1, #startdate) <= #enddate
Modify to meet your date range needs.
SQLFiddle demo
Using WITH clause and recursion we generate Days table with all days between MIN and MAX dates.
Then generate table Accounts with distinct AccountID.
Finally JOIN all these tables and group all with SUM.
WITH MINMAX as
( SELECT MIN(StartDate) as MinDate,
MAX(EndDate) as MaxDate
from T
),
DAYS as
( SELECT MinDate as D from MINMAX
UNION ALL
SELECT D+1 as D FROM DAYS WHERE D+1<=
(
SELECT MaxDate FROM MINMAX
)
),
Accounts as
(
select distinct AccountID from T
)
select A.AccountId,Days.D,sum(T.DailyVolume) from Days
CROSS JOIN Accounts A
JOIN T on A.AccountID=T.AccountID
AND
Days.D between T.StartDate and T.EndDate
GROUP BY A.AccountId,Days.D
ORDER BY A.AccountId,Days.D
OPTION (MAXRECURSION 10000)