split the date range by every 6 months dynamically. Start date can be any thing, but add up to month range - sql

Please help to split the date range by every 6 moths and the start date could be anything but using the start date we need to add up to 09-30 only and the next day which is 10/01 should become start date. I tried using recursive cte but still not getting the exact result
startdate enddate
06-22-2018 09-30-2022
output
startdate enddate
06-22-2018 09-30-2018
10-01-2018 03-31-2019
04-01-2019 09-30-2019
10-01-2019 03-31-2020
04-01-2020 09-30-2020

Here is another option which uses an ad-hoc tally table
Example
Declare #YourTable table (startdate date, enddate date)
Insert Into #YourTable values
('06/22/2018','09/30/2022')
;with cte as (
Select *
,Grp = sum( case when day(D)=1 and month(D) in (4,10) then 1 else 0 end) over (order by d)
From #YourTable A
Cross Apply (
Select Top (DateDiff(DAY,startdate,enddate)+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),startdate)
From master..spt_values n1,master..spt_values n2
) B
)
Select StartDate = min(D)
,EndDate = max(D)
From cte
Group by Grp
Order By min(D)
Returns
StartDate EndDate
2018-06-22 2018-09-30
2018-10-01 2019-03-31
2019-04-01 2019-09-30
2019-10-01 2020-03-31
2020-04-01 2020-09-30
2020-10-01 2021-03-31
2021-04-01 2021-09-30
2021-10-01 2022-03-31
2022-04-01 2022-09-30
Option where we JOIN to an ad-hoc calendar table (note the TOP 10000 and base date of 2000-01-01)
Declare #YourTable table (id int,startdate date, enddate date)
Insert Into #YourTable values
(1,'06/22/2018','09/30/2022')
;with cte as (
Select A.*
,B.D
,Grp = sum( case when day(D)=1 and month(D) in (4,10) then 1 else 0 end) over (order by d)
From #YourTable A
Join (
Select Top 10000 D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),'2000-01-01')
From master..spt_values n1,master..spt_values n2
) B on D between startDate and EndDate
and (D in (startdate,EndDate)
or ( day(D) in (1,day(eomonth(d))) and month(D) in (3,4,9,10))
)
)
Select ID
,StartDate = min(D)
,EndDate = max(D)
From cte
Group by ID,Grp
Order By ID,min(D)
Returns
ID StartDate EndDate
1 2018-06-22 2018-09-30
1 2018-10-01 2019-03-31
1 2019-04-01 2019-09-30
1 2019-10-01 2020-03-31
1 2020-04-01 2020-09-30
1 2020-10-01 2021-03-31
1 2021-04-01 2021-09-30
1 2021-10-01 2022-03-31
1 2022-04-01 2022-09-30

You can use a recursive CTE:
with cte as (
select startdate, eomonth(datefromparts(year(startdate), 9, 1)) as enddate, enddate as orig_enddate
from t
union all
select dateadd(day, 1, enddate), eomonth(dateadd(month, 5, dateadd(day, 1, enddate))) as enddate, orig_enddate
from cte
where enddate < orig_enddate
)
select *
from cte;
Here is a db<>fiddle.
It is unclear what year you want for the first row. As per your question, this uses Sep 30th of the year of the startdate.
If you need more than 100 dates, then add option max(recursion 0).

Related

Get startdate and enddate of a every week within the specified date range

