Convert Date Range to Individual Days - sql

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)

Related

How to Convert a Date Span to Monthly Records using SQL

I have multiple date spans for the user over a period of few months, I would like to split each span to multiple rows by month and year(default to first day of the month) for which user has been active during the span period. Active user will have future end date records to be split up until the current month and year
Existing Data
ID
Start date
end date
1234
2019-01-01
2019-03-31
1234
2019-09-18
2020-01-31
1234
2022-11-15
2025-01-31
Tried to place the below date month query into the spans
Select Top 500 mmdd=cast (dateadd(Month,-1+Row_Number() Over (Order By (Select NULL)),'2019-01-01') as date)
From master..spt_values n1
order by 1 asc
EXPECTED OUTPUT
ID
active month
1234
2019-01-01
1234
2019-02-01
1234
2019-03-01
1234
2019-09-01
1234
2019-10-01
1234
2019-11-01
1234
2019-12-01
1234
2020-01-01
1234
2022-11-01
1234
2022-12-01
1234
2023-01-01
Larnu is on the right track. One of the easiest ways I've found is to create a calendar table or a function (which can effectively do the same thing).
Try this:
CREATE FUNCTION [dbo].[udfCalendar]
(
#StartDate Date,
#EndDate Date
)
RETURNS #Calendar TABLE (ID int, DateValue DateTime, DayValue int, MonthValue int, YearValue int)
AS
BEGIN
WHILE #StartDate < #EndDate
BEGIN
INSERT #Calendar
SELECT --like 20190101, 1/1/2019, 1, 1, 2019
YEAR (#StartDate) * 10000 + MONTH (#StartDate) * 100 + Day (#StartDate) AS ID,
#StartDate AS DateValue,
DATEPART (dd, #StartDate) AS DayValue,
DATEPART (mm, #StartDate) AS MonthValue,
DATEPART (yy, #StartDate) AS YearValue;
SET #StartDate = DateAdd(m, 1, #StartDate);
END
RETURN;
END
Then you can join to it
Select n1.ID, cal.DateValue as ActiveMonth
From master..spt_values n1 inner join
dbo.udfCalendar('1/1/2019', '1/1/2023') cal
On cal.DateValue Between n1.StartDate and n1.EndDate
Order By DateValue

Find all instances of a specific day of the week between two dates in SQL

I'm trying to find all instances of a day of a week between two given dates. The day of the week can change. I've seen a similar question posted on here, but it doesn't seem to work for variables
SET DATEFIRST 1;
DECLARE #earliestStartDate DATETIME = '2016-08-01 00:00:00.000';
DECLARE #latestStartDate DATETIME = '2016-09-30 00:00:00.000';
DECLARE #weeklyCoursesStartDay INT = 1;
DECLARE #maxCourses INT = 30;
CREATE TABLE #Dates(CourseDate DATETIME);
WITH CTE(dt)
AS
(SELECT #earliestStartDate
UNION ALL
SELECT DATEADD(day, #weeklyCoursesStartDay, dt) FROM CTE--SELECT DATEADD(day, #weeklyCoursesStartDay, dt) FROM CTE
WHERE dt < #latestStartDate)
INSERT INTO #Dates(CourseDate)
SELECT TOP(#maxCourses) dt
FROM CTE
WHERE DATEPART(DW, dt) = #weeklyCoursesStartDay
OPTION (MAXRECURSION 0);
SELECT * FROM #Dates
DROP TABLE #Dates
This returns the below, as expected.
2016-08-01 00:00:00.000
2016-08-08 00:00:00.000
2016-08-15 00:00:00.000
2016-08-22 00:00:00.000
2016-08-29 00:00:00.000
2016-09-05 00:00:00.000
2016-09-12 00:00:00.000
2016-09-19 00:00:00.000
2016-09-26 00:00:00.000
But when the #weeklyCourseStartDay is changed to say, 5 (indicating a friday) it only returns two results:
2016-08-26 00:00:00.000
2016-09-30 00:00:00.000
The line
SELECT DATEADD(day, #weeklyCoursesStartDay, dt) FROM CTE
should actually be:
SELECT DATEADD(day, 1, dt) FROM CTE
This means that the recursive CTE will generate every day between #earliestStartDate and #latestStartDate, and, at a later stage, the WHERE DATEPART(DW, dt) = #weeklyCoursesStartDay clause will make sure only the days with the correct week day will remain.

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

How to find overlapping periods recursively in SQL Server

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

How to count between no of days between two dates month wise in SQL Server?

I have Two Date in SQL sever which overlap in two month i want to find how many days over lap in each month.
For example:
Start date is : 26-Sep-2012
End Date is : 10-Oct-2012
Sept- 5 days
October - 10 days
along with the list of date of each month.
declare #start datetime;
declare #end datetime;
set #start = '20120926';
set #end = '20121010';
SELECT (DAY(DATEADD (m, 1, DATEADD(d, 1 - DAY(#start), #start))-1)
- DAY(#start) + 1) AS DaysLeftStart,
DAY(#end) AS DaysEnd
Fiddle: http://sqlfiddle.com/#!3/d41d8/4441/0
DECLARE #start DATETIME, #end DATETIME;
SELECT #start = '20120926', #end = '20121010';
;WITH c(d) AS
(
SELECT TOP (DATEDIFF(DAY, #start, #end)+1)
DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY name)-1, #start)
FROM sys.all_columns
)
SELECT
[date] = DATEADD(MONTH, DATEDIFF(MONTH, 0, d), 0),
[days] = COUNT(*)
FROM c GROUP BY DATEDIFF(MONTH, 0, d)
UNION ALL SELECT d, NULL FROM c;
Results:
date days
----------------------- -----
2012-09-01 00:00:00.000 5
2012-10-01 00:00:00.000 10
2012-09-26 00:00:00.000 NULL
2012-09-27 00:00:00.000 NULL
2012-09-28 00:00:00.000 NULL
2012-09-29 00:00:00.000 NULL
2012-09-30 00:00:00.000 NULL
2012-10-01 00:00:00.000 NULL
2012-10-02 00:00:00.000 NULL
2012-10-03 00:00:00.000 NULL
2012-10-04 00:00:00.000 NULL
2012-10-05 00:00:00.000 NULL
2012-10-06 00:00:00.000 NULL
2012-10-07 00:00:00.000 NULL
2012-10-08 00:00:00.000 NULL
2012-10-09 00:00:00.000 NULL
2012-10-10 00:00:00.000 NULL
Here you can find some detail about creating a calendar table. You can use it to perform such query:
SELECT CalendarMonth, count(*) day_num
FROM dbo.Calendar
WHERE CalendarDate between #start_date and #end_date;