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

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.

Related

Best way to get a column for each month's total orders

Considering the sample table below:
Product
Amount
Date
p1
5
jan-1-2022
p1
7
jan-7-2022
p1
7
feb-17-2022
p2
12
jan-18-2022
p2
16
feb-1-2022
p2
16
feb-4-2022
p3
23
jan-28-2022
p4
2
mar-22-2022
p4
1
mar-4-2022
What would be the best way to get a column for each month from the last 12 months, with the sum of amount for each product? I'd like to get something like this:
Product
jan
feb
mar
p1
12
7
0
p2
12
32
0
p3
23
0
0
p4
0
0
3
I'm trying to use the PIVOT function, but I haven't been able to make it work:
select * ,
monthname(to_date(Date)) month
from PRODUCTS
pivot(sum(Amount) for month in ('Jan','Feb','Mar','Apr','Jun','Jul','Aug','Sep','Oct','Nov','Dec'))
as p
WHERE Date >= '2021-08-01'
order by Product;
Try this, using date format TO_VARCHAR(Date, 'MON') AS Month:
WITH PRODUCTS AS (
SELECT Product, Amount, TO_DATE(DateText, 'MON-DD-YYYY') AS Date
FROM (VALUES
('p1', 5 , 'jan-1-2022'),
('p1', 7 , 'jan-7-2022'),
('p1', 7 , 'feb-17-2022'),
('p2', 12, 'jan-18-2022'),
('p2', 16, 'feb-1-2022'),
('p2', 16, 'feb-4-2022'),
('p3', 23, 'jan-28-2022'),
('p4', 2 , 'mar-22-2022'),
('p4', 1 , 'mar-4-2022'))t(Product, Amount, DateText)
), ADD_MONTH AS (
SELECT Product, Amount, TO_VARCHAR(Date, 'MON') AS Month
FROM PRODUCTS
WHERE Date >= '2021-08-01'
)
SELECT Product, Jan, Feb, Mar, Apr, Jun, Jul, Aug, Sep, Oct, Nov, Dec
FROM ADD_MONTH
PIVOT (SUM(Amount) FOR Month IN ('Jan','Feb','Mar','Apr','Jun','Jul','Aug','Sep','Oct','Nov','Dec')) AS p(Product, Jan, Feb, Mar, Apr, Jun, Jul, Aug, Sep, Oct, Nov, Dec)
ORDER BY Product;
you could try as follows.
SELECT * FROM (
select column1 as product , column2 as amount, monthname(to_date(COLUMN3, 'MON-DD-YYYY')) as date1 from values
('p1', 5, 'jan-1-2022' ),
('p1', 7, 'jan-7-2022'),
('p1', 7, 'feb-17-2022'),
('p2', 12, 'jan-18-2022'),
('p2', 16, 'feb-1-2022'),
('p2', 16, 'feb-4-2022'),
('p3', 23, 'jan-28-2022'),
('p4', 2, 'mar-22-2022'),
('p4', 1, 'mar-4-2022')
)
--pivot(max(service_date) for dose in ('dose1', 'dose2'))
pivot(sum(amount) for date1 in ('Jan','Feb','Mar','Apr','Jun','Jul','Aug','Sep','Oct','Nov','Dec'))
as p (product, Jan,Feb,Mar,Apr,Jun,Jul,Aug,Sep,Oct,Nov,Dec);

Subtract value from group of same table