I want to get startdate and enddate of a every week within the specified date range for eg: I want start_date of week and end_date of week within 2021-01-01 and 2022-12-31
But want week_start_date as 2021-01-01 for the first week and 2021-12-31 as end date of the 52nd week at the end of the year .
Table should fill like this if start_date is: 2021-01-01 and 2022-12-31
week_start date week_end_date
2021-01-01 2021-01-02
2021-01-03 2021-01-09
2021-01-10 2021-01-16
.
.
.
.
.
.
2021-12-26 2021-12-31
2022-01-01 2022-01-01
2022-01-02 2022-01-08
.
.
How to give first_week_date (year no matter what)-01-01 and end_week_date( year no matter what)-12-31 and in between the logic should be able to calculate week_start_date and week_end_date without problem.
I am using SQL Server for this implementation
You can do something like this
DECLARE #fromDate DATE = '2021-01-01';
DECLARE #toDate DATE = '2021-12-31';
SELECT CASE WHEN WeekStart<#fromDate THEN #fromDate ELSE WeekStart END AS week_start_date,
CASE WHEN WeekEnd>#toDate THEN #toDate ELSE WeekEnd END AS week_end_date
FROM
(
SELECT DATEADD(dd, -(DATEPART(dw, date_column)-1), date_column) AS 'WeekStart',
DATEADD(dd, 7-(DATEPART(dw, date_column)), date_column) AS 'WeekEnd'
FROM table_name
WHERE date_column BETWEEN #fromDate AND #toDate
) a;
Where,
table_name is the name of your table.
date_column is the name of the column containing the date.
This code produces the table requested:
DECLARE #minDate DateTime = '20210101'
DECLARE #maxDate DateTime = '20221231';
WITH lv0 AS (SELECT 0 g UNION ALL SELECT 0)
,lv1 AS (SELECT 0 g FROM lv0 a CROSS JOIN lv0 b) -- 4
,lv2 AS (SELECT 0 g FROM lv1 a CROSS JOIN lv1 b) -- 16
,lv3 AS (SELECT 0 g FROM lv2 a CROSS JOIN lv2 b) -- 256
,lv4 AS (SELECT 0 g FROM lv3 a CROSS JOIN lv3 b) -- 65,536
,lv5 AS (SELECT 0 g FROM lv4 a CROSS JOIN lv4 b) -- 4,294,967,296
,Tally (n) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM lv5)
,DateList AS (
SELECT TOP (dateDiff(day,#minDate,#maxDate)+1)
#minDate+n-1 AS DateValue
FROM Tally)
SELECT
datepart(year,DateValue) AS [Year],
datepart(week,DateValue) AS [Week],
min(dateValue) AS StartDate,
max(dateValue) AS EndDate
FROM DateList
GROUP BY
datepart(year,DateValue),
datepart(week,DateValue)
ORDER BY 3

Splitting dates into intervals using Start Date and End Date

I have scenario where I need to split the given date range into monthly intervals.
For example, the input is like below:
StartDate EndDate
2018-01-21 2018-01-29
2018-01-30 2018-02-23
2018-02-24 2018-03-31
2018-04-01 2018-08-16
2018-08-17 2018-12-31
And the expected output should be like below:
StartDate EndDate
2018-01-21 2018-01-29
2018-01-30 2018-01-31
2018-02-01 2018-02-23
2018-02-24 2018-02-28
2018-03-01 2018-03-31
2018-04-01 2018-04-30
2018-05-01 2018-05-31
2018-06-01 2018-06-30
2018-07-01 2018-07-31
2018-08-01 2018-08-16
2018-08-17 2018-08-31
2018-09-01 2018-09-30
2018-10-01 2018-10-31
2018-11-01 2018-11-30
2018-12-01 2018-12-31
Below is the sample data.
CREATE TABLE #Dates
(
StartDate DATE,
EndDate DATE
);
INSERT INTO #Dates
(
StartDate,
EndDate
)
VALUES
('2018-01-21', '2018-01-29'),
('2018-01-30', '2018-02-23'),
('2018-02-24', '2018-03-31'),
('2018-04-01', '2018-08-16'),
('2018-08-17', '2018-12-31');
You can use a recursive CTE. The basic idea is to start with the first date 2018-01-21 and build a list of all months' start and end date upto the last date 2018-12-31. Then inner join with your data and clamp the dates if necessary.
DECLARE #Dates TABLE (StartDate DATE, EndDate DATE);
INSERT INTO #Dates (StartDate, EndDate) VALUES
('2018-01-21', '2018-01-29'),
('2018-01-30', '2018-02-23'),
('2018-02-24', '2018-03-31'),
('2018-04-01', '2018-08-16'),
('2018-08-17', '2018-12-31');
WITH minmax AS (
-- clamp min(start date) to 1st day of that month
SELECT DATEADD(MONTH, DATEDIFF(MONTH, CAST('00010101' AS DATE), MIN(StartDate)), CAST('00010101' AS DATE)) AS mindate, MAX(EndDate) AS maxdate
FROM #Dates
), months AS (
-- calculate first and last day of each month
-- e.g. for February 2018 it'll return 2018-02-01 and 2018-02-28
SELECT mindate AS date01, DATEADD(DAY, -1, DATEADD(MONTH, 1, mindate)) AS date31, maxdate
FROM minmax
UNION ALL
SELECT DATEADD(MONTH, 1, prev.date01), DATEADD(DAY, -1, DATEADD(MONTH, 2, prev.date01)), maxdate
FROM months AS prev
WHERE prev.date31 < maxdate
)
SELECT
-- clamp start and end date to first and last day of corresponding month
CASE WHEN StartDate < date01 THEN date01 ELSE StartDate END,
CASE WHEN EndDate > date31 THEN date31 ELSE EndDate END
FROM months
INNER JOIN #Dates ON date31 >= StartDate AND EndDate >= date01
If rCTE is not an option you can always JOIN with a table of numbers or table of dates (the idea above still applies).
You can Cross Apply with the Master..spt_values table to get a row for each month between StartDate and EndDate.
SELECT *
into #dates
FROM (values
('2018-01-21', '2018-01-29')
,('2018-01-30', '2018-02-23')
,('2018-02-24', '2018-03-31')
,('2018-04-01', '2018-08-16')
,('2018-08-17', '2018-12-31')
)d(StartDate , EndDate)
SELECT
SplitStart as StartDate
,case when enddate < SplitEnd then enddate else SplitEnd end as EndDate
FROM #dates d
cross apply (
SELECT
cast(dateadd(mm, number, dateadd(dd, (-datepart(dd, d.startdate) +1) * isnull((number / nullif(number, 0)), 0), d.startdate)) as date) as SplitStart
,cast(dateadd(dd, -datepart(dd, dateadd(mm, number+1, startdate)), dateadd(mm, number+1, startdate)) as date) as SplitEnd
FROM
master..spt_values
where type = 'p'
and number between 0 and (((year(enddate) - year(startdate)) * 12) + month(enddate) - month(startdate))
) s
drop table #dates
The following should also work
First i put startdates and enddates into a single column in the cte-block data.
In the block som_eom, i create the start_of_month and end_of_month for all 12 months.
I union steps 1 and 2 into curated_set
I create curated_set which is ordered by the date column
Finally i reject the unwanted records, in my filter clause not in('som','StartDate')
with data
as (select *
from dates
unpivot(x for y in(startdate,enddate))t
)
,som_eom
as (select top 12
cast('2018-'+cast(row_number() over(order by (select null)) as varchar(2))+'-01' as date) as som
,dateadd(dd
,-1
,dateadd(mm
,1
,cast('2018-'+cast(row_number() over(order by (select null)) as varchar(2))+'-01' as date)
)
) as eom
from information_schema.tables
)
,curated_set
as(select *
from data
union all
select *
from som_eom
unpivot(x for y in(som,eom))t
)
,curated_data
as(select x
,y
,lag(x) over(order by x) as prev_val
from curated_set
)
select prev_val as st_dt,x as end_dt
,y
from curated_Data
where y not in('som','StartDate')
Start with the initial StartDate and calculate the end of month or simply use the EndDate if it's within the same month.
Use the newly calculated EndDate+1 as StartDate for recursion and repeat the calculation.
WITH cte AS
( SELECT StartDate, -- initial start date
CASE WHEN EndDate < DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+1,0))
THEN EndDate
ELSE DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+1,0))
END AS newEnd, -- LEAST(end of current month, EndDate)
EndDate
FROM #Dates
UNION ALL
SELECT dateadd(DAY,1,newEnd), -- previous end + 1 day, i.e. 1st of current month
CASE WHEN EndDate <= DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+2,0))
THEN EndDate
ELSE DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+2,0))
END, -- LEAST(end of next month, EndDate)
EndDate
FROM cte
WHERE newEnd < EndDate
)
SELECT StartDate, newEnd
FROM cte

