Rak
Rak

Reputation: 139

How to create dynamic columns in a pivot

So I have two tables and I want to pivot a table and the number of columns in the pivot table should be based on how many columns I want.

This is the first table

DefectiveTableList

SysID  |    DefectClassificationName  | CreatedAt
-------|------------------------------|-------------
    4  |    Wrong Color               | 2022-08-26 16:31:20.470
    5  |    Paper Is Thin             | 2022-08-26 16:31:20.470
    6  |    Misaligned Print          | 2022-08-26 16:31:20.470
    7  |    Defective Device          | 2022-08-26 16:31:20.470
    8  |    Delamination              | 2022-08-26 16:31:20.470
    9  |    Burned Lamination         | 2022-08-26 16:31:20.470
   10  |    Cracked Box               | 2022-08-26 16:31:20.470
   11  |    Faded Color               | 2022-08-26 16:31:20.470
   12  |    Overlapping               | 2022-08-26 16:31:20.470

And this is the second table

SchoolDetail

ID  |   SchoolHeaderID | DefectClassification | Sample | Score
----|------------------|----------------------|--------|-------
   1|                 1| Overlapping          |       0|    3.0
   2|                 1| Delamination         |       0|    2.0
   5|                 1| Cracked Box          |       0|    1.5
   8|                 1| Wrong Color          |       1|    3.0
  13|                 3| Wrong Color          |       0|    3.0
  14|                 3| Burned Lamination    |       0|    1.0
  17|                 3| Misaligned Print     |       2|    1.5
  20|                 3| Paper Is Thin        |      10|    2.0
  23|                 3| Overlapping          |      11|    1.0

Now what I want is to get the maximum value in the sample column based on schoolHeaderID and used it to display a number of columns in the pivot table.

For example, I want to display the result in SchoolHeaderID=3, so this should be the query:

Declare @NoOfCol as int 
SELECT @NoOfCol=MAX(Sample) FROM SchoolDetail WHERE SchoolHeaderID=3 

The result should be 11 and the number of columns should be 12 starting from 0. The rows should be the DefectClassificationName in DefectiveTableList and the Score is based on what sample and what defect.

The result should be like this:

DefectClassificationName |  0 |  1 |  2 |  3 |  4 |  5 |  6 |  7 |  8 |  9 |  10 |  11 
-------------------------|----|----|----|----|----|----|----|----|----|----|-----|-----
Wrong Color              | 3.0|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL| NULL| NULL
Paper Is Thin            |NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|  2.0| NULL
Misaligned Print         |NULL|NULL| 1.5|NULL|NULL|NULL|NULL|NULL|NULL|NULL| NULL| NULL
Defective Device         |NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL| NULL| NULL
Delamination             |NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL| NULL| NULL
Burned Lamination        | 1.0|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL| NULL| NULL
Cracked Box              |NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL| NULL| NULL
Faded Color              |NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL| NULL| NULL
Overlapping              |NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL|NULL| NULL|  1.0

How to achieve this?

Upvotes: 1

Views: 398

Answers (1)

Stuck at 1337
Stuck at 1337

Reputation: 2084

The query you want is, for example:

SELECT dtl.DefectClassificationName,
 [0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11]
FROM dbo.DefectiveTableList AS dtl
LEFT OUTER JOIN 
(
  SELECT * FROM dbo.SchoolDetail
  PIVOT (MAX(Score) FOR Sample IN (
    [0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11]
  )
  ) AS p WHERE SchoolHeaderID = @sh
) AS sd ON dtl.DefectClassificationName = sd.DefectClassification;

To get there, we can first pull the MAX(Sample), use a recursive CTE to build a comma-separated list of column names, inject those into the larger query, and then use sp_executesql.

DECLARE @SchoolHeaderID int = 3;

DECLARE @cols nvarchar(max) = N'',
  @maxCol int = (SELECT MAX(Sample) 
    FROM dbo.SchoolDetail 
    WHERE SchoolHeaderID = @SchoolHeaderID);

WITH recursiveCTE(num) AS
(
  SELECT 0 UNION ALL SELECT num+1 
  FROM recursiveCTE WHERE num < @maxCol
)
SELECT @cols += CONCAT(N',', QUOTENAME(num)) 
  FROM recursiveCTE OPTION (MAXRECURSION 32767);

DECLARE @sql nvarchar(max) = N'SELECT 
  dtl.DefectClassificationName' + @cols + N'
