Need Sql query output like this format - sql

I have one time sheet table when i fetch the records it will display like this for one week.
This record is from one week from June 10 to June 16
EmpId Monday Tuesday Wednesday Thursday Friday Saturday Sunday StartDate EndDate
1 08:00 08:12 00:00 04:00 00:00 03:00 00:00 05/10/2013 05/16/2013
Need the output like this
Empid Monday startdate EndDate
1 08:00 05/10/2013 05/10/2013
1 08:12 05/11/2013 05/11/2013
1 04:00 05/13/2013 05/13/2013
1 03:00 05/15/2013 05/15/2013

This is basically an unpivot query. Because of the time fields, this version chooses to do it explicitly (using cross join and case) rather than using unpivot:
select t.*
from (select h.empid,
(case when n = 0 then Monday
when n = 1 then Tuesday
when n = 2 then Wednesday
when n = 3 then Thursday
when n = 4 then Friday
when n = 5 then Saturday
when n = 6 then Sunday
end) as hours,
(startdate + n) as StartDate,
(startdate + n) as EndDate
from hours h join
(select 0 as n union all
select 1 union all
select 2 union all
select 3 union all
select 4 union all
select 5 union all
select 6
) n
) t
where hours > 0;
You can see the SQLFiddle here. And there is no problem running this on larger amounts of data.

