Subtotals and grand totals SQL Pivot - sql

Currently have a script that creates a pivot table with current year values subtraction prior year values.
use devmreports
-- Creates dynamic values for pivot table
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(month)
from ABR
group by ',' + QUOTENAME(month)
order by datalength(',' + QUOTENAME(month))
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
-- Pivot table for YOY change in booked passengers
set #query
=
'SELECT Region,
CityPair,
Year,
' + #cols + '
FROM
(
SELECT ABR.Region,
ABR.CityPair,
ABR.Year,
ABR.Month,
ABR.Adv_B - ABRP.Adv_B as Total
FROM ABR LEFT OUTER JOIN
ABRP ON
ABR.Month = ABRP.Month AND
ABR.CityPair = ABRP.CityPair) P
PIVOT
(
SUM(Total)
FOR MONTH IN
(
'+#cols+'))as pvt'
execute (#Query)
Current Pivot looks like this:
+------------+----------+----+-----+-----+----+
| Region | CityPair | 8 | 9 | 10 | 11 |
+------------+----------+----+-----+-----+----+
| A | 1 | 16 | 17 | 18 | 7 |
| A | 2 | 17 | -20 | -10 | 1 |
| B | 3 | 5 | 8 | 4 | -3 |
| B | 4 | 21 | 10 | 3 | 2 |
| C | 5 | 15 | -14 | -12 | 1 |
+------------+----------+----+-----+-----+----+
What I would like to have is this:
+-----------------+----------+----+-----+-----+----+
| Region | CityPair | 8 | 9 | 10 | 11 |
+-----------------+----------+----+-----+-----+----+
| A | 1 | 16 | 17 | 18 | 7 |
| A | 2 | 17 | -20 | -10 | 1 |
| A Total | | 33 | -3 | 8 | 8 |
| B | 3 | 5 | 8 | 4 | -3 |
| B | 4 | 21 | 10 | 3 | 2 |
| B Total | | 26 | 18 | 7 | -1 |
| C | 5 | 15 | -14 | -12 | 1 |
| C Total | | 15 | -14 | -12 | 1 |
| Grand Total | | 74 | 1 | 3 | 8 |
+-----------------+----------+----+-----+-----+----+
Any assistance would be greatly appreciated.

My suggestion would be to look at using GROUP BY ROLLUP to get the total rows.
The basic syntax if you were hard-coding the query would be:
select
case
when region is null then 'Grand Total'
when citypair is null then region +' Total'
else region end region,
coalesce(cast(citypair as varchar(10)), '') citypair,
sum([8]) [8],
sum([9]) [9]
from
(
select region, citypair, month, total
from yourtable
) d
pivot
(
sum(total)
for month in ([8], [9])
) piv
GROUP BY rollup(region, citypair);
See SQL Fiddle with Demo. Then to use your dynamic SQL version you could alter the code to use:
-- Creates dynamic values for pivot table
DECLARE #cols AS NVARCHAR(MAX),
#colsRollup AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(month)
from ABR
group by ',' + QUOTENAME(month)
order by datalength(',' + QUOTENAME(month))
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #colsRollup = STUFF((SELECT ', sum(' + QUOTENAME(month)+ ') as '+ QUOTENAME(month)
from ABR
group by ',' + QUOTENAME(month)
order by datalength(',' + QUOTENAME(month))
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
-- Pivot table for YOY change in booked passengers
set #query
=
'SELECT
case
when region is null then ''Grand Total''
when citypair is null then region +'' Total' '
else region end region,
coalesce(cast(citypair as varchar(10)), '''') citypair,
' + #colsRollup + '
FROM
(
SELECT ABR.Region,
ABR.CityPair,
ABR.Year,
ABR.Month,
ABR.Adv_B - ABRP.Adv_B as Total
FROM ABR LEFT OUTER JOIN
ABRP ON
ABR.Month = ABRP.Month AND
ABR.CityPair = ABRP.CityPair
) P
PIVOT
(
SUM(Total)
FOR MONTH IN ('+#cols+')
)as pvt
GROUP BY rollup(region, citypair);'
execute sp_executesql #Query

That's why I prefer to pivot by hand with aggregates and not by pivot. Here's a query which will show you all sums with totals and grand totals:
select
case
when grouping(Region) = 1 then 'Grand Total'
when grouping(CityPair) = 1 then Region + ' Total'
else Region
end as Region,
isnull(cast(CityPair as nvarchar(max)), '') as CityPair,
sum(case when Month = 8 then Value end) as [8],
sum(case when Month = 9 then Value end) as [9],
sum(case when Month = 10 then Value end) as [10],
sum(case when Month = 11 then Value end) as [11]
from test
group by rollup(Region, CityPair)
sql fiddle demo
Here's dynamic one:
declare #stmt nvarchar(max)
select
#stmt = isnull(#stmt + ',', '') +
'sum(case when Month = ' + cast(Month as nvarchar(max)) +
' then Value end) as [' + cast(Month as nvarchar(max)) + ']'
from (select distinct Month from test) as a
select #stmt = '
select
case
when grouping(Region) = 1 then ''Grand Total''
when grouping(CityPair) = 1 then Region + '' Total''
else Region
end as Region,
isnull(cast(CityPair as nvarchar(max)), ''''), ' + #stmt + '
from test
group by rollup(Region, CityPair)'
exec sp_executesql #stmt = #stmt
sql fiddle demo
As always, tried to make it as readable as possible.
I'm suggesting to use group by rollup instead of with rollup, because last one will be deprecated:
WITH ROLLUP This feature will be removed in a future version of
Microsoft SQL Server. Avoid using this feature in new development
work, and plan to modify applications that currently use this feature.

Related

How to Convert (null) values with 0 output in PIVOT SQL

I tried to Replace the (null) values with 0 (zeros) output by Using PIVOT.
Code below:
declare #col as nvarchar(max),
#query AS NVARCHAR(MAX)
select #col = STUFF((SELECT ',' + QUOTENAME(Week)
from pivote_created group by Week order by Week FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)'),1,1,'')
set #query = 'select * from ( select store, week, xCount from pivote_created ) src pivot (sum(xcount)for week in (' + #col + ')) piv';
EXEC sp_executesql #query
output
store | 1 | 2 | 3 | 5
____________________________
101 | 138| 282| 220 | NULL
102 | 96 | 212| 123 | NULL
105 | 37 | 78 | NULL| 60
109 | 59 | 97 | 87 | NULL
I would like this
store | 1 | 2 | 3 | 5
___________________________
10 | 138 | 282 | 220 | 0
102 | 96 | 212 | 123 | 0
105 | 37 | 78 | 0 | 60
109 | 59 | 97 | 87 | 0
The most natural function to use is COALESCE() because it is the ANSI-standard function for this purpose. Under some circumstances in SQL Server, ISNULL() has better performance, but this is not one of those circumstances.
In the code, this looks like:
declare #cols nvarchar(max),
#colnames nvarchar(MAX),
#query nvarchar(MAX);
select #cols = stuff((select ',' + QUOTENAME(Week)
from pivote_created
group by Week
order by Week FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, ''
);
select #colnames = stuff((select ', COALESCE(' + QUOTENAME(Week) + ', ''0'') as ' + QUOTENAME(Week)
from pivote_created
group by Week
order by Week FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 2, ''
);
set #query = 'select ' + #colnames + '
from (select store, week, xCount from pivote_created
) src
pivot (sum(xcount)for week in (' + #cols + ')
) piv';
exec sp_executesql #query;
If you are using Oracle, you can use NVL Function.
select NVL(xCount, 0) from dual
If you are using SQL Server, you can use IsNull function:
select ISNULL(xCount, 0)
I think Its work
#Ezlo code work properly but add the missing second paramater given below:
COALESCE([1],NULL,'0') as [1]

