TSQL how to specify column list range for PIVOT? - sql-server

The SELECT statement below is from a stored procedure I am using to retrieve data which is subsequently sent to JSON:
SELECT *
FROM (
SELECT CAST(DateTimeUTC as SmallDateTime) as [DateTime], DataValue, VariableID
FROM DataValues
WHERE SiteID = #siteID and VariableID BETWEEN 9 and 10 and DateTimeUTC >= DATEADD (day, #pastDays, getdate())
) TableDate
PIVOT (SUM(DataValue) FOR VariableID IN ([9],[10])) PivotTable ORDER BY [DateTime]
What I would like to accomplish is to modify the procedure to accept a column range to pivot. In the above example I am only retrieving values for two variables. What if I want to retrieve VariableID 1 through 10, or 1 through 50? There has to be a way to retrieve 1 through 10 in a way other than:
VariableID IN ([1],[2],[3],[4],[5],[6],[7],[8],[9],[10])
The Using PIVOT and UNPIVOT on MSDN doesn't mention a way to specify a range.
I realize that it is possible to use dynamic SQL in a stored procedure, but given my own knowledge of SQL and the knowledge of those who will have to maintain this long term I am hesitant to introduce additional complexity by using dynamic SQL. There is a conceptually similar question on here, see TSQL Pivot Long List of Columns, and it was answered with a dynamic SQL solution.

