Use Dyamic Pivot query for this? - sql

i have the below table. (no primary key in this table)
ID | IC | Name | UGCOS | MCOS
---------------------------------------------------------
1AA | A123456B | Edmund | Australia | Denmark
1AA | A123456B | Edmund | Australia | France
2CS | C435664C | Grace | Norway | NULL
3TG | G885595H | Rae | NULL | Japan
I need to get the result like this.
ID | IC | Name | UGCOS | MCOS | MCOS1
--------------------------------------------------------------------
1AA | A123456B | Edmund | Australia | Denmark | France
2CS | C435664C | Grace | Norway | NULL | NULL
3TG | G885595H | Rae | NULL | Japan | NULL
Did googled around and seems like PIVOT is what i need to do that. However i am not sure how can that be implemented to my tables. It would be great help if somebody can help me with it. Thanks!

I'll create a second answer, as this approach is something completely different from my first:
This dynamic query will first find the max count of a distinct ID and then build a dynamic pivot
CREATE TABLE #tmpTbl (ID VARCHAR(100),IC VARCHAR(100),Name VARCHAR(100),UGCOS VARCHAR(100),MCOS VARCHAR(100))
INSERT INTO #tmpTbl VALUES
('1AA','A123456B','Edmund','Australia','Denmark')
,('1AA','A123456B','Edmund','Australia','France')
,('1AA','A123456B','Edmund','Australia','OneMore')
,('2CS','C435664C','Grace','Norway',NULL)
,('3TG','G885595H','Rae',NULL,'Japan');
GO
DECLARE #maxCount INT=(SELECT TOP 1 COUNT(*) FROM #tmpTbl GROUP BY ID ORDER BY COUNT(ID) DESC);
DECLARE #colNames VARCHAR(MAX)=
(
STUFF
(
(
SELECT TOP(#maxCount)
',MCOS' + CAST(ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS VARCHAR(10))
FROM sys.objects --take any large table or - better! - an numbers table or a tally CTE
FOR XML PATH('')
),1,1,''
)
);
DECLARE #cmd VARCHAR(MAX)=
'SELECT p.*
FROM
(
SELECT *
,''MCOS'' + CAST(ROW_NUMBER() OVER(PARTITION BY ID ORDER BY (SELECT NULL)) AS VARCHAR(10)) AS colName
FROM #tmpTbl
) AS tbl
PIVOT
(
MIN(MCOS) FOR colName IN(' + #colNames + ')
) AS p';
EXEC(#cmd);
GO
DROP TABLE #tmpTbl;
The result
1AA A123456B Edmund Australia Denmark France OneMore
2CS C435664C Grace Norway NULL NULL NULL
3TG G885595H Rae NULL Japan NULL NULL

This is a suggestion with a concatenated result:
CREATE TABLE #tmpTbl (ID VARCHAR(100),IC VARCHAR(100),Name VARCHAR(100),UGCOS VARCHAR(100),MCOS VARCHAR(100))
INSERT INTO #tmpTbl VALUES
('1AA','A123456B','Edmund','Australia','Denmark')
,('1AA','A123456B','Edmund','Australia','France')
,('2CS','C435664C','Grace','Norway',NULL)
,('3TG','G885595H','Rae',NULL,'Japan');
SELECT ID,IC,Name,UGCOS,
(
STUFF(
(
SELECT ' ,' + x.MCOS
FROM #tmpTbl AS x
WHERE x.ID=outerTbl.ID
FOR XML PATH('')
),1,2,''
)
) AS MCOS
FROM #tmpTbl AS outerTbl
GROUP BY ID,IC,Name,UGCOS;
GO
DROP TABLE #tmpTbl;
The result
1AA A123456B Edmund Australia Denmark ,France
2CS C435664C Grace Norway NULL
3TG G885595H Rae NULL Japan

Using Cross Apply and Pivot we can achieve this
DECLARE #Table1 TABLE
( ID varchar(3), IC varchar(8), Name varchar(6), UGCOS varchar(9), MCOS varchar(7))
;
INSERT INTO #Table1
( ID , IC , Name , UGCOS , MCOS )
VALUES
('1AA', 'A123456B', 'Edmund', 'Australia', 'Denmark'),
('1AA', 'A123456B', 'Edmund', 'Australia', 'France'),
('2CS', 'C435664C', 'Grace', 'Norway', NULL),
('3TG', 'G885595H', 'Rae', NULL, 'Japan')
;
Select ID , IC , Name , UGCOS,MAX([MCOS1])[MCOS1],MAX([MCOS2])[MCOS2] from (
select ID , IC , Name , UGCOS , MCOS,col,val,col +''+CAST(ROW_NUMBER()OVER(PARTITION BY ID ORDER BY col) AS VARCHAR)RN from #Table1
CROSS APPLY (values('MCOS',MCOS))CS(col,val))T
PIVOT (MAX(val) FOR RN IN ([MCOS1],[MCOS2]))PVT
GROUP BY ID , IC , Name , UGCOS

Do you always have a maximum of 2 rows of data that you'll want to turn into columns? If so, this would do you;
CREATE TABLE #TableName (ID varchar(3), IC varchar(8), Name varchar(6), UCGOS varchar(9), MCOS varchar(7))
INSERT INTO #TableName
VALUES
('1AA','A123456B','Edmund','Australia','Denmark')
,('1AA','A123456B','Edmund','Australia','France')
,('2CS','C435664C','Grace','Norway',NULL)
,('3TG','G885595H','Rae',NULL,'Japan')
SELECT DISTINCT a.ID
,a.IC
,a.NAME
,a.UCGOS
,b.Mcos1 MCOS
,c.Mcos2 MCOS1
FROM #TableName a
LEFT JOIN (
SELECT ID
,MAX(MCOS) Mcos1
FROM #TableName
GROUP BY ID
) b ON a.ID = b.ID
LEFT JOIN (
SELECT ID
,MIN(MCOS) Mcos2
FROM #TableName
GROUP BY ID
) c ON a.ID = c.ID
AND (
b.ID = c.ID
AND b.Mcos1 <> c.Mcos2
)
DROP TABLE #TableName
Gives you the result you're after.

Related

How to select distinct value from two columns into one column

There are two location related columns in my table like below
| Service_No | A_LOC | Z_LOC |
|------------|-------|-------|
| 001 | A | B |
| 002 | A | C |
| 003 | Null | C |
| 004 | F | B |
How do I select the distinct values of columns A_LOC and Z_LOC combined into a single list? The result of this query would be:
A, B, C, F
here is one way:
select string_Agg(Location,',') as distinct_location_list
from (
select A_LOC Location FROM tablename
union
select Z_LOC Location FROM tablename
) tt
I think, I have a quick solution for you. However, you can optimize it or you can do it in different ways.
DECLARE #YourTable TABLE
(
Service_No VARCHAR(30),
A_LOC VARCHAR(30),
Z_LOC VARCHAR(30)
);
INSERT INTO #YourTable Values('001','A','B');
INSERT INTO #YourTable Values('002','A','C');
INSERT INTO #YourTable Values('003',NULL,'C');
INSERT INTO #YourTable Values('004','F','B');
DECLARE #TempTable TABLE(FINALDATA VARCHAR(30));
INSERT INTO #TempTable
SELECT A_LOC FROM #YourTable WHERE A_LOC IS NOT NULL;
INSERT INTO #TempTable
SELECT Z_LOC FROM #YourTable WHERE Z_LOC IS NOT NULL;
SELECT DISTINCT FINALDATA FROM #TempTable;
Note: This code is written in SQL SERVER. Please check the code and let me know.
Alternatively try this method if the string_agg function is not available.
select
STUFF(
(SELECT ', ' + Loc FROM
(
select distinct A_Loc as Loc from #t
union
select distinct Z_Loc as Loc from #t
) t2
FOR XML PATH (''))
, 1, 1, '')
I would recommend unpivoting using apply and then filtering and distincting:
select string_agg(loc, ',')
from (select distinct loc
from t cross apply
(values (a_loc), (z_loc)) v(loc)
where loc is not null
) v;

SSIS - Concatenating and Lookups to create a string

I am trying to combine many columns together to create a full string.
This is what I mean:
I have a table named QRFormats that looks like this:
| Id | Text1 | Variable1 | Text2 | Variable2 | Text3 | Variable3 |
| 1 | The Color | Color | Is designated | Description | *NULL* | *NULL* |
| 2 | The Company | Company| Is Located | Location| In Country | Country|
| 3 | The Part is | PartNumber| Work Order:| WorkOrder| *NULL* | CompanyCode |
.
.
The complete string should print the Text and lookup the Variable in completely different table using FK/PK correlations.
All the Variables are located in two possible tables called PartData and Company
PartData
| Id | PartNumber | Color | WorkOrder | Description |
| 1 | 123456789 | Blue | 111222333 | Microchip |
| 2 | 101441414 | Silver | 55556666 | Handel |
Company
| Id | Company | Location | CompanyCode | Country |
| 1 | Microsoft | Seattle | 1234 | USA |
| 2 | Apple | California | 1122 | USA |
.
Some Examples of what I am trying to do....
.
The Complete string should look as follows if my:
-QRFormats FK is a 1:
-PartDataFK is a 1:
-Company FK is a 1:
"The Color Blue Is designated Microchip"
.
The Complete string should look as follows if my:
-QRFormats FK is a 2:
-PartDataFK is a 1:
-Company FK is a 3:
"The Company Apple Is Located California In Country USA"
.
The Complete string should look as follows if my:
-QRFormats FK is a 3:
-PartDataFK is a 2:
-Company FK is a 1:
"The Part is 101441414 WorkOrder: 555566661234"
.
Thank you very much for you help, I am using SSIS in VisualStudio 2015.
This is a very interesting request. First the DML:
create table #QFR
(
Id int
, Text1 varchar(100)
, Variable1 varchar(100)
, Text2 varchar(100)
, Variable2 varchar(100)
, Text3 varchar(100)
, Variable3 varchar(100)
)
insert into #QFR
values
( 1,' The Color ','Color',' Is designated ','Description ',NULL ,NULL )
,( 2,' The Company ','Company',' Is Located ','Location',' In Country ','Country' )
,( 3,' The Part is ','PartNumber','Work Order:','WorkOrder',NULL ,'CompanyCode' )
create table #parts
(
Id int
, PartNumber varchar(100)
, Color varchar(100)
, WorkOrder varchar(100)
, Description varchar(100)
)
insert into #parts
values
(1,'123456789','Blue ','111222333',' Microchip '),
(2,'101441414','Silver ','55556666',' Handel ')
create table #company
(
Id int
, Company varchar(100),
Location varchar(100)
, CompanyCode varchar(100)
, Country varchar(100)
)
insert into #company
values
(1,' Microsoft ','Seattle ','1234',' USA ')
,(2,' Apple ','California ','1122',' USA ')
I create a CTE to flip the variable logic on its side and joined it in several times to make this work...
declare #qID int = 1
,#pID int = 1
,#cID int = 1
;with cte as
(
select Label,Value
from #parts
cross apply (values('PartNumber',PartNumber),('Color',Color),('WorkOrder',WorkOrder),('Description',Description)) as a(Label,Value)
where id =#pID
union all
select Label,Value
from #company
cross apply (values('Company',Company),('Location',Location),('CompanyCode',CompanyCode),('Country',Country)) as a(Label,Value)
where id =#cID
)
select Text1 , t1.Value , text2 , t2.Value , text3 , t3.Value
from #QFR q
left join cte t1 on q.Variable1=t1.Label
left join cte t2 on t2.Label = q.Variable2
left join cte t3 on t3.label = q.Variable3
where q.id = #qID
Changed to a function:
create function fn_BuildAsentence (#qID int
,#pID int
,#cID int)
returns varchar(max)
as
BEGIN
declare #v varchar(Max)
;with cte as
(
select Label,Value
from parts
cross apply (values('PartNumber',PartNumber),('Color',Color),('WorkOrder',WorkOrder),('Description',Description)) as a(Label,Value)
where id =#pID
union all
select Label,Value
from company
cross apply (values('Company',Company),('Location',Location),('CompanyCode',CompanyCode),('Country',Country)) as a(Label,Value)
where id =#cID
)
select #v = concat(Text1 , t1.Value , text2 , t2.Value , text3 , t3.Value)
from QFR q
left join cte t1 on q.Variable1=t1.Label
left join cte t2 on t2.Label = q.Variable2
left join cte t3 on t3.label = q.Variable3
where q.id = #qID
return #v
END
Finally, I would use a SQL Source (I presume that you are doing this in a data flow to go somewhere)
Select dbo.fn_BuildAsentence(QFR,parts,company),QFR,parts,company
from [whereever]

MS SQL Group rows based on start and end

I have a table that looks like this with repeating rows of 3 and 3...
Column1 | Column2
CustomerID | 22
CustomerName | ”ABC”
Responsible | ”Allan”
CustomerID | 23
CustomerName | ”DEF”
Responsible | ”Jessica”
CustomerID | 24
CustomerName | ”GHI”
Responsible | ”Paul”
The following script can be used to create the table and populate it with sample data...
CREATE TABLE Responsible
( [ RowType ] VARCHAR(12),
[ Value ] VARCHAR(9) )
;
INSERT INTO Responsible
( [RowType],
[ Value ] )
VALUES
( 'CustomerID',
'22' ),
( 'CustomerName',
'ABC'),
( 'Responsible',
'Allan' ),
( 'CustomerID',
'23' ),
( 'CustomerName',
'DEF' ),
( 'Responsible',
'Jessica' ),
( 'CustomerID',
'24' ),
( 'CustomerName',
'GHI' ),
( 'Responsible',
'Paul' );
And I would like to get it like a table that looks like this:
CustomerID | CustomerName | Responsible
22 | ABC | Allan
23 | DEF | Jessica
24 | GHI | Paul
What is the best way forward?
I got it to work like this in SQL Server. I don't see any other option, but to use a cursor to go down one row at a time. The script below works only in your unique situation.
Create the new table
USE [YOURDATABASE NAME GOES HERE]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Table_2](
[CustomerID] [int] NULL,
[CustomerName] [varchar](50) NULL,
[Responsible] [varchar](50) NULL
) ON [PRIMARY]
GO
Insert Data Into Table
DECLARE #CustomerId INT
DECLARE #CustomerName VARCHAR(50)
DECLARE #Responsible VARCHAR(50)
DECLARE myCursor CURSOR
FOR SELECT Column2 FROM Table_1;
OPEN myCursor;
FETCH NEXT FROM myCursor
INTO #CustomerId;
FETCH NEXT FROM myCursor
INTO #CustomerName;
FETCH NEXT FROM myCursor
INTO #Responsible;
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO [dbo].[Table_2](CustomerID,CustomerName,Responsible)
VALUES (#CustomerId,#CustomerName,#Responsible)
FETCH NEXT FROM myCursor
INTO #CustomerId;
FETCH NEXT FROM myCursor
INTO #CustomerName;
FETCH NEXT FROM myCursor
INTO #Responsible;
END
CLOSE myCursor;
DEALLOCATE myCursor;
GO
edit, SQL server version :
Query 10:
select CustomerID, CustomerName, Responsible
from (
select row_number() over(order by k) as id , v as CustomerID
from t1
where k ='CustomerID') tt1
inner join (
select row_number() over(order by k) as id , v as CustomerName
from t1
where k ='CustomerName') tt2
on tt1.id = tt2.id
inner join (
select row_number() over(order by k) as id , v as Responsible
from t1
where k ='Responsible') tt3
on tt1.id = tt3.id
Results:
| CustomerID | CustomerName | Responsible |
|------------|--------------|-------------|
| 22 | ABC | Allan |
| 23 | DEF | Jessica |
| 24 | GHI | Paul |
This is what you want I think ?
SQL Fiddle
MySQL 5.6 Schema Setup:
CREATE TABLE t1
(`k` varchar(12), `v` varchar(9))
;
INSERT INTO t1
(`k`, `v`)
VALUES
('CustomerID', '22'),
('CustomerName', 'ABC'),
('Responsible', 'Allan'),
('CustomerID', '23'),
('CustomerName', 'DEF'),
('Responsible', 'Jessica'),
('CustomerID', '24'),
('CustomerName', 'GHI'),
('Responsible', 'Paul')
;
Query 1:
set #v1 = 0, #v2 = 0, #v3 = 0
Query 2:
select CustomerID, CustomerName, Responsible
from (
select #v1:= #v1+1 as id , v as CustomerID
from t1
where k ='CustomerID'
) tt1
inner join (
select #v2:= #v2+1 as id , v as CustomerName
from t1
where k ='CustomerName'
) tt2
on tt1.id = tt2.id
inner join (
select #v3:= #v3+1 as id , v as Responsible
from t1
where k ='Responsible'
) tt3
on tt1.id = tt3.id;
Results:
| CustomerID | CustomerName | Responsible |
|------------|--------------|-------------|
| 22 | ABC | Allan |
| 23 | DEF | Jessica |
| 24 | GHI | Paul |

SQL: How to Pivot a Table with Pivot Columns Sharing the Same Name? [duplicate]

This question already has answers here:
SQL Pivot Table dynamic
(2 answers)
Closed 7 years ago.
I am using SQL Server 2008 R2 version 10.50.1600.1.
I'm trying to pivot a table with strings but the pivot column shares the same name. This is what my data looks like.
+------------+-----------+------------+
| patient_ID | code_type | code_value |
+------------+-----------+------------+
| 1 | ICD9 | V70 |
| 1 | ICD9 | 401.9 |
| 1 | ICD9 | 616 |
| 1 | ICD9 | 338.21 |
| 2 | ICD9 | V10 |
| 2 | ICD9 | 250 |
+------------+-----------+------------+
What I'm trying to get to is this ...
+------------+--------+--------+--------+--------+--------+--------+
| patient_id | ICD9_1 | ICD9_2 | ICD9_3 | ICD9_4 | ICD9_5 | ICD9_x |
+------------+--------+--------+--------+--------+--------+--------+
| 1 | V70 | 401.9 | 616 | 338.21 | null | null |
| 2 | V10 | 250 | null | null | null | null |
+------------+--------+--------+--------+--------+--------+--------+
ICD9_x can stretch to infinity because I don't know how many ICD9 codes there will be for a given patient.
Is there a way to do this in SQL?
Thanks!
Update:
Thanks for all the help! I have received two errors. It looks like pivoting requires the values to be an int because of the sum right? Is there a way to use the pivot table for string values? The ICD9 codes are all strings.
Secondly, I hit an unexpected error. It says that "the number of elements in the select list exceeds the maximum allowed number of 4096 elements." Is there a solution for a large data set?
Thanks again!
Do you have a primary key or any column to use for ordering to make sure the codes will be in the correct order?
If you have one then you can concatenate the code_type value to the output of:
row_number () OVER (PARTITION BY patient_ID, code_type ORDER BY patient_id, /* codes order column here */)
... And then you will be able to use PIVOT ( technet info ) with the concatenated values.
Here is some code to illustrate what I mean:
-- Preparing some demo data as per your sample:
DECLARE #YourTable TABLE (
ID INT IDENTITY (1,1) PRIMARY KEY,
patient_id INT,
code_type VARCHAR(20),
code_value VARCHAR(20)
)
INSERT INTO #YourTable
(patient_id, code_type, code_value)
VALUES
(1, 'ICD9', 'V70'),
(1, 'ICD9', '401.9'),
(1, 'ICD9', '616'),
(1, 'ICD9', '338.21'),
(2, 'ICD9', 'V10'),
(2, 'ICD9', '250')
-- That should look like your starting point:
SELECT * FROM #YourTable
-- Now we suffix the code_type:
SELECT
patient_id,
code_type + '_' + cast(
row_number () OVER (PARTITION BY patient_id, code_type ORDER BY patient_id, ID)
AS VARCHAR(20)
) AS code_type,
code_value
FROM
#YourTable
-- ... and finally we pivot:
SELECT
patient_id,
ICD9_1,
ICD9_2,
ICD9_3,
ICD9_4
FROM (
SELECT
patient_id,
code_type + '_' + cast(
row_number () OVER (PARTITION BY patient_id, code_type ORDER BY patient_id, ID)
AS VARCHAR(20)
) AS code_type,
code_value
FROM
#YourTable
) data
PIVOT (
max(code_value)
-- you need to list all here:
FOR code_type IN (ICD9_1, ICD9_2, ICD9_3, ICD9_4)
) piv
You can Try something like this. I hope that Works for you.
DECLARE #cols as varchar(max)
DECLARE #query as varchar(max)
select #cols = (SELECT STUFF((SELECT ',' + QUOTENAME(t.code_type+CAST(row_number() over (partition by patient_id order by patient_id, code_type) as varchar(10)))
FROM YOURTABLE as t
FOR XML PATH('')),1,1,''))
select #query = 'select patient_id, ' + #cols + ' from
(select
code_type+CAST(row_number() over (partition by patient_id order by patient_id, code_type) as varchar(10)) as code_type, SUM(code_value) as code_value from YOURTABLE
group by
code_type+CAST(row_number() over (partition by patient_id order by patient_id, code_type) as varchar(10))) d
Pivot (SUM(code_value) for code_type in (' + #cols + ')) p'
EXECUTE(#query)

Pivoting 2 columns from 3 Tables and creating pivot-column-names to avoid conflict - SQL-Server 2008R2

Intro and Problem
In my example i have teachers, students and courses.I would like to have an overview which course is teached by whom in which rooms and all the studends in this course. I have the basic setup runnig (with some handcoded statements). But until now i had no luck to prepare the correct STUFF statement:
Prepare #colsStudents so that i can put the name in the column header and remove the need to mess with the ids (adding 100) to avoid a conflict between rooms.id and students.id
Prepare #colsRooms so that i do not have to hardocde the roomnames
Putting i all together by using EXEC sp_executesql #sql;
You can find all sql-statements to create this schema and the data at the end.
Wanted Result Overview Courses,
I would like pivot the columns RoomName and StudentName and use the column values as the new column names. All SQL-Statements to create tables and data are at the end.
Id | Course | Teacher | A3 | E7 | Penny | Cooper | Koothrap. | Amy
---+--------+---------+----+----+-------+--------+-----------+-----+
1 | C# 1 | Marc G. | | 1 | 1 | | |
2 | C# 2 | Sam S. | | 1 | 1 | | 1 |
3 | C# 3 | John S. | 1 | | | 1 | |
4 | C# 3 | Reed C. | | 1 | | | 1 |
5 | SQL 1 | Marc G. | 1 | | | | |
6 | SQL 2 | Marc G. | 1 | | | | |
7 | SQL 3 | Marc G. | | 1 | | 1 | | 1
8 | SQL 3 | Gbn | 1 | | | | 1 |
What i have so far
With PivotData as (
Select cd.Id, c.CourseName as Course, t.TeacherName as Teacher
,r.Id as RoomId, r.RoomName as RoomName
,100 + s.Id as StudentId, s.StudentName as Student
FROM CourseDetails cd
Left JOIN Courses c ON cd.CourseId = c.Id
Left JOIN Teachers t ON cd.TeacherId = t.Id
Left JOIN CourseMember cm ON cd.Id = cm.CourseDetailsId
Left JOIN Students s ON cm.StudentId = s.Id
Left JOIN Rooms r ON cd.RoomId = r.Id
)
Select Course, Teacher
, [1] as A3, [2] as E7 -- RoomColumns
, [101] as Koothrappali, [102] as Cooper, [103] as Penny, [104] as Amy -- StudentColumns
FROM (
Select Course, Teacher, RoomName, RoomId,Student, StudentId
From PivotData) src
PIVOT( Max(RoomName) FOR RoomId IN ([1],[2])) as P1
PIVOT( Count(Student) FOR StudentId IN ([101],[102],[103],[104]) ) as P2
What is missing
The above statement is prepared by hand. Since i do not know the Rooms or Students in advance i need to create the Pivot Statement for the Columns Rooms and Students dynamically. On SO are plenty of examples how to do it. The normal way to do that is to use STUFF:
DECLARE #colsStudents AS NVARCHAR(MAX);
SET #colsStudents = STUFF(
(SELECT N',' + QUOTENAME(y) AS [text()] FROM
(SELECT DISTINCT 100 + Id AS y FROM dbo.Students) AS Y
ORDER BY y
FOR XML PATH('')
),1
,1
,N'');
Select #colsStudents
This returns [101],[102],[103],[104] for the Student Ids. I added 100 to each id to avoid conflicts between the students.id and teh rooms.id column.
As mentioned in the intro i need to dynamically create something like this
[1] as RoomName_1, [2] as RoomName_1 -- RoomColumns
[1] as StudentName1, [2] as StudentName2, ... ,[4] as Amy -- StudentColumns
But all my tries with the stuff statement failed.
All SQL Statements to create the tables and data
CREATE TABLE [dbo].[Teachers](
[Id] [int] IDENTITY(1,1) NOT NULL,
[TeacherName] [nvarchar](120) NULL,
CONSTRAINT PK_Teachers PRIMARY KEY CLUSTERED (Id))
CREATE TABLE [dbo].[Students](
[Id] [int] IDENTITY(1,1) NOT NULL,
[StudentName] [nvarchar](120) NULL,
CONSTRAINT PK_Students PRIMARY KEY CLUSTERED (Id))
CREATE TABLE [dbo].[Courses](
[Id] [int] IDENTITY(1,1) NOT NULL,
[CourseName] [nvarchar](120) NULL,
CONSTRAINT PK_Courses PRIMARY KEY CLUSTERED (Id))
CREATE TABLE [dbo].[Rooms](
[Id] [int] IDENTITY(1,1) NOT NULL,
[RoomName] [nchar](120) NULL,
CONSTRAINT PK_Rooms PRIMARY KEY CLUSTERED (Id))
CREATE TABLE [dbo].[CourseDetails](
[Id] [int] IDENTITY(1,1) NOT NULL,
[CourseId] [int] NOT NULL,
[TeacherId] [int] NOT NULL,
[RoomId] [int] NOT NULL,
CONSTRAINT PK_CourseDetails PRIMARY KEY CLUSTERED (Id),
CONSTRAINT FK_CourseDetails_Teachers_Id FOREIGN Key (TeacherId)
REFERENCES dbo.Teachers (Id),
CONSTRAINT FK_CourseDetails_Courses_Id FOREIGN Key (CourseId)
REFERENCES dbo.Courses (Id),
CONSTRAINT FK_CourseDetails_Rooms_Id FOREIGN Key (RoomId)
REFERENCES dbo.Rooms (Id)
)
CREATE TABLE [dbo].[CourseMember](
[Id] [int] IDENTITY(1,1) NOT NULL,
[CourseDetailsId] [int] NOT NULL,
[StudentId] [int] NOT NULL,
CONSTRAINT PK_CourseMember PRIMARY KEY CLUSTERED (Id),
CONSTRAINT FK_CourseMember_CourseDetails_Id FOREIGN Key (CourseDetailsId)
REFERENCES dbo.CourseDetails (Id),
CONSTRAINT FK_CourseMember_Students_Id FOREIGN Key (StudentId)
REFERENCES dbo.Students (Id)
)
INSERT INTO dbo.Courses (CourseName)
VALUES ('SQL 1 - Basics'),
('SQL 2 - Intermediate'),
('SQL 3 - Advanced'),
('C# 1 - Basics'),
('C# 2 - Intermediate'),
('C# 3 - Advanced')
INSERT INTO dbo.Students (StudentName)
VALUES
('Koothrappali'),
('Cooper'),
('Penny'),
('Amy')
INSERT INTO dbo.Teachers (TeacherName)
VALUES
('gbn '),
('Sam S.'),
('Marc G.'),
('Reed C.'),
('John S.')
INSERT INTO dbo.Rooms (RoomName)
VALUES ('A3'), ('E7')
INSERT [dbo].[CourseDetails] (CourseId, TeacherId, RoomId)
VALUES (4, 3, 2),(5, 2, 2),
(6, 5, 1),(6, 4, 2),
(1,3,1),(2,3,1),(3,3,2),
(3,1,1)
INSERT [dbo].[CourseMember] (CourseDetailsId, StudentId)
VALUES (1,3),(2,3),(2,1),(3,2),(4,1),(7,2),(7,4),(8,1)
I personally would do this a bit different. Since you are trying to pivot two separate columns that screams to use the UNPIVOT function.
The unpivot will convert your multiple columns into rows to then pivot.
Since you have SQL Server 2008, you can use CROSS APPLY and values:
select id, course, teacher, col, flag
from
(
Select cd.Id, c.CourseName as Course, t.TeacherName as Teacher
,cast(r.Id as varchar(10))as RoomId
, r.RoomName as RoomName
,cast(100 + s.Id as varchar(10)) as StudentId
, s.StudentName as Student
, '1' flag
FROM CourseDetails cd
Left JOIN Courses c
ON cd.CourseId = c.Id
Left JOIN Teachers t
ON cd.TeacherId = t.Id
Left JOIN CourseMember cm
ON cd.Id = cm.CourseDetailsId
Left JOIN Students s
ON cm.StudentId = s.Id
Left JOIN Rooms r
ON cd.RoomId = r.Id
) d
cross apply
(
values ('roomname', roomname),('student',student)
) c (value, col)
See Demo. The unpivot generates a result similar to this:
| ID | COURSE | TEACHER | COL | FLAG |
-------------------------------------------------------------
| 1 | C# 1 - Basics | Marc G. | E7 | 1 |
| 1 | C# 1 - Basics | Marc G. | Penny | 1 |
| 2 | C# 2 - Intermediate | Sam S. | E7 | 1 |
| 2 | C# 2 - Intermediate | Sam S. | Penny | 1 |
| 2 | C# 2 - Intermediate | Sam S. | E7 | 1 |
| 2 | C# 2 - Intermediate | Sam S. | Koothrappali | 1 |
| 3 | C# 3 - Advanced | John S. | A3 | 1 |
| 3 | C# 3 - Advanced | John S. | Cooper | 1 |
You will see that the col data contains all the values that you want to pivot. Once the data is in the rows, if will be easy to apply one pivot:
select id, course, teacher,
coalesce(A3, '') A3,
coalesce(E7, '') E7,
coalesce(Koothrappali, '') Koothrappali,
coalesce(Cooper, '') Cooper,
coalesce(Penny, '') Penny,
coalesce(Amy, '') Amy
from
(
select id, course, teacher, col, flag
from
(
Select cd.Id, c.CourseName as Course, t.TeacherName as Teacher
,cast(r.Id as varchar(10))as RoomId
, r.RoomName as RoomName
,cast(100 + s.Id as varchar(10)) as StudentId
, s.StudentName as Student
, '1' flag
FROM CourseDetails cd
Left JOIN Courses c
ON cd.CourseId = c.Id
Left JOIN Teachers t
ON cd.TeacherId = t.Id
Left JOIN CourseMember cm
ON cd.Id = cm.CourseDetailsId
Left JOIN Students s
ON cm.StudentId = s.Id
Left JOIN Rooms r
ON cd.RoomId = r.Id
) d
cross apply
(
values ('roomname', roomname),('student',student)
) c (value, col)
) d
pivot
(
max(flag)
for col in (A3, E7, Koothrappali, Cooper, Penny, Amy)
) piv
See SQL Fiddle with Demo.
Then to convert this to dynamic SQL, you are only pivoting one column, so you will use the following to get the list of columns:
select #cols = STUFF((SELECT ',' + QUOTENAME(col)
from
(
select id, roomname col, 1 SortOrder
from rooms
union all
select id, StudentName, 2
from Students
) d
group by id, col, sortorder
order by sortorder, id
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
This will get the list of distinct rooms and students that are then used in the pivot. So the final code will be:
DECLARE #cols AS NVARCHAR(MAX),
#colsNull AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(col)
from
(
select id, roomname col, 1 SortOrder
from rooms
union all
select id, StudentName, 2
from Students
) d
group by id, col, sortorder
order by sortorder, id
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #colsNull = STUFF((SELECT ', coalesce(' + QUOTENAME(col)+', '''') as '+QUOTENAME(col)
from
(
select id, roomname col, 1 SortOrder
from rooms
union all
select id, StudentName, 2
from Students
) d
group by id, col, sortorder
order by sortorder, id
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query
= 'SELECT
id, course, teacher,' + #colsNull + '
from
(
select id, course, teacher, col, flag
from
(
Select cd.Id, c.CourseName as Course, t.TeacherName as Teacher
,cast(r.Id as varchar(10))as RoomId
, r.RoomName as RoomName
,cast(100 + s.Id as varchar(10)) as StudentId
, s.StudentName as Student
, ''1'' flag
FROM CourseDetails cd
Left JOIN Courses c
ON cd.CourseId = c.Id
Left JOIN Teachers t
ON cd.TeacherId = t.Id
Left JOIN CourseMember cm
ON cd.Id = cm.CourseDetailsId
Left JOIN Students s
ON cm.StudentId = s.Id
Left JOIN Rooms r
ON cd.RoomId = r.Id
) d
cross apply
(
values (''roomname'', roomname),(''student'',student)
) c (value, col)
) d
pivot
(
max(flag)
for col in (' + #cols + ')
) p '
execute(#query)
See SQL Fiddle with Demo.
Note I implemented a flag to be used in the pivot, this basically generates a Y/N if there is a value for the room or student.
This gives a final result:
| ID | COURSE | TEACHER | A3 | E7 | KOOTHRAPPALI | COOPER | PENNY | AMY |
---------------------------------------------------------------------------------------
| 1 | C# 1 - Basics | Marc G. | | 1 | | | 1 | |
| 2 | C# 2 - Intermediate | Sam S. | | 1 | 1 | | 1 | |
| 3 | C# 3 - Advanced | John S. | 1 | | | 1 | | |
| 4 | C# 3 - Advanced | Reed C. | | 1 | 1 | | | |
| 5 | SQL 1 - Basics | Marc G. | 1 | | | | | |
| 6 | SQL 2 - Intermediate | Marc G. | 1 | | | | | |
| 7 | SQL 3 - Advanced | Marc G. | | 1 | | 1 | | 1 |
| 8 | SQL 3 - Advanced | gbn | 1 | | 1 | | | |
As a side note, this data can also be unpivoted using the unpivot function in sql server. (See Demo with unpivot)
You can create alias string for both pivot columns using dynamic sql query,
For example, for student columns :
DECLARE #colsStudents AS NVARCHAR(MAX),
#colsstudentalias AS NVARCHAR(MAX),
#colsRooms AS NVARCHAR(MAX),
#colsRoomsalias AS NVARCHAR(MAX)
SELECT #colsStudents = STUFF
(
(
SELECT DISTINCT ',' + QUOTENAME(100 + Id)
FROM dbo.Students
FOR XML PATH('')
), 1, 1, ''
)
SELECT #colsstudentalias = STUFF
(
(
SELECT DISTINCT ',' + QUOTENAME(100 + Id)
+ ' as ' + QUOTENAME(ltrim(rtrim(StudentName)))
FROM dbo.Students
FOR XML PATH('')
), 1, 1, ''
)
SELECT #colsRooms = STUFF
(
(
SELECT DISTINCT ',' + QUOTENAME(Id)
FROM dbo.Rooms
FOR XML PATH('')
), 1, 1, ''
)
SELECT #colsRoomsalias = STUFF
(
(
SELECT DISTINCT ',' + QUOTENAME(Id)
+ ' as ' + QUOTENAME(ltrim(rtrim(RoomName)))
FROM dbo.Rooms
FOR XML PATH('')
), 1, 1, ''
)
--SELECT #colsStudents, #colsstudentalias, #colsRooms, #colsRoomsalias
DECLARE #sql varchar(max)
set #sql = ';With PivotData as (
Select cd.Id, c.CourseName as Course, t.TeacherName as Teacher
,r.Id as RoomId, r.RoomName as RoomName
,100 + s.Id as StudentId, s.StudentName as Student
FROM CourseDetails cd
Left JOIN Courses c ON cd.CourseId = c.Id
Left JOIN Teachers t ON cd.TeacherId = t.Id
Left JOIN CourseMember cm ON cd.Id = cm.CourseDetailsId
Left JOIN Students s ON cm.StudentId = s.Id
Left JOIN Rooms r ON cd.RoomId = r.Id
)
Select Course, Teacher
, ' + #colsRoomsalias + '
, ' + #colsstudentalias + '
FROM (
Select Course, Teacher, RoomName, RoomId,Student, StudentId
From PivotData) src
PIVOT( Max(RoomName) FOR RoomId IN (' + #colsRooms + ')) as P1
PIVOT( Count(Student) FOR StudentId IN (' + #colsStudents + ') ) as P2'
exec (#sql)
SQL DEMO
I am going to take a deeper look at both answers above and compare them with the one below.
My problem was in filling the local variables #RoomNames and #StudentNames with the Stuff() Function. One reason was that i had choosen the datatype nchar(120) instead of
nvarchar(120) for the columns StudentName, RoomName.
Another problem i had was that the new columnNames (Student instead of StudentName) where not recognized; therefore i replaced them with * in this statement: Select * From (' + #PivotSrc + N') src
Philip Kelley suggested to use SELECT #RoomIds = isnull(#RoomIds + ',', '') + '[' + Cast(Id as nvarchar(20))+ ']' FROM Rooms instead of STUFF() and since i find it shorter and easier to read i am using it now.
Working Solution
DECLARE #StudentNames NVARCHAR(2000),
#RoomIds NVARCHAR(2000),
#RoomNames NVARCHAR(2000),
#PivotSrc NVARCHAR(MAX),
#PivotBase NVARCHAR(MAX);
SELECT #StudentNames = isnull(#StudentNames + ',', '') + '[' + StudentName + ']' FROM Students
SELECT #RoomIds = isnull(#RoomIds + ',', '') + '[' + Cast(Id as nvarchar(20))+ ']' FROM Rooms
SELECT #RoomNames = isnull(#RoomNames + ',', '') + '[' + RoomName + ']' FROM Rooms
SET #PivotSrc = N'Select cd.Id, c.CourseName as Course, t.TeacherName as Teacher
,r.Id as RoomId, r.RoomName as RoomName
,100 + s.Id as StudentId, s.StudentName as Student
FROM CourseDetails cd
Left JOIN Courses c ON cd.CourseId = c.Id
Left JOIN Teachers t ON cd.TeacherId = t.Id
Left JOIN CourseMember cm ON cd.Id = cm.CourseDetailsId
Left JOIN Students s ON cm.StudentId = s.Id
Left JOIN Rooms r ON cd.RoomId = r.Id'
SET #PivotBase = N' Select Course, Teacher, '
+ #RoomNames + N', '
+ #StudentNames + N' FROM (
Select * From (' + #PivotSrc + N') src
PIVOT( Max(RoomName) FOR RoomName IN ('+#RoomNames+ N')) as P1
PIVOT( Count(Student) FOR Student IN ('+#StudentNames+N') ) as P2) as T'
execute(#PivotBase)