Number of Shifts betweens two times - sql

Can you please help me with the following.
I am trying to calculate how may shifts a patient was in the hospital. The shifts timings start from 7:00 AM to 6:59 PM and 7:00 PM to 6.59 AM.
If a patient was admitted to a location after the start of the shift, we ignore that shift in the total calculation.
Here is the sample data and how the end result should look like :
DECLARE #T AS TABLE
(
ID INT,
LOCATION VARCHAR(10),
Date_entered DATETIME,
date_left datetime
);
DECLARE #endresult AS TABLE
(
ID INT,
LOCATION VARCHAR(10),
Date_entered DATETIME,
date_left datetime,
Total_shifts int
)
insert into #T VALUES
(1,'AB','01/01/2019 07:10','01/01/2019 20:30'),
(2,'CD','01/01/2019 20:30','01/04/2019 02:30'),
(3,'EF','01/04/2019 02:30','01/07/2019 19:30'),
(4,'GH','01/07/2019 19:30','01/08/2019 13:30')
insert into #endresult VALUES
(1,'AB','01/01/2019 07:10','01/01/2019 20:30',1),
(2,'CD','01/01/2019 20:30','01/04/2019 02:30',4),
(3,'EF','01/04/2019 02:30','01/07/2019 19:30',8),
(4,'GH','01/07/2019 19:30','01/08/2019 13:30',1)
SELECT * FROM #t
select * from #endresult
I tried using a recursive CTE, but the query is taking too much time to complete. Any simple way to calculate the timings?

Here is a query that returns the correct results for your sample data :
select
t.*,
DATEDIFF(DAY, date_entered, date_left) * 2
- CASE WHEN DATEPART(HOUR, date_entered) < 7 THEN 0 WHEN DATEPART(HOUR, date_entered) < 19 THEN 1 ELSE 2 END
+ CASE WHEN DATEPART(HOUR, date_left) < 7 THEN 0 WHEN DATEPART(HOUR, date_left) < 19 THEN 1 ELSE 2 END
AS Total_shifts
from #t t;
The logic is to first count how many days occured between entrance and exit, then multiply it by two to get a raw number of shifts ; then adjust the raw count by checking the times when the patient entered and exited.
This demo on DB Fiddle with your sample data returns :
ID | LOCATION | Date_entered | date_left | Total_shifts
-: | :------- | :------------------ | :------------------ | -----------:
1 | AB | 01/01/2019 07:10:00 | 01/01/2019 20:30:00 | 1
2 | CD | 01/01/2019 20:30:00 | 04/01/2019 02:30:00 | 4
3 | EF | 04/01/2019 02:30:00 | 07/01/2019 19:30:00 | 8
4 | GH | 07/01/2019 19:30:00 | 08/01/2019 13:30:00 | 1

Here's a query that will give you the results you want. It uses 3 CTEs, the first simply selects the minimum Date_entered and maximum date_left values, the second aligns the minimum Date_entered value to a shift start time (7AM or 7PM), and the third recursive CTE generates a list of all the shift start times between the minimum Date_entered and maximum date_left values. Finally we JOIN that CTE to the admissions table and count the number of shift start times between Date_entered and date_left:
WITH cte AS
(SELECT MIN(Date_entered) AS min_date, MAX(date_left) AS max_date
FROM T),
cte1 AS
(SELECT CASE WHEN DATEPART(hour, min_date) < 7 THEN DATEADD(hour, 19, DATEADD(day, -1, CONVERT(DATE, min_date)))
ELSE DATEADD(hour, 7, CONVERT(DATETIME, CONVERT(DATE, min_date)))
END AS min_shift
FROM cte),
shifts AS
(SELECT min_shift AS shift_start
FROM cte1
UNION ALL
SELECT DATEADD(hour, 12, shift_start)
FROM shifts
WHERE shift_start < (SELECT max_date FROM cte))
SELECT T.ID, T.LOCATION, T.Date_Entered, T.date_left, COUNT(s.shift_start) AS Total_shifts
FROM T
JOIN shifts s ON s.shift_start BETWEEN T.Date_Entered AND T.date_left
GROUP BY T.ID, T.LOCATION, T.Date_Entered, T.date_left
Output:
ID LOCATION Date_Entered date_left Total_shifts
1 AB 01/01/2019 07:10:00 01/01/2019 20:30:00 1
2 CD 01/01/2019 20:30:00 04/01/2019 06:59:00 4
3 EF 04/01/2019 07:00:00 07/01/2019 19:30:00 8
4 GH 07/01/2019 19:30:00 08/01/2019 13:30:00 1
Demo on dbfiddle