Unfortunately, the PIVOT function does not have the ability to generate the list of columns, without using dynamic sql.
So your SQL code will be similar to this:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(VariableID)
from DataValues
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT [DateTime], ' + #cols + ' from
(
SELECT CAST(DateTimeUTC as SmallDateTime) as [DateTime],
DataValue,
VariableID
FROM DataValues
WHERE SiteID = '+cast(#siteID as varchar(10))+'
-- and VariableID BETWEEN 9 and 10 -- remove the variableID filter
and DateTimeUTC >= DATEADD (day, +'cast(#pastDays as varchar(10))+', getdate())
) x
pivot
(
SUM(DataValue)
for VariableID in (' + #cols + ')
) p '
execute(#query)
The key to this is the following code which generates the list of variableIds to become columns:
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(VariableID)
from DataValues
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
This will create the list of all distinct variable id's. This list is then added to the SQL query string to return.

Related

SQL Server : pivot table from Dynamic SQL first time with pivot

I am trying to create a pivot table that lists the time frames (responseText) as columns with the average answer for each one under that particular column based on the question asked. I am trying to do this with a PIVOT function - to no avail.
I have the following table
I first create a temp table and populate it with values from a stored procedure, then I am building a dynamic query
CREATE TABLE #Data
(
QuestionText varchar(400),
ResponseText varchar(200),
AverageAnswer float
)
INSERT INTO #Data
EXECUTE AverageDemographicGroupAnswer #GroupID, #Survey, #Demographic
--SELECT * FROM #Data
DECLARE #PivotQuery NVARCHAR(MAX)
DECLARE #Cols varchar(max)
SET #Cols = STUFF((SELECT distinct ',' + QUOTENAME(ResponseText,'[]') FROM dbo.SurveyResponses WHERE QuestionID = #Demographic FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'),1,1,'')
--SELECT #Cols
SET #PivotQuery = 'SELECT QuestionText, '+ #Cols +' FROM
(
SELECT QuestionText, ResponseText, AverageAnswer FROM #Data
)x
PIVOT
( AverageAnswer FOR ResponseText in (' + #Cols + ')
)p'
--select #PivotQuery
EXECUTE(#PivotQuery)
DROP TABLE #Data
Which produces this query
SELECT QuestionText, [At least 1 year but less than 3 years],[At least 3 years but less than 5 years],[Less than 1 year],[More than 5 years] FROM (SELECT QuestionText, AverageAnswer, ResponseText FROM #Data) AS SourceTable
PIVOT( AverageAnswer FOR ResponseText in ([At least 1 year but less than 3 years],[At least 3 years but less than 5 years],[Less than 1 year],[More than 5 years])) AS PivotTable
but it is returning this error
Incorrect syntax near the keyword 'FOR'.
This is my first time creating a pivot and I am not sure what is wrong

Issue adding order by to PIVOT group by SQL

The SQL Below works gr8, but the order, how to set that to put Jan, Feb, Mar order. I realize I need to do this with ordinal month but when I add in the order by I get error 'The ORDER BY clause is invalid in views, inline functions, derived tables, subqueries, and common table expressions, unless TOP, OFFSET or FOR XML is also specified.'
The results look like :
DECLARE #cols AS NVARCHAR(MAX)
DECLARE #query AS NVARCHAR(MAX)
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(year(TransactionDateTime))
FROM Quotations
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SET #query =
'SELECT *
FROM (
SELECT
left(datename(month,TransactionDateTime),3) as [month], year(TransactionDateTime) as [year],
isnull(count(*),0) as Total
FROM quotations
group by left(datename(month,TransactionDateTime),3), year(TransactionDateTime)
) as s
PIVOT
(
SUM(Total)
FOR [year] IN (' + #cols + ')
) AS QuotationResults'
EXECUTE(#query)
As the error says, you can't order a subquery unless there is a reason for the order (TOP, FOR XML etc). The reason for this is that just because you have ordered the subquery there is no reason that this order would be maintained in your outer query. SQL Server is essentially telling you that your ORDER BY is pointless, therefore not valid.
The solution is to simply add a column with month number to your subuquery s, then you can order by it. You would also need to explicitly state your select list to ensure that this new column does not appear in it:
DECLARE #cols AS NVARCHAR(MAX);
DECLARE #query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(year(TransactionDateTime))
FROM Quotations
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'');
SET #query =
'SELECT [month], ' + #Cols + '
FROM (
SELECT
left(datename(month,TransactionDateTime),3) as [month],
datepart(month,TransactionDateTime) as [monthNum],
year(TransactionDateTime) as [year],
isnull(count(*),0) as Total
FROM quotations
group by left(datename(month,TransactionDateTime),3), datepart(month,TransactionDateTime), year(TransactionDateTime)
) as s
PIVOT
(
SUM(Total)
FOR [year] IN (' + #cols + ')
) AS QuotationResults
ORDER BY QuotationResults.MonthNum;';
EXECUTE(#query);
ADDENDUM
The ISNULL() does not trap the null values because at the point of using ISNULL() they don't exist. COUNT(*) will never return null, so your ISNULL() is actually redundant.
In a very simple example if you have:
TransactionDateTime
----------------------
2015-01-01
2015-02-01
2015-02-01
2014-03-01
To skip ahead one step, after your pivot you will end up with:
Month 2014 2015
------------------------
Jan NULL 1
Feb NULL 2
Mar 1 NULL
So you end up with NULL values, now to go back a step, if you look at the results after your aggregation you have:
Month MonthNum Year Total
-----------------------------------
Jan 1 2015 1
Feb 2 2015 2
Mar 3 2014 1
So there are no rows for Jan or Feb in 2014, therefore SUM(NULL) will yield NULL. I would suggest leaving all the aggregation to the pivot function. So your non dynamic query would look something like:
SELECT pvt.[Month], pvt.[2014], pvt.[2015]
FROM ( SELECT [Month] = LEFT(DATENAME(MONTH, TransactionDateTime), 3),
[MonthNum] = DATEPART(MONTH, TransactionDateTime),
[Year] = DATEPART(YEAR, TransactionDateTime),
Value = 1
FROM Quotations
) AS t
PIVOT
(
COUNT(Value)
FOR [year] IN ([2014], [2015])
) AS pvt
ORDER BY pvt.MonthNum;
And put into dynamic SQL:
DECLARE #cols AS NVARCHAR(MAX);
DECLARE #query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT DISTINCT ',' + QUOTENAME(DATEPART(YEAR, TransactionDateTime))
FROM Quotations
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'');
SET #query =
'SELECT pvt.[Month], ' + #cols + '
FROM ( SELECT [Month] = LEFT(DATENAME(MONTH, TransactionDateTime), 3),
[MonthNum] = DATEPART(MONTH, TransactionDateTime),
[Year] = DATEPART(YEAR, TransactionDateTime),
Value = 1
FROM Quotations
) AS t
PIVOT
(
COUNT(Value)
FOR [year] IN (' + #cols + ')
) AS pvt
ORDER BY pvt.MonthNum;
(
SUM(Total)
FOR [year] IN (' + #cols + ')
) AS QuotationResults
ORDER BY QuotationResults.MonthNum;';
EXECUTE sp_executesql #query;

How to Group on Pivoted Table with Dynamic Rows?

I have a dataset like this
[student][datetime][dictionary][value]
[1234 ][datetime][1 ][a ]
[1234 ][datetime][2 ][b ]
I want to create a dataset like this
[student][1 ][2 ]
[1234 ][a ][b ]
I understand that SQL Server pivots are a way to accomplish this. I've written the below code to attempt to pivot the data however, it's naturally not grouped.
So my results end up like this:
[student][datetime][1 ][2 ]
[1234 ][datetime][null ][b ]
[1234 ][datetime][a ][null ]
How can I best group these? I don't care about having the other source columns such as datetime in the final dataset, only the matrix of dictionaries and values
Thank you
DECLARE #columns AS VARCHAR(MAX);
DECLARE #sql AS VARCHAR(MAX);
SELECT #columns = substring((Select DISTINCT ',' + QUOTENAME(dictionary) FROM syuservalues FOR XML PATH ('')),2, 1000);
SELECT #sql =
'SELECT Pivoted.*
FROM syuservalues
PIVOT
( MAX(value)
FOR dictionary IN( ' + #columns + ' )) as Pivoted where student = 327392'; ' Will eventually want it for all students
EXECUTE(#sql);
a and b is brought to two rows because, you have added [datetime] column for pivot and the [datetime] value for a and b will be different.
SAMPLE TABLE
CREATE TABLE #TEMP([student] INT,[datetime] DATETIME,[dictionary] INT,[value] VARCHAR(30))
INSERT INTO #TEMP
SELECT 1234,'2015-02-01',1,'a'
UNION ALL
SELECT 1234, '2015-02-01',2,'b'
Declare a variable to get columns for pivot dynamically
DECLARE #cols NVARCHAR (MAX)
SELECT #cols = COALESCE (#cols + ',[' + CAST([dictionary] AS VARCHAR(10)) + ']',
'[' + CAST([dictionary] AS VARCHAR(10)) + ']')
FROM
(
SELECT DISTINCT [dictionary]
FROM #TEMP
) PV
ORDER BY CAST([dictionary] AS INT)
Now pivot it. I have written the logic inside.
DECLARE #query NVARCHAR(MAX)
SET #query = '-- Your pivoted result is here
SELECT *
FROM
(
-- Select data before pivot. We are not taking [datetime] column because
-- it will not bring ur expected result
SELECT student,[dictionary],value
FROM #TEMP
) x
PIVOT
(
-- Value in each dynamic column
MIN(value)
-- Tell the columns to pivot
FOR [dictionary] IN (' + #cols + ')
) p
ORDER BY student;'
EXEC SP_EXECUTESQL #query
For this kind of problem, you can just use an arbitrary aggregate function so that you get rid of the nulls. I usually use max because it can be used on both numeric and string data types.
select pivoted.student, [1] = max([1]), [2] = max([2])
from syuservalues
pivot(max(value) for dictionary in(...)) pivoted
group by pivoted.student

SQL Results data from rows to columns

I have created a query which returns me the following:
[Branch], [SaleDate], [ReceiptNo]
My [SaleDate] column is a concatenation of the SaleMonth and SaleYear in the format '01/2013'
What I require is these results to come out as a form of a table with:
A Column for each [SaleDate]
A Row for each [Branch]
The value for each Row/Column will be a COUNT([ReceiptNo])
Any pointers will be greatly appreciated.
Thanks in advance.
Use the PIVOT table operator:
SELECT *
FROM
(
SELECT ReceiptNo, SaleDate, Branch
FROM tablename
) AS t
PIVOT
(
COUNT(ReceiptNo)
FOR SaleDate IN([01/2013], [02/2013], [03/2013], ...)
) AS p;
SQL Fiddle Demo
To do this dynamically instead of listing the values of SaleDate manually, you have to use dynamic SQL, like this:
DECLARE #cols AS NVARCHAR(MAX);
DECLARE #query AS NVARCHAR(MAX);
select #cols = STUFF((SELECT distinct ',' +
QUOTENAME(SaleDate)
FROM tablename
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
, 1, 1, '');
SELECT #query = 'SELECT *
FROM
(
SELECT ReceiptNo, SaleDate, Branch
FROM tablename
) AS t
PIVOT
(
COUNT(ReceiptNo)
FOR SaleDate IN(' + #cols + ')) AS p;';
execute(#query);
Updated SQL Fiddle Demo

TSQL Pivot Long List of Columns

I am looking to use pivot function to convert row values of a column into separate columns. There are 100+ distinct values in that column and hard-coding each and every single value in the 'for' clause of the pivot function would be very time consuming and not good from maintainability purposes. I was wondering if there is any easier way to tackle this problem?
Thanks
You can use Dynamic SQL in a PIVOT for this type of query. Dynamic SQL will get the list of the items that you want to transform on execution which prevents the need to hard-code each item:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(c.condition_id)
FROM t c
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT memid, ' + #cols + ' from
(
select MemId, Condition_id, condition_result
from t
) x
pivot
(
sum(condition_result)
for condition_id in (' + #cols + ')
) p '
execute(#query)
See SQL Fiddle with Demo
If you post a sample of data that you need to transform, then I can adjust my query to demonstrate.
I was trying to do this and had db2 Data like this.
id,MCD1,MCD2,MCD3,MCD4
1,4,5,4,3
2,6,7,8,9
3,3,6,2,6
4,2,5,8,4
5,3,4,5,6
I wrote this Dynamic SQL in TSQL
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
SET #cols = STUFF((SELECT ',', COLUMN_NAME FROM my_db.INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME LIKE 'PivotLonger' AND COLUMN_NAME like 'MCD%'
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT id, MCD, Value
from [my_db].[dbo].[PivotLonger]
UNPIVOT
(Value FOR MCD IN (' + #cols + ')) AS unpvt'
execute(#query)
Resulted in expanding the columns into a nice long table :)
id,MCD,Value
1,MCD1,4
1,MCD2,5
1,MCD3,4
1,MCD4,3
2,MCD1,6
2,MCD2,7
2,MCD3,8
2,MCD4,9
3,MCD1,3
3,MCD2,6
3,MCD3,2
3,MCD4,6
4,MCD1,2
4,MCD2,5
4,MCD3,8
4,MCD4,4
5,MCD1,3
5,MCD2,4
5,MCD3,5
5,MCD4,6

Resources