FROM dbo.DefectiveTableList AS dtl
LEFT OUTER JOIN 
(
  SELECT * FROM dbo.SchoolDetail
  PIVOT (MAX(Score) FOR Sample IN ('
   + STUFF(@cols, 1, 1, N'') + N'
  )) AS p
  WHERE SchoolHeaderID = @sh
) AS sd
ON dtl.DefectClassificationName = sd.DefectClassification;';

EXEC sys.sp_executesql @sql, N'@sh int', @sh = @SchoolHeaderID;

Here's an example fiddle.

On the other hand, after you changed the sample data to make it clear that SchoolHeaderID + DefectClassification was in fact not unique, the query you need is slightly different (you need to aggregate after joining):

DECLARE @sh int = 1;

WITH d AS 
(
  SELECT dtl.DefectClassificationName, sd.Score, sd.Sample
  FROM dbo.DefectiveTableList AS dtl
  INNER JOIN dbo.SchoolDetail AS sd
    ON dtl.DefectClassificationName = sd.DefectClassification
  WHERE sd.SchoolHeaderID = @sh
)
SELECT DefectClassificationName, [0],[1]
  FROM d PIVOT (MAX(Score) FOR Sample IN ([0],[1])) AS p;

And you would build this in much the same way (example fiddle):

DECLARE @SchoolHeaderID int = 1;

DECLARE @cols nvarchar(max) = N'',
  @maxCol int = (SELECT MAX(Sample) 
    FROM dbo.SchoolDetail 
    WHERE SchoolHeaderID = @SchoolHeaderID);

WITH recursiveCTE(num) AS
(
  SELECT 0 UNION ALL SELECT num+1 
  FROM recursiveCTE WHERE num < @maxCol
)
SELECT @cols += CONCAT(N',', QUOTENAME(num)) 
  FROM recursiveCTE OPTION (MAXRECURSION 32767);

DECLARE @sql nvarchar(max) = N'WITH d AS 
(
  SELECT dtl.DefectClassificationName, sd.Score, sd.Sample
  FROM dbo.DefectiveTableList AS dtl
  INNER JOIN dbo.SchoolDetail AS sd
    ON dtl.DefectClassificationName = sd.DefectClassification
  WHERE sd.SchoolHeaderID = @sh
)
SELECT DefectClassificationName' + @cols + N'
  FROM d PIVOT (MAX(Score) FOR Sample IN ('
  + STUFF(@cols, 1, 1, N'') + N')) AS p;';

EXEC sys.sp_executesql @sql, N'@sh int', @sh = @SchoolHeaderID;

That all said, why is the join to DefectiveTableList even necessary? If your Detail table contained the ID values that would make sense, but you don't have a very normalized design here, as you've repeated the strings in the detail table, which makes the join unnecessary. Just think: what are you actually getting from the other table? If there are other details we don't know, okay, but in the present state it feels like the query could be much simpler:

DECLARE @sh int = 1;

WITH d AS
(
  SELECT DefectClassification, Score, Sample
  FROM dbo.SchoolDetail AS sd
  WHERE SchoolHeaderID = @sh
)
SELECT DefectClassificationName = DefectClassification,
  [0],[1] FROM d
  PIVOT (MAX(Score) FOR Sample IN ([0],[1])) AS p;

And again we would generate it the same way (example fiddle):

DECLARE @SchoolHeaderID int = 1;

DECLARE @cols nvarchar(max) = N'',
  @maxCol int = (SELECT MAX(Sample) 
    FROM dbo.SchoolDetail 
    WHERE SchoolHeaderID = @SchoolHeaderID);

WITH recursiveCTE(num) AS
(
  SELECT 0 UNION ALL SELECT num+1 
  FROM recursiveCTE WHERE num < @maxCol
)
SELECT @cols += CONCAT(N',', QUOTENAME(num)) 
  FROM recursiveCTE OPTION (MAXRECURSION 32767);

DECLARE @sql nvarchar(max) = N'WITH d AS
(
  SELECT DefectClassification, Score, Sample
  FROM dbo.SchoolDetail AS sd
  WHERE SchoolHeaderID = @sh
)
SELECT DefectClassificationName = DefectClassification
  ' + @cols + N'
  FROM d PIVOT (MAX(Score) FOR Sample IN ('
  + STUFF(@cols, 1, 1, N'') + N')) AS p;';

EXEC sys.sp_executesql @sql, N'@sh int', @sh = @SchoolHeaderID;

Note that MAXRECURSION is only necessary if Sample can be greater than 100 (and if it can be greater than 32,767, you will have other problems generating that many columns anyway). And STRING_AGG() could be used instead of += concatenation on SQL Server 2017+.

Upvotes: 3

Related Questions