Related

Oracle SQL - Get difference between dates based on check in and checkout records

Assume I have the following table data.
# | USER | Entrance | Transaction Date Time
-----------------------------------------------
1 | ALEX | INBOUND | 2020-01-01 10:20:00
2 | ALEX | OUTBOUND | 2020-01-02 10:00:00
3 | ALEX | INBOUND | 2020-01-04 11:30:00
4 | ALEX | OUTBOUND | 2020-01-07 15:00:00
5 | BEN | INBOUND | 2020-01-08 08:00:00
6 | BEN | OUTBOUND | 2020-01-09 09:00:00
I would like to know the total of how many days the user has stay outbound.
For each inbound and outbound is considered one trip, every trip exceeded 24 hours is considered as 2 days.
Below is my desired output:
No. of Days | Trips Count
----------------------------------
Stay < 1 day | 1
Stay 1 day | 1
Stay 2 days | 0
Stay 3 days | 0
Stay 4 days | 1
I would use lead() and aggregation. Assuming that the rows are properly interlaced:
select floor( (next_dt - dt) ) as num_days, count(*)
from (select t.*,
lead(dt) over (partition by user order by dt) as next_dt
from trips t
) t
where entrance = 'INBOUND'
group by floor( (next_dt - dt) )
order by num_days;
Note: This does not include the 0 rows. That does not seem central to your question and is a significant complication.
I still don't know what you mean with < 1 day, but this I got this far
Setup
create table trips (id number, name varchar2(10), entrance varchar2(10), ts TIMESTAMP);
insert into trips values( 1 , 'ALEX','INBOUND', TIMESTAMP '2020-01-01 10:20:00');
insert into trips values(2 , 'ALEX','OUTBOUND',TIMESTAMP '2020-01-02 10:00:00');
insert into trips values(3 , 'ALEX','INBOUND',TIMESTAMP '2020-01-04 11:30:00');
insert into trips values(4 , 'ALEX','OUTBOUND',TIMESTAMP '2020-01-07 15:00:00');
insert into trips values(5 , 'BEN','INBOUND',TIMESTAMP '2020-01-08 08:00:00');
insert into trips values(6 , 'BEN','OUTBOUND',TIMESTAMP '2020-01-09 07:00:00');
Query
select decode (t.days, 0 , 'Stay < 1 day', 1, 'Stay 1 day', 'Stay ' || t.days || ' days') Days , count(d.days) Trips_count
FROM (Select Rownum - 1 days From dual Connect By Rownum <= 6) t left join
(select extract (day from b.ts - a.ts) + 1 as days from trips a
inner join trips b on a.name = b.name
and a.entrance = 'INBOUND'
and b.entrance = 'OUTBOUND'
and a.ts < b.ts
and not exists (select ts from trips where entrance = 'OUTBOUND' and ts > a.ts and ts < b.ts)) d
on t.days = d.days
group by t.days order by t.days
Result
DAYS | TRIPS_COUNT
----------------|------------
Stay < 1 day | 0
Stay 1 day | 2
Stay 2 days | 0
Stay 3 days | 0
Stay 4 days | 1
Stay 5 days | 0
You could replace the 6 with a select max with the second subquery repeated

How to find all dates between two dates and check if there is a "holiday"?

