How to create Sql Server 2008 Cross Tab Dynamic Query? - sql

I have 3 tables:
Locations: LocationID, LocationName
Defect: DefectID, DefectType
Feedback: feedbackID, DefectID, LocationID
I need cross tab report in the below format: Number below the locations column is the total number of defects for that location. Locations can be any number. it should be dynamic..
DefectID DefectType NewYork NewJersey Texas Houston
1 Defect1 0 10 3 6
2 Defect2 0 0 9 10
3 Defect3 8 8 4 6
I have a SQL query which is hardcoded. Also, it's not displaying the DefectID..
select
DefectType,
[1] as NewYork,
[4] as NewJersy,
[5] as Texas,
[6] as Houston
from (select
Defect.DefectID,
Defect.DefectType,
Location.LocationID
from Feedback
inner join Locations on (Feedback.LocationID= Location.LocationID)
inner join DefectType on (Feedback.DefectID= Defect.DefectID)
) p
pivot
( count (DefectID) for LocationID in ( [1], [4], [5],[6] ) ) as pvt
order by pvt.DefectType;

You can't dynamically build the cross tab values with SQL Server PIVOT and pure static SQL. You have to build the SQL dynamically, either with some external scripting language building the query, or with T-SQL using execute
DECLARE #locationID int, #LocationName nvarchar(50),
#columnList nvarchar(max), #idList nvarchar(max), #sql nvarchar(max) ;
DECLARE location_cursor CURSOR
FOR SELECT locationID, LocationName FROM Locations
SET #columnList = '';
SET #idList = '';
OPEN location_cursor
FETCH NEXT FROM vendor_cursor
INTO #locationID, #LocationName;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #columnList = #columnList + ', [' + #locationID + '] as [' + #locationName + ']'
SET #idList = #idList + '[' + #locationID + '],'
END
CLOSE location_cursor
SET #sql = 'select DefectType' + #columnList + ' from (select Defect.DefectID, Defect.DefectType, Location.LocationID from Feedback inner join Locations on (Feedback.LocationID= Location.LocationID)
inner join DefectType on (Feedback.DefectID= Defect.DefectID)
) p pivot
( count (DefectID) for LocationID in (' + left(#idList,len(#idList)-1) + ') ) as pvt order by pvt.DefectType'
EXECUTE (#sql)
I haven't tested this, obviously, but it should work (with minor tweaks possibly needed).

Related

Aggregate dynamic columns in SQL Server

I have a narrow table containing unique key and source data
Unique_Key
System
1
IT
1
ACCOUNTS
1
PAYROLL
2
IT
2
PAYROLL
3
IT
4
HR
5
PAYROLL
I want to be able to pick a system as a base - in this case IT - then create a dynamic SQL query where it counts:
distinct unique key in the chosen system
proportion of shared unique key with other systems. These systems could be dynamic and there are lot more than 4
I'm thinking of using dynamic SQL and PIVOT to first pick out all the system names outside of IT. Then using IT as a base, join to that table to get the information.
select distinct Unique_Key, System_Name
into #staging
from dbo.data
where System_Name <> 'IT'
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(System_Name)
FROM #staging
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT Unique_Key, ' + #cols + ' into dbo.temp from
(
select Unique_Key, System_Name
from #staging
) x
pivot
(
count(System_Name)
for System_Name in (' + #cols + ')
) p '
execute(#query)
select *
from
(
select distinct Unique_Key
from dbo.data
where System_Name = 'IT'
) a
left join dbo.temp b
on a.Unique_Key = b.Unique_Key
So the resulting table is:
Unique_Key
PAYROLL
ACCOUNTS
HR
1
1
1
0
2
1
0
0
3
0
0
0
What I want is one step further:
Distinct Count IT Key
PAYROLL
ACCOUNTS
HR
3
67%
33%
0%
I can do a simple join with specific case when/sum statement but wondering if there's a way to do it dynamically so I don't need to specify every system name.
Appreciate any tips/hints.
You can try to use dynamic SQL as below, I would use condition aggregate function get pivot value then we might add OUTER JOIN or EXISTS condition in dynamic SQL.
I would use sp_executesql instead of exec to avoid sql-injection.
DECLARE #System_Name NVARCHAR(50) = 'IT'
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#parameter AS NVARCHAR(MAX);
SET #parameter = '#System_Name NVARCHAR(50)'
select DISTINCT System_Name
into #staging
from dbo.data t1
WHERE t1.System_Name <> #System_Name
SET #cols = STUFF((SELECT distinct ', SUM(IIF(System_Name = '''+ System_Name+''',1,0)) * 100.0 / SUM(IIF(System_Name = #System_Name,0,1)) ' + QUOTENAME(System_Name)
FROM #staging
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT SUM(IIF(System_Name = #System_Name,0,1)) [Distinct Count IT Key], ' + #cols + ' from dbo.data t1
WHERE EXISTS (
SELECT 1
FROM dbo.data tt
WHERE tt.Unique_Key = t1.Unique_Key
AND tt.System_Name = #System_Name
) '
EXECUTE sp_executesql #query, #parameter, #System_Name
sqlfiddle
When writing Dynamic Query, you start off with a non-dynamic query. Make sure you gets the result of the query is correct before you convert to dynamic query.
For the result that you required, the query will be
with cte as
(
select it.Unique_Key, ot.System_Name
from data it
left join data ot on it.Unique_Key = ot.Unique_Key
and ot.System_Name <> 'IT'
where it.System_Name = 'IT'
)
select [ITKey] = count(distinct Unique_Key),
[ACCOUNTS] = count(case when System_Name = 'ACCOUNTS' then Unique_Key end) * 100.0
/ count(distinct Unique_Key),
[HR] = count(case when System_Name = 'HR' then Unique_Key end) * 100.0
/ count(distinct Unique_Key),
[PAYROLL] = count(case when System_Name = 'PAYROLL' then Unique_Key end) * 100.0
/ count(distinct Unique_Key)
from cte;
Once you get the result correct, it is not that difficult to convert to dynamic query. Use string_agg() or for xml path for those repeated rows
declare #sql nvarchar(max);
; with cte as
(
select distinct System_Name
from data
where System_Name <> 'IT'
)
select #sql = string_agg(sql1 + ' / ' + sql2, ',' + char(13))
from cte
cross apply
(
select sql1 = char(9) + quotename(System_Name) + ' = '
+ 'count(case when System_Name = ''' + System_Name + ''' then Unique_Key end) * 100.0 ',
sql2 = 'count(distinct Unique_Key)'
) a
select #sql = 'with cte as' + char(13)
+ '(' + char(13)
+ ' select it.Unique_Key, ot.System_Name' + char(13)
+ ' from data it' + char(13)
+ ' left join data ot on it.Unique_Key = ot.Unique_Key' + char(13)
+ ' and ot.System_Name <> ''IT''' + char(13)
+ ' where it.System_Name = ''IT''' + char(13)
+ ')' + char(13)
+ 'select [ITKey] = count(distinct Unique_Key), ' + char(13)
+ #sql + char(13)
+ 'from cte;' + char(13)
print #sql;
exec sp_executesql #sql;
db<>fiddle demo
This solution changes the aggregation function of the PIVOT itself.
First, let's add a column [has_it] to #staging that keeps track of whether each Unique_Key has an IT row:
select Unique_Key, System_Name, case when exists(select 1 from data d2 where d2.Unique_Key=d1.Unique_Key and d2.System_Name='IT') then 1 else 0 end as has_it
into #staging
from data d1
where System_Name <> 'IT'
group by Unique_Key, System_Name
Now, the per-System aggregation (sum) of this column divided by the final total unique keys needed (example case=3) returns the requests numbers. Change the PIVOT to the following and it's ready as-is, without further queries:
set #query = ' select *
from
(
select System_Name,cnt as [Distinct Count IT Key],has_it*1.0/cnt as divcnt
from #staging
cross join
(
select count(distinct Unique_Key) as cnt
from dbo.data
where System_Name = ''IT''
)y
) x
pivot
(
sum(divcnt)
for System_Name in (' + #cols + ')
) p'

SQL Server - Using COALESCE across a Pivot with variable columns

I am amending a SQL query that populates a UI table. I want to eliminate any nulls that result from the query and replace them with 0.00. Typically a COALESCE function on the final SELECT would give me what I need, but the columns in the pivot (#ListOfYears) might change (SELECT PV.* may give me 1 column or 10 columns depending on #ListofYears).
DECLARE #ListofYears varchar(6000)
DECLARE #SQL varchar(2000)
SET #ListofYears = '[2021],[2022]'
SET #SQL = '
SELECT PV.*
FROM
(SELECT
p.Pkey,
p.Code,
P.Name as Name,
Price,
GroupName
FROM
Catalogue C
LEFT JOIN CatalogueDetail CD on CD.CataloguePkey=C.Pkey
LEFT JOIN CatalogueYear CY on CY.Pkey=CD.CatalogueYearPkey where C.IsActive=1
) as D
PIVOT
(
COALESCE(Sum(D.Price), 0.00) for GroupName in ( ' + #ListofYears + ' )
) AS PV
ORDER BY Pkey'
PRINT #sql
EXEC (#sql)
Is there any way to use COALESCE or ISNULL so that my top SELECT statement does not result in any NULLS? Putting COALESCE in the pivot results in:
Msg 102, Level 15, State 1, Line 4
Incorrect syntax near ','.
Msg 156, Level 15, State 1, Line 22
Incorrect syntax near the keyword 'COALESCE'.
As I mentioned in the question, you need to wrap the COALESCE in the outer SELECT, not in the PIVOT. This means, in short, you need something like this:
SELECT {Other Columns},
COALESCE([2020],0) AS [2020],
COALESCE([2021],0) AS [2021]
...
FROM(SELECT p.Pkey,
p.Code,
P.Name as Name,
Price,
GroupName
FROM dbo.Catalogue C
LEFT JOIN dbo.CatalogueDetail CD on CD.CataloguePkey=C.Pkey
LEFT JOIN dbo.CatalogueYear CY on CY.Pkey=CD.CatalogueYearPkey
WHERE C.IsActive=1
) as D
PIVOT(SUM(D.Price)
FOR GroupName in ([2021],[2022])) AS PV
ORDER BY Pkey;
As you are using dynamic SQL, and injecting a list into you are, unsurprisingly, finding this difficult.
In truth, I would suggest switching to conditional aggregation, as this is much easier. Non-dynamic it would look like this:
SELECT {Other Columns}
SUM(CASE GroupName WHEN 2020 THEN D.Price ELSE 0 END) AS [2020],
SUM(CASE GroupName WHEN 2021 THEN D.Price ELSE 0 END) AS [2021]
FROM dbo.Catalogue C
LEFT JOIN dbo.CatalogueDetail CD on CD.CataloguePkey=C.Pkey
LEFT JOIN dbo.CatalogueYear CY on CY.Pkey=CD.CatalogueYearPkey
WHERE C.IsActive=1
GROUP BY {Other Columns};
As you want to do this dynamically, it would look a little like this (assuming you are using SQL Server 2017+):
DECLARE #Years table (Year int);
INSERT INTO #Years (Year)
VALUES (2020),(2021);
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
DECLARE #Delim nvarchar(20) = N',' + #CRLF;
SET #SQL = (SELECT N'SELECT {Other Columns}' + #CRLF +
STRING_AGG(N' SUM(CASE GroupName WHEN ' + QUOTENAME(Y.Year,'''') + N'THEN D.Price ELSE 0 END) AS ' + QUOTENAME(Y.Year),#Delim) WITHIN GROUP (ORDER BY Y.Year) + #CRLF +
N'FROM dbo.Catalogue C' + #CRLF +
N' LEFT JOIN dbo.CatalogueDetail CD on CD.CataloguePkey=C.Pkey' + #CRLF +
N' LEFT JOIN dbo.CatalogueYear CY on CY.Pkey=CD.CatalogueYearPkey' + #CRLF +
N'WHERE C.IsActive=1' + #CRLF +
N'GROUP BY {Other Columns};'
FROM #Years Y);
EXEC sys.sp_executesql #SQL;
If you are using a version of SQL Server that isn't fully supported, you'll need to use the "old" FOR XML PATH (and STUFF) method.
Obviously, you'll also need to replace the parts in braces with the appropriate SQL.

Passing parameter to stored procedure using dynamic SQL into pivot data

I am trying to pass a parameter into Stored procedure to filter data in my select statement but when i use the parameter it gives error Message: Invalid column name 'SessionId2075'. when I use static value in the where clause the procedure works fine. Can you please give me fix the issue. I checked all the previous answers and could not find the working solution.
Alter PROCEDURE [dbo].GetPivotFeeReport
(
#SessionId varchar(50)
)
as
begin
DECLARE #SQL as VARCHAR(MAX)
DECLARE #Columns as VARCHAR(MAX)
SET NOCOUNT ON;
SELECT #Columns =
COALESCE(#Columns + ', ','') + QUOTENAME(GroupHeaderValue)
FROM
(SELECT DISTINCT mgh.GroupHeaderValue
FROM StudentFeeDetail sf
INNER JOIN MasterGroupHeaderValue mgh
ON mgh.GroupHeaderValueId = sf.FeeForId
) AS B
ORDER BY B.GroupHeaderValue
SET #SQL = 'SELECT ClassName,' + #Columns + ',TOTAL
FROM
(
SELECT
distinct mc.className,
sf.FinalAmount,
mgh.GroupHeaderValue,
Sum (isnull(sf.FinalAmount,0)) over (partition by ClassName) AS TOTAL
--0 AS TOTAL
FROM StudentFeeDetail sf
INNER JOIN StudentAdmission sa
ON sa.AdmissionId = sf.AdmissionId
INNER JOIN MasterClass mc
ON mc.ClassId = sa.ClassId
INNER JOIN MasterGroupHeaderValue mgh
ON mgh.GroupHeaderValueId = sf.FeeForId
WHERE sa.SessionId = (' + #SessionId + ') -- this is where I am trying to use the parameter when used static value like this ''SessionId2075'' the procedure works fine
and sf.FeeAmt >0
GROUP BY className, FinalAmount, GroupHeaderValue
) as PivotData
PIVOT
(
sum(FinalAmount)
FOR GroupHeaderValue IN (' + #Columns + ')
) AS PivotResult
ORDER BY (ClassName)
'
EXEC ( #sql)
end

How to order known values as columns in Dynamic Pivot and new ones to the end?

I am setting up a dynamic pivot where the columns (Department IDs) must be in a set order and any new values that will create a column must be at the end of the table. I set up a Sequence number for the "known" departments and any new departments get the next number in the sequence. I need the Department IDs to be the headings but I need them in the order of the Sequence number.
1) I have pivoted on the Sequence number:
FROM (SELECT DISTINCT [SEQ] FROM #TABLE) AS [SEQ]
ORDER BY [SEQ]
SET #DynamicPivotQuery =
N'SELECT [DATE], ' + #ColumnName + '
FROM #TABLE
PIVOT(SUM([COUNT])
FOR [SEQ] IN (' + #ColumnName + ')) AS PVTTable
ORDER BY [DATE]'
--Execute the Dynamic Pivot Query
EXEC sp_executesql #DynamicPivotQuery
This comes out in the correct order but the Sequence number is the column heading
2) I also have pivoted on the Dept but the columns are in the sequence of the Dept ID:
--Get distinct values of the PIVOT Column
SELECT #ColumnName= ISNULL(#ColumnName + ',','')
+ QUOTENAME([DEPT_ID])
FROM (SELECT DISTINCT [DEPT_ID] FROM #TABLE) AS [DEPT_ID]
ORDER BY [DEPT_ID]
SET #DynamicPivotQuery =
N'SELECT [DATE], ' + #ColumnName + '
FROM #TABLE
PIVOT(SUM([COUNT])
FOR [DEPT_ID] IN (' + #ColumnName + ')) AS PVTTable
ORDER BY [DATE]'
--Execute the Dynamic Pivot Query
EXEC sp_executesql #DynamicPivotQuery
The data/table that is ready to be pivoted is:
SEQ DEPT DATE COUNT
----------------------------------
1 8 1/1/2019 5 (Dept 8 is known Dept)
1 8 1/2/2019 7
2 3 1/1/2019 6 (Dept 3 is known Dept)
2 3 1/2/2019 4
3 1 1/1/2019 7 (Dept 1 is an unknown dept)
3 1 1/2/2019 3
The results I want to see are:
DATE 8 3 1
----------------------------------
1/1/2019 5 6 7
1/2/2019 7 4 3
Or as an image:
You can use the following solution using a dynamic query and PIVOT:
-- declare the variables.
DECLARE #ColumnName NVARCHAR(MAX)
DECLARE #DynamicQuery NVARCHAR(MAX)
-- set the columns (ORDER BY SEQ).
SET #ColumnName = STUFF((
SELECT ',' + QUOTENAME(CONVERT(VARCHAR(10), DEPT))
FROM table_name
GROUP BY SEQ, DEPT
ORDER BY SEQ
FOR XML PATH('')
) , 1 , 1 , '')
-- set the pivot query to get the result.
SET #DynamicQuery = N'
SELECT [DATE], ' + #ColumnName + '
FROM (
SELECT DEPT, DATE, COUNT
FROM table_name
) st PIVOT(
SUM([COUNT])
FOR [DEPT] IN (' + #ColumnName + ')
) pt
ORDER BY [DATE]'
-- execute the dynamic query.
EXEC sp_executesql #DynamicQuery
demo on dbfiddle.uk
In case the order of the columns doesn't matter you can use the following solution:
-- pivot query without specific order of columns.
SELECT DATE, [8], [3], [1]
FROM (
SELECT DEPT, DATE, COUNT
FROM table_name
) st PIVOT (
SUM(COUNT)
FOR DEPT IN ([8], [3], [1])
) pt
ORDER BY [DATE];

Group Pivot results

After a longtime of googling and figuring out how to put my data in a decent pivot table with dynamic row headers I got to this point.
The only thing I can't figure out is how to group the results by [Location] and how to replace NULL by 'zero' / 0?
To replace NULL by 0 I tried ISNULL() and COALESCE() in this line, but it doesnt change the NULL:
SELECT COALESCE(ROUND(CAST([Remaining Quantity] AS decimal (2,0)), 1),0) AS [Remaining QuantityRound], *
or
SELECT ISNULL(ROUND(CAST([Remaining Quantity] AS decimal (2,0)), 1),0) AS [Remaining QuantityRound], *
The SQL query I have now:
DECLARE #DynamicPivotQuery AS NVARCHAR(MAX)
DECLARE #ColumnName AS NVARCHAR(MAX)
declare #item varchar(max);
declare #open varchar(max);
set #item = 291557
set #open = 1
--Get distinct values of the PIVOT Column
SELECT #ColumnName= ISNULL(#ColumnName + ',','')
+ QUOTENAME([Size])
FROM (SELECT DISTINCT [Size] FROM [Table] WHERE [Item] = #item AND [Open] = #open) AS Items
--Prepare the PIVOT query using the dynamic
SET #DynamicPivotQuery =
'SELECT [Location], ' + #ColumnName + '
FROM
(SELECT ROUND(CAST([Quantity] AS decimal (2,0)), 1) AS [QuantityRound], * FROM [Table]
WHERE [Item] = ''' + #item + ''' AND [Open] = ''' + #open + ''') x
PIVOT(SUM([QuantityRound])
FOR [Size] IN (' + #ColumnName + ')) AS PVTTable'
--Execute the Dynamic Pivot Query
EXEC sp_executesql #DynamicPivotQuery
Result:
Location S M L
001 1 NULL NULL
001 NULL 1 NULL
002 NULL NULL 2
002 NULL 1 NULL
What I would like to achieve:
Location S M L
001 1 1 0
002 0 1 2
Remove the * in the subquery:
(SELECT ROUND(CAST([Quantity] AS decimal (2,0)), 1) AS [QuantityRound], SIZE, LOCATION FROM [Table]
The extra columns are causing the extra rows.