This should get you started:
declare #Hours as Table ( EmpId Int, Monday Time, Tuesday Time, Wednesday Time,
Thursday Time, Friday Time, Saturday Time, Sunday Time, StartDate Date, EndDate Date );
insert into #Hours
( EmpId, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday, StartDate, EndDate )
values
( 1, '08:00', '08:12', '00:00', '04:00', '00:00', '03:00', '00:00', '20130510', '20130516' );
select * from #Hours;
with Week as (
-- Build a table of all of the dates in the work week.
select StartDate as WorkDate, EndDate
from #Hours
union all
select DateAdd( day, 1, WorkDate ), EndDate
from Week
where WorkDate < EndDate )
-- Output the result.
select EmpId,
case DatePart( weekday, W.WorkDate )
when 1 then H.Monday
when 2 then H.Tuesday
when 3 then H.Wednesday
when 4 then H.Thursday
when 5 then H.Friday
when 6 then H.Saturday
when 7 then H.Sunday
end as Hours,
WorkDate as StartDate, WorkDate as EndDate,
DatePart( weekday, W.WorkDate ) as DayOfWeek
from Week as W inner join
#Hours as H on H.StartDate <= W.WorkDate and W.WorkDate <= H.EndDate;
with Week as (
-- Build a table of all of the dates in the work week.
select StartDate as WorkDate, EndDate
from #Hours
union all
select DateAdd( day, 1, WorkDate ), EndDate
from Week
where WorkDate < EndDate ),
DaysHours as (
-- Build a table of the hours assigned to each date.
select EmpId,
case DatePart( weekday, W.WorkDate )
when 1 then H.Monday
when 2 then H.Tuesday
when 3 then H.Wednesday
when 4 then H.Thursday
when 5 then H.Friday
when 6 then H.Saturday
when 7 then H.Sunday
end as Hours,
WorkDate as StartDate, WorkDate as EndDate
from Week as W inner join
#Hours as H on H.StartDate <= W.WorkDate and W.WorkDate <= H.EndDate )
-- Output the non-zero hours.
select EmpId, Hours, StartDate, EndDate
from DaysHours
where Hours <> Cast( '00:00' as Time );
This works for a single row, but you will need to make some changes if your dataset grows.

Related

Query based on day-of-week/time range

I'm on SQL Server 2014. I have a table with columns like this:
id (int, PK, identity)
effectiveDate (datetime)
expirationDate (datetime)
1
2022-07-06 18:00:00.000
2022-07-06 23:00:00.000
2
2022-07-08 22:00:00.000
2022-07-09 02:00:00.000
I need to select rows where the current date/time (GETDATE()) lands within the day-of-week/time range represented by these datetimes, beginning on the effectiveDate. So think of row 1 as the range Wednesday 18:00 -> Wednesday 23:00 and row 2 as Friday 22:00 -> Saturday 02:00. (Keep in mind the day-of-week/time range can span multiple days, as in the 2nd row.)
Examples:
GETDATE() = 2022-07-06 19:30:00.000 (i.e Wednesday at 19:30)
Selects row 1
GETDATE() = 2022-07-30 01:00:00.000 (i.e. Saturday at 01:00)
Selects row 2
GETDATE() = 2022-06-30 19:00:00.000 (i.e. Wednesday at 19:00 which matches row 1 on the day-of-week/time range, but is before the effective date)
Selects nothing
I'm not quite sure how to attack this. Maybe we could date adjust each row's effectiveDate and expirationDate as many weeks forward as needed to place the effectiveDate before GETDATE() (assuming the effectiveDate <= GETDATE()). Any thoughts?
The DATEPART with weekday is the key to checking the weekday range. There are two cases. For example, Mon to Wed is different than Wed to Mon. Mon to Wed is easy with a test of Mon<=day<= Wed. For Wed to Mon, it becomes day < Mon or day > Wed. However, this is the same as NOT Mon<=day<=Wed. The query might not be exactly what you need, but should be a good start.
with TestData as (
select *
from (
values
(1, CAST('2022-07-06 18:00:00.000' as datetime), CAST('2022-07-06 23:00:00.000' as datetime)),
(2, CAST('2022-07-08 22:00:00.000' as datetime), CAST('2022-07-09 02:00:00.000' as datetime)),
(3, CAST('2022-07-09 22:00:00.000' as datetime), CAST('2022-07-11 10:00:00.000' as datetime))
) t (id, effectiveDate, expirationDate)
), TestSamples as (
select *
from (
values
(CAST('2022-07-06 19:30:00.000' as datetime)),
(CAST('2022-07-30 01:00:00.000' as datetime)),
(CAST('2022-07-31 01:00:00.000' as datetime)),
(CAST('2022-06-30 19:00:00.000' as datetime)),
(CAST('2022-08-04 19:00:00.000' as datetime))
) t (testDate)
), WeekDayRange as (
select id, effectiveDate, expirationDate,
datename(weekday, effectiveDate) as WDNAME1, datename(weekday, expirationDate) as WDNAME2,
datepart(weekday, effectiveDate) as WD1, datepart(weekday, expirationDate) as WD2
from TestData
) select t.testDate, datename(weekday, t.testDate) as [WDNAME], datepart(weekday, t.testDate) as [WD], r.*
from TestSamples t
left join WeekDayRange r
on (r.WD1 <= r.WD2 and datepart(weekday, t.testDate) BETWEEN r.WD1 and r.WD2)
or (r.WD1 > r.WD2 and not datepart(weekday, t.testDate) BETWEEN r.WD2 and r.WD1)
where t.testDate > r.effectiveDate
testDate WDNAME WD id effectiveDate expirationDate WDNAME1 WDNAME2 WD1 WD2
----------------------- --------- ----------- ----------- ----------------------- ----------------------- --------- --------- ----------- -----------
2022-07-06 19:30:00.000 Wednesday 4 1 2022-07-06 18:00:00.000 2022-07-06 23:00:00.000 Wednesday Wednesday 4 4
2022-07-30 01:00:00.000 Saturday 7 2 2022-07-08 22:00:00.000 2022-07-09 02:00:00.000 Friday Saturday 6 7
2022-07-31 01:00:00.000 Sunday 1 3 2022-07-09 22:00:00.000 2022-07-11 10:00:00.000 Saturday Monday 7 2
I think I got this working with a bit of math:
DECLARE #myGetDate DATETIME;
SET #myGetDate = '2022-07-30 01:00:00.000';
WITH AdjustedWeeklyDates AS (
SELECT
ID,
DATEADD(WEEK, DATEDIFF(day, effectiveDate, #myGetDate) / 7, effectiveDate) AS adjustedEffectiveDate,
DATEADD(WEEK, DATEDIFF(day, effectiveDate, #myGetDate) / 7, expirationDate) AS adjustedExpirationDate
FROM
dbo.myTable
WHERE
effectiveDate <= #myGetDate
)
SELECT
mt.ID, mt.effectiveDate, mt.expirationDate, awd.adjustedEffectiveDate, awd.adjustedExpirationDate
FROM dbo.myTable mt
INNER JOIN AdjustedWeeklyDates awd ON mt.ID = awd.ID
WHERE
awd.adjustedEffectiveDate <= #myGetDate
AND awd.adjustedExpirationDate > #myGetDate
To explain:
DATEDIFF(day, effectiveDate, #myGetDate) returns the number of days between the effectiveDate and the current date. So for example, say it was 20 days ago.
/ 7 gets the number of weeks as an int, since the DATEDIFF returns an int. This also results in the floor of the quotient. So, with our example 20 days / 7, the quotient is about 2.86, but this will result in an even 2
DATEADD adds the number of weeks to bring us up to or before the current date/time. We add the same number of weeks to the expiration date, resulting in the same range as the original effective/expiration dates, which may or may not extend around the current date.
Finally the check for effectiveDate <= #myGetDate guarantees the current date is after or equal to the effective date.

How to get 1st and 3rd Saturday and all Sunday between 2 dates using sql

Given a date range, I'd like to return all of the Saturdays and Sundays that fall within that range, with these conditions:
Include Saturdays only if their ordinal position is either the 1st or 3rd Saturday within the month they fall (not within the entire range).
Include all Sundays, along with the ordinal position of that Sunday within the month it falls.
So for example, if the start date is Aug 15, 2021 and the end date is Sep 20, 2021, the output would be:
Dates Saturday Number (in its own month)
---------- ---------------
2021-08-21 3
2021-09-04 1
2021-09-18 3
Dates Sunday Number (in its own month)
---------- ---------------
2021-08-15 3
2021-08-22 4
2021-08-29 5
2021-09-05 1
2021-09-12 2
2021-09-19 3
Then I can take the date range in total (37 days), and subtract the Sundays (6), and the 1st and 3rd Saturdays from each month (3), to end at 28.
Tried this query
DECLARE #sd DATETIME = '2021-08-15' DECLARE #ed DATETIME =
'2021-09-20'
--find first saturday WHILE DATEPART(dw, #sd)<>7 BEGIN SET #sd = DATEADD(dd,1,#sd) END
--get next saturdays ;WITH Saturdays AS (
--initial value SELECT #sd AS MyDate, 1 AS SatNo UNION ALL
--recursive part SELECT DATEADD(dd,7,MyDate) AS MyDate, CASE WHEN SatNo + 1 =6 THEN 1 ELSE SatNo+1 END AS SatNo FROM Saturdays
WHERE DATEADD(dd,7,MyDate)<=#ed
) SELECT * FROM Saturdays WHERE SatNo IN (1,3) OPTION(MAXRECURSION 0)
it does not work properly.
Also Tried this solution
Get number of weekends between two dates in SQL
for calculate week days, but I want only 1st and 3 Saturday and all Sundays
Get a calendar table; it makes this type of business problem a breeze. Here's a simpler one:
CREATE TABLE dbo.Calendar
(
TheDate date PRIMARY KEY,
WeekdayName AS (CONVERT(varchar(8), DATENAME(WEEKDAY, TheDate))),
WeekdayInstanceInMonth tinyint
);
;WITH x(d) AS -- populate with 2020 -> 2029
(
SELECT CONVERT(date, '20200101')
UNION ALL
SELECT DATEADD(DAY, 1, d)
FROM x
WHERE d < '20291231'
)
INSERT dbo.Calendar(TheDate)
SELECT d FROM x
OPTION (MAXRECURSION 0);
;WITH c AS
(
SELECT *, rn = ROW_NUMBER() OVER
(PARTITION BY YEAR(TheDate), MONTH(TheDate), WeekdayName
ORDER BY TheDate)
FROM dbo.Calendar
)
UPDATE c SET WeekdayInstanceInMonth = rn;
Now your query is easy:
DECLARE #start date = '20210815', #end date = '20210920';
SELECT Dates = TheDate,
[Saturday Number] = WeekdayInstanceInMonth
FROM dbo.Calendar
WHERE TheDate >= #start
AND TheDate <= #end
AND WeekdayName = 'Saturday'
AND WeekdayInstanceInMonth IN (1,3);
SELECT Dates = TheDate,
[Sunday Number] = WeekdayInstanceInMonth
FROM dbo.Calendar
WHERE TheDate >= #start
AND TheDate <= #end
AND WeekdayName = 'Sunday';
Results (db<>fiddle example here):
Dates Saturday Number
---------- ---------------
2021-08-21 3
2021-09-04 1
2021-09-18 3
Dates Sunday Number
---------- ---------------
2021-08-15 3
2021-08-22 4
2021-08-29 5
2021-09-05 1
2021-09-12 2
2021-09-19 3
And to get just the number 28:
DECLARE #start date = '20210815', #end date = '20210920';
SELECT DATEDIFF(DAY, #start, #end) + 1
-
(SELECT COUNT(*)
FROM dbo.Calendar
WHERE TheDate >= #start
AND TheDate <= #end
AND WeekdayName = 'Saturday'
AND WeekdayInstanceInMonth IN (1,3))
-
(SELECT COUNT(*)
FROM dbo.Calendar
WHERE TheDate >= #start
AND TheDate <= #end
AND WeekdayName = 'Sunday');
Assuming datefirst is set for Sunday:
(
day(dt) +
datepart(weekday, dateadd(dt, 1 - day(dt))) % 7
) / 7 as SaturdayNumber,
(
day(dt) - 1 +
datepart(weekday, dateadd(dt, 1 - day(dt)))
) / 7 as SundayNumber
For all dates this essentially computes a week number (0-5) within the month, relative to Saturday/Sunday.

How do I calculate amount of time in each week between two dates - SQL Server

[EDITED TO SIMPLIFY]
I have 500+ records. All of which have a reference number, a start date, an end date and a total machining time.
Ref StartDate EndDate MachineTimeHours
123 24/01/2020 30/01/2020 28
321 25/02/2020 27/02/2020 18
Starting at the start date, I need to calculate how many machining hours fall into 1 week and how many fall into the next.
Our working days are Monday to Thursday 8 Hours & Friday 4 Hours.
Ref 321 has a start of 25/2 which is a Tuesday and a finish date of 27/2 which is a Thursday in the same week. This will calculate as all 18 hours being in the same week.
Ref 123 has a start of 24/01. This is a Friday in Week 4 of 2020.
Based on my rules, that would be 4 hours in week 4 and 24 Hours in week 5.
I have a table called 'DatesList' which has all days on it (as well as week number and working hours).
I need my table to list each record for each week and I'll do the grouping on a separate report.
In effect I'd like
Ref StartDate EndDate MachineTimeHours Week Hours
123 24/01/2020 30/01/2020 28 4 4
123 24/01/2020 30/01/2020 28 5 24
321 25/02/2020 27/02/2020 18 9 18
You can start with creating some reference tables.
For the example those are just temporary tables.
Reference data:
--
-- Reference tables
--
CREATE TABLE #ref_calendar
(
CalDate DATE PRIMARY KEY,
DayOfWeek SMALLINT NOT NULL,
WeekNr SMALLINT NOT NULL,
IsHoliday BIT NOT NULL DEFAULT 0
);
DECLARE #year int = 2020;
SET DATEFIRST 1; -- 1: monday start
;WITH RCTE_DATES AS
(
SELECT
DATEFROMPARTS(#year, 1, 1) AS caldate
UNION ALL
SELECT dateadd(day, 1, caldate)
FROM RCTE_DATES
WHERE caldate <= DATEFROMPARTS(#year, 12, 31)
)
INSERT INTO #ref_calendar (CalDate, DayOfWeek, WeekNr)
SELECT
caldate,
DATEPART(weekday, caldate) AS DayOfWeek,
DATEPART(week, caldate) AS WeekNr
FROM rcte_dates c
WHERE NOT EXISTS
(
SELECT 1
FROM #ref_calendar ref
WHERE ref.CalDate = c.caldate
)
OPTION (MAXRECURSION 366);
CREATE TABLE #ref_workhours
(
Id INT IDENTITY(1,1) PRIMARY KEY,
DayOfWeek SMALLINT NOT NULL,
WorkHours DECIMAL(4,2) NOT NULL,
ActiveFrom DATE NOT NULL DEFAULT GetDate(),
ActiveTill DATE
);
INSERT INTO #ref_workhours
(DayOfWeek, WorkHours) VALUES
(1, 8.0), (2, 8.0), (3, 8.0), (4, 8.0), (5, 4.0),
(6, 0), (7, 0);
Some sample data:
--
-- Sample data
--
CREATE TABLE YourDateRangeTable
(
Id INT IDENTITY(1,1) PRIMARY KEY,
JobNumber INT NOT NULL,
PartNumber VARCHAR(8) NOT NULL,
Machine CHAR(3) NOT NULL,
StartDate DATE NOT NULL,
EndDate DATE NOT NULL
);
INSERT INTO YourDateRangeTable
(JobNumber, PartNumber, Machine, StartDate, EndDate) values
(12345, 'XYZ321', 'DL8', '2020-01-24', '2020-01-30');
Then you can run a query that uses the reference tables.
SELECT JobNumber, PartNumber, Machine
, YEAR(cal.CalDate) AS [Year]
, cal.WeekNr AS [Week]
, SUM(w.WorkHours) AS [Hours]
FROM YourDateRangeTable t
JOIN #ref_calendar cal
ON cal.CalDate >= t.StartDate
AND cal.CalDate < t.EndDate
JOIN #ref_workhours w
ON w.DayOfWeek = cal.DayOfWeek
GROUP BY JobNumber, PartNumber, Machine
, YEAR(cal.CalDate), cal.WeekNr;
Returns:
JobNumber PartNumber Machine Year Week Hours
12345 XYZ321 DL8 2020 4 4.00
12345 XYZ321 DL8 2020 5 24.00
A test on db<>fiddle here
You can get all the detes of two given date and also weeknumber and a case statement for the working hour. Based on the result from the inner query write an outer query which will give the sum of total working hour.
Here is the given query.
DECLARE #MinDate DATE = '20200124',
#MaxDate DATE = '20200130'
--Fri Week 4 = 4 hours
--Mon Week 5 = 8 hours
--Tue Week 5 = 8 hours
--Wed Week 5 = 8 hours
Select WeekNo, SUM(WorkingHour) as TotalWorkingHour from(
Select [DATE], DATEPART(WEEK, [DATE]) - DATEPART(WEEK, DATEADD(MM, DATEDIFF(MM,0,[DATE]), 0))+ 1 as WeekNo,
DATENAME(weekday, [DATE]) as WeekDay, Case DATENAME(weekday, [DATE])
when 'Friday' then 4
when 'Monday' then 8
when 'Tuesday' then 8
when 'Wednesday' then 8
else 0
end as WorkingHour from(
SELECT TOP (DATEDIFF(DAY, #MinDate, #MaxDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #MinDate)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
)a
)b group by WeekNo
It will give the result as below:
WeekNo TotalWorkingHour
------------------------
4 4
5 24
You can find the demo Here.

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

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