Year Month Code Value
2021 January 201 100.00
2021 February 201 250.00
2021 January 202 300.00
2021 February 202 200.00
2021 March 201 50.00
2021 March 202 150.00
Need to subtract code 201 from 202 , grouping both code by month.
Output :
Year Month Value
2021 january 200
2021 february -50
2021 March 100
I was trying to get desired output but its not working..
SELECT Code,Value
,
(
SELECT sum(Value) as Value
FROM [TestD]
Where Code = '202'
GROUP BY Code
)-(
SELECT sum(Value) as Value
FROM [TestD]
where Code = '201'
GROUP BY Code
) AS item_total
FROM [TestD] group by Code,Value
You can use a case expression inside sum(...), with group by Year, Month:
select Year,
Month,
sum(case when Code = '201' then -Value when Code = '202' then Value end) as Value
from TestD
group by Year, Month
Fiddle
You can try this provided only 2 values are going to be subtracted
declare #tbl table (year int, [month] varchar(50),code int, [value] int)
insert into #tbl values(2021, 'January', 201, 100.00),(2021, 'Feb', 201, 250.00)
,(2021, 'January', 202, 300.00),(2021, 'Feb', 202, 200.00)
select year,[month],[value], row_number()over(partition by [month] order by [value] desc) rownum
into #temp
from #tbl
select year,
month
,case when rownum = 1 then value else (
(select value from #temp t1 where t1.[month] = t.[month] and t1.rownum = 1) -
value) end as diff
from #temp t
where t.rownum = 2
order by month desc
drop table #temp

Pivoting unique users for each month, each year

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

How do I write a query that imputes values for records that are not present in a table?

I have a table that looks like this:
MONTH | WIDGET | VALUE
------+--------+------
Dec | A | 3
Jan | B | 5
Feb | B | 6
Mar | B | 7
and I want to write a query that produces, for each MONTH and WIDGET the difference in VALUE between the current month and the previous month. So I want an output table like this:
MONTH | WIDGET | VALUE
------+--------+------
Dec | A | 3
Jan | A | -3
Feb | A | 0
Mar | A | 0
Dec | B | 0
Jan | B | 5
Feb | B | 1
Mar | B | 1
If there is no recorded value for the previous month for a given widget, I want to assume the previous month's value is zero. Conversely, if there is no recorded value for the current month, I want to assume the current month's value is zero.
I believe a cross join over all combinations of month and widget might work, by giving me a "spine" to which I can left join my data and then use coalesce - but is there a better way?
Edit: We can assume the MONTH column actually has a numeric representation to make it easier to identify the previous.
I would use the lag function. IBM Reference I just defaulted to 0 for values whose prior value doesn't exist but you can handle that a number of different ways.
create temp table test (
mth date
,widget char(1)
,value integer
)
distribute on random;
insert into test values('2013-12-01','A',3);
insert into test values('2014-01-01','A',-3);
insert into test values('2014-02-01','A',0);
insert into test values('2014-03-01','A',0);
insert into test values('2013-12-01','B',0);
insert into test values('2014-01-01','B',5);
insert into test values('2014-02-01','B',1);
insert into test values('2014-03-01','B',1);
select *
,lag(value,1) over(partition by widget order by mth) as prior_row
,value - nvl(lag(value,1) over(partition by widget order by mth),0) as diff
from test
OK. I have solved this in MS SQL but it should be transferable to PostgresQL. I have SQLFiddled the answer:
CREATE TABLE WidgetMonths (Month tinyint, Widget varchar(1), Value int)
CREATE TABLE Months (Month tinyint, MonthOrder tinyint)
insert into WidgetMonths Values
(12, 'A', 3),
(1,'B', 5),
(2,'B', 6),
(3,'B', 7);
insert into Months Values
(12, 1), (1, 2), (2, 3), (3, 4)
Select
AllWidgetMonths.Widget,
AllWidgetMonths.Month,
IsNull(wm.Value,0) - IsNull(wmn.Value,0) as Value
from (
select Distinct Widget, Months.Month, Months.MonthOrder
from WidgetMonths
Cross Join months
) AllWidgetMonths
left join WidgetMonths wm on wm.Widget = AllWidgetMonths.Widget
AND wm.Month = AllWidgetMonths.Month
left join WidgetMonths wmn on wmn.Widget = AllWidgetMonths.Widget
AND Case When wmn.Month = 12 Then 1 Else wmn.Month + 1 End = AllWidgetMonths.Month
Order by AllWidgetMonths.Widget, AllWidgetMonths.MonthOrder
I have started off with a Table of WidgetMonths from your example the only difference being I have converted the months into a representative integer.
I have then Created the Months Table of All months we are interested in from your example. If you want months for the whole year you can simply add to this table or find another way of generating a 1-12 row result set. The MonthOrder is optional and just helped me achieve your answer ordering.
As you mentioned AllwidgetMonths has the Cross join which gives us all combinations of Widgets and Months. This maybe better achieved by a Cross join between 'Widgets' Table and the Months Table. But I wasn't sure if this existed so left this out.
We left join WidgetMonths onto our master table of All widget months to show us which months we have a value for.
The trick up the sleeve is then left joining the same table again but this time adding 1 to the month number inside the join. This shifts the rows down one. Notice I have a Case statement (not sure about this in PostgresSql) to deal with the roll over of Month 12 to Month 1. This effectively gives me the values for each month and its previous on each row of AllwidgetMonths.
The final bit is to take one value from the other.
Hey presto. I can try to update this to PostgresSQL but you may have more knowledge and can solve it quicker than I.
Here is another alternative to get the required data. Two CTE's are used, including one to contain the month numbers.
The SQL Fiddle can be accessed here.
WITH month_order as
(
SELECT 'Jan' as month, 1 as month_no, 12 as prev_month_no
UNION ALL
SELECT 'Feb' as month, 2 as month_no, 1 as prev_month_no
UNION ALL
SELECT 'Mar' as month, 3 as month_no, 2 as prev_month_no
UNION ALL
SELECT 'Apr' as month, 4 as month_no, 3 as prev_month_no
UNION ALL
SELECT 'May' as month, 5 as month_no, 4 as prev_month_no
UNION ALL
SELECT 'Jun' as month, 6 as month_no, 5 as prev_month_no
UNION ALL
SELECT 'Jul' as month, 7 as month_no, 6 as prev_month_no
UNION ALL
SELECT 'Aug' as month, 8 as month_no, 7 as prev_month_no
UNION ALL
SELECT 'Sep' as month, 9 as month_no, 8 as prev_month_no
UNION ALL
SELECT 'Oct' as month, 10 as month_no, 9 as prev_month_no
UNION ALL
SELECT 'Nov' as month, 11 as month_no, 10 as prev_month_no
UNION ALL
SELECT 'Dec' as month, 12 as month_no, 11 as prev_month_no
)
, values_all_months as
(
SELECT
month_order.prev_month_no as prev_month_no
, month_order.month_no as month_no
, w4.month as month
, w4.widget as widget
, COALESCE(w3.value, 0) as value
FROM widgets w3
RIGHT OUTER JOIN
(
SELECT
w1.widget as widget
,w2.month as month
FROM
(SELECT
DISTINCT
widget
FROM widgets) w1,
(SELECT
DISTINCT
month
FROM widgets) w2
) w4
ON w3.month = w4.month and w3.widget = w4.widget
INNER JOIN month_order
ON w4.month = month_order.month
)
SELECT mo.month, vam1.widget, vam1.value - COALESCE(vam2.value, 0) VALUE
FROM values_all_months vam1
LEFT OUTER JOIN values_all_months vam2
ON vam1.widget = vam2.widget AND vam1.prev_month_no = vam2.month_no
INNER JOIN month_order mo
ON vam1.month_no = mo.month_no
ORDER BY vam1.widget, (SELECT CASE vam1.month_no WHEN 12 THEN 0 ELSE vam1.month_no END);

How to convert rows to column in T-SQL, and write it in a temp table?

This is a question maybe already asked.
My query is
SELECT Year, Month, Line, SUM(value) as total FROM myTable
I've the following query result table:
Year Month Line Total
-------------------------------------------
2011 2 B1 5203510.00
2011 3 B1 2228850.00
2011 4 B1 7258075.00
2011 5 B1 6305370.00
2011 6 B1 5540180.00
2011 7 B1 7624430.00
2011 8 B1 4042300.00
2011 9 B1 3308870.00
2011 10 B1 4983875.00
2011 11 B1 4636500.00
2011 12 B1 3987350.00
2012 1 B1 518400.00
I would like the following:
Year Line Jan Feb Mar Apr ..... December
2011 B1 0 52035 2228 725 ..... 3987350
2012 B1 51840 ... ... ....
Please, can you help me how to translate query SQL from rows to columns?
Essentially, you need to PIVOT your data. There are several examples on SO on how to do this. The tricky part is to convert the month number to a month name.
This is accomplished in the example with DATENAME(month, DateAdd(month, [Month], 0)-1)
SQL Statement
SELECT *
FROM (
SELECT Year, Line, Total, mnt = DATENAME(month, DateAdd(month, [Month], 0)-1)
FROM myTable
) mt
PIVOT (MAX(Total) FOR [mnt] IN ([January],[February],[March],[April],[May],[June],[July],[August],[September],[October],[November],[December])) AS PVT
Test script
;WITH myTable AS (
SELECT * FROM (VALUES
(2011 , 2 , 'B1', 5203510.00)
, (2011 , 3 , 'B1', 2228850.00)
, (2011 , 4 , 'B1', 7258075.00)
, (2011 , 5 , 'B1', 6305370.00)
, (2011 , 6 , 'B1', 5540180.00)
, (2011 , 7 , 'B1', 7624430.00)
, (2011 , 8 , 'B1', 4042300.00)
, (2011 , 9 , 'B1', 3308870.00)
, (2011 , 10 , 'B1', 4983875.00)
, (2011 , 11 , 'B1', 4636500.00)
, (2011 , 12 , 'B1', 3987350.00)
, (2012 , 1 , 'B1', 518400.00)
) AS myTable (Year, Month, Line, Total)
)
SELECT *
FROM (
SELECT Year, Line, Total, mnt = DATENAME(month, DateAdd(month, [Month], 0)-1)
FROM myTable
) mt
PIVOT (MAX(Total) FOR [mnt] IN ([January],[February],[March],[April],[May],[June],[July],[August],[September],[October],[November],[December])) AS PVT
What you're trying to do is pivot the data. I will just use the Month and Total (relevant) columns.
If you're using MS SQL 2008 or up:
SELECT [1] AS Jan, [2] AS Feb, .. [12] AS Dec,
Total
FROM ( SELECT Month, Total FROM tableA ) AS SOURCE
PIVOT
( MAX(Total) AS Total
FOR
Month IN ([1],[2],...[12]) ) AS PIVOT
PIVOT is the T-SQL keyword you seek.