Update end date based on its succeeding start date in sql server - sql

I am new to SQL Server, I tried few methods but couldn't able to get succeed to update below nulls with the value of their immediate successive to respective products (start_day-1 day), It is my production scenario, so I cant able to publish original query I tried. So kindly help me to achieve this scenario.
Table_Name - Product
Actual data:
------------------------------------------
Product_cd | Start_date | end_date
------------------------------------------
A | 2017-01-01 | 2017-01-10
A | 2017-01-11 | null
A | 2017-03-10 | 2099-12-31
B | 2015-01-01 | null
B | 2017-01-11 | 2099-12-31
C | 2015-01-01 | 2015-01-10
C | 2015-01-11 | null
C | 2015-03-10 | 2015-03-09
C | 2015-03-10 | 2099-12-31
D | 2000-01-01 | 2000-10-21
D | 2000-10-22 | 2000-11-12
D | 2000-11-13 | null
D | 2015-03-10 | 2099-12-31
Correct data expecting: (After Null in end_date, min(start_date) for same product- 1 day)
------------------------------------------
Product_cd | Start_date | end_date
------------------------------------------
A | 2017-01-01 | 2017-01-10
A | 2017-01-11 | 2017-03-09
A | 2017-03-10 | 2099-12-31
B | 2015-01-01 | 2017-01-10
B | 2017-01-11 | 2099-12-31
C | 2015-01-01 | 2015-01-10
C | 2015-01-11 | 2015-03-09
C | 2015-03-10 | 2015-03-09
C | 2015-03-10 | 2099-12-31
D | 2000-01-01 | 2000-10-21
D | 2000-10-22 | 2000-11-12
D | 2000-11-13 | 2015-03-09
D | 2015-03-10 | 2099-12-31

As etsa says the LEAD window function is what you need to use here (see here). You can only put this in a SELECT though so your update will need to be via something like a CTE. Try something like this...
DROP TABLE IF EXISTS StartEnd
CREATE TABLE StartEnd
( Product_cd char(1),
Startdate date,
end_date date
)
INSERT dbo.StartEnd (Product_cd,Startdate,end_date)
VALUES
('A','2017-01-01','2017-01-10' ),
('A','2017-01-11',null ),
('A','2017-03-10','2099-12-31' ),
('B','2015-01-01',null ),
('B','2017-01-11','2099-12-31' ),
('C','2015-01-01','2015-01-10' ),
('C','2015-01-11',null ),
('C','2015-03-10','2015-03-09' ),
('C','2015-03-10','2099-12-31' ),
('D','2000-01-01','2000-10-21' ),
('D','2000-10-22','2000-11-12' ),
('D','2000-11-13',null ),
('D','2015-03-10','2099-12-31' );
SELECT * FROM dbo.StartEnd AS se;
WITH UpdateRows AS
(
SELECT se.Product_cd,
se.Startdate,
se.end_date,
CASE WHEN se.end_date IS NULL
THEN dateadd(DAY,-1,lead(se.StartDate,1) OVER(PARTITION BY se.Product_cd ORDER BY se.Startdate))
ELSE se.end_date END AS newEndDate
FROM dbo.StartEnd AS se
)
UPDATE UpdateRows
SET end_date = newEndDate
WHERE end_date IS NULL;
SELECT * FROM dbo.StartEnd AS se;

In SQL Server 2012+, you can use lead(). In earlier versions, you need another method. Here is one:
update p
set end_date = dateadd(day, -1, p2.start_date)
from product p outer apply
(select top 1 p2.*
from product p2
where p2.product_cd = p.product_cd and
p2.start_date > p.start_date
order by p2.start_date desc
) p2
where p.end_date is null;
If you just want to retrieve the data, then you can use the same from clause in a select.

Try this.....
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT 1)) rownum,* INTO #Temp_table
FROM dbo.StartEnd f1
SELECT t1.Product_cd,t1.Startdate,DATEADD(DAY,-1,t2.Startdate)end_date
FROM #Temp_table t1
LEFT JOIN #Temp_table t2 ON t1.rownum = t2.rownum - 1

