简体   繁体   中英

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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM