How to Pivot on two columns in SQL Server [duplicate] - sql

This question already has an answer here:
TSQL PIVOT MULTIPLE COLUMNS
(1 answer)
Closed 4 years ago.
Below is the data rows I have:
ID Code OtherCol
7 Code1 NULL
7 code2 NULL
2 unk NULL
4 unk NULL
3 Code2 NULL
3 Code3 NULL
3 Code5 Other1
5 Code4 NULL
5 Code5 Other2
I am trying get this displayed as
ID name1 name2 name3 name4 name5 nameunk Othername
2 unk
3 code2 code3 code5 Other1
4 unk
5 code4 code5 Other2
7 code1 code2
I was able to pivot the first column but having a problem pivoting the second one.
And also there is a name for a given code, but the value under OtherCol are random.

I recommend conditional aggregation:
select id,
max(case when code = 'code1' then code end) as name1,
max(case when code = 'code2' then code end) as name2,
max(case when code = 'code3' then code end) as name3,
max(case when code = 'code4' then code end) as name4,
max(case when code = 'code5' then code end) as name5,
max(case when code = 'unk' then code end) as nameunk,
max(othercol) as othercol
from t
group by id;

This is full working example. You can change it a little bit to match your real data.
CREATE TABLE #DataSource
(
[ID] INT
,[Code] VARCHAR(12)
,[OtherCol] VARCHAR(12)
);
INSERT INTO #DataSource ([ID], [Code], [OtherCol])
VALUES (7, 'Code1', NULL)
,(7, 'code2', NULL)
,(2, 'Unk', NULL)
,(4, 'Unk', NULL)
,(3, 'Code2', NULL)
,(3, 'Code3', NULL)
,(3, 'Code5', 'Other1')
,(5, 'Code4', NULL)
,(5, 'Code4', 'Other2');
DECLARE #DynammicTSQLStatement NVARCHAR(MAX)
,#DynamicPIVOTColumns NVARCHAR(MAX);
SET #DynamicPIVOTColumns = STUFF
(
(
SELECT ',[' + CAST([value] AS VARCHAR(12)) + ']'
FROM
(
SELECT 0
,DENSE_RANK() OVER (ORDER BY [Code])
,REPLACE([Code], 'Code', 'name')
FROM #DataSource
WHERE [Code] IS NOT NULL
UNION
SELECT 1
,1
,'OtherCol'
) DS ([GroupID],[RowID], [value])
ORDER BY [GroupID], [RowID]
FOR XML PATH('') ,TYPE
).value('.', 'NVARCHAR(MAX)')
,1
,1
,''
);
SET #DynammicTSQLStatement = N'
SELECT *
FROM
(
SELECT [ID]
,[Code]
,REPLACE([Code], ''Code'', ''name'')
FROM #DataSource
UNION ALL
SELECT [ID]
,[OtherCol]
,''OtherCol''
FROM #DataSource
) DS ([ID], [value], [column])
PIVOT
(
MAX([value]) FOR [column] IN (' + #DynamicPIVOTColumns + ')
) PVT';
EXEC sp_executesql #DynammicTSQLStatement;
DROP TABLE #DataSource;

--PIVOT THE TABLE
select ID,[code1],[code2], [code3],[code4],[code5],[Unk]
into #resPivot
from
(
select ID, code
from tblTest
) src
pivot
(
max(code)
for code in ([code1], [code2], [code3],[code4],[code5],[Unk])
) piv;
--FIND ALL COLS WHERE OTHER COLUMN have value row 3,5 in your example
SELECT * INTO #distinct FROM tblTest where tblTest.otherCol IS NOT NULL
--PIVOTED RESULT WITH ABOVE TABLE
select distinct #resPivot.ID,[code1], [code2], [code3],[code4],[code5],[Unk],#distinct.otherCol
into #otherCol
from #resPivot inner join #distinct
on #distinct.id = #resPivot.id
--THIS IS PIVOTED RESULT WITH ALL RESULTS THAT HAS NO OTHER COL VALUE UNION with OTHER CALL VALUE
select distinct #resPivot.ID,[code1], [code2], [code3],[code4],[code5],[Unk],tblTest.otherCol
from #resPivot inner join tblTest
on tblTest.id = #resPivot.id
WHERE otherCol IS NULL and tblTest.ID NOT IN (SELECT ID FROM #otherCol)
UNION ALL
Select * from #otherCol
--DROP TEMP TABLES
Drop Table #resPivot
Drop Table #distinct
Drop Table #otherCol
A little simpler and faster version

Related

Sql Grouping insight Xml

I need to get different row data to 1 row based with id and need to group by its status.
I tried in several ways and finally found a solution using xml, but when I use xml I don't get the output I want.
This is a sample code I used to try the logic.
drop table #TestTable
-- Create table
CREATE TABLE #TestTable (id VARCHAR(100), title VARCHAR(100),
progress VARCHAR(100))
GO
-- Populate tableid
INSERT INTO #TestTable (id, title, progress)
SELECT '1', 'test1', 'inprogress'
UNION ALL
SELECT '1', 'test2', 'inprogress'
UNION ALL
SELECT '1', 'test3', 'completed'
UNION ALL
SELECT '1', 'test4', 'completed'
GO
SELECT id,progress, comments = STUFF((SELECT +' , '+ TITLE
FROM #TestTable AS x2 WHERE ID = x.ID
ORDER BY ID
FOR XML PATH(''), TYPE).value(N'(./text())[1]', N'varchar(max)'), 1, 1, '')
FROM #TestTable AS x
GROUP BY id,progress 
returned output:
id progress comments
1 completed , test1 , test2 , test3 , test4
1 inprogress , test1 , test2 , test3 , test4
expected output:
id progress comments
1 completed , test1 , test2
1 inprogress , test3 , test4
I think you are missing a condition in your join (progress = x.progress):
SELECT id,progress, comments = STUFF((SELECT +' , '+ TITLE
FROM #TestTable AS x2 WHERE ID = x.ID
and progress = x.progress -- <-- add this
ORDER BY ID
FOR XML PATH(''), TYPE).value(N'(./text())[1]', N'varchar(max)'), 1, 1, '')
FROM #TestTable AS x
GROUP BY id,progress
Output:

Row result manipulation

