Pivoting unique users for each month, each year - sql

I'm learning about PIVOT function and I want to try it in my DB, in the table DDOT I have events (rows) made by users during X month Y year in the YYYYMM format.
id_ev iddate id_user ...
------------------------
1 201901 321
2 201902 654
3 201903 987
4 201901 321
5 201903 987
I'm basing my query on the MS Documentation and I'm not getting errors but I'm not able to fill it with the SUM of those unique events (users). In simple words I want to know how many users (unique) checked up each month (x axis) in the year (y axis). However, I'm getting NULL as result
YYYY jan feb mar
----------------------------
2019 NULL NULL NULL
I'm expecting a full table with what I mentionted before.
YYYY jan feb mar
----------------------------
2019 2 1 1
In the code I've tried with different aggregate functions but this block is the closest to a result from SQL.
CREATE TABLE ddot
(
id_ev int NOT NULL ,
iddate int NOT NULL ,
id_user int NOT NULL
);
INSERT INTO DDOT
(
[id_ev], [iddate], [id_user]
)
VALUES
(
1, 201901, 321
),
(
2, 201902, 654
),
(
3, 201903, 987
),
(
4, 201901, 321
),
(
5, 201903, 987
)
GO
SELECT *
FROM (
SELECT COUNT(DISTINCT id_user) [TOT],
DATENAME(YEAR, CAST(iddate+'01' AS DATETIME)) [YYYY], --concat iddate 01 to get full date
DATENAME(MONTH, CAST(iddate+'01' AS DATETIME)) [MMM]
FROM DDOT
GROUP BY DATENAME(YEAR, CAST(iddate+'01' AS DATETIME)),
DATENAME(MONTH, CAST(iddate+'01' AS DATETIME))
) AS DOT_COUNT
PIVOT(
SUM([TOT])
FOR MMM IN (jan, feb, mar)
) AS PVT

Ideally you should be using an actual date in the iddate column, and not a string (number?). We can workaround this using the string functions:
SELECT
CONVERT(varchar(4), LEFT(iddate, 4)) AS YYYY,
COUNT(CASE WHEN CONVERT(varchar(2), RIGHT(iddate, 2)) = '01' THEN 1 END) AS jan,
COUNT(CASE WHEN CONVERT(varchar(2), RIGHT(iddate, 2)) = '02' THEN 1 END) AS feb,
COUNT(CASE WHEN CONVERT(varchar(2), RIGHT(iddate, 2)) = '03' THEN 1 END) AS mar,
...
FROM DDOT
GROUP BY
CONVERT(varchar(4), LEFT(iddate, 4));
Note that if the iddate column already be text, then we can remove all the ugly calls to CONVERT above:
SELECT
LEFT(iddate, 4) AS YYYY,
COUNT(CASE WHEN RIGHT(iddate, 2) = '01' THEN 1 END) AS jan,
COUNT(CASE WHEN RIGHT(iddate, 2) = '02' THEN 1 END) AS feb,
COUNT(CASE WHEN RIGHT(iddate, 2) = '03' THEN 1 END) AS mar,
...
FROM DDOT
GROUP BY
LEFT(iddate, 4);

Related

SQL Sum over several columns of an entry with ad hoc created columns