To extract the values you want you can use following query. It use windows analytical function LEAD() to find next value for a PRODUCT_CD, using START_DATE ordering). (As Gordon pointed out, in MSSQL 2012+)
SELECT *
FROM (SELECT PRODUCT_CD, START_DATE, END_DATE
, LEAD(START_DATE) OVER (PARTITION BY PRODUCT_CD ORDER BY START_DATE)-1 AS DATE_SUCC
FROM PRODUCT) A
WHERE END_DATE IS NULL AND DATE_SUCC IS NOT NULL;
Try to make the UPDATE by yourself. If you find any problem let me know and we'll see together.
I thought it would be useful for you to try to do the UPDATE, but others don't think so.
Here is the UPDATE, starting from my SELECT (I don't think CTE is necessary). I used it inside a BEGIN TRAN / ROLLBACK TRAN, so you can check it.
BEGIN TRAN
UPDATE A SET END_DATE = A.DATE_SUCC
FROM (SELECT PRODUCT_CD, START_DATE, END_DATE
, LEAD(START_DATE) OVER (PARTITION BY PRODUCT_CD ORDER BY START_DATE)-1 AS DATE_SUCC
FROM PRODUCT) A
WHERE A.END_DATE IS NULL AND A.DATE_SUCC IS NOT NULL
SELECT * FROM PRODUCT
ROLLBACK TRAN
Output sample:
PRODUCT_CD START_DATE END_DATE
A 2017-01-01 00:00:00.000 2017-01-10 00:00:00.000
A 2017-01-11 00:00:00.000 2017-03-09 00:00:00.000
A 2017-03-10 00:00:00.000 2099-12-31 00:00:00.000
B 2015-01-01 00:00:00.000 2017-01-10 00:00:00.000
B 2017-01-11 00:00:00.000 2099-12-31 00:00:00.000
...

Related

Oracle SQL join date dimension table with another table on date value

I have a date dimension table containing all dates and another table containing the value of items at specific dates.
E.g
(a) Date_Dim table
|Full_Date |
|-----------|
| .... |
|1-jan-2021 |
|2-Jan-2021 |
|3-jan-2021 |
| ... |
(b) Item_value table
|P_Date | ITEM | Value |
|-----------:|:------|-------:|
|20-Dec-2020 |AA1 |9 |
|1-jan-2021 |AA1 |10 |
|1-jan-2021 |AA2 |100 |
| ... | ... | ... |
I am trying to build a fact table containing the latest value of every item in the item_value table for every date in the date_dim table. i.e the value of the items every day.
e.g
|Full_date | ITEM | Value |
|-----------:|-------:|------:|
|31-Dec-2020 |AA1 | 9 |
|31-Dec-2020 |AA2 | null |
|1-Jan-2021 |AA1 | 10 |
|1-Jan-2021 |AA2 | 100 |
|2-Jan-2021 |AA1 | 10 |
|2-Jan-2021 |AA2 | 100 |
|3-Jan-2021 |AA1 | 10 |
|3-Jan-2021 |AA2 | 100 |
|4-Jan-2021 |AA1 | 10 |
|4-Jan-2021 |AA2 | 100 |
How can this query be built, please?
I have tried the following but not working
select full_date,p_date,item,value
from dim_date
left outer join item_value on full_date=p_date;
Not sure whether max(p_date) over (partition by ...) will work.
Thank you
You can use a partitioned outer join and then aggregate:
WITH date_dim ( full_date ) AS (
SELECT DATE '2020-12-31' + LEVEL - 1 AS full_Date
FROM DUAL
CONNECT BY DATE '2020-12-31' + LEVEL - 1 <= DATE '2021-01-04'
)
SELECT item,
full_date,
MAX( value ) KEEP ( DENSE_RANK LAST ORDER BY p_date ) AS value
FROM date_dim d
LEFT OUTER JOIN item_value i
PARTITION BY ( i.item )
ON ( d.full_date >= i.p_date )
GROUP BY item, full_date
Which, for the sample data:
CREATE TABLE item_value ( P_Date, ITEM, Value ) AS
SELECT DATE '2020-12-20', 'AA1', 9 FROM DUAL UNION ALL
SELECT DATE '2021-01-01', 'AA1', 10 FROM DUAL UNION ALL
SELECT DATE '2021-01-01', 'AA2', 100 FROM DUAL;
Outputs:
ITEM | FULL_DATE | VALUE
:--- | :-------- | ----:
AA1 | 31-DEC-20 | 9
AA1 | 01-JAN-21 | 10
AA1 | 02-JAN-21 | 10
AA1 | 03-JAN-21 | 10
AA1 | 04-JAN-21 | 10
AA2 | 31-DEC-20 | null
AA2 | 01-JAN-21 | 100
AA2 | 02-JAN-21 | 100
AA2 | 03-JAN-21 | 100
AA2 | 04-JAN-21 | 100
Note: You do not need to store the date_dim dimension table; it can be generated on-the-fly and will reduce the need to perform (expensive) IO operations reading the table from the hard disk.
db<>fiddle here
You may simple add a validity interval for you ITEM table using the analtical function LEAD
select
P_DATE,
lead(P_DATE-1,1,(select max(full_date) from date_dim)) over (partition by ITEM order by P_DATE) P_DATE_TO,
ITEM, VALUE
from item_value
;
P_DATE P_DATE_TO ITE VALUE
------------------- ------------------- --- ----------
20.12.2020 00:00:00 31.12.2020 00:00:00 AA1 9
01.01.2021 00:00:00 04.01.2021 00:00:00 AA1 10
01.01.2021 00:00:00 04.01.2021 00:00:00 AA2 100
In some case this is enough for your use case as you can query the VALUE of a specific ITEM on a given date with
select VALUE from item_value_hist h where ITEM = 'AA2'
and <query_date> BETWEEN h.P_DATE and h.P_DATE_TO
Note, that the validity interval is inclusive, as we for P_DATE_TO subtract one day from the adjacent P_DATE. You should take some care is the DATEs have a time component.
If you want the ITEM per DAY overview you must first add the missing early history with the VALUE of NULL
select
(select min(full_date) from date_dim) P_DATE, min(P_DATE)-1 P_DATE_TO, ITEM, null VALUE
from item_value
group by ITEM
having min(P_DATE) > (select min(full_date) from date_dim)
P_DATE P_DATE_TO ITE VALUE
------------------- ------------------- --- -----
31.12.2020 00:00:00 31.12.2020 00:00:00 AA2
Than simple outer join to your dimension table matching all day from your validity interval
with item as (
select
P_DATE,
lead(P_DATE-1,1,(select max(full_date) from date_dim)) over (partition by ITEM order by P_DATE) P_DATE_TO,
ITEM, VALUE
from item_value
union all
select
/* add the missing early history without a VALUE */
(select min(full_date) from date_dim) P_DATE, min(P_DATE)-1 P_DATE_TO, ITEM, null VALUE
from item_value
group by ITEM
having min(P_DATE) > (select min(full_date) from date_dim)
)
select dt.full_date, item.ITEM, item.VALUE from item
join date_dim dt
on dt.full_date between item.P_DATE and item.P_DATE_TO
order by item.ITEM, dt.full_date
FULL_DATE ITE VALUE
------------------- --- ----------
31.12.2020 00:00:00 AA1 9
01.01.2021 00:00:00 AA1 10
02.01.2021 00:00:00 AA1 10
03.01.2021 00:00:00 AA1 10
04.01.2021 00:00:00 AA1 10
31.12.2020 00:00:00 AA2
01.01.2021 00:00:00 AA2 100
02.01.2021 00:00:00 AA2 100
03.01.2021 00:00:00 AA2 100
04.01.2021 00:00:00 AA2 100
Two steps:
Cross join dates and items. If you don't have an item table (which you should), join distinct items from your item_value table.
Get the value in the FROM clause with OUTER APPLY or in the SELECT clause with a subquery using FETCH FIRST ROW ONLY.
The query:
select
d.full_date,
i.item,
(
select iv.value
from Item_value iv
where iv.item = i.item
and iv.p_date <= d.full_date
order by iv.p_date desc
fetch first row only
) as value
from dim_date d
cross join (select distinct item from item_value) i
order by d.full_date, i.item;
You can generate the full list of dates and items using cross join followed by a left join to bring in the existing values. Then you can use last_value() or lag() to fill in the values:
select d.p_date, i.item,
coalesce(v.value,
lag(v.value ignore nulls) over (partition by i.item order by d.p_date)
) as value
from date_dim d cross join
(select distinct iv.item from item_value iv) i left join
item_value iv
on iv.p_date = d.p_date and iv.item = i.item;
You can also do this using a join by adding an "end" date to the values table:
select d.p_date, i.item,
coalesce(v.value,
lag(v.value ignore nulls) over (partition by i.item order by d.p_date)
) as value
from date_dim d cross join
(select distinct iv.item from item_value iv) i left join
(select iv.*,
lead(p_date) over (partition by item order by p_date) as next_p_date
from item_value iv
) iv
on i.item = iv.item and
d.p_date >= iv.p_date and
(iv.next_p_date is null or d.p_date < iv.next_p_date);

Flag the date when they return

Story:
For each id , they have a join date to a subscription and when they get rebilled monthly, they have a returning date. The first part of the exercise was to flag consecutive months of returned dates from the join date. Here's an example:
+----+------------+----------------+------+
| id | join_date | returning_date | flag |
+----+------------+----------------+------+
| 1 | 2018-12-01 | 2019-01-01 | 1 |
| 1 | 2018-12-01 | 2019-02-01 | 1 |
| 1 | 2018-12-01 | 2019-03-01 | 1 |
+----+------------+----------------+------+
Objective:
What I would like to add is to flag those who return from a canceled subscription. That flag can be in another column. For example the following results shows that on May 1st 2019 , he returned. This date needs to be flagged:
+----+------------+----------------+------+
| id | join_date | returning_date | flag |
+----+------------+----------------+------+
| 1 | 2018-12-01 | 2019-01-01 | 1 |
| 1 | 2018-12-01 | 2019-02-01 | 1 |
| 1 | 2018-12-01 | 2019-03-01 | 1 |
| 1 | 2018-12-01 | 2019-05-01 | 0 |
| 1 | 2018-12-01 | 2019-06-01 | 0 |
+----+------------+----------------+------+
Fiddle Data:
DROP TABLE IF EXISTS #T1
create table #t1 (id int,join_date date, returning_date date)
insert into #t1 values
(1,'2018-12-01', '2019-01-01'),
(1,'2018-12-01', '2019-02-01'),
(1,'2018-12-01', '2019-03-01'),
(1,'2018-12-01', '2019-05-01'),
(1,'2018-12-01', '2019-06-01'),
(2,'2018-12-01', '2019-02-01'),
(2,'2018-12-01', '2019-03-01'),
(2,'2018-12-01', '2019-05-01'),
(2,'2018-12-01', '2019-06-01'),
(3,'2019-05-01', '2019-06-01'),
(3,'2019-05-01', '2019-08-01'),
(3,'2019-05-01', '2019-10-01')
Current query with flag for consecutive months:
select *
,CASE WHEN DATEDIFF(MONTH,join_date,returning_date) = ROW_NUMBER() OVER (PARTITION BY id ORDER BY returning_date ASC) THEN 1 ELSE 0 END AS flag
from #t1
ORDER BY ID,returning_date
You seem to be asking if there are any gaps since an id first returned (with a given join_date).
If so, that is simply counting. How many months since the first return_date? How many rows? Compare these to see if there are gaps:
select t1.*,
(case when datediff(month, min(returning_date) over (partition by id, join_date order by returning_date), returning_date) <>
row_number() over (partition by id, join_date order by returning_date) - 1
then 0 else 1
end) as flag
from t1;
Here is a db<>fiddle.
since you didn't specify which recurrence of returning as the target to flag, my query flags any non-consecutive date as a return date cause a subscriber could leave and return many times after their join date (the subscriber with [id] 3 technically returned in August and then again in October so that's returning twice but October is marked as LAST instead based on the data set). i also made it easier to read by adding in start date and end date based on the data set in your fiddle.
you can use this query as a temp table, cte, basis, or whatever to continue to query against if you need to manipulate the data further.
select a.*
,case
when a.returning_date = (select min(c.returning_date) from subscription c where c.id = a.id and c.join_date = a.join_date) then 'START'
when a.returning_date = (select max(c.returning_date) from subscription c where c.id = a.id and c.join_date = a.join_date) then 'END'
when b.id is null then 'RETURN'
else 'CONSECUTIVE'
end as SubStatus
from subscription a
left join subscription b on a.id = b.id and a.join_date = b.join_date and DATEADD(month,-1,a.returning_date) = b.returning_date
here is the result set from my query:
id join_date returning_date SubStatus
----------- ---------- -------------- -----------
1 2018-12-01 2019-01-01 START
1 2018-12-01 2019-02-01 CONSECUTIVE
1 2018-12-01 2019-03-01 CONSECUTIVE
1 2018-12-01 2019-05-01 RETURN
1 2018-12-01 2019-06-01 END
2 2018-12-01 2019-02-01 START
2 2018-12-01 2019-03-01 CONSECUTIVE
2 2018-12-01 2019-05-01 RETURN
2 2018-12-01 2019-06-01 END
3 2019-05-01 2019-06-01 START
3 2019-05-01 2019-08-01 RETURN
3 2019-05-01 2019-10-01 END
flag consecutive months
and
renders all future payments
are not phrases that are going to lead to a pretty query. Which is why you had to resort to a while loop. Nevertheless, what you seek is possible, and with work may prove more performant than your while loop for large data. I present my sample code below using cte's, but you may want to use temp tables ore update an originally null 'flag' column on the base table.
In flagNonConsecutive, a flag is applied for any date that is not consecutive with the previous date (as identified using the lag window function) or by the join_date.
This meets the first requirement. Then in minNonConsecutives, you identify the earliest of those flags for each id.
In the main query, any dates after the minimum get the 0 treatment:
with
flagNonConsecutive as (
select *,
nonConsecutive =
case
when datediff(month, join_date, returning_date) = 1 then 1
when datediff(
month,
lag(returning_date) over(
partition by id
order by returning_date
),
returning_date
) = 1 then 1
else 0
end
from #t1
),
minNonConsecutives as (
select id,
minNonConsec = min(returning_date)
from flagNonConsecutive
where nonConsecutive = 0
group by id
)
select fnc.id,
fnc.join_date,
fnc.returning_date,
flag = iif(fnc.returning_date >= mnc.minNonConsec, 0, 1)
from flagNonConsecutive fnc
left join minNonConsecutives mnc on fnc.id = mnc.id;

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

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 |
+------------+---------------+-------+

Get a list of dates between few dates

There are some quite similar questions, but not the same.
I have to solve the next problem:
From table with such structure
| DATE_FROM | DATE_TO |
|------------|------------|
| 2010-05-17 | 2010-05-19 |
| 2017-01-02 | 2017-01-04 |
| 2017-05-01 | NULL |
| 2017-06-12 | NULL |
I need to get a list like the one below
| DATE_LIST |
|------------|
| 2010-05-17 |
| 2010-05-18 |
| 2010-05-19 |
| 2017-01-02 |
| 2010-01-03 |
| 2010-01-04 |
| 2017-05-01 |
| 2017-06-12 |
How can I get it with SQL? SQL Server 2016.
Another option is with a CROSS APPLY and an ad-hoc tally table
Select Date_List=B.D
from YourTable A
Cross Apply (
Select Top (DateDiff(DAY,[DATE_FROM],IsNull([DATE_TO],[DATE_FROM]))+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),[DATE_FROM])
From master..spt_values n1,master..spt_values n2
) B
Returns
Date_List
2010-05-17
2010-05-18
2010-05-19
2017-01-02
2017-01-03
2017-01-04
2017-05-01
2017-06-12
One method uses a recursive CTE:
with cte as (
select date_from as date_list, date_to
from t
union all
select dateadd(day, 1, date_from), date_to
from cte
where date_from < date_to
)
select date_list
from cte;
By default, the recursive CTE is limited to a recursive depth of 100 (and then it returns an error). That works for spans of up to 100 days. You can remove the limit with OPTION (MAXRECURSION 0).
Although you could create the date range on the fly in your query, consider creating a permanent calendar table. This will provide better performance and can be extended with other attributes like day of week, fiscal quarter, etc. You can find many examples of loading such a table with an internet search.
Below is an example with 40 years of dates.
--example calendar table load script
CREATE TABLE dbo.Calendar(
CalendarDate date NOT NULL
CONSTRAINT PK_Calendar PRIMARY KEY
);
WITH
t4 AS (SELECT n FROM (VALUES(0),(0),(0),(0)) t(n))
,t256 AS (SELECT 0 AS n FROM t4 AS a CROSS JOIN t4 AS b CROSS JOIN t4 AS c CROSS JOIN t4 AS d)
,t64k AS (SELECT ROW_NUMBER() OVER (ORDER BY (a.n)) AS num FROM t256 AS a CROSS JOIN t256 AS b)
INSERT INTO dbo.Calendar WITH(TABLOCKX)
SELECT DATEADD(day, num, '20000101')
FROM t64k
WHERE DATEADD(day, num, '20000101') < '20400101'
GO
DECLARE #example TABLE(
DATE_FROM date NOT NULL
,DATE_TO date NULL
);
GO
--example query
INSERT INTO #example VALUES
('2010-05-17', '2010-05-19')
, ('2017-01-02', '2017-01-04')
, ('2017-05-01', NULL)
, ('2017-06-12', NULL)
SELECT
c.CalendarDate
FROM #example AS e
JOIN dbo.Calendar AS c ON
c.CalendarDate BETWEEN e.DATE_FROM AND COALESCE(e.DATE_TO, e.DATE_FROM);