I have an existing query that retrieves this data:
Key Type TextF
--- ---- ------
1 R NULL
1 T TEST
1 T TEST2
2 R NULL
2 T FOO
3 R NULL
Scenario:
Row type R will always have a NULL on TextF. However if the Key has a type T data existing, I should place the TextF on R data, joining them with CRLF or char(13)
Expected output based on given data:
Key Type TextF
--- ---- ----------
1 R TEST TEST2
2 R FOO
3 R NULL
How can I achieve this through a query? I'm trying to make my existing query to be a subquery but I cant seem to make it work.
SELECT T0.*, *formatting here* FROM ( [myQuery] ) T0
I don't think it's the best solution but you could use the STUFF function to achieve your desired results:
SELECT t1.[Key],
'R' [Type],
STUFF((SELECT ' ' + t2.[TextF]
FROM yourTable t2
WHERE t2.[Key] = t1.[Key]
FOR XML PATH('')), 1, 1, '') [TextF]
FROM yourTable t1
GROUP BY t1.[Key]
You can use this.
DECLARE #MyTable TABLE ([Key] INT, Type VARCHAR(5), TextF VARCHAR(100))
INSERT INTO #MyTable VALUES
(1 ,'R', NULL),
(1 ,'T', 'TEST'),
(1 ,'T', 'TEST2'),
(2 ,'R', NULL),
(2 ,'T', 'FOO'),
(3 ,'R', NULL)
SELECT
T.[Key],
T.Type,
CASE WHEN Type = 'R' THEN REPLACE(STUFF(X.TextF,1,1,''),'|', CHAR(13)) ELSE T.TextF END TextF
FROM #MyTable T
OUTER APPLY( SELECT '|' + TextF FROM #MyTable T1
WHERE T.[Key] = T1.[Key]
AND T1.Type <> 'R'
AND T1.TextF IS NOT NULL FOR XML PATH('')) X(TextF)
WHERE T.Type = 'R'
Result:
Key Type TextF
----------- ----- -------------
1 R TEST
TEST2
2 R FOO
3 R NULL
In SQL Server 2017 you can use a new built-in function STRING_AGG
SELECT T0.[Key], T0.[Type],
(SELECT STRING_AGG (T1.TextF, CHAR(13)) AS TextF
FROM [myTable] T1
WHERE T1.[Type]='T' AND T1.[Key]=T0.[Key]
) TextF
FROM [myTable] T0
WHERE T0.[Type]='R'
Slightly different from other solutions--
DECLARE #MyTable TABLE ([Key] INT, Type VARCHAR(5), TextF VARCHAR(100))
INSERT INTO #MyTable VALUES
(1 ,'R', NULL),
(1 ,'T', 'TEST'),
(1 ,'T', 'TEST2'),
(2 ,'R', NULL),
(2 ,'T', 'FOO'),
(3 ,'R', NULL)
SELECT
T.[Key],
T.Type,
STUFF
((
SELECT ' ' + TextF
FROM #MyTable a
WHERE ( a.[Key] = T.[Key] )
FOR XML PATH('')
) ,1,2,'')
AS cusr
FROM #MyTable T
WHERE T.Type = 'R'
OUTPUT
Key Type cusr
----------- ----- --------------
1 R TEST TEST2
2 R FOO
3 R NULL
(3 rows affected)

SQL Transpose Data with Column Name