I have the following table, like:
woker
date
amount
jeff
04-04-2022
4.00
jeff
04-05-2022
2.00
jeff
04-08-2022
3.50
dave
04-04-2022
1.00
dave
04-07-2022
6.50
It contains the date and the amount of hours worked by a worker.
Now I want to create a table like the following with a select to show hours per weekday. The column "count" should represent the amount of days where the worker has hours. The column "sum" should sum the hours for this week.
So the final result should be like this:
worker
mon
tue
wed
thu
fri
sat
sun
count
sum
jeff
4.00
2.00
null
null
3.50
null
null
3
9.50
dave
1.00
null
null
6.50
null
null
null
2
7.50
My statement so far is:
SELECT worker,
CASE WHEN DATEPART(weekday,date) = 1 THEN amount END AS mon,
CASE WHEN DATEPART(weekday,date) = 2 THEN amount END AS tue,
CASE WHEN DATEPART(weekday,date) = 3 THEN amount END AS wed,
CASE WHEN DATEPART(weekday,date) = 4 THEN amount END AS thu,
CASE WHEN DATEPART(weekday,date) = 5 THEN amount END AS fri,
CASE WHEN DATEPART(weekday,date) = 6 THEN amount END AS sat,
CASE WHEN DATEPART(weekday,date) = 7 THEN amount END AS sun
FROM table
So now I need help to get the last two columns. Can anybody explain, how to sum up / count values over multiple columns withi one entry?
Thank you.
We use the function SUM so as to be able to agregate the different lines. We add the week number in the SELECT and the GROUP BY so as to separate the weeks and know which week of the year we are looking at. We could also add the year if the same table will be used for long enough.
SET DATEFIRST 1;
SELECT
DATEPART(week, date) AS "week",
worker,
CASE WHEN DATEPART(weekday,date) = 1 THEN amount END ) AS mon,
SUM( CASE WHEN DATEPART(weekday,date) = 2 THEN amount END ) AS tue,
SUM( CASE WHEN DATEPART(weekday,date) = 3 THEN amount END ) AS wed,
SUM( CASE WHEN DATEPART(weekday,date) = 4 THEN amount END ) AS thu,
SUM( CASE WHEN DATEPART(weekday,date) = 5 THEN amount END ) AS fri,
SUM( CASE WHEN DATEPART(weekday,date) = 6 THEN amount END ) AS sat,
SUM( CASE WHEN DATEPART(weekday,date) = 7 THEN amount END ) AS sun,
COUNT(DISTINCT DATEPART(weekday,date) ) AS "count",
amount AS "sum"
FROM table
GROUP BY
DATEPART(week, date),
worker
ORDER BY
DATEPART(week, date),
worker;
Conditional aggregation is an option (don't forget to set the first day of the week using SET DATEFIRST):
SET DATEFIRST 1
SELECT
worker,
SUM(CASE WHEN DATEPART(weekday, date) = 1 THEN amount END) AS mon,
SUM(CASE WHEN DATEPART(weekday, date) = 2 THEN amount END) AS tue,
SUM(CASE WHEN DATEPART(weekday, date) = 3 THEN amount END) AS wed,
SUM(CASE WHEN DATEPART(weekday, date) = 4 THEN amount END) AS thu,
SUM(CASE WHEN DATEPART(weekday, date) = 5 THEN amount END) AS fri,
SUM(CASE WHEN DATEPART(weekday, date) = 6 THEN amount END) AS sat,
SUM(CASE WHEN DATEPART(weekday, date) = 7 THEN amount END) AS sun,
COUNT(DISTINCT DATEPART(weekday, date)) AS [count],
SUM(amount) AS [sum]
FROM (VALUES
('jeff', CONVERT(date, '20220404'), 4.00),
('jeff', CONVERT(date, '20220405'), 2.00),
('jeff', CONVERT(date, '20220408'), 3.50),
('dave', CONVERT(date, '20220404'), 1.00),
('dave', CONVERT(date, '20220407'), 6.50)
) t (worker, date, amount)
GROUP BY worker
ORDER BY worker
Result:
worker
mon
tue
wed
thu
fri
sat
sun
count
sum
dave
1.00
6.50
2
7.50
jeff
4.00
2.00
3.50
3
9.50
If you want to summarize the input data based on time interval, you need a different statement:
SET DATEFIRST 1
SELECT
worker,
--DATEPART(week, date) AS week,
DATEPART(month, date) AS month,
SUM(CASE WHEN DATEPART(weekday, date) = 1 THEN amount END) AS mon,
SUM(CASE WHEN DATEPART(weekday, date) = 2 THEN amount END) AS tue,
SUM(CASE WHEN DATEPART(weekday, date) = 3 THEN amount END) AS wed,
SUM(CASE WHEN DATEPART(weekday, date) = 4 THEN amount END) AS thu,
SUM(CASE WHEN DATEPART(weekday, date) = 5 THEN amount END) AS fri,
SUM(CASE WHEN DATEPART(weekday, date) = 6 THEN amount END) AS sat,
SUM(CASE WHEN DATEPART(weekday, date) = 7 THEN amount END) AS sun,
COUNT(DISTINCT DATEPART(weekday, date)) AS [count],
SUM(amount) AS [sum]
FROM (VALUES
('jeff', CONVERT(date, '20220404'), 4.00),
('jeff', CONVERT(date, '20220405'), 2.00),
('jeff', CONVERT(date, '20220408'), 3.50),
('dave', CONVERT(date, '20220404'), 1.00),
('dave', CONVERT(date, '20220407'), 6.50)
) t (worker, date, amount)
--GROUP BY worker, DATEPART(week, date)
--ORDER BY worker, DATEPART(week, date)
GROUP BY worker, DATEPART(month, date)
ORDER BY worker, DATEPART(month, date)
Instead om doing alot aggregates, cases and dateparts I would do a pivot table out of it like below. (Note that my days-weekdays ref may not be the same as yours)
Test data:
declare #tbl table (worker varchar(20), [date] date, amount dec(5,2))
INSERT INTO #tbl
SELECT 'jeff', '04-04-2022', 4.00
UNION SELECT 'jeff', '05-04-2022', 2.00
UNION SELECT 'jeff', '08-04-2022', 3.50
UNION SELECT 'dave', '04-04-2022', 1.00
UNION SELECT 'dave', '07-04-2022', 6.50
The actual code:
SELECT
worker
, [1] as sun
, [2] as mon
, [3] as tue
, [4] as wed
, [5] as thu
, [6] as fri
, [7] as sat
, [count]
, [sum]
FROM (select
worker
, amount
, datepart(weekday, t.[date]) dp
, COUNT(amount) OVER( partition by worker) as [count]
, SUM(amount) OVER( partition by worker) as [sum]
from #tbl t
) p
PIVOT(
SUM(amount)
FOR dp in ([1], [2], [3], [4], [5], [6], [7])
) piv

Teradata - Split date range into month columns with day count

I need to split different date ranges over a quarter period into month columns with only the days actually used in that month. Each record (range) would be different.
Example:
Table
Record_ID Start_Date End_Date
1 10/27 11/30
2 11/30 12/14
3 12/14 12/31
Range 1 = 10/5 to 12/14
Range 2 = 11/20 to 12/31
Range 3 = 10/28 to 12/2
Output:
Range 1
Oct Nov Dec
27 30 14
Similar to #ULick's answer using sys_calendar.calendar, but a little more succinct:
CREATE VOLATILE MULTISET TABLE datetest (record_id int, start_date date, end_date date) ON COMMIT PRESERVE ROWS;
INSERT INTO datetest VALUES (1, '2017-10-05', '2017-12-14');
INSERT INTO datetest VALUES (2, '2017-11-20','2017-12-31');
SELECT record_id,
SUM(CASE WHEN month_of_year = 10 THEN 1 ELSE 0 END) as October,
SUM(CASE WHEN month_of_year = 11 THEN 1 ELSE 0 END) as November,
SUM(CASE WHEN month_of_year = 12 THEN 1 ELSE 0 END) as December
FROM datetest
INNER JOIN sys_calendar.calendar cal
ON cal.calendar_date BETWEEN start_date and end_date
GROUP BY record_id;
DROP TABLE datetest;
Because Quarter was mentioned in the question (I'm not sure how it relates here) there is also quarter_of_year and month_of_quarter available in the sys_calendar to slice and dice this even further.
Also, if you are on 16.00+ There is PIVOT functionality which may help get rid of the CASE statements here.
First join with the calendar to get all the dates within the range and get the number of days per each month (incl. full month, not mentioned in Start_Date and End_Date).
Then sum up each month in a column per Range.
create table SplitDateRange ( Range bigint, Start_Date date, End_Date date );
insert into SplitDateRange values ( 1, '2018-10-05', '2018-12-14' );
insert into SplitDateRange values ( 2, '2018-11-20', '2018-12-31' );
insert into SplitDateRange values ( 3, '2018-10-28', '2018-12-02' );
select
Range
, sum(case when mon = 10 then days else 0 end) as "Oct"
, sum(case when mon = 11 then days else 0 end) as "Nov"
, sum(case when mon = 12 then days else 0 end) as "Dec"
from (
select
Range
, extract(MONTH from C.calendar_date) as mon
, max(C.calendar_date) - min(calendar_date) +1 as days
from Sys_Calendar.CALENDAR as C
inner join SplitDateRange as DR
on C.calendar_date between DR.Start_Date and DR.End_Date
group by 1,2
) A
group by Range
order by Range
;
Different approach, avoids the cross join to the calendar by applying Teradata Expand On feature for creating time series. More text, but should be more efficient for larger tables/ranges:
SELECT record_id,
Sum(CASE WHEN mth = 10 THEN days_in_month ELSE 0 END) AS October,
Sum(CASE WHEN mth = 11 THEN days_in_month ELSE 0 END) AS November,
Sum(CASE WHEN mth = 12 THEN days_in_month ELSE 0 END) AS December
FROM
( -- this Derived Table simply avoids repeating then EXTRACT/INTERVAL calculations (can't be done directly in the nested Select)
SELECT record_id,
Extract(MONTH From Begin(expanded_pd)) AS mth,
Cast((INTERVAL( base_pd P_INTERSECT expanded_pd) DAY) AS INT) AS days_in_month
FROM
(
SELECT record_id,
PERIOD(start_date, end_date+1) AS base_pd,
expanded_pd
FROM datetest
-- creates one row per month
EXPAND ON base_pd AS expanded_pd BY ANCHOR PERIOD Month_Begin
) AS dt
) AS dt
GROUP BY 1

How to sum total by a field name by month for the previous six month

I have a table with following fields
Status, branch, reason code description, year, month, day, count
I am trying to get a report like this for each branch:
Reason code desc.,July,August,sept.,October.,nov.,Dec.,Total,% of Total
Reason 1. 4. 6. 2. 5. 0. 2. 19 79.1
Reason 2. 1 0. 2. 1. 1. 0 5. 20.9
--------------------------
5. 6. 4. 6. 1. 2. 24 100.0
Assuming that your database is Oracle the query can be:
select RCMonth.*, RCTotal.Total, round(RCTotal.Total*100/Total.Total, 2) "Total%"
from
(
select reasoncode , to_char(createdon,'Mon') mon
from rc_report
where createdon > sysdate - 180
union all
select 'Sum' , to_char(createdon,'Mon') mon
from rc_report
where createdon > sysdate - 180
) pivot
( count(*)
for mon in ('Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan')
) RCMonth
join ( select count(*) Total,
reasoncode
from rc_report
where createdon > sysdate - 180
group by reasoncode
union all
select count(*) Total,
'Sum'
from rc_report
where createdon > sysdate - 180
) RCTotal
on RCTotal.ReasonCode = RCMonth.ReasonCode
cross join (
select count(*) Total
from rc_report
where createdon > sysdate - 180 ) Total
order by RCMonth.ReasonCode
Sample result is:
Row# REASONCODE 'Aug' 'Sep' 'Oct' 'Nov' 'Dec' 'Jan' TOTAL Total%
1 Reason1 2 2 0 3 2 0 9 81.82
2 Reason2 0 1 0 1 0 0 2 18.18
3 Sum 2 3 0 4 2 0 11 100
Table definition is :
create table rc_report
(reasoncode varchar2(20),
createdon date)
And also version for SQL Server in SQL Fiddle
It's only a little bit different.
select RCMonth.*, RCTotal.Total, round( cast ( RCTotal.Total as decimal) *100 /Total.Total,2) "Total%"
from
(
select reasoncode, [Aug], [Sep], [Oct], [Nov], [Dec], [Jan] from (
select reasoncode , left( datename(Month,createdon),3) mon
from rc_report
where createdon >= DATEADD(MONTH, -6, GETDATE())
union all
select 'Sum' , left( datename(Month, createdon),3) mon
from rc_report
where createdon >= DATEADD(MONTH, -6, GETDATE())
) Source
pivot
( count(Source.mon)
for mon in ([Aug], [Sep], [Oct], [Nov], [Dec], [Jan])
) as PivotTable
) RCMonth
join ( select count(*) Total,
reasoncode
from rc_report
where createdon >= DATEADD(MONTH, -6, GETDATE())
group by reasoncode
union all
select count(*) Total,
'Sum'
from rc_report
where createdon >= DATEADD(MONTH, -6, GETDATE())
) RCTotal
on RCTotal.ReasonCode = RCMonth.ReasonCode
cross join (
select count(*) Total
from rc_report
where createdon >= DATEADD(MONTH, -6, GETDATE()) ) Total
order by RCMonth.ReasonCode

SQL - Convert columns to rows (use a pivot or other means)?

I am struggling with reformatting an answer set I have from a query. This is data from a monitoring tool that I am calculating a monthly average along with a linear regression projection. This is all performed in other functions and working fine. These functions all feed the below two tables with the final data that I use. The only thing I am currently grappling with is re-formatting the output to show more columns than rows.
To start, let me give you the sample data that I'm working with Using SQL Server 2008R2
create table finalactual(Target varchar(100), MonthNum int, [Actual] real)
create table finalprojection(Target varchar(100), MonthNum int, [monthname] varchar(20), [Forecast] real)
Insert into finalprojection values ('C:\', 2, 'February', 65609.29)
Insert into finalprojection values ('C:\', 3, 'March', 65850.27)
Insert into finalprojection values ('C:\', 4, 'April', 66091.26)
Insert into finalprojection values ('C:\', 5, 'May', 66332.25)
Insert into finalprojection values ('C:\', 6, 'June', 66573.23)
Insert into finalprojection values ('C:\', 7, 'July', 66814.22)
Insert into finalprojection values ('C:\', 8, 'August', 67055.2)
Insert into finalprojection values ('C:\', 9, 'September', 67296.19)
Insert into finalprojection values ('C:\', 10, 'October', 67537.17)
Insert into finalprojection values ('C:\', 11, 'November', 67778.16)
Insert into finalprojection values ('C:\', 12, 'December', 68019.14)
Insert into finalprojection values ('C:\', 13, 'January', 68260.13)
Insert into finalprojection values ('E:\', 2, 'February', 385251.0)
Insert into finalprojection values ('E:\', 3, 'March', 401171.1)
Insert into finalprojection values ('E:\', 4, 'April', 417091.2)
Insert into finalprojection values ('E:\', 5, 'May', 433011.3)
Insert into finalprojection values ('E:\', 6, 'June', 448937.3)
Insert into finalprojection values ('E:\', 7, 'July', 464851.4)
Insert into finalprojection values ('E:\', 8, 'August', 480771.4)
Insert into finalprojection values ('E:\', 9, 'September', 496691.5)
Insert into finalprojection values ('E:\', 10, 'October', 512611.6)
Insert into finalprojection values ('E:\', 11, 'November', 528531.6)
Insert into finalprojection values ('E:\', 12, 'December', 544451.8)
Insert into finalprojection values ('E:\', 13, 'January', 560371.8)
Insert into finalactual values ('C:\', 2, 62927.88)
Insert into finalactual values ('C:\', 3, 64534.62)
Insert into finalactual values ('C:\', 4, 67215.3)
Insert into finalactual values ('C:\', 5, 70991.05)
Insert into finalactual values ('C:\', 6, 69857.11)
Insert into finalactual values ('C:\', 7, 64440.7)
Insert into finalactual values ('C:\', 8, 64359.08)
Insert into finalactual values ('E:\', 2, 382691.4)
Insert into finalactual values ('E:\', 3, 400543.9)
Insert into finalactual values ('E:\', 4, 418160.4)
Insert into finalactual values ('E:\', 5, 435643.2)
Insert into finalactual values ('E:\', 6, 451213.5)
Insert into finalactual values ('E:\', 7, 466608.2)
Insert into finalactual values ('E:\', 8, 476218.2)
If you run the following query
select a.target, a.monthnum as 'Month Value', case when a.monthnum <= 12 then
a.monthnum else a.monthnum-12 end as 'Month Number', a.monthname as [Month],
b.Actual as 'Actual', a.forecast from finalprojection a
left join
finalactual b on a.Target = b.Target AND a.monthnum = b.MonthNum
order by Target ASC, [Month Value] ASC
The resulting answer set looks like the following:
target Month Value Month Number Month Actual forecast
C:\ 2 2 February 62927.88 65609.29
C:\ 3 3 March 64534.62 65850.27
C:\ 4 4 April 67215.3 66091.26
C:\ 5 5 May 70991.05 66332.25
C:\ 6 6 June 69857.11 66573.23
C:\ 7 7 July 64440.7 66814.22
C:\ 8 8 August 64359.08 67055.2
C:\ 9 9 September NULL 67296.19
C:\ 10 10 October NULL 67537.17
C:\ 11 11 November NULL 67778.16
C:\ 12 12 December NULL 68019.14
C:\ 13 1 January NULL 68260.13
E:\ 2 2 February 382691.4 385251
E:\ 3 3 March 400543.9 401171.1
E:\ 4 4 April 418160.4 417091.2
E:\ 5 5 May 435643.2 433011.3
E:\ 6 6 June 451213.5 448931.3
E:\ 7 7 July 466608.2 464851.4
E:\ 8 8 August 476218.2 480771.4
E:\ 9 9 September NULL 496691.5
E:\ 10 10 October NULL 512611.6
E:\ 11 11 November NULL 528531.6
E:\ 12 12 December NULL 544451.8
E:\ 13 1 January NULL 560371.8
This is fine, as I have 6 or 7 months of real data, combined with a linear regression calculation extending an additional 6 months (which is why the "null" values for Sept to Jan) under actual.
What I would like to have for the output would be something along this line:
Target A/F Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Jan
C:\ A 62927 64534 67215 70991 69857 64440 64359 NULL NULL NULL NULL NULL
C:\ F 65609 65850 66091 66332 66573 66814 67055 67296 67537 67778 68019 68260
I have abbreviated the decimals along with the A/F (meaning Actual vs Forecast) for space reasons. I have also left off the E:\ drive...again, in the interest of brevity.
One issue that will likely be a problem is that the starting month (in the example above, it is February) will be dynamic. Meaning if the query runs in September, the first month will be March...October will have April as the first month...and so on.
I have created a SQL Fiddle: http://www.sqlfiddle.com/#!3/901c4/1 to save some time for you all.
I thank you all for your help in advance.
Updated info related to the sliding date need:
Hi guys...all excellent solutions.
Let me explain what I meant by the starting month.
Running the report today (in August) will need to have the first month of the result set being February and ending in January of 2015. This is because the report reaches back 6 months in history to gather the monthly results. From there, it extends (the linear regression) past the actual data for an additional 6 months (forecast). This is one quirk that I really need to accommodate (i know...pain in the butt).
So if this query were to run next month (in September), the starting month would need to be March and end in February 2015. If I run the report in October, the starting month would need to be April, which would increment through May of 2015. I hope that clarifies how that sliding scale of the months needs to be presented.
Just adding a clarification on how the month scale is being used:
the column "month value" is how the output is presented in the necessary order. This column will increment based from the starting month and continue for 12 increments. For example:
If the query is run in August (like the above sample data) the starting month for the data will be February (Run date is "month value" of 8 data begins 6 months prior or "month value" of 2. This sets up the order you see above where "month value" begins in the answer set at 2 and continues to 13. Part of the routine that generates these tables evaluates the "month value" column and generates the "month number" column (if it is < 12 then use the original value...if > 12 then subtract 12). This is why the rows that have a "month value" of 13 show a "month number" of 1 which, using a datename() conversion returns "January".
If this same query were to be run next month (September or month 10), everything would shift by 1 in the answer set. The starting "month value" would be 3 or March and the end "month value" would be 14 or February (the system will subtract 12 from the value over 12 to give us the 2). In this case, the answer set returned would look like this:
target Month Value Month Number Month Actual forecast
C:\ 3 3 March 62927.88 65609.29
C:\ 4 4 April 64534.62 65850.27
C:\ 5 5 May 67215.3 66091.26
C:\ 6 6 June 70991.05 66332.25
C:\ 7 7 July 69857.11 66573.23
C:\ 8 8 August 64440.7 66814.22
C:\ 9 9 Sept 64359.08 67055.2
C:\ 10 10 October NULL 67296.19
C:\ 11 11 November NULL 67537.17
C:\ 12 12 December NULL 67778.16
C:\ 13 1 January NULL 68019.14
C:\ 14 2 February NULL 68260.13
Note: removed the E:\ drive for brevity above
The pivoted answer should then look like below:
Target A/F Mar Apr May Jun Jul Aug Sep Oct Nov Dec Jan Feb
C:\ A 62927 64534 67215 70991 69857 64440 64359 NULL NULL NULL NULL NULL
C:\ F 65609 65850 66091 66332 66573 66814 67055 67296 67537 67778 68019 68260
The people seeing the output don't need to see years mentioned as they understand the report is showing a 12 month span of real and forecast data...so as a "January" shows up, they know that they are spanning into a new year.
I hope this helps give you the idea of how the answer set is structured.
Again...thank you for the current ideas...very good stuff.
While the other two answers will get you the result that you want, I should do this slightly different. You can unpivot the data in your Actual and Forecast columns first, then pivot the months.
You didn't specify what version of SQL Server you are using but starting in SQL Server 2005+ you can use CROSS APPLY to unpivot.
The basic syntax would be similar to:
select a.target,
Left(a.monthname, 3) as [Month],
AorF,
value
from finalprojection a
left join finalactual b
on a.Target = b.Target
AND a.monthnum = b.MonthNum
cross apply
(
select 'A', b.Actual union all
select 'F', a.forecast
) c(AorF, value);
See Demo. This is going to convert your multiple columns Actual and Forecast into multiple rows. Once the data is in this format, you can easily pivot the months making the full script:
select target,
AorF,
Jan, Feb, Mar, Apr, May, Jun, Jul,
Aug, Sep, Oct, Nov, Dec
from
(
select a.target,
Left(a.monthname, 3) as [Month],
AorF,
value
from finalprojection a
left join finalactual b
on a.Target = b.Target
AND a.monthnum = b.MonthNum
cross apply
(
select 'A', b.Actual union all
select 'F', a.forecast
) c(AorF, value)
) d
pivot
(
max(value)
for month in (Jan, Feb, Mar, Apr, May, Jun, Jul,
Aug, Sep, Oct, Nov, Dec)
) piv
order by target;
See SQL Fiddle with Demo. This gives a final result of:
| TARGET | AORF | JAN | FEB | MAR | APR | MAY | JUN | JUL | AUG | SEP | OCT | NOV | DEC |
|--------|------|---------------|----------------|----------------|---------------|--------------|---------------|----------------|--------------|------------|--------------|-------------|--------------|
| C:\ | A | (null) | 62927.87890625 | 64534.62109375 | 67215.296875 | 70991.046875 | 69857.109375 | 64440.69921875 | 64359.078125 | (null) | (null) | (null) | (null) |
| C:\ | F | 68260.1328125 | 65609.2890625 | 65850.2734375 | 66091.2578125 | 66332.25 | 66573.2265625 | 66814.21875 | 67055.203125 | 67296.1875 | 67537.171875 | 67778.15625 | 68019.140625 |
| E:\ | A | (null) | 382691.40625 | 400543.90625 | 418160.40625 | 435643.1875 | 451213.5 | 466608.1875 | 476218.1875 | (null) | (null) | (null) | (null) |
| E:\ | F | 560371.8125 | 385251 | 401171.09375 | 417091.1875 | 433011.3125 | 448937.3125 | 464851.40625 | 480771.40625 | 496691.5 | 512611.59375 | 528531.625 | 544451.8125 |
If you needs the columns to be ordered differently based on a the start month, then you'll have to use dynamic SQL similar to:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#start int
set #start = 8
select #cols = STUFF((SELECT ',' + QUOTENAME(Left(monthname, 3))
from finalprojection
where MonthNum > (#start - 6)
and MonthNum <= (#start + 6)
group by monthNum,MonthName
order by MonthNum
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT target, AorF,' + #cols + '
from
(
select a.target,
Left(a.monthname, 3) as [Month],
AorF,
value
from finalprojection a
left join finalactual b
on a.Target = b.Target
AND a.monthnum = b.MonthNum
cross apply
(
select ''A'', b.Actual union all
select ''F'', a.forecast
) c (AorF, value)
) x
pivot
(
max(value)
for month in (' + #cols + ')
) p
order by target'
exec sp_executesql #query;
See Demo
I don't understand exactly what you mean by the columns are in different order based on the month this is run. If you can explain that I can help figure out a solution to that. This query looks lengthy but it is actually quite simple once you break it down. This is using what is known as a cross tab. To break the Actual and Forecast I did them separately and then used a UNION to put them back together.
with MyCte as
(
select a.target
, a.monthnum
, b.Actual
, a.forecast
, ROW_NUMBER() over(partition by a.target order by a.monthnum) as RowNum
from finalprojection a
left join finalactual b on a.Target = b.Target AND a.monthnum = b.MonthNum
)
select Target
, 'A' as [A/F]
, max(case when monthnum = 2 then Actual end) as Feb
, max(case when monthnum = 3 then Actual end) as Mar
, max(case when monthnum = 4 then Actual end) as Apr
, max(case when monthnum = 5 then Actual end) as May
, max(case when monthnum = 6 then Actual end) as Jun
, max(case when monthnum = 7 then Actual end) as Jul
, max(case when monthnum = 8 then Actual end) as Aug
, max(case when monthnum = 9 then Actual end) as Sep
, max(case when monthnum = 10 then Actual end) as Oct
, max(case when monthnum = 11 then Actual end) as Nov
, max(case when monthnum = 12 then Actual end) as Dec
, max(case when monthnum = 1 then Actual end) as Jan
from MyCte
group by Target
union all
select Target
, 'F' as [A/F]
, max(case when monthnum = 2 then Forecast end) as Feb
, max(case when monthnum = 3 then Forecast end) as Mar
, max(case when monthnum = 4 then Forecast end) as Apr
, max(case when monthnum = 5 then Forecast end) as May
, max(case when monthnum = 6 then Forecast end) as Jun
, max(case when monthnum = 7 then Forecast end) as Jul
, max(case when monthnum = 8 then Forecast end) as Aug
, max(case when monthnum = 9 then Forecast end) as Sep
, max(case when monthnum = 10 then Forecast end) as Oct
, max(case when monthnum = 11 then Forecast end) as Nov
, max(case when monthnum = 12 then Forecast end) as Dec
, max(case when monthnum = 1 then Forecast end) as Jan
from MyCte
group by Target
order by Target, [A/F]
Here is a pivot table based on the query you gave the results from. It does Actuals and Forecast separately. To have the columns in a different order you would need to select them in a different order.
SELECT Target, 'Actual' AS [A/F]
, [February], [March], [April], [May], [June], [July], [August]
, [September], [October], [November], [December], [January]
FROM (
SELECT [Target], [Month], [Actual] FROM (
select a.target, a.monthnum as 'Month Value', case when a.monthnum <= 12 then
a.monthnum else a.monthnum-12 end as 'Month Number', a.monthname as [Month],
b.Actual as 'Actual', a.forecast
from #finalprojection a left join #finalactual b on a.Target = b.Target AND a.monthnum = b.MonthNum
) AS YourTable
) AS SourceTable
PIVOT (
SUM([Actual]) FOR [Month] IN ([February], [March], [April], [May], [June], [July], [August]
, [September], [October], [November], [December], [January])
) AS PivotData
UNION ALL
SELECT Target, 'Forecast' AS [A/F]
, [February], [March], [April], [May], [June], [July], [August]
, [September], [October], [November], [December], [January]
FROM (
SELECT [Target], [Month], [forecast] FROM (
select a.target, a.monthnum as 'Month Value', case when a.monthnum <= 12 then
a.monthnum else a.monthnum-12 end as 'Month Number', a.monthname as [Month],
b.Actual as 'Actual', a.forecast
from #finalprojection a left join #finalactual b on a.Target = b.Target AND a.monthnum = b.MonthNum
) AS YourTable
) AS SourceTable
PIVOT (
SUM([forecast]) FOR [Month] IN ([February], [March], [April], [May], [June], [July], [August]
, [September], [October], [November], [December], [January])
) AS PivotData
When I ran this with the data you provided I got the following:
Target A/F February March April May June July August September October November December January
C:\ Actual 62927.87890625 64534.62109375 67215.296875 70991.046875 69857.109375 64440.69921875 64359.078125 NULL NULL NULL NULL NULL
E:\ Actual 382691.40625 400543.90625 418160.40625 435643.1875 451213.5 466608.1875 476218.1875 NULL NULL NULL NULL NULL
C:\ Forecast 65609.2890625 65850.2734375 66091.2578125 66332.25 66573.2265625 66814.21875 67055.203125 67296.1875 67537.171875 67778.15625 68019.140625 68260.1328125
E:\ Forecast 385251 401171.09375 417091.1875 433011.3125 448937.3125 464851.40625 480771.40625 496691.5 512611.59375 528531.625 544451.8125 560371.8125
Note that the pivot uses a sum of all the data - so could give you dodgy results if you have more than one entry for each target & month. This is in there because the pivot needs something to group on. This might be where your problem of the order of the months comes in, but I need you to be more specific there to be sure.

How to get the date for the previous month which are closed in the current month

Iam using SSRS Report builder application to create BI Report for my System which is tracking the numbers of incidents logged and closed based on each month.
the below is the table which i need to create the query
Month Logged Received Closed Remaining
January 200 220 150 70
February 150 220 200 20
March 110 130 100 30
April 200 230 200 30
and each column define as follow:
Logged= Open Incident in the Current Month for example open from 1/1/2014 to 31/1/2014 (Contain only the current month data )
Received = Logged incident+ the remaining from the previous months which are still open not close for example the month febreuary will be 150 for the current moth+70 from previous month remaining will give me total 220 which is received.
Closed= incident which are opened in the current month and closed in the current month + the remaining from the previous month which closed in this month
Remaining= Received – closed
the code which i used is not giving me the close incident for the previous months also its only giving me which were closed in the current month
the below is the code which i used for my query:
SELECT group_id, YEAR(Opendate) AS Year, MONTH(Opendate) AS Month,
COUNT(CASE WHEN Month(Closedate) = Month(Opendate)
AND Month(closedate)> Month (opendate) THEN 1 ELSE NULL END) AS closed,
COUNT(*) AS Logged,
FROM Incidents
WHERE (Opendate >= #YearStart) AND (Opendate <= #YearEnd)
GROUP BY YEAR(Opendate), MONTH(Opendate), group_id
ORDER BY Year, Month,group_id
Logged is working fine the closed, Received and remaining i am stuck on it.
I tried to use Union and got the Logged and Closed Data
Select count(*) logged,year(opendate) as year1,MONTH(opendate) as
month1,'Logged' as status1
From Incidents
where opendate is not null
GROUP BY year(opendate),MONTH(opendate)
UNION
Select count(*) closed,year(Closedate) as year1,MONTH(Closedate) as
month1,'All_Closed' as status1
From Incidents
where Closedate is not null
GROUP BY year(Closedate),MONTH(Closedate)
UNION
Select count(*) Remaining,year(opendate) as year1,MONTH(opendate) as
month1,'Current_Month_Not_Closed' as status1
From Incidents
where Month(Closedate) > MONTH(Opendate)
GROUP BY year(opendate),MONTH(opendate)
UNION
Select count(*) Month_Closed,year(opendate) as year1,MONTH(opendate) as
month1,'Current_Month_Close' as status1
From Incidents
where MONTH(Closedate) = MONTH(Opendate)
GROUP BY year(opendate),MONTH(opendate)
order by year1,month1
the data which I received are as follow:
logged | year1 | month1 | status1
-------+-------+--------+-------------------------
1093 | 2014 | 1 | Logged
1089 | 2014 | 1 | All_Closed
997 | 2014 | 1 | Current_Month_Close
96 | 2014 | 1 | Current_Month_Not_Closed
1176 | 2014 | 2 | Logged
1176 | 2014 | 2 | All_Closed
91 | 2014 | 2 | Current_Month_Not_Closed
1085 | 2014 | 2 | Current_Month_Close
1340 | 2014 | 3 | Logged
1327 | 2014 | 3 | All_Closed
107 | 2014 | 3 | Current_Month_Not_Closed
1232 | 2014 | 3 | Current_Month_Close
116 | 2014 | 4 | Current_Month_Not_Closed
1320 | 2014 | 4 | Current_Month_Close
1424 | 2014 | 4 | All_Closed
1441 | 2014 | 4 | Logged
1167 | 2014 | 5 | Current_Month_Close
105 | 2014 | 5 | Current_Month_Not_Closed
1277 | 2014 | 5 | Logged
1283 | 2014 | 5 | All_Closed
To have a reliable data a calendar table as anchor can help, and is needed in case the tickets can be alive for months from their opening date or there can be a month without ticket created.
For example with the fake data
CREATE TABLE Incidents (
id int identity(1, 1)
, group_id nvarchar(100)
, Opendate Datetime
, Closedate Datetime
)
INSERT INTO Incidents
VALUES ('Service Desk', '20140107', '20140120')
, ('Service Desk', '20140117', '20140123')
, ('Service Desk', '20140127', '20140313')
, ('Service Desk', '20140310', '')
-- from an OP comment the open tickets have the Closedate '' (1900-01-01)
without a calendar table (or a temp, or a CTE) there is no way to add february in the resultset, even if the third record is both "Received" and "Remaining" in that month.
To create a calendar there are several way, in this case we need the some information about months but nothing about the days, so those are not generated.
declare #YearStart date = '20140101'
declare #YearEnd date = '20140430'
;WITH D(N) AS (
SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7
UNION ALL SELECT 8 UNION ALL SELECT 9
)
SELECT EOM
= DATEADD(D, -1, DATEADD(M, u.N + 10 * t.N + 1
, DATEADD(Y, DATEDIFF(Y, 0, #YearStart), 0)))
, pMonth = u.N + 10 * t.N
FROM D u
CROSS JOIN D t
WHERE u.N + 10 * t.N <= DATEDIFF(M, #YearStart, #YearEnd)
Here EOM is the date of the end of the month, it'll be used to check if the incidents are closed in the month and pMonth is the progressive month starting from #YearStart.
Now we need to prepare the data in the incident table to be used
SELECT ID
, OpenDate
, Closedate = COALESCE(NULLIF(Closedate, ''), '99991231')
, pOpenDate = DATEDIFF(M, #YearStart, OpenDate)
, pClosedate = DATEDIFF(M, #YearStart
, COALESCE(NULLIF(Closedate, ''), '99991231'))
FROM Incidents
the Closedate need to always have a value higher than the OpenDate, for this is used the constant date 9999-12-31, pOpenDate and pClosedate, as pMonth before, are the progressive month starting from #YearStart respectively of OpenDate and Closedate.
Putting those togheter it's possible to create the main query
declare #YearStart date = '20140101'
declare #YearEnd date = '20140430'
;WITH D(N) AS (
SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7
UNION ALL SELECT 8 UNION ALL SELECT 9
), CM AS (
SELECT EOM
= DATEADD(D, -1, DATEADD(M, u.N + 10 * t.N + 1
, DATEADD(Y, DATEDIFF(Y, 0, #YearStart), 0)))
, pMonth = u.N + 10 * t.N
FROM D u
CROSS JOIN D t
WHERE u.N + 10 * t.N <= DATEDIFF(M, #YearStart, #YearEnd)
), I AS (
SELECT ID
, OpenDate
, Closedate = COALESCE(NULLIF(Closedate, ''), '99991231')
, pOpenDate = DATEDIFF(M, #YearStart, OpenDate)
, pClosedate = DATEDIFF(M, #YearStart
, COALESCE(NULLIF(Closedate, ''), '99991231'))
FROM Incidents
)
SELECT MONTH(CM.EOM) [Month]
, Logged = SUM(CASE WHEN pOpenDate = pMonth
THEN 1
ELSE 0
END)
, Received = Count(i.id)
, Closed = SUM(CASE WHEN pClosedate = pMonth
AND i.Closedate < CM.EOM
THEN 1
ELSE 0
END)
, Remaining = SUM(CASE WHEN i.Closedate > CM.EOM
THEN 1
ELSE 0
END)
FROM CM
INNER JOIN I ON CM.pMonth
BETWEEN i.pOpenDate AND i.pClosedate
WHERE CM.EOM <= #YearEnd
GROUP BY CM.EOM
ORDER BY CM.EOM
SQLFiddle Demo
using a JOIN to get the month from the calendar table between #YearStart and #YearEnd and all the incident alive in the month. Their attribute are calculated with the CASE logic, in case of Received if a ticket is alive it's received so no logic is needed.
All the CASE can be transformed in BIT logic
declare #YearStart date = '20140101'
declare #YearEnd date = '20140430'
;WITH D(N) AS (
SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7
UNION ALL SELECT 8 UNION ALL SELECT 9
), CM AS (
SELECT EOM
= DATEADD(D, -1, DATEADD(M, u.N + 10 * t.N + 1
, DATEADD(Y, DATEDIFF(Y, 0, #YearStart), 0)))
, pMonth = u.N + 10 * t.N
FROM D u
CROSS JOIN D t
WHERE u.N + 10 * t.N <= DATEDIFF(M, #YearStart, #YearEnd)
), I AS (
SELECT ID
, OpenDate
, Closedate = COALESCE(NULLIF(Closedate, ''), '99991231')
, pOpenDate = DATEDIFF(M, #YearStart, OpenDate)
, pClosedate = DATEDIFF(M, #YearStart
, COALESCE(NULLIF(Closedate, ''), '99991231'))
FROM Incidents
)
SELECT MONTH(CM.EOM) [Month]
, Logged = SUM(1 - CAST(pOpenDate - pMonth AS BIT))
, Received = Count(i.id)
, Closed = SUM(1 - CAST(pClosedate - pMonth AS BIT))
, Remaining = SUM(0 + CAST(i.pClosedate / (CM.pMonth + 1) AS BIT))
FROM CM
INNER JOIN I ON CM.pMonth
BETWEEN i.pOpenDate AND i.pClosedate
WHERE CM.EOM <= #YearEnd
GROUP BY CM.EOM
ORDER BY CM.EOM;
SQLFiddle Demo
The bit logic is base on how the CAST to BIT works:
0 go to 0
everything else go to 1
based on that (with A and B integer):
1 - CAST(A - B AS BIT) is 1 when A = B
CAST(A / (B + 1) AS BIT) is 1 when A > B (the 0 + is to force an implicit cast to INT as BIT cannot be SUMmed)
Received would be the number of tickets that were opened before the end of the month, and not closed before the start of the month.
count(case when OpenDate <= #EndOfMonth and
(#StartOfMonth >= CloseDate or CloseDate is null) then 1 end)
as Received
Closed is straightforward:
count(case when CloseDate between #StartOfMonth and #EndOfMonth
then 1 end) as Closed
You should be able to figure out how to calculate the start and end of a month using Google.