Select Multiple Rows from Timespan - sql

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

Related

Create all months list from a date column in ORACLE SQL

CREATE TABLE dates(
alldates date);
INSERT INTO dates (alldates) VALUES ('1-May-2017');
INSERT INTO dates (alldates) VALUES ('1-Mar-2018');
I want to generate all months beginning between these two dates. I am very new to Oracle SQL. My solution is below, but it is not working properly.
WITH t1(test) AS (
SELECT MIN(alldates) as test
FROM dates
UNION ALL
SELECT ADD_MONTHS(test,1) as test
FROM t1
WHERE t1.test<= (SELECT MAX(alldates) FROM date)
)
SELECT * FROM t1
The result I want should look like
Test
2017-02-01
2017-03-01
...
2017-12-01
2018-01-01
2018-02-01
2018-03-01
You made a typo and wrote date instead of dates but you also need to make a second change and use ADD_MONTHS in the recursive query's WHERE clause or you will generate one too many rows.
WITH t1(test) AS (
SELECT MIN(alldates)
FROM dates
UNION ALL
SELECT ADD_MONTHS(test,1)
FROM t1
WHERE ADD_MONTHS(test,1) <= (SELECT MAX(alldates) FROM dates)
)
SELECT * FROM t1
Which outputs:
| TEST |
| :-------- |
| 01-MAY-17 |
| 01-JUN-17 |
| 01-JUL-17 |
| 01-AUG-17 |
| 01-SEP-17 |
| 01-OCT-17 |
| 01-NOV-17 |
| 01-DEC-17 |
| 01-JAN-18 |
| 01-FEB-18 |
| 01-MAR-18 |
However, a more efficient query would be to get the minimum and maximum values in the same query and then iterate using these pre-found bounds:
WITH t1(min_date, max_date) AS (
SELECT MIN(alldates),
MAX(alldates)
FROM dates
UNION ALL
SELECT ADD_MONTHS(min_date,1),
max_date
FROM t1
WHERE ADD_MONTHS(min_date,1) <= max_date
)
SELECT min_date AS month
FROM t1
db<>fiddle here
Update
Oracle 11gR2 has bugs handling recursive date queries; this is fixed in later Oracle versions but if you want to use SQL Fiddle and Oracle 11gR2 then you need to iterate over a numeric value and not a date. Something like this:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE dates(
alldates date);
INSERT INTO dates (alldates) VALUES ('1-May-2017');
INSERT INTO dates (alldates) VALUES ('1-Mar-2018');
Query 1:
WITH t1(min_date, month, total_months) AS (
SELECT MIN(alldates),
0,
MONTHS_BETWEEN(MAX(alldates),MIN(alldates))
FROM dates
UNION ALL
SELECT min_date,
month+1,
total_months
FROM t1
WHERE month+1<=total_months
)
SELECT ADD_MONTHS(min_date,month) AS month
FROM t1
Results:
| MONTH |
|----------------------|
| 2017-05-01T00:00:00Z |
| 2017-06-01T00:00:00Z |
| 2017-07-01T00:00:00Z |
| 2017-08-01T00:00:00Z |
| 2017-09-01T00:00:00Z |
| 2017-10-01T00:00:00Z |
| 2017-11-01T00:00:00Z |
| 2017-12-01T00:00:00Z |
| 2018-01-01T00:00:00Z |
| 2018-02-01T00:00:00Z |
| 2018-03-01T00:00:00Z |
You seem to want a recursive CTE. That syntax would be:
WITH CTE(min_date, max_date) as (
SELECT MIN(alldates) as min_date, MAX(alldates) as max_date
FROM dates
UNION ALL
SELECT add_months(min_date, 1), max_date
FROM CTE
WHERE min_date < max_date
)
SELECT min_date
FROM CTE;
Here is a db<>fiddle.
You just made a typo: date instead of dates:
WITH t1(test) AS (
SELECT MIN(alldates) as test
FROM dates
UNION ALL
SELECT ADD_MONTHS(test,1) as test
FROM t1
WHERE t1.test<= (SELECT MAX(alldates) FROM dateS) -- fixed here
)
SELECT * FROM t1

Number of Shifts betweens two times

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

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.

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);

SQL Server - Insert lines with null values when month doesn't exist