I am trying to transpose data in a SQL Server table with one row of data but with several columns, all into one column along with their respective column headers.
Original Data Table:
**TABLE Column Names:** Id, ColumnA , ColumnB , ColumnC , StartDate
**Data:** 1, 'aa' , 'bb' , 'cc', 2016-10-10
Required Format of Data:
**ColumnName Values**
Id 1
ColumnA aa
ColumnB bb
ColumnC cc
StartDate 2016-10-10
CREATE DATABASE ToDelete
GO
USE [ToDelete]
GO
CREATE TABLE [dbo].[sourceData](
[id] [int] NULL,
[ColumnA] [varchar](50) NULL,
[ColumnB] [varchar](50) NULL,
[ColumnC] [varchar](50) NULL,
[StartDate] [datetime] NULL
)
GO
INSERT [dbo].[sourceData] ([id], [ColumnA], [ColumnB], [ColumnC], [StartDate], [EndDate]) VALUES (1, 'aa', N'bb', N'cc', GETDATE())
GO
The query I am using to pull the table column names is:
SELECT c.name
FROM sys.tables t
JOIN sys.columns c
ON t.object_id = c.object_id
WHERE t.name = 'sourceData'
Your help would be appreciated.
Thank you
Here is an option that may help you create something more like an EAV structure
Example
Declare #YourTable Table ([Id] varchar(50),[ColumnA] varchar(50),[ColumnB] varchar(50),[ColumnC] varchar(50),[StartDate] date)
Insert Into #YourTable Values
(1,'aa','bb','cc','2016-10-10')
Select A.ID
,C.*
From #YourTable A
Cross Apply ( values (cast((Select A.* for XML RAW) as xml))) B(XMLData)
Cross Apply (
Select Field = a.value('local-name(.)','varchar(100)')
,Value = a.value('.','varchar(max)')
From B.XMLData.nodes('/row') as C1(n)
Cross Apply C1.n.nodes('./#*') as C2(a)
Where a.value('local-name(.)','varchar(100)') not in ('ID','OtherColumnsTo','Exclude')
) C
Returns
ID Field Value
1 ColumnA aa
1 ColumnB bb
1 ColumnC cc
1 StartDate 2016-10-10
Try the Following Solution, I have Referred from the following article to write this query:
https://www.red-gate.com/simple-talk/sql/t-sql-programming/questions-about-pivoting-data-in-sql-server-you-were-too-shy-to-ask/
USE [ToDelete]
GO
DECLARE #sql AS NVARCHAR(2000);DECLARE #col AS NVARCHAR(2000);
DECLARE #col1 AS NVARCHAR(2000);
SELECT #col = ISNULL(#col + ', ', '') +
concat('cast(',QUOTENAME(column_name),'as nvarchar(max))',' ',
QUOTENAME(column_name) ),#col1=ISNULL(#col1 + ', ', '') +QUOTENAME(column_name)
FROM (SELECT DISTINCT column_name FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='sourceData') AS Colname;
SET #sql =N'select columnname,[values] from (select '+#col+ ' from dbo.sourceData) as D Unpivot
([values] for columnname in (' + #col1 + '))
as unpiv'
EXEC sp_executesql #sql;
Simply use Union all like this:
select Id,'ColumnA' columnName, ColumnA [Values]
from t
union all
select Id,'ColumnB' , ColumnB
from t
union all
select Id,'ColumnC' , ColumnC
from t
union all
select Id,'StartDate' , cast(StartDate as nvarchar(max))
from t;
SQL Fiddle Demo
Additional variant using unpivot:
dataset
DECLARE #YourTable TABLE
([Id] VARCHAR(10),
[ColumnA] VARCHAR(10),
[ColumnB] VARCHAR(10),
[ColumnC] VARCHAR(10),
[StartDate] DATE
);
INSERT INTO #YourTable
VALUES
(1, 'aa', 'bb', 'cc', '2016-10-10'),
(2, 'cc', 'dd', 'zz', '2016-10-11');
query
SELECT Id,
Field,
[Value]
FROM
( SELECT Id,
ColumnA,
ColumnB,
ColumnC,
CONVERT(VARCHAR(10), StartDate) AS StartDate
FROM #YourTable
) AS t UNPIVOT([Value] FOR [Field] IN ( ColumnA,
ColumnB,
ColumnC,
StartDate)) up;

Rows to single cell

