sql turn dynamic rows into columns - sql

Subject: Issues and their tasks.
Environment: SQL Server 2008 or above
Database tables: Issues, Tasks, and IssuesTasks
Let's say I have a single input screen that deals with a single issue and their associated tasks.
We're dealing with Issue1 and there are 7 tasks listed to check off.
The user checks 3 of the 7 tasks as completed and saves to database.
Is it possible to write a SQL that shows Issue1 with the 7 tasks on the same row? (Keep in mind only 3 were checked, so the others should be null).
Also note, there are only 3 tasks in the IssuesTasks join table representing what the user checked.

Use SQL server build in PIVOT function:
SELECT <non-pivoted column>,
[first pivoted column] AS <column name>,
[second pivoted column] AS <column name>,
...
[last pivoted column] AS <column name>
FROM
(<SELECT query that produces the data>)
AS <alias for the source query>
PIVOT
(
<aggregation function>(<column being aggregated>)
FOR
[<column that contains the values that will become column headers>]
IN ( [first pivoted column], [second pivoted column],
... [last pivoted column])
) AS <alias for the pivot table>
<optional ORDER BY clause>;
You can use the PIVOT and UNPIVOT relational operators to change a table-valued expression into another table. PIVOT rotates a table-valued expression by turning the unique values from one column in the expression into multiple columns in the output, and performs aggregations where they are required on any remaining column values that are wanted in the final output. UNPIVOT performs the opposite operation to PIVOT by rotating columns of a table-valued expression into column values.
Simple AdventureWorks example:
-- Pivot table with one row and five columns
SELECT 'AverageCost' AS Cost_Sorted_By_Production_Days,
[0], [1], [2], [3], [4]
FROM
(SELECT DaysToManufacture, StandardCost
FROM Production.Product) AS SourceTable
PIVOT
(
AVG(StandardCost)
FOR DaysToManufacture IN ([0], [1], [2], [3], [4])
) AS PivotTable;
More complex example:
USE AdventureWorks2008R2;
GO
SELECT VendorID, [250] AS Emp1, [251] AS Emp2, [256] AS Emp3, [257] AS Emp4, [260] AS Emp5
FROM
(SELECT PurchaseOrderID, EmployeeID, VendorID
FROM Purchasing.PurchaseOrderHeader) p
PIVOT
(
COUNT (PurchaseOrderID)
FOR EmployeeID IN
( [250], [251], [256], [257], [260] )
) AS pvt
ORDER BY pvt.VendorID;
For more information see here

While you've not provided schema for your tables, it's not easy to write you exact query, but take a look at this
select
I.Id,
I.Name,
max(case when IT.TaskId = 1 then T.Name end) as Task1,
max(case when IT.TaskId = 2 then T.Name end) as Task2,
max(case when IT.TaskId = 3 then T.Name end) as Task3,
max(case when IT.TaskId = 4 then T.Name end) as Task4,
max(case when IT.TaskId = 5 then T.Name end) as Task5,
max(case when IT.TaskId = 6 then T.Name end) as Task6,
max(case when IT.TaskId = 7 then T.Name end) as Task7
from Issues as I
left outer join IssuesTasks as IT on IT.IssueId = I.Id
left outer join Tasks as T on T.Id = IT.TaskID
group by I.Id, I.Name;
See SQL FIDDLE example