Dynamically append columns based on another table in SQL

Suppose I want the following behavior in SQL
QTY Table
SERIAL_NO QTY CODE
1111111 1 AA
1111112 1 AA
1111111 2 BB
1111111 4 BB
1111113 7 CC
Code Table
CODE CODE_NAME
AA NameA
BB NameB
CC NameC
Query Result
SERIAL_NO NameA NameB NameC
1111111 1 6 0
1111112 1 0 0
1111113 0 0 7
NameA,B,C column in query result are basically the sums grouped by their respective code.
How do I achieve this behavior? The hardest part I'm trying to grasp is to dynamically add the columns to the query result based on the code table. For example, if the user adds another code called DD, the query result should automatically append NameD to the right side, and get the corresponding sum for that new code.
If you really need it in SQL you can leverage PIVOT and dynamic SQL in the following way
DECLARE #cols NVARCHAR(MAX), #colp NVARCHAR(MAX), #sql NVARCHAR(MAX)
SET #cols = STUFF(
(
SELECT DISTINCT ',COALESCE(' + QUOTENAME(code_name) + ',0) AS ' + QUOTENAME(code_name)
FROM code
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
SET #colp = STUFF(
(
SELECT DISTINCT ',' + QUOTENAME(code_name)
FROM code
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
SET #sql = 'SELECT serial_no, ' + #cols +
' FROM
(
SELECT serial_no, code_name, qty
FROM qty q JOIN code c
ON q.code = c.code
) x
PIVOT
(
SUM(qty) FOR code_name IN (' + #colp + ')
) p
ORDER BY serial_no'
EXECUTE(#sql)
Output:
| SERIAL_NO | NAMEA | NAMEB | NAMEC |
|-----------|-------|-------|-------|
| 1111111 | 1 | 6 | 0 |
| 1111112 | 1 | 0 | 0 |
| 1111113 | 0 | 0 | 7 |
Here is SQLFiddle demo

Transforming a column in rows [duplicate]

I have the following dataset
Account Contact
1 324324324
1 674323234
2 833343432
2 433243443
3 787655455
4 754327545
4 455435435
5 543544355
5 432455553
5 432433242
5 432432432
I'd like output as follows:
Account Contact1 Contact2 Contact3 Contact4
1 324324324 674323234
2 833343432 433243443
3 787655455
4 754327545 455435435
5 543544355 432455553 432433242 432432432
The problem is also that I have an unfixed amount of Accounts & unfixed amount of Contacts
If you are going to apply the PIVOT function, you will need to use an aggregate function to get the result but you will also want to use a windowing function like row_number() to generate a unique sequence for each contact in the account.
First, you will query your data similar to:
select account, contact,
'contact'
+ cast(row_number() over(partition by account
order by contact) as varchar(10)) seq
from yourtable
See SQL Fiddle with Demo. This will create a new column with the unique sequence:
| ACCOUNT | CONTACT | SEQ |
|---------|-----------|----------|
| 1 | 324324324 | contact1 |
| 1 | 674323234 | contact2 |
If you have a limited number of columns, then you could hard-code your query:
select account,
contact1, contact2, contact3, contact4
from
(
select account, contact,
'contact'
+ cast(row_number() over(partition by account
order by contact) as varchar(10)) seq
from yourtable
) d
pivot
(
max(contact)
for seq in (contact1, contact2, contact3, contact4)
) piv;
See SQL Fiddle with Demo
If you have an unknown number of columns, then you will have to use dynamic SQL:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(seq)
from
(
select 'contact'
+ cast(row_number() over(partition by account
order by contact) as varchar(10)) seq
from yourtable
) d
group by seq
order by seq
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT account, ' + #cols + '
from
(
select account, contact,
''contact''
+ cast(row_number() over(partition by account
order by contact) as varchar(10)) seq
from yourtable
) x
pivot
(
max(contact)
for seq in (' + #cols + ')
) p '
execute sp_executesql #query;
See SQL Fiddle with Demo. Both will give you a result of:
| ACCOUNT | CONTACT1 | CONTACT2 | CONTACT3 | CONTACT4 |
|---------|-----------|-----------|-----------|-----------|
| 1 | 324324324 | 674323234 | (null) | (null) |
| 2 | 433243443 | 833343432 | (null) | (null) |
| 3 | 787655455 | (null) | (null) | (null) |
| 4 | 455435435 | 754327545 | (null) | (null) |
| 5 | 432432432 | 432433242 | 432455553 | 543544355 |
Just a slightly different way to generate the dynamic PIVOT:
DECLARE #c INT;
SELECT TOP 1 #c = COUNT(*)
FROM dbo.YourTable
GROUP BY Account
ORDER BY COUNT(*) DESC;
DECLARE #dc1 NVARCHAR(MAX) = N'', #dc2 NVARCHAR(MAX) = N'', #sql NVARCHAR(MAX);
SELECT #dc1 += ',Contact' + RTRIM(i), #dc2 += ',[Contact' + RTRIM(i) + ']'
FROM (SELECT TOP (#c) i = number + 1
FROM master.dbo.spt_values WHERE type = N'P' ORDER BY number) AS x;
SET #sql = N'SELECT Account ' + #dc1 +
' FROM (SELECT Account, Contact, rn = ''Contact''
+ RTRIM(ROW_NUMBER() OVER (PARTITION BY Account ORDER BY Contact))
FROM dbo.YourTable) AS src PIVOT (MAX(Contact) FOR rn IN ('
+ STUFF(#dc2, 1, 1, '') + ')) AS p;';
EXEC sp_executesql #sql;
SQLiddle demo

Dynamically create columns in SQL select query and join tables

I have 2 tables. They are as follows
Table : Grade
GradeID | Grade
-----------------
1 | Chopsaw
2 | Classic
3 | Chieve
Table : Moulded Quantity
Batch ID | Grade | Moulded | Date
-------------------------------------
1 | 1 | 150 | 21st May
2 | 1 | 150 | 22nd May
3 | 2 | 150 | 21st May
4 | 2 | 150 | 21st May
5 | 2 | 150 | 22nd May
I should get the Output like the following
Date | Moulded | Chopsaw | Classic | Cieve
--------------------------------------------------
21st May | 450 | 150 | 300 | 0
22nd May | 300 | 150 | 150 | 0
I am using MSSQL 2008 and i use Crystal report to display the same.
If the number of grades is known beforehand then you can do it with a static query.
SELECT date,
SUM(moulded) moulded,
SUM(CASE WHEN grade = 1 THEN moulded ELSE 0 END) Chopsaw,
SUM(CASE WHEN grade = 2 THEN moulded ELSE 0 END) Classic,
SUM(CASE WHEN grade = 3 THEN moulded ELSE 0 END) Chieve
FROM moulded_quantity
GROUP BY date
This query is not vendor specific so it should work on any major RDBMS.
Now, if the number of grades is unknown or you want it to work even if you make changes to grade table (without changing the query itself) you can resort to dynamic query. But dynamic SQL is vendor specific. Here is an example of how you can do that in MySql
SELECT CONCAT (
'SELECT date, SUM(moulded) moulded,',
GROUP_CONCAT(DISTINCT
CONCAT('SUM(CASE WHEN grade = ',gradeid,
' THEN moulded ELSE 0 END) ', grade)),
' FROM moulded_quantity GROUP BY date') INTO #sql
FROM grade;
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Output (in both cases):
| DATE | MOULDED | CHOPSAW | CLASSIC | CHIEVE |
---------------------------------------------------
| 21st May | 450 | 150 | 300 | 0 |
| 22nd May | 300 | 150 | 150 | 0 |
Here is SQLFiddle demo (for both approaches).
UPDATE In Sql Server you can use STUFF and PIVOT to produce expected result with dynamic sql
DECLARE #colx NVARCHAR(MAX), #colp NVARCHAR(MAX), #sql NVARCHAR(MAX)
SET #colx = STUFF((SELECT ', ISNULL(' + QUOTENAME(Grade) + ',0) ' + QUOTENAME(Grade)
FROM grade
ORDER BY GradeID
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'),1,1,'')
SET #colp = STUFF((SELECT DISTINCT ',' + QUOTENAME(Grade)
FROM grade
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'),1,1,'')
SET #sql = 'SELECT date, total moulded, ' + #colx +
' FROM
(
SELECT date, g.grade gradename, moulded,
SUM(moulded) OVER (PARTITION BY date) total
FROM moulded_quantity q JOIN grade g
ON q.grade = g.gradeid
) x
PIVOT
(
SUM(moulded) FOR gradename IN (' + #colp + ')
) p
ORDER BY date'
EXECUTE(#sql)
Output is the same as in MySql case.
Here is SQLFiddle demo.
I would suggest you before asking any question research first as it is very common question.
Updated
DECLARE #COLUMNS varchar(max)
SELECT #COLUMNS = COALESCE(#COLUMNS+'],[' ,'') + CAST(Grade as varchar)
FROM Grade
GROUP BY Grade
SET #COLUMNS = '[' + #COLUMNS + ']'
DECLARE #COLUMNS_WITH_NULL varchar(max)
SELECT #COLUMNS_WITH_NULL = COALESCE(#COLUMNS_WITH_NULL+',ISNULL([' ,'ISNULL([') + CAST(Grade as varchar) + '], 0) AS ' + CAST(Grade as varchar)
FROM Grade
GROUP BY Grade
DECLARE #COLUMNS_SUMS varchar(max)
SELECT #COLUMNS_SUMS = COALESCE(#COLUMNS_SUMS+' + ISNULL([' ,'ISNULL([') + CAST(Grade as varchar) + '], 0) '
FROM Grade
GROUP BY Grade
SET #COLUMNS_SUMS = '(' + #COLUMNS_SUMS + ') as Moulded'
PRINT #COLUMNS_SUMS
EXECUTE (
'
SELECT
Date, ' + #COLUMNS_SUMS + ', ' + #COLUMNS_WITH_NULL + '
FROM (
SELECT
m.Moulded,
m.date AS Date,
g.Grade
FROM Grade g
INNER JOIN [Moulded Quantity] m
ON m.GRADE = g.GradeID
) up
PIVOT (SUM(Moulded) FOR Grade IN ('+ #COLUMNS +')) AS pvt')

Convert Access TRANSFORM/PIVOT query to SQL Server

TRANSFORM Avg(CASE WHEN [temp].[sumUnits] > 0
THEN [temp].[SumAvgRent] / [temp].[sumUnits]
ELSE 0
END) AS Expr1
SELECT [temp].[Description]
FROM [temp]
GROUP BY [temp].[Description]
PIVOT [temp].[Period];
Need to convert this query for sql server
I have read all other posts but unable to convert this into the same
Here is the equivalent version using the PIVOT table operator:
SELECT *
FROM
(
SELECT
CASE
WHEN sumUnits > 0
THEN SumAvgRent / sumUnits ELSE 0
END AS Expr1,
Description,
Period
FROM temp
) t
PIVOT
(
AVG(Expr1)
FOR Period IN(Period1, Period2, Period3)
) p;
SQL Fiddle Demo
For instance, this will give you:
| DESCRIPTION | PERIOD1 | PERIOD2 | PERIOD3 |
---------------------------------------------
| D1 | 10 | 0 | 20 |
| D2 | 100 | 1000 | 0 |
| D3 | 50 | 10 | 2 |
Note that When using the MS SQL Server PIVOT table operator, you have to enter the values for the pivoted column. However, IN MS Access, This was the work that TRANSFORM with PIVOT do, which is getting the values of the pivoted column dynamically. In this case you have to do this dynamically with the PIVOT operator, like so:
DECLARE #cols AS NVARCHAR(MAX);
DECLARE #query AS NVARCHAR(MAX);
SELECT #cols = STUFF((SELECT distinct
',' +
QUOTENAME(Period)
FROM temp
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'');
SET #query = ' SELECT Description, ' + #cols + '
FROM
(
SELECT
CASE
WHEN sumUnits > 0
THEN SumAvgRent / sumUnits ELSE 0
END AS Expr1,
Description,
Period
FROM temp
) t
PIVOT
(
AVG(Expr1)
FOR Period IN( ' + #cols + ')
) p ';
Execute(#query);
Updated SQL Fiddle Demo
This should give you the same result:
| DESCRIPTION | PERIOD1 | PERIOD2 | PERIOD3 |
---------------------------------------------
| D1 | 10 | 0 | 20 |
| D2 | 100 | 1000 | 0 |
| D3 | 50 | 10 | 2 |