Group time series by time intervals (e.g. days) with aggregate of duration

I have a table containing a time series with following information. Each record represents the event of "changing the mode".
Timestamp | Mode
------------------+------
2018-01-01 12:00 | 1
2018-01-01 18:00 | 2
2018-01-02 01:00 | 1
2018-01-02 02:00 | 2
2018-01-04 04:00 | 1
By using the LEAD function, I can create a query with the following result. Now each record contains the information, when and how long the "mode was active".
Please check the 2nd and the 4th record. They "belong" to multiple days.
StartDT | EndDT | Mode | Duration
------------------+------------------+------+----------
2018-01-01 12:00 | 2018-01-01 18:00 | 1 | 6:00
2018-01-01 18:00 | 2018-01-02 01:00 | 2 | 7:00
2018-01-02 01:00 | 2018-01-02 02:00 | 1 | 1:00
2018-01-02 02:00 | 2018-01-04 04:00 | 2 | 50:00
2018-01-04 04:00 | (NULL) | 1 | (NULL)
Now I would like to have a query that groups the data by day and mode and aggregates the duration.
This result table is needed:
Date | Mode | Total
------------+------+-------
2018-01-01 | 1 | 6:00
2018-01-01 | 2 | 6:00
2018-01-02 | 1 | 1:00
2018-01-02 | 2 | 23:00
2018-01-03 | 2 | 24:00
2018-01-04 | 2 | 04:00
I didn't known how to handle the records that "belongs" to multiple days. Any ideas?
create table ChangeMode ( ModeStart datetime2(7), Mode int )
insert into ChangeMode ( ModeStart, Mode ) values
( '2018-11-15T21:00:00.0000000', 1 ),
( '2018-11-16T17:18:19.1231234', 2 ),
( '2018-11-16T18:00:00.5555555', 1 ),
( '2018-11-16T18:00:01.1234567', 2 ),
( '2018-11-16T19:02:22.8888888', 1 ),
( '2018-11-16T20:00:00.9876543', 2 ),
( '2018-11-17T09:00:00.0000000', 1 ),
( '2018-11-17T23:23:23.0230450', 2 ),
( '2018-11-19T17:00:00.0172839', 1 ),
( '2018-11-20T03:07:00.7033077', 2 )
;
with
-- Determine the earliest and latest dates.
-- Cast to date to remove the time portion.
-- Cast results back to datetime because we're going to add hours later.
MinMaxDates
as
(select cast(min(cast(ModeStart as date))as datetime) as MinDate,
cast(max(cast(ModeStart as date))as datetime) as MaxDate from ChangeMode),
-- How many days have passed during that period
Dur
as
(select datediff(day,MinDate,MaxDate) as Duration from MinMaxDates),
-- Create a list of numbers.
-- These will be added to MinDate to get a list of dates.
NumList
as
( select 0 as Num
union all
select Num+1 from NumList,Dur where Num<Duration ),
-- Create a list of dates by adding those numbers to MinDate
DayList
as
( select dateadd(day,Num,MinDate)as ModeDate from NumList, MinMaxDates ),
-- Create a list of day periods
PeriodList
as
( select ModeDate as StartTime,
dateadd(day,1,ModeDate) as EndTime
from DayList ),
-- Use LEAD to get periods for each record
-- Final record would return NULL for ModeEnd
-- We replace that with end of last day
ModePeriodList
as
( select ModeStart,
coalesce( lead(ModeStart)over(order by ModeStart),
dateadd(day,1,MaxDate) ) as ModeEnd,
Mode from ChangeMode, MinMaxDates ),
ModeDayList
as
( select * from ModePeriodList, PeriodList
where ModeStart<=EndTime and ModeEnd>=StartTime
),
-- Keep the later of the mode start time, and the day start time
-- Keep the earlier of the mode end time, and the day end time
ModeDayPeriod
as
( select case when ModeStart>=StartTime then ModeStart else StartTime end as StartTime,
case when ModeEnd<=EndTime then ModeEnd else EndTime end as EndTime,
Mode from ModeDayList ),
SumDurations
as
( select cast(StartTime as date) as ModeDate,
Mode,
DateDiff_Big(nanosecond,StartTime,EndTime)
/3600000000000
as DurationHours from ModeDayPeriod )
-- List the results in order
-- Use MaxRecursion option in case there are more than 100 days
select ModeDate as [Date], Mode, sum(DurationHours) as [Total Duration Hours]
from SumDurations
group by ModeDate, Mode
order by ModeDate, Mode
option (maxrecursion 0)
Result is:
Date Mode Total Duration Hours
---------- ----------- ---------------------------------------
2018-11-15 1 3.00000000000000
2018-11-16 1 18.26605271947221
2018-11-16 2 5.73394728052777
2018-11-17 1 14.38972862361111
2018-11-17 2 9.61027137638888
2018-11-18 2 24.00000000000000
2018-11-19 1 6.99999519891666
2018-11-19 2 17.00000480108333
2018-11-20 1 3.11686202991666
2018-11-20 2 20.88313797008333
you could use a CTE to create a table of days then join the time slots to it
DECLARE #MAX as datetime2 = (SELECT MAX(CAST(Timestamp as date)) MX FROM process);
WITH StartEnd AS (select p1.Timestamp StartDT,
P2.Timestamp EndDT ,
p1.mode
from process p1
outer apply
(SELECT TOP 1 pOP.* FROM
process pOP
where pOP.Timestamp > p1.Timestamp
order by pOP.Timestamp asc) P2
),
CAL AS (SELECT (SELECT MIN(cast(StartDT as date)) MN FROM StartEnd) DT
UNION ALL
SELECT DATEADD(day,1,DT) DT FROM CAL WHERE CAL.DT < #MAX
),
TMS AS
(SELECT CASE WHEN S.StartDT > C.DT THEN S.StartDT ELSE C.DT END AS STP,
CASE WHEN S.EndDT < DATEADD(day,1,C.DT) THEN S.ENDDT ELSE DATEADD(day,1,C.DT) END AS STE
FROM StartEnd S JOIN CAL C ON NOT(S.EndDT <= C.DT OR S.StartDT>= DATEADD(day,1,C.dt))
)
SELECT *,datediff(MI ,TMS.STP, TMS.ste) as x from TMS
The following uses recursive CTE to build a list of dates (a calendar or number table works equally well). It then intersect the dates with date times so that missing dates are populated with matching data. The important bit is that for each row, if start datetime belongs to previous day then it is clamped to 00:00. Likewise for end datetime.
DECLARE #t TABLE (timestamp DATETIME, mode INT);
INSERT INTO #t VALUES
('2018-01-01 12:00', 1),
('2018-01-01 18:00', 2),
('2018-01-02 01:00', 1),
('2018-01-02 02:00', 2),
('2018-01-04 04:00', 1);
WITH cte1 AS (
-- the min and max dates in your data
SELECT
CAST(MIN(timestamp) AS DATE) AS mindate,
CAST(MAX(timestamp) AS DATE) AS maxdate
FROM #t
), cte2 AS (
-- build all dates between min and max dates using recursive cte
SELECT mindate AS day_start, DATEADD(DAY, 1, mindate) AS day_end, maxdate
FROM cte1
UNION ALL
SELECT DATEADD(DAY, 1, day_start), DATEADD(DAY, 2, day_start), maxdate
FROM cte2
WHERE day_start < maxdate
), cte3 AS (
-- pull end datetime from next row into current
SELECT
timestamp AS dt_start,
LEAD(timestamp) OVER (ORDER BY timestamp) AS dt_end,
mode
FROM #t
), cte4 AS (
-- join datetime with date using date overlap query
-- then clamp start datetime to 00:00 of the date
-- and clamp end datetime to 00:00 of next date
SELECT
IIF(dt_start < day_start, day_start, dt_start) AS dt_start_fix,
IIF(dt_end > day_end, day_end, dt_end) AS dt_end_fix,
mode
FROM cte2
INNER JOIN cte3 ON day_end > dt_start AND dt_end > day_start
)
SELECT dt_start_fix, dt_end_fix, mode, datediff(minute, dt_start_fix, dt_end_fix) / 60.0 AS total
FROM cte4
DB Fiddle
Thanks everybody!
The answer from Cato put me on the right track. Here my final solution:
DECLARE #Start AS datetime;
DECLARE #End AS datetime;
DECLARE #Interval AS int;
SET #Start = '2018-01-01';
SET #End = '2018-01-05';
SET #Interval = 24 * 60 * 60;
WITH
cteDurations AS
(SELECT [Timestamp] AS StartDT,
LEAD ([Timestamp]) OVER (ORDER BY [Timestamp]) AS EndDT,
Mode
FROM tblLog
WHERE [Timestamp] BETWEEN #Start AND #End
),
cteTimeslots AS
(SELECT #Start AS StartDT,
DATEADD(SECOND, #Interval, #Start) AS EndDT
UNION ALL
SELECT EndDT,
DATEADD(SECOND, #Interval, EndDT)
FROM cteTimeSlots WHERE StartDT < #End
),
cteDurationsPerTimesplot AS
(SELECT CASE WHEN S.StartDT > C.StartDT THEN S.StartDT ELSE C.StartDT END AS StartDT,
CASE WHEN S.EndDT < C.EndDT THEN S.EndDT ELSE C.EndDT END AS EndDT,
C.StartDT AS Slot,
S.Mode
FROM cteDurations S
JOIN cteTimeslots C ON NOT(S.EndDT <= C.StartDT OR S.StartDT >= C.EndDT)
)
SELECT Slot,
Mode,
SUM(DATEDIFF(SECOND, StartDT, EndDT)) AS Duration
FROM cteDurationsPerTimesplot
GROUP BY Slot, Mode
ORDER BY Slot, Mode;
With the variable #Interval you are able to define the size of the timeslots.
The CTE cteDurations creates a subresult with the durations of all necessary entries by using the TSQL function LEAD (available in MSSQL >= 2012). This will be a lot faster than an OUTER APPLY.
The CTE cteTimeslots generates a list of timeslots with start time and end time.
The CTE cteDurationsPerTimesplot is a subresult with a JOIN between cteDurations and cteTimeslots. This this the magic JOIN statement from Cato!
And finally the SELECT statement will do the grouping and sum calculation per Slot and Mode.
Once again: Thanks a lot to everybody! Especially to Cato! You saved my weekend!
Regards
Oliver

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

Split date range to year-month rows on SQL Server 2005

I need to create an output where I got one row per year-month.
Assume the dataset is:
id | dateStart | dateEnd
1 | 2015-01-01 00:00:00.000 | 2015-03-31 00:00:00.000
2 | 2014-07-01 00:00:00.000 | 2014-08-31 00:00:00.000
...
I need the following output:
id | year-month
1 | 2015-01
1 | 2015-02
1 | 2015-03
2 | 2014-07
2 | 2014-08
The output can be any datatype since I can just change that later.
That is for 2015-01 the following is ok, "2015-01-01 00:00:00.000", "2015-01-01", "201501", "2015 | jan" ect.
Note I'm using SQL Server 2005.
Here is a method that uses recursive CTEs:
with CTE as (
select id, dateStart as dte, dateEnd
from t
union all
select id, dateadd(month, 1, dte), dateEnd
from CTE
where dateadd(month, 1, dte) < dateEnd
)
select id, dte
from CTE;
You can convert the final result into any format you like. For instance:
select id, year(dte) * 10000 + month(dte) as yyyymm_int
or
select id, cast(year(dte) * 10000 + month(dte) as varchar(255)) as yyyymm
Generate tally table(just make sure you get enough rows there). tally will contain values 0,1,2,.....n. Then you do a join with condition adding thise values as months to startDate until it is greater then endDate:
DECLARE #t TABLE
(
id INT ,
dateStart DATETIME ,
dateEnd DATETIME
)
INSERT INTO #t
VALUES ( 1, '2015-01-01 00:00:00.000', '2015-03-31 00:00:00.000' ),
( 2, '2014-07-01 00:00:00.000', '2014-08-31 00:00:00.000' )
;WITH cte AS(SELECT -1 + ROW_NUMBER() OVER(ORDER BY t1.m) m
FROM(VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))t1(m) CROSS JOIN
(VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))t2(m))
SELECT t.id,
DATEADD(mm, c.m, t.dateStart) AS year_month
FROM cte c
JOIN #t t ON DATEADD(mm, c.m, t.dateStart) <= t.dateEnd
ORDER BY t.id, year_month
Output:
id year_month
1 2015-01-01 00:00:00.000
1 2015-02-01 00:00:00.000
1 2015-03-01 00:00:00.000
2 2014-07-01 00:00:00.000
2 2014-08-01 00:00:00.000
In an ideal world you would have a calendar table, then your query would simply be:
SELECT t.id,
c.FirstDayOfMonth
FROM YourTable AS t
INNER JOIN dbo.Calendar c
ON c.FirstDayOfMonth >= t.DateStart
AND c.FirstDayOfMonth <= t.DateEnd
AND c.DayOfMonth = 1;
Assuming that you don't have a calendar table then you can do it with a list of numbers generated on the fly (Read this article for more on this). The following will generate a list from 1-10,000:
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT ROW_NUMBER() OVER(ORDER BY N3.N) FROM N3)
SELECT * FROM Numbers;
Then you can join this to your original table:
DECLARE #T TABLE (id INT, DateStart DATE, DateEnd DATE);
INSERT #T (ID, DateStart, DateEnd)
VALUES (1, '20150101', '20150331'), (2, '20140701', '20140831');
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT ROW_NUMBER() OVER(ORDER BY N3.N) FROM N3)
SELECT t.ID,
[year-month] = DATEADD(MONTH, n.Number + DATEDIFF(MONTH, 0, t.DateStart), 0)
FROM #T AS t
INNER JOIN Numbers AS N
ON N.Number - 1 <= DATEDIFF(MONTH, t.DateStart, t.DateEnd);