I would like to find all the dates between two date columns and then check if there were holidays between those two dates for each row.
I have another table with all the holidays listed which I can join to.
If there is any holiday between the dates then put a yes flag for holiday.
What would be the best way to do it?
My database is Snowflake.
Table1
id Country Date1 Date2
1 DE 2018-12-23 2018-12-30
2 DE 2019-08-01 2019-08-09
...
3 DE 2019-04-28 2019-05-02
Table 2
Country Date Holiday
DE 2018-12-25 Christmas
DE 2019-05-01 Labor Day
I would like the result to look like
Result:
id Country Date1 Date2 is_holiday
1 DE 2018-12-23 2018-12-30 Yes
2 DE 2019-08-01 2019-08-09 No
...
3 DE 2019-04-28 2019-05-02 Yes
With a LEFT JOIN of the tables and group by:
select
t1.id, t1.Country, t1.date1, t1.date2,
case count(t2.holiday) when 0 then 'No' else 'Yes' end is_holiday
from table1 t1 left join table2 t2
on t1.country = t2.country and t2.date between t1.date1 and t1.date2
group by t1.id, t1.Country, t1.date1, t1.date2
See the demo (for MySQL but since I used standard SQL I believe it will work for Snowflake too).
Results:
| id | Country | date1 | date2 | is_holiday |
| --- | ------- | ------------------- | ------------------- | ---------- |
| 1 | DE | 2018-12-23 00:00:00 | 2018-12-30 00:00:00 | Yes |
| 2 | DE | 2019-08-01 00:00:00 | 2019-08-09 00:00:00 | No |
| 3 | DE | 2019-04-28 00:00:00 | 2019-05-02 00:00:00 | Yes |
Implying you have a tally table (it's on TSQL but you can work with something similar) with every day and holiday within a range then :
SELECT E.*,
TOTAL_DAYS = (SELECT COUNT(CONVERT(INT,holiday,112)) AS D FROM CALENDAR C WHERE holiday = 0 AND C.dateValue BETWEEN begDATE AND endDATE)
FROM yourTable E
With a table Table_DateFromTo:
And table Table_HolidayDates:
You can run a query like this:
SELECT DateFrom as MinDate,
DateTo as MaxDate,
(SELECT count(*)
FROM Table_HolidayDates
WHERE Date >= A.DateFrom
AND Date < A.DateTo) as HolidayCount
FROM Table_DateFromTo A
It gives you following output:
You could find the DISTINCT COUNT of the holidays for the given dates and subtract it from the actual day difference. The following query should do what you want:,
CREATE TABLE #table1 (Date1 DATE, Date2 DATE)
INSERT INTO #table1 VALUES
('2018-12-23','2018-12-30'),
('2019-04-28','2019-05-02')
CREATE TABLE #table2 (Country VARCHAR(2), [Date] DATE, Holiday VARCHAR(25))
INSERT INTO #table2 VALUES
('DE','2018-12-25','Christmas'),
('DE','2019-05-01','Labor Day'),
('DE','2019-04-29','SoMe Holiday1')
SELECT Date1,Date2,DATEDIFF(DAY,Date1,Date2)-T.holidays [Days (Excl Holidays)], T.holidays AS [Number_of_Holidays]
FROM #table1
CROSS APPLY (VALUES((SELECT COUNT(DISTINCT Holiday) FROM #table2 WHERE[Date] BETWEEN Date1 and Date2) ) ) AS T(Holidays)
The result is as below,
Date1 Date2 Days (Excl Holidays) Number_of_Holidays
2018-12-23 2018-12-30 6 1
2019-04-28 2019-05-02 2 2

How to add rolling 7 and 30 day columns to my daily count of distinct logins in SQL Server

I have half of my query that outputs the total distinct users logging in in my website for each day. But I need my third and fourth column to show the rolling week and month activity for my users.
DECLARE #StartDate AS Date = DATEADD(dd,-31,GETDATE())
SELECT CAST(ml.login AS Date) AS Date_Login
,COUNT(DISTINCT ml.email) AS Total
FROM database.members_log AS ml
WHERE 1=1
AND ml.login > #StartDate
GROUP BY CAST(ml.login AS Date)
ORDER BY CAST(ml.login AS Date) DESC
How could I complement my code to include 7-day & 30-day rolling count of distinct users
In other words: the unique amount of users who logged in within a given amount of time (Daily, Last 7 days, Last 30 days)
Not sure if this is what you're going for, but you can use window functions for rolling totals/counts. For example, if you wanted to keep your report of count by day, but also count by rolling week and month, you could do something like the following (using an intermediate CTE):
declare #StartDate AS Date = DATEADD(day, -31, getdate());
WITH
-- this is your original query, with the ISO week and month number added.
members_log_aggr(login_date, year_nbr, iso_week_nbr, month_nbr, email_count) AS
(
SELECT
CAST(ml.login AS Date),
DATEPART(YEAR, ml.login),
DATEPART(ISO_WEEK, ml.login),
DATEPART(MONTH, ml.login),
COUNT(DISTINCT ml.email) AS Total
FROM members_log AS ml
WHERE
ml.login > #StartDate
GROUP BY
CAST(ml.login AS Date),
DATEPART(YEAR, ml.login),
DATEPART(ISO_WEEK, ml.login),
DATEPART(MONTH, ml.login)
)
-- here, we use window functions for a rolling total of email count.
SELECT *,
SUM(email_count) OVER
(
PARTITION BY year_nbr, iso_week_nbr
ORDER BY login_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS count_by_week,
SUM(email_count) OVER
(
PARTITION BY year_nbr, month_nbr
ORDER BY login_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) as count_by_month
FROM members_log_aggr
giving you this data:
+------------+----------+--------------+-----------+-------------+---------------+----------------+
| login_date | year_nbr | iso_week_nbr | month_nbr | email_count | count_by_week | count_by_month |
+------------+----------+--------------+-----------+-------------+---------------+----------------+
| 2018-12-12 | 2018 | 50 | 12 | 1 | 6 | 7 |
| 2018-12-13 | 2018 | 50 | 12 | 1 | 6 | 7 |
| 2018-12-14 | 2018 | 50 | 12 | 1 | 6 | 7 |
| 2018-12-15 | 2018 | 50 | 12 | 1 | 6 | 7 |
| 2018-12-16 | 2018 | 50 | 12 | 2 | 6 | 7 |
| 2018-12-19 | 2018 | 51 | 12 | 1 | 1 | 7 |
| 2019-01-13 | 2019 | 2 | 1 | 2 | 2 | 3 |
| 2019-01-21 | 2019 | 4 | 1 | 1 | 1 | 3 |
+------------+----------+--------------+-----------+-------------+---------------+----------------+
A couple of additional notes:
Your original query has 1=1 in your WHERE clause. You don't need that.
There's no need to use abbreviations in your DATEADD function (or other date functions) For example, DATEADD(DAY, -31, GETDATE()) is more clearer and just as performant as DATEADD(DD, -31, GETDATE())
It might be a good idea to replace GETDATE() with CURRENT_TIMESTAMP. They're the same function, but CURRENT_TIMESTAMP is a SQL standard.
Perhaps "Conditional aggregates" can be used for this (basically just put a case expression inside an aggregate function) e.g.
DECLARE #StartDate AS date = DATEADD( dd, -31, GETDATE() )
SELECT
CAST( ml.login AS date ) AS Date_Login
, COUNT( DISTINCT CASE
WHEN CAST( ml.login AS date ) >= DATEADD( dd, -7, CAST( GETDATE() AS date ) ) THEN ml.email
END ) AS in_week
, COUNT( DISTINCT ml.email ) AS Total
FROM dbo.members_log AS ml
WHERE 1 = 1
AND ml.login > #StartDate
GROUP BY
CAST( ml.login AS date )
ORDER BY
CAST( ml.login AS date ) DESC
But as you are already filtering for just the past 31 days, I'm not sure what you mean by "rolling" week or "rolling" month.
count(distinct) is quite tricky -- particularly for rolling averages. If you are really looking for the unique users over a time span (rather than just the average of the daily unique visitors), then I think apply may be the simplest approach:
with d as (
select cast(ml.login AS Date) AS Date_Login,
count(distinct ml.email) AS Total
from database.members_log ml
where ml.login > #StartDate
group by CAST(ml.login AS Date)
)
select t.date_login, t.total, t7.total_7d, t30.total_30d
from t outer apply
(select count(distinct ml2.email) as total_7d
from database.members_log ml2
where ml2.login <= dateadd(day, 1, t.date_login) and
ml2.login > dateadd(day, -7, t.date_login)
) t7 outer apply
(select count(distinct ml2.email) as total_30d
from database.members_log ml2
where ml2.login <= dateadd(day, 1, t.date_login) and
ml2.login > dateadd(day, -30, t.date_login)
) t30
order by date_login desc;
The date arithmetic is my best understanding of what you mean by the rolling averages. It includes the current day, but not the day days ago.

Select all dates from a given date range having to and from dates

I am working on a report where I want the calendar of all of the days in an year who must be printed in a sequential manner i.e. sort by dates.
I have two tables, one who has holiday(s) details and the other has the standard in and out time of working days.
Here are the column details of both of the tables :
Working_hour_parameter : Dt(Date),STD_INTIME ,STD_OUTTIME
Holidays : from_dt, to_dt ,holiday_type, remarks
Example : If I pass the date range from 1st november to 5 november and let say 2nd,3rd november is holiday, it must return result as :
From date To date std_intime std_outtime remarks
------- -------- -------- ------- ------
1st-Nov 1st-Nov 09:00 17:30 working day
2nd-Nov 3rd-Nov (null) (null) holiday
4th-Nov 5th-Nov 09:00 17:30 working day
The problem is, one of the table has date range columns i.e. to and from dates and the other table is based on single date.
How is it possible?
Can anybody help me please?
Snapshot of tables :
The general solution for this problem is to generate a row for each date needed which can be done with connect by rownum (Oracle specific) or a variety of other means such as a recursive common table expression (many dbs). You might might even consider creating a "calendar table" if you need to do this on a very regular basis. e.g.
SELECT * from (
SELECT to_date('2017-01-01','yyyy-mm-dd') + rownum - 1 dt
FROM DUAL CONNECT BY ROWNUM < 366
)
WHERE dt < to_date('2018-01-01','yyyy-mm-dd')
Once you have the dates in rows, left join both sets of your data to those dates and the job is done.
select cal.dt, wp.*, h.*
from (
SELECT to_date('2017-01-01','yyyy-mm-dd') + rownum - 1 dt
FROM DUAL CONNECT BY ROWNUM < 36
) cal
left join Working_hour_parameter wp on cal.dt = wp.Dt
left join Holidays h on cal.dt between h.from_dt and h.to_dt
WHERE cal.dt < to_date('2018-01-01','yyyy-mm-dd')
----
To also propose a way to summaize this per ay data, first_value() and `last_value() are used to forms "islands" for grouping the ranges.
available as a demo at SQL Fiddle
CREATE TABLE WORKING_HOUR_PARAMETER
(DT timestamp, STD_INTIME varchar2(5), STD_OUTTIME varchar2(5))
;
INSERT ALL
INTO WORKING_HOUR_PARAMETER ("DT", "STD_INTIME", "STD_OUTTIME")
VALUES ('01-Jan-2017 12:00:00 AM', '09:00', '17:30')
INTO WORKING_HOUR_PARAMETER ("DT", "STD_INTIME", "STD_OUTTIME")
VALUES ('04-Jan-2017 12:00:00 AM', '09:00', '17:30')
INTO WORKING_HOUR_PARAMETER ("DT", "STD_INTIME", "STD_OUTTIME")
VALUES ('05-Jan-2017 12:00:00 AM', '09:00', '17:30')
INTO WORKING_HOUR_PARAMETER ("DT", "STD_INTIME", "STD_OUTTIME")
VALUES ('06-Jan-2017 12:00:00 AM', '09:00', '17:30')
SELECT * FROM dual
;
CREATE TABLE HOLIDAYS
(FROM_DT timestamp, TO_DT timestamp, HOLIDAY_TYPE varchar2(8), REMARKS varchar2(12))
;
INSERT ALL
INTO HOLIDAYS ("FROM_DT", "TO_DT", "HOLIDAY_TYPE", "REMARKS")
VALUES ('02-Jan-2017 12:00:00 AM', '03-Jan-2017 12:00:00 AM', 'whatever', 'avagoodbreak')
SELECT * FROM dual
;
Query 1:
select
min(dt) span_from
, max(dt) span_to
, std_intime
, std_outtime
, from_dt hol_start
, to_dt hol_to
, fval1 first_value
, lval1 last_value
from (
select cal.dt, h.FROM_DT, h.TO_DT, wp.std_intime, wp.std_outtime
, first_value(FROM_DT ignore nulls) over(order by cal.dt rows between current row and unbounded following) fval1
, last_value(TO_DT ignore nulls) over(order by cal.dt) lval1
from (
SELECT to_timestamp('2017-01-01','yyyy-mm-dd') + rownum - 1 dt
FROM DUAL CONNECT BY ROWNUM < 36
) cal
left join Working_hour_parameter wp on cal.dt = wp.Dt
left join Holidays h on cal.dt between h.from_dt and h.to_dt
WHERE cal.dt < to_date('2017-01-07','yyyy-mm-dd')
)
group by
std_intime
, std_outtime
, from_dt
, to_dt
, fval1
, lval1
order by 1, 2
Results:
| SPAN_FROM | SPAN_TO | STD_INTIME | STD_OUTTIME | HOL_START | HOL_TO | FIRST_VALUE | LAST_VALUE |
|----------------------|----------------------|------------|-------------|-----------------------|-----------------------|-----------------------|-----------------------|
| 2017-01-01T00:00:00Z | 2017-01-01T00:00:00Z | 09:00 | 17:30 | (null) | (null) | 2017-01-02 00:00:00.0 | (null) |
| 2017-01-02T00:00:00Z | 2017-01-03T00:00:00Z | (null) | (null) | 2017-01-02 00:00:00.0 | 2017-01-03 00:00:00.0 | 2017-01-02 00:00:00.0 | 2017-01-03 00:00:00.0 |
| 2017-01-04T00:00:00Z | 2017-01-06T00:00:00Z | 09:00 | 17:30 | (null) | (null) | (null) | 2017-01-03 00:00:00.0 |

Select Multiple Rows from Timespan

Problem
In my sql-server-2014 I store projects in a table with the columns:
Startdate .. | Enddate ....| Projectname .................| Volume
2017-02-13 | 2017-04-12 | GenerateRevenue .........| 20.02
2017-04-02 | 2018-01-01 | BuildRevenueGenerator | 300.044
2017-05-23 | 2018-03-19 | HarvestRevenue ............| 434.009
I need a SELECT to give me one row per month of the project for each project. the days of the month don't have to be considered.
Date .......... | Projectname..................| Volume
2017-02-01 | GenerateRevenue .........| 20.02
2017-03-01 | GenerateRevenue .........| 20.02
2017-04-01 | GenerateRevenue .........| 20.02
2017-04-01 | BuildRevenueGenerator | 300.044
2017-05-01 | BuildRevenueGenerator | 300.044
2017-06-01 | BuildRevenueGenerator | 300.044
...
Extra
Ideally the logic of the SELECT allows me both to calculate the monthly volume and also the difference between each month and the previous.
Date .......... | Projectname..................| VolumeMonthly
2017-02-01 | GenerateRevenue .........| 6.6733
2017-03-01 | GenerateRevenue .........| 6.6733
2017-04-01 | GenerateRevenue .........| 6.6733
2017-04-01 | BuildRevenueGenerator | 30.0044
2017-05-01 | BuildRevenueGenerator | 30.0044
2017-06-01 | BuildRevenueGenerator | 30.0044
...
Also...
I know I can map it on a temporary calendar table, but that tends to get bloated and complex very fast. Im really looking for a better way to solve this problem.
Solution
Gordons solution worked very nicely and it doesn't require a second table or mapping on a calendar of some sort. Although I had to change a few things, like making sure both sides of the union have the same SELECT.
Here my adapted version:
with cte as (
select startdate as mondate, enddate, projectName, volume
from projects
union all
select dateadd(month, 1, mondate), enddate, projectName, volume
from cte
where eomonth(dateadd(month, 1, mondate)) <= eomonth(enddate)
)
select * from cte;
Volume monthly can be achieved by replacing volume with:
CAST(Cast(volume AS DECIMAL) / Cast(Datediff(month,
startdate,enddate)+ 1 AS DECIMAL) AS DECIMAL(15, 2))
END AS [volumeMonthly]
Another option is with an ad-hoc tally table
Example
-- Some Sample Data
Declare #YourTable table (StartDate date,EndDate date,ProjectName varchar(50), Volume float)
Insert Into #YourTable values
('2017-03-15','2017-07-25','Project X',25)
,('2017-04-01','2017-06-30','Project Y',50)
-- Set Your Desired Date Range
Declare #Date1 date = '2017-01-01'
Declare #Date2 date = '2017-12-31'
Select Period = D
,B.*
,MonthlyVolume = sum(Volume) over (Partition By convert(varchar(6),D,112))
From (Select Top (DateDiff(MONTH,#Date1,#Date2)+1) D=DateAdd(MONTH,-1+Row_Number() Over (Order By (Select Null)),#Date1)
From master..spt_values n1
) A
Join #YourTable B on convert(varchar(6),D,112) between convert(varchar(6),StartDate,112) and convert(varchar(6),EndDate,112)
Order by Period,ProjectName
Returns
Note: Use a LEFT JOIN to see gaps
You can use a recursive subquery to expand the rows for each project, based on the table:
with cte as (
select stardate as mondate, p.*
from projects
union all
select dateadd(month, 1, mondate), . . . -- whatever columns you want here
from cte
where eomonth(dateadd(month, 1, mondate)) <= eomonth(enddate)
)
select *
from cte;
I'm not sure if this actually answers your question. When I first read the question, I figured the table had one row per project.
Using a couple of common table expressions, an adhoc calendar table for months and lag() (SQL Server 2012+) for the final delta calculation:
create table projects (id int identity(1,1), StartDate date, EndDate date, ProjectName varchar(32), Volume float);
insert into projects values ('20170101','20170330','SO Q1',240),('20170214','20170601','EX Q2',120)
declare #StartDate date = '20170101'
, #EndDate date = '20170731';
;with Months as (
select top (datediff(month,#startdate,#enddate)+1)
MonthStart = dateadd(month, row_number() over (order by number) -1, #StartDate)
, MonthEnd = dateadd(day,-1,dateadd(month, row_number() over (order by number), #StartDate))
from master.dbo.spt_values
)
, ProjectMonthlyVolume as (
select p.*
, MonthlyVolume = Volume/(datediff(month,p.StartDate,p.EndDate)+1)
, m.MonthStart
from Months m
left join Projects p
on p.EndDate >= m.MonthStart
and p.StartDate <= m.MonthEnd
)
select
MonthStart = convert(char(7),MonthStart,120)
, MonthlyVolume = isnull(sum(MonthlyVolume),0)
, Delta = isnull(sum(MonthlyVolume),0) - lag(Sum(MonthlyVolume)) over (order by MonthStart)
from ProjectMonthlyVolume pmv
group by MonthStart
rextester demo: http://rextester.com/DZL54787
returns:
+------------+---------------+-------+
| MonthStart | MonthlyVolume | Delta |
+------------+---------------+-------+
| 2017-01 | 80 | NULL |
| 2017-02 | 104 | 24 |
| 2017-03 | 104 | 0 |
| 2017-04 | 24 | -80 |
| 2017-05 | 24 | 0 |
| 2017-06 | 24 | 0 |
| 2017-07 | 0 | -24 |
+------------+---------------+-------+