Please excuse me if this isn't kosher; I'm still getting acclimated with the rules around here (long time reader of stackoverflow, first day posting). I actually just wrote an article on this on my new blog, and I sincerely think it would help. Basically, you can dynamically build your pivoted column values and pass them in to a dynamically-built PIVOT query like this:
IF (OBJECT_ID('tempdb..#TEMP') is not null) DROP TABLE #TEMP
DECLARE #cols NVARCHAR(2000)
SELECT DISTINCT DATE
INTO #TEMP
FROM T_EMPLOYEE_PRODUCTIVITY
SELECT #cols = ISNULL(#cols + ',', '') + '[' + CONVERT(NVARCHAR, DATE) + ']'
FROM #TEMP
ORDER BY DATE
SELECT #cols
DECLARE #query NVARCHAR(4000)
SET #query = 'SELECT EMPLOYEE_NAME, ' + #cols +
'FROM
(
SELECT EMPLOYEE_NAME, DATE, UNITS
FROM T_EMPLOYEE_PRODUCTIVITY
) AS SourceTable
PIVOT
(
SUM(UNITS)
FOR DATE IN ('+ #cols + ')
) AS PivotTable
ORDER BY EMPLOYEE_NAME'
SELECT #query
EXECUTE(#query)
If you need a more detailed explanation with sample data, check it out here: http://thrillhouseblog.blogspot.com/2013/08/dynamic-pivot-query-in-tsql-microsoft.html
I hope this helps!

Related

SQL Server use column value as column names and convert to json array

Let's say I have the following table ProductValues:
ProductID
Name
Value
1
Market
A
1
Customer
B
2
Market
C
2
Customer
D
I'm able to group them by their ProductID and get these values as an array with the following code:
SELECT
(
SELECT Name, Value FROM ProductValues
WHERE P.ID = ProductID
FOR JSON PATH
)
FROM #ProductIDs P '#ProductIDs is a table containing the productIDs that Id like to retrieve'
This returns the following:
(No column name)
[{"Name":"Market","Value":"A"},{"Name":"Customer","Value":"B"}]
[{"Name":"Market","Value":"C"},{"Name":"Customer","Value":"D"}]
I would like to dynamically create key value pairs using Pivot. I want to achieve the following:
(No column name)
[{"Market":"A"},{"Customer":"B"}]
[{"Market":"C"},{"Customer":"D"}]
Looking at another answer, I tried the following, but this doesn't set the keys dynamically and won't execute (states that "Value" and "TechName" in the Pivot are undefined):
SELECT(
SELECT Market, Customer
FOR JSON PATH
)
FROM(
SELECT(
SELECT Name, Value FROM ProductValues
WHERE ProductID = P.ID
)
FROM #ProductIDs P
) t
PIVOT(
MAX(Value) '<--- "Value" Undefined'
FOR Name IN ( '<--- "Name" Undefined'
Market, Customer
)
) AS pvt
GROUP BY
Market, Customer
You can pivot with conditional aggregation, the convert to JSON:
select (
select
max(case when name = 'Market' then value end) as market,
max(case when name = 'Customer' then value end) as customer
from productvalues pv
where pv.productid = p.productid
for json path
) as js
from #ProductIDs p
Here is a demo on DB Fiddle.
SQL Server is declarative by design. If you are looking for dynamic columns, you will need DYNAMIC SQL.
Example
Declare #sql nvarchar(max) = stuff( (Select Distinct ','+QUOTENAME(Name) From ProductValues FOR XML PATH('')),1,1,'')
SET #sql = 'Select B.*
From (
SELECT '+#sql+'
FROM ProductValues
PIVOT (max([Value]) FOR [Name] IN ('+#sql+')) AS pvt
) A
Cross Apply ( (Select A.* for json path ) ) B (JSONData)'
exec(#sql)
Returns
JSONData
[{"Customer":"B","Market":"A"}]
[{"Customer":"D","Market":"C"}]

Get Pivot values on specific condition

I have query in SQL SEVRER -
SELECT alarm,annual_calendar,chronograph,day_flag,equation_of_time,flyback_chronograph,
gmt_time,jumping_hour,minute_repeater,moon_phase,perpetual_calendar,power_reserve,seconds_flag
,split_seconds_chrono,tachymeter,tourbillon,ultra_thin,world_time,catalog_item_id
FROM catalog_item_watches where alarm is not null
order by id desc
which return output like
And i want result like
Thanks in advance.
First make UNPIVOT, then exclude the N values. After that make a PIVOT. You may want to make it dynamic.
It should be something like this:
SELECT *
FROM
(
SELECT [catalog_item_id]
,[column]
,'Web' + CAST(DENSE_RANK() OVER(PARTITION BY [catalog_item_id] ORDER BY [column]) AS VARCHAR(12))
FROM
(
SELECT [catalog_item_id], [alarm], [annual_calendar], [chronograph], [day_flag], [equation_of_time], [flyback_chronograph], [ gmt_time], [jumping_hour], [minute_repeater], [moon_phase], [perpetual_calendar], [power_reserve], [seconds_flag], [split_seconds_chrono], [tachymeter], [tourbillon], [ultra_thin], [world_time]
FROM catalog_item_watches
where [alarm] is not null
) DS
UNPIVOT
(
[value] FOR [column] IN ([alarm], [annual_calendar], [chronograph], [day_flag], [equation_of_time], [flyback_chronograph], [ gmt_time], [jumping_hour], [minute_repeater], [moon_phase], [perpetual_calendar], [power_reserve], [seconds_flag], [split_seconds_chrono], [tachymeter], [tourbillon], [ultra_thin], [world_time])
) UNPVT
WHERE UNPVT.[value] = 'Y'
) DataSource ([catalog_item_id], [column_value], [column_name])
PIVOT
(
MAX([column_value]) FOR [column_name] IN ([web1], [web2], [web3], [web4], [web5], [web6], [web7], [web8], [web9], [web10], [web11], [web12], [web13], [web14], [web15], [web16], [web17], [web18])
) PVT

How to pivot multiple columns without aggregation

I use SqlServer and i have to admit that i'm not realy good with it ...
This might be and easy question for the advanced users (I hope)
I have two tables which look like this
First table (ID isn't the primary key)
ID IdCust Ref
1 300 123
1 300 124
2 302 345
And the second (ID isn't the primary key)
ID Ref Code Price
1 123 A 10
1 123 Y 15
2 124 A 14
3 345 C 18
In the second table, the column "Ref" is the foreign key of "Ref" in the first table
I'm trying to produce the following output:
[EDIT]
The column "Stock", "Code" and "Price" can have x values, so I don't know it, in advance...
I tried so many things like "PIVOT" but it didn't give me the right result, so i hope someone can solve my problem ...
Use row_number() function and do the conditional aggregation :
select id, IdCust, Ref,
max(case when Seq = 1 then stock end) as [Stock A], -- second table *id*
max(case when Seq = 1 then code end) as [Code 1],
max(case when Seq = 1 then price end) as [Price1],
max(case when Seq = 2 then stock end) as [Stock B], -- second table *id*
max(case when Seq = 2 then code end) as [Code 2],
max(case when Seq = 2 then price end) as [Price2]
from (select f.*, s.Id Stock, s.Code, s.Price,
row_number() over (partition by f.Ref order by s.id) as Seq
from first f
inner join second s on s.Ref = f.Ref
) t
group by id, IdCust, Ref;
However, this would go with known values else you would need go with dynamic solution for that.
#YogeshSharma's provided an excellent answer.
Here's the same done using Pivot; SQL Fiddle Demo.
Functionally there's no difference between the two answers. However, Yogesh's solution's simpler to understand, and performs better; so personally I'd opt for that... I included this answer only because you mention PIVOT in the question:
select ft.Id
, ft.IdCust
, ft.Ref
, x.Stock1
, x.Code1
, x.Price1
, x.Stock2
, x.Code2
, x.Price2
from FirstTable ft
left outer join (
select Ref
, max([Stock1]) Stock1
, max([Stock2]) Stock2
, max([Code1]) Code1
, max([Code2]) Code2
, max([Price1]) Price1
, max([Price2]) Price2
from
(
select Ref
, Id Stock
, Code
, Price
, ('Stock' + cast(Row_Number() over (partition by Ref order by Id, Code) as nvarchar)) StockLineNo
, ('Code' + cast(Row_Number() over (partition by Ref order by Id, Code) as nvarchar)) CodeLineNo
, ('Price' + cast(Row_Number() over (partition by Ref order by Id, Code) as nvarchar)) PriceLineNo
from SecondTable
) st
pivot (max(Stock) for StockLineNo in ([Stock1],[Stock2])) pvtStock
pivot (max(Code) for CodeLineNo in ([Code1],[Code2])) pvtCode
pivot (max(Price) for PriceLineNo in ([Price1],[Price2])) pvtPrice
Group by Ref
) x
on x.Ref = ft.Ref
order by ft.Ref
Like Yogesh's solution, this will only handle as many columns as you specify; it won't dynamically alter the number of columns to match the data. For that you'd need to do dynamic SQL. However; if you need to do that, it's more likely you're attempting to solve the problem in the wrong way... so consider your design / determine if you really need additional columns per result rather than additional rows / some alternate approach...
Here's a Dynamic SQL implementation based on #YogeshSharma's answer: DBFiddle
declare #sql nvarchar(max) = 'select id, IdCust, Ref'
select #sql = #sql + '
,max(case when Seq = 1 then stock end) as [Stock' + rowNumVarchar + ']
,max(case when Seq = 1 then code end) as [Code' + rowNumVarchar + ']
,max(case when Seq = 1 then price end) as [Price' + rowNumVarchar + ']
'
from
(
select distinct cast(row_number() over (partition by ref order by ref) as nvarchar) rowNumVarchar
from second s
) z
set #sql = #sql + '
from (select f.*, s.Id Stock, s.Code, s.Price,
row_number() over (partition by f.Ref order by s.id) as Seq
from first f
inner join second s on s.Ref = f.Ref
) t
group by id, IdCust, Ref;
'
print #sql --see what the SQL produced is
exec (#sql)
(Here's a SQL Fiddle link for this one; but it's not working despite the SQL being valid

Dynamic SubSelects in SQL Select Statement

I am querying a table for some basic information, file number, case type, status, etc. In addition I need a column for every single one of 138 case status types that will display the date the case had that status. Here is a sample:
SELECT FileNum,
CaseType,
CurrentCaseStatus,
(SELECT TOP 1 EventDt FROM caseStatusHistory WHERE CaseID = c.caseID AND CaseStatus = 'CS001' ORDER BY EventDt DESC) AS [Charge - Phone],
(SELECT TOP 1 EventDt FROM caseStatusHistory WHERE CaseID = c.caseID AND CaseStatus = 'CS002' ORDER BY EventDt DESC) AS [Charge - Written],
-- 136 more just like the live above
FROM Case c
I can query another table for all the case status types:
SELECT Code, Description
FROM caseStatus
WHERE Code BETWEEN 'CS001' AND 'CS138'
ORDER BY Code
How can I dynamically create each of those columns instead of having to manually write 138 select statements?
That's going to be terribly slow -- 138 correlated subqueries. I think you can achieve the same result with an OUTER JOIN and a GROUP BY with MAX and CASE:
Select c.filenum,
c.casetype,
c.currentcasestatus,
max(case when csh.CaseStatus = 'CS001' then EventDt end) as [Charge - Phone],
max(case when csh.CaseStatus = 'CS002' then EventDt end) as [Charge - Written]
from case c
left join casestatushistory csh on c.caseid = csh.caseid
group by c.filenum,
c.casetype,
c.currentcasestatus
BTW, I would suggest just writing the statement out -- it won't take that long and it will out perform a dynamic sql approach. I'm not completely sure how you'd get your column names with dynamic sql either unless Phone and Written are in another column.
Try using a PIVOT. The SQL below should work -
--Select the pivot data into a temp table
SELECT c.caseID,
c.FileNum,
c.CaseType,
c.CurrentCaseStatus,
csh.EventDt,
cs.Description
INTO #StatusDates
FROM [Case] c
LEFT JOIN caseStatusHistory csh
ON csh.caseID = c.caseID
LEFT JOIN caseStatus cs
ON cs.Code = csh.CaseStatus
--From the pivot data, get the list of field names (assumes description field is the source for the field name)
DECLARE #statusDescriptions VARCHAR(MAX)
SET #statusDescriptions = ''
SELECT #statusDescriptions = COALESCE(#statusDescriptions+'[','') + Description
FROM (
SELECT DISTINCT Description
FROM #StatusDates
WHERE Description IS NOT NULL
) x
SET #statusDescriptions = REPLACE(#statusDescriptions, '[', '],[') + ']'
SET #statusDescriptions = SUBSTRING(#statusDescriptions, 3, LEN(#statusDescriptions))
--Create a SQL statement to pivot the data into the fields.
DECLARE #sql VARCHAR(MAX)
SET #SQL = '
SELECT *
FROM #StatusDates
PIVOT(MIN(EventDt)
FOR Description IN (' + #statusDescriptions + '))
AS PVTTable '
PRINT #sql
EXEC(#sql)
DROP TABLE #StatusDates

Using pivot table with column and row totals in sql server 2008

I have a table with following columns
defect_id, developer_name, status, summary, root_cause,
Secondary_RC, description, Comments, environment_name
The column root_cause has Enviro, Requi, Dev, TSc, TD, Unkn as its values and
column environment_name has QA1, QA2, QA3
I need to prepare a report in the below format
Enviro Requi Dev TSc TD Unkn Total
QA1 9 1 14 17 2 3 46
QA2 8 1 14 0 5 1 29
QA3 1 1 7 0 0 1 10
Total 18 3 35 17 7 5 85
I have prepare the report till
Enviro Requi Dev TSc TD Unkn
QA1 9 1 14 17 2 3
QA2 8 1 14 0 5 1
QA3 1 1 7 0 0 1
I used the below query to get the above result
select *
from
(
select environment_name as " ", value
from test1
unpivot
(
value
for col in (root_cause)
) unp
) src
pivot
(
count(value)
for value in ([Enviro] , [Requi] , [Dev] , [Tsc], [TD] , [Unkn])
) piv
Can anyone help to get the totals for columns and rows?
There may be various approaches to this. You can calculate all the totals after the pivot, or you can get the totals first, then pivot all the results. It is also possible to have kind of middle ground: get one kind of the totals (e.g. the row-wise ones), pivot, then get the other kind, although that might be overdoing it.
The first of the mentioned approaches, getting all the totals after the pivot, could be done in a very straightforward way, and the only thing potentially new to you in the below implementation might be GROUP BY ROLLUP():
SELECT
[ ] = ISNULL(environment_name, 'Total'),
[Enviro] = SUM([Enviro]),
[Requi] = SUM([Requi]),
[Dev] = SUM([Dev]),
[Tsc] = SUM([Tsc]),
[TD] = SUM([TD]),
[Unkn] = SUM([Unkn]),
Total = SUM([Enviro] + [Requi] + [Dev] + [Tsc] + [TD] + [Unkn])
FROM (
SELECT environment_name, root_cause
FROM test1
) s
PIVOT (
COUNT(root_cause)
FOR root_cause IN ([Enviro], [Requi], [Dev], [Tsc], [TD], [Unkn])
) p
GROUP BY
ROLLUP(environment_name)
;
Basically, the GROUP BY ROLLUP() part produces the Total row for you. The grouping is first done by environment_name, then the grand total row is added.
To do just the opposite, i.e. get the totals prior to pivoting, you could employ GROUP BY CUBE() like this:
SELECT
[ ] = environment_name,
[Enviro] = ISNULL([Enviro], 0),
[Requi] = ISNULL([Requi] , 0),
[Dev] = ISNULL([Dev] , 0),
[Tsc] = ISNULL([Tsc] , 0),
[TD] = ISNULL([TD] , 0),
[Unkn] = ISNULL([Unkn] , 0),
Total = ISNULL(Total , 0)
FROM (
SELECT
environment_name = ISNULL(environment_name, 'Total'),
root_cause = ISNULL(root_cause, 'Total'),
cnt = COUNT(*)
FROM test1
WHERE root_cause IS NOT NULL
GROUP BY
CUBE(environment_name, root_cause)
) s
PIVOT (
SUM(cnt)
FOR root_cause IN ([Enviro], [Requi], [Dev], [Tsc], [TD], [Unkn], Total)
) p
;
Both methods can be tested and played with at SQL Fiddle:
Method 1
Method 2
Note. I've omitted the unpivoting step in both suggestions because unpivoting a single column seemed clearly redundant. If there's more to it, though, adjusting either of the queries should be easy.
You can find Total for root_cause and environment_name using ROLLUP.
RNO_COLTOTAL - Logic to place Total in last column, since the columns Tsc,Unkn will overlap the column Total when pivoting, since its ordering alphabetically.
RNO_ROWTOTAL - Logic to place Total in last row since a value that is starting with U,W,X,Y,Z can overlap the value Total, since its ordering alphabetically.
SUM(VALUE) - Can define on what aggregate function we can use with ROLLUP.
QUERY 1
SELECT CASE WHEN root_cause IS NULL THEN 1 ELSE 0 END RNO_COLTOTAL,
CASE WHEN environment_name IS NULL THEN 1 ELSE 0 END RNO_ROWTOTAL,
ISNULL(environment_name,'Total')environment_name,
ISNULL(root_cause,'Total')root_cause,
SUM(VALUE) VALUE
INTO #NEWTABLE
FROM
(
-- Find the count for environment_name,root_cause
SELECT DISTINCT *,COUNT(*) OVER(PARTITION BY environment_name,root_cause)VALUE
FROM #TEMP
)TAB
GROUP BY root_cause,environment_name
WITH CUBE
We will get the following logic when CUBE is used
We declare variables for pivoting.
#cols - Column values for pivoting.
#NulltoZeroCols - Replace null values with zero.
QUERY 2
DECLARE #cols NVARCHAR (MAX)
SELECT #cols = COALESCE (#cols + ',[' + root_cause + ']',
'[' + root_cause + ']')
FROM (SELECT DISTINCT RNO_COLTOTAL,root_cause FROM #NEWTABLE) PV
ORDER BY RNO_COLTOTAL,root_cause
DECLARE #NulltoZeroCols NVARCHAR (MAX)
SET #NullToZeroCols = SUBSTRING((SELECT ',ISNULL(['+root_cause+'],0) AS ['+root_cause+']'
FROM(SELECT DISTINCT RNO_COLTOTAL,root_cause FROM #NEWTABLE GROUP BY RNO_COLTOTAL,root_cause)TAB
ORDER BY RNO_COLTOTAL FOR XML PATH('')),2,8000)
Now pivot it dynamically
DECLARE #query NVARCHAR(MAX)
SET #query = 'SELECT environment_name,'+ #NulltoZeroCols +' FROM
(
SELECT RNO_ROWTOTAL,environment_name,root_cause,VALUE
FROM #NEWTABLE
) x
PIVOT
(
MIN(VALUE)
FOR [root_cause] IN (' + #cols + ')
) p
ORDER BY RNO_ROWTOTAL,environment_name;'
EXEC SP_EXECUTESQL #query
RESULT
I think you need to calculate the Total separately. Using this simple query for the total (sorry, had to give the alias name for your " " column):
select environment_name as en,
count (*) AS Total
FROM test1
WHERE value in ('Enviro', 'Requi', 'Dev', 'Tsc', 'TD', 'Unkn')
GROUP BY environment_name
you can easily join both queries together to get the required report:
SELECT * FROM
(select *
from
(
select environment_name as en, value
from test1
unpivot
(
value
for col in (root_cause)
) unp
) src
pivot
(
count(value)
for value in ([Enviro] , [Requi] , [Dev] , [Tsc], [TD] , [Unkn])
) piv
) AS a
INNER JOIN
( select environment_name as en,
count (*) AS Total
FROM test1
WHERE value in ('Enviro', 'Requi', 'Dev', 'Tsc', 'TD', 'Unkn')
GROUP BY environment_name
) AS b ON a.en = b.en
UNION ALL
SELECT * FROM
(select *
from
(
select 'Total' as en, value
from test1
unpivot
(
value
for col in (root_cause)
) unp
) src
pivot
(
count(value)
for value in ([Enviro] , [Requi] , [Dev] , [Tsc], [TD] , [Unkn])
) piv
) AS a
INNER JOIN
( select 'Total' as en,
count (*) AS Total
FROM test1
WHERE value in ('Enviro', 'Requi', 'Dev', 'Tsc', 'TD', 'Unkn')
) AS b
I have not tested it, but believe it will work
You can also put you select pivot, without totals, in a temp table and then add the totalS with a new select:
SELECT environment_name=ISNULL(environment_name, ‘Total’) , Enviro=SUM(Enviro), Requi=SUM(Requi), Dev=SUM(Dev), TSc=SUM(TSc), TD=SUM(TD), Unkn =SUM(Unkn),
Total = sum(Enviro+Requi+Dev+TSc+TD+Unkn)
FROM #temp
GROUP BY ROLLUP(environment_name)