I would like to get the desired output marked in green
the data points for each id get put into a single cell
Basically take all the events that have happened with A and attach it in the same order
Use Stuff Function:
DECLARE #tblTest AS Table(
ID INT,
EVENT VARCHAR(5)
)
INSERT INTO #tblTest VALUES
(1,'A'),
(1,'A'),
(1,'C'),
(2,'A'),
(2,'B'),
(2,'C')
SELECT DISTINCT
T1.ID,
STUFF
(
(SELECT '' + convert(varchar(10), T2.EVENT, 120)
FROM #tblTest T2
where T1.ID = T2.ID
FOR XML PATH (''))
, 1, 0, '') AS EVENT
FROM #tblTest T1
You can use FOR XML:
SELECT DISTINCT
ID,
(SELECT [EVENT] +''
FROM YourTable
WHERE ID = y.ID
FOR XML PATH('')
) as [EVENT]
FROM YourTable y
Output:
ID EVENT
1 AABCD
2 AABBCC
You can use UDF to do so as follows:
CREATE TABLE t(
id INT,
col CHAR(1)
);
INSERT INTO t VALUES (1,'a');
INSERT INTO t VALUES (1,'b');
INSERT INTO t VALUES (1,'c');
INSERT INTO t VALUES (1,'d');
INSERT INTO t VALUES (2,'e');
INSERT INTO t VALUES (2,'f');
INSERT INTO t VALUES (3,'g');
INSERT INTO t VALUES (4,'h');
The UDF (User defined function) -
USE [t]
GO
CREATE FUNCTION dbo.ConcatenateCols(#Id INT)
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #RtnStr VARCHAR(MAX)
SELECT #RtnStr = COALESCE(#RtnStr + '','') + col
FROM dbo.t
WHERE id = #Id AND col > ''
RETURN #RtnStr
END
GO
Finally the query and result:
SELECT id, dbo.ConcatenateCols(id) AS Cols -- UDF - ConcatenateCols(id)
FROM t GROUP BY Id
CREATE TABLE #temp(Id INt,Event Nvarchar(25))
INSERT INTO #temp
SELECT 1,
'A'
UNION ALL
SELECT 1,
'A'
UNION ALL
SELECT 1,
'B'
UNION ALL
SELECT 1,
'C'
UNION ALL
SELECT 1,
'D'
UNION ALL
SELECT 2,
'A'
UNION ALL
SELECT 2,
'A'
UNION ALL
SELECT 2,
'B'
UNION ALL
SELECT 2,
'B'
UNION ALL
SELECT 2,
'C'
UNION ALL
SELECT 2,
'C'
SELECT DISTINCT ID,
(SELECT [EVENT] +''
FROM #temp
WHERE ID = y.ID
FOR XML PATH('') ) AS [EVENT]
FROM #temp y

How to replace the 'Strings' with numerical values based on a group by clause

I have the below table (#temp1) where I need to replace the string in the column'Formula' with the matching input 'VALUE' column based on the group 'Yearmonth'.
The 'Formula' column may be of any mathematical expression for better understanding I have mentioned a simple example below.
IDNUM formula INPUTNAME VALUE YEARMONTH
---------------------------------------------------------------------
1 imports(398)+imports(399) imports(398) 17.000 2003:1
2 imports(398)+imports(399) imports(398) 56.000 2003:2
3 imports(398)+imports(399) imports(399) 15.000 2003:1
4 imports(398)+imports(399) imports(399) 126.000 2003:2
For eg :From the above table i need the output as
Idnum Formula Yearmonth
1. 17.00 +15.00 2003:1
2. 56.00 +126.00 2003:2
I tried with the below different query from various suggestions but coludnt achieve it. Could someone help me this out ?
Type1 :
SELECT
REPLACE(FORMULA, INPUTName, AttributeValue) AS realvalues,
yearmonth
FROM #temp1
GROUP BY yearmonth
TYPE2 :
USING XML PATH... In this case it got worked but I need to replace only the strings with the values and not to stuff the strings based on mathematcal operation.(Because the formula might be of any type).
SELECT
IDNUM = MIN(IDNUM),
FORMULA =
(SELECT STUFF(
(SELECT ' +' + CONVERT(VARCHAR(10), Value)
FROM #temp1
WHERE YEARMONTH = t1.YEARMONTH
FOR XML PATH(''))
,1, 2, '')),
YEARMONTH
FROM #TEMP1 t1
GROUP BY YEARMONTH
TYPE3:Using Recursions...This is returning only the null values...
;with t as (
select t.*,
row_number() over (partition by yearmonth order by idnum) as seqnum,
count(*) over (partition by yearmonth) as cnt
from #temp1 t
)
,cte as (
select t.seqnum, t.yearmonth, t.cnt,
replace(formula, inputname, AttributeValue) as formula1
from t
where seqnum = 1
union all
select cte.seqnum, cte.yearmonth, cte.cnt,
replace(CTE.formula1, T.inputname, T.AttributeValue) as formula2
from cte join
t
on cte.yearmonth = t.yearmonth
AND cte.seqnum = t.seqnum + 1
)
select row_number() over (order by (select null)) as id,formula1
from cte
where seqnum = cnt
This is full working example using recursive CTE:
DECLARE #DataSource TABLE
(
[IDNUM] TINYINT
,[formula] VARCHAR(MAX)
,[INPUTNAME] VARCHAR(128)
,[VALUE] DECIMAL(9,3)
,[YEARMONTH] VARCHAR(8)
);
INSERT INTO #DataSource ([IDNUM], [formula], [INPUTNAME], [VALUE], [YEARMONTH])
VALUES ('1', 'imports(398)+imports(399)', 'imports(398)', '17.000', '2003:1')
,('2', 'imports(398)+imports(399)', 'imports(398)', '56.000', '2003:2')
,('3', 'imports(398)+imports(399)', 'imports(399)', '15.000', '2003:1')
,('4', 'imports(398)+imports(399)', 'imports(399)', '126.000', '2003:2')
,('5', '(imports(391)+imports(392)-imports(393))/imports(394)', 'imports(391)', '5.000', '2003:3')
,('6', '(imports(391)+imports(392)-imports(393))/imports(394)', 'imports(392)', '10.000', '2003:3')
,('7', '(imports(391)+imports(392)-imports(393))/imports(394)', 'imports(393)', '3.000', '2003:3')
,('8', '(imports(391)+imports(392)-imports(393))/imports(394)', 'imports(394)', '-5.000', '2003:3');
WITH DataSource AS
(
SELECT ROW_NUMBER() OVER(PARTITION BY [YEARMONTH] ORDER BY [IDNUM]) AS [ReplacementOrderID]
,[YEARMONTH]
,[formula]
,[INPUTNAME] AS [ReplacementString]
,[VALUE] AS [ReplacementValue]
FROM #DataSource
),
RecursiveDataSource AS
(
SELECT [ReplacementOrderID]
,[YEARMONTH]
,REPLACE([formula], [ReplacementString], [ReplacementValue]) AS [formula]
FROM DataSource
WHERE [ReplacementOrderID] = 1
UNION ALL
SELECT DS.[ReplacementOrderID]
,DS.[YEARMONTH]
,REPLACE(RDS.[formula], DS.[ReplacementString], DS.[ReplacementValue]) AS [formula]
FROM RecursiveDataSource RDS
INNER JOIN DataSource DS
ON RDS.[ReplacementOrderID] + 1 = DS.[ReplacementOrderID]
AND RDS.[YEARMONTH] = DS.[YEARMONTH]
)
SELECT RDS.[YEARMONTH]
,RDS.[formula]
FROM RecursiveDataSource RDS
INNER JOIN
(
SELECT [YEARMONTH]
,MAX([ReplacementOrderID]) AS [ReplacementOrderID]
FROM DataSource
GROUP BY [YEARMONTH]
) DS
ON RDS.[YEARMONTH] = DS.[YEARMONTH]
AND RDS.[ReplacementOrderID] = DS.[ReplacementOrderID]
ORDER BY RDS.[YEARMONTH]
Generally, you simply want to perform multiple replacements over a string in one statement. You can have many replacement values, just play with the MAXRECURSION option.
--Create sample data
DROP TABLE #temp1
CREATE TABLE #temp1 (IDNUM int, formula varchar(max), INPUTNAME varchar(max), VALUE decimal, YEARMONTH varchar(max))
INSERT INTO #temp1 VALUES
(1, 'imports(398)+imports(399)', 'imports(398)', 17.000, '2003:1'),
(2, 'imports(398)+imports(399)', 'imports(398)', 56.000, '2003:2'),
(3, 'imports(398)+imports(399)', 'imports(399)', 15.000, '2003:1'),
(4, 'imports(398)+imports(399)', 'imports(399)', 126.000, '2003:2')
--Query
;WITH t as (
SELECT formula, YEARMONTH, IDNUM
FROM #temp1
UNION ALL
SELECT REPLACE(a.formula, b.INPUTNAME, CAST(b.VALUE AS varchar(100))) AS formula, a.YEARMONTH, a.IDNUM
FROM t a
JOIN #temp1 b ON a.YEARMONTH = b.YEARMONTH AND a.formula LIKE '%' + b.INPUTNAME + '%'
)
SELECT MIN(IDNUM) AS IDNUM, formula, YEARMONTH
FROM t
WHERE formula not LIKE '%imports(%'
GROUP BY formula, YEARMONTH