I have a table like this one:
Yr | Mnth | W_ID | X_ID | Y_ID | Z_ID | Purchases | Sales | Returns |
2015 | 10 | 1 | 5210 | 1402 | 2 | 1000.00 | etc | etc |
2015 | 12 | 1 | 5210 | 1402 | 2 | 12000.00 | etc | etc |
2016 | 1 | 1 | 5210 | 1402 | 2 | 1000.00 | etc | etc |
2016 | 3 | 1 | 5210 | 1402 | 2 | etc | etc | etc |
2014 | 3 | 9 | 880 | 2 | 7 | etc | etc | etc |
2014 | 12 | 9 | 880 | 2 | 7 | etc | etc | etc |
2015 | 5 | 9 | 880 | 2 | 7 | etc | etc | etc |
2015 | 7 | 9 | 880 | 2 | 7 | etc | etc | etc |
For each combination of (W, X, Y, Z) I would like to insert the months that don't appear in the table and are between the first and last month.
In this example, for combination (W=1, X=5210, Y=1402, Z=2), I would like to have additional rows for 2015/11 and 2016/02, where Purchases, Sales and Returns are NULL. For combination (W=9, X=880, Y=2, Z=7) I would like to have additional rows for months between 2014/4 and 2014/11, 2015/01 and 2015/04, 2016/06.
I hope I have explained myself correctly.
Thank you in advance for any help you can provide.
The process is rather cumbersome in this case, but quite possible. One method uses a recursive CTE. Another uses a numbers table. I'm going to use the latter.
The idea is:
Find the minimum and maximum values for the year/month combination for each set of ids. For this, the values will be turned into months since time 0 using the formula year*12 + month.
Generate a bunch of numbers.
Generate all rows between the two values for each combination of ids.
For each generated row, use arithmetic to re-extract the year and month.
Use left join to bring in the original data.
The query looks like:
with n as (
select row_number() over (order by (select null)) - 1 as n -- start at 0
from master.spt_values
),
minmax as (
select w_id, x_id, y_id, z_id, min(yr*12 + mnth) as minyyyymm,
max(yr*12 + mnth) as maxyyyymm
from t
group by w_id, x_id, y_id, z_id
),
wxyz as (
select minmax.*, minmax.minyyyymm + n.n,
(minmax.minyyyymm + n.n) / 12 as yyyy,
((minmax.minyyyymm + n.n) % 12) + 1 as mm
from minmax join
n
on minmax.minyyyymm + n.n <= minmax.maxyyyymm
)
select wxyz.yyyy, wxyz.mm, wxyz.w_id, wxyz.x_id, wxyz.y_id, wxyz.z_id,
<columns from t here>
from wxyz left join
t
on wxyz.w_id = t.w_id and wxyz.x_id = t.x_id and wxyz.y_id = t.y_id and
wxyz.z_id = t.z_id and wxyz.yyyy = t.yr and wxyz.mm = t.mnth;
Thank you for your help.
Your solution works, but I noticed it is not very good in terms of performance, but meanwhile I have managed to get a solution for my problem.
DECLARE #start_date DATE, #end_date DATE;
SET #start_date = (SELECT MIN(EOMONTH(DATEFROMPARTS(Yr , Mnth, 1))) FROM Table_Input);
SET #end_date = (SELECT MAX(EOMONTH(DATEFROMPARTS(Yr , Mnth, 1))) FROM Table_Input);
DECLARE #tdates TABLE (Period DATE, Yr INT, Mnth INT);
WHILE #start_date <= #end_date
BEGIN
INSERT INTO #tdates(PEriod, Yr, Mnth) VALUES(#start_date, YEAR(#start_date), MONTH(#start_date));
SET #start_date = EOMONTH(DATEADD(mm,1,DATEFROMPARTS(YEAR(#start_date), MONTH(#start_date), 1)));
END
DECLARE #pks TABLE (W_ID NVARCHAR(50), X_ID NVARCHAR(50)
, Y_ID NVARCHAR(50), Z_ID NVARCHAR(50)
, PerMin DATE, PerMax DATE);
INSERT INTO #pks (W_ID, X_ID, Y_ID, Z_ID, PerMin, PerMax)
SELECT W_ID, X_ID, Y_ID, Z_ID
, MIN(EOMONTH(DATEFROMPARTS(Ano, Mes, 1))) AS PerMin
, MAX(EOMONTH(DATEFROMPARTS(Ano, Mes, 1))) AS PerMax
FROM Table1
GROUP BY W_ID, X_ID, Y_ID, Z_ID;
INSERT INTO Table_Output(W_ID, X_ID, Y_ID, Z_ID
, ComprasLiquidas, RTV, DevManuais, ComprasBrutas, Vendas, Stock, ReceitasComerciais)
SELECT TP.DB, TP.Ano, TP.Mes, TP.Supplier_Code, TP.Depart_Code, TP.BizUnit_Code
, TA.ComprasLiquidas, TA.RTV, TA.DevManuais, TA.ComprasBrutas, TA.Vendas, TA.Stock, TA.ReceitasComerciais
FROM
(
SELECT W_ID, X_ID, Y_ID, Z_ID
FROM #tdatas CROSS JOIN #pks
WHERE Period BETWEEN PerMin And PerMax
) AS TP
LEFT JOIN Table_Input AS TA
ON TP.W_ID = TA.W_ID AND TP.X_ID = TA.X_ID AND TP.Y_ID = TA.Y_ID
AND TP.Z_ID = TA.Z_ID
AND TP.Yr = TA.Yr
AND TP.Mnth = TA.Mnth
ORDER BY TP.W_ID, TP.X_ID, TP.Y_ID, TP.Z_ID, TP.Yr, TP.Mnth;
I do the following:
Get the Min and Max date of the entire table - #start_date and #end_date variables;
Create an auxiliary table with all dates between Min and Max - #tdates table;
Get all the combinations of (W_ID, X_ID, Y_ID, Z_ID) along with the min and max dates of that combination - #pks table;
Create the cartesian product between #tdates and #pks, and in the WHERE clause I filter the results between the Min and Max of the combination;
Compute a LEFT JOIN of the cartesian product table with the input data table.