The question often comes up: how do you pivot data in SQL Server? In looking closer at the question, there are two similar effects that are wanted. One is the equivalent to a pivot table in Excel, the other is a matrix transposition, where a table is simply rotated, as if it were an array, so that …
1 2 3 |
1 2 3 4 5 6 7 8 9 |
… becomes …
1 2 3 |
1 4 7 2 5 8 3 6 9 |
To put it a bit more formally,
These transposes used to be done by producing a normalised version of the first table, and then creating the transpose via a simple GROUP BY clause. Then came the PIVOT and UNPIVOT clause which remains probably the simplest way of doing it. Then came a few rather scary XML-based approaches that never caught on. Now, with JSON in SQL Server 2016, 2017, there is a new, versatile, approach.
Let’s start off by creating the test data, consisting of table representing a six by six array of random integers between one and a hundred. We’ll create a type to do this, so this can be passed as a parameter to a function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
IF NOT EXISTS (SELECT * FROM sys.types AS T WHERE name LIKE 'MyMatrix') CREATE TYPE dbo.MyMatrix AS TABLE ( [row] INT IDENTITY NOT NULL PRIMARY KEY, [1] INT NOT null, [2] INT NOT null, [3] INT NOT null, [4] INT NOT null, [5] INT NOT null, [6] INT NOT null ) go DECLARE @MyMatrixTable MyMatrix INSERT INTO @MyMatrixTable([1], [2], [3], [4], [5], [6]) SELECT [first]*100, [second]*100, third*100, fourth*100, fifth*100, sixth*100 FROM (VALUES (Rand(), Rand(),Rand(),Rand(),Rand(),Rand()), (Rand(), Rand(),Rand(),Rand(),Rand(),Rand()), (Rand(), Rand(),Rand(),Rand(),Rand(),Rand()), (Rand(), Rand(),Rand(),Rand(),Rand(),Rand()), (Rand(), Rand(),Rand(),Rand(),Rand(),Rand()), (Rand(), Rand(),Rand(),Rand(),Rand(),Rand()) )RandomNumbers([first], [second], third, fourth, fifth, sixth) SELECT * FROM @MyMatrixTable |
Notice that we have inserted the row number. This is because tables have no intrinsic order, so we have to make it explicit.
The Unpivot/Pivot solution is fairly simple
1 2 3 4 5 6 7 8 9 10 |
SELECT Transposed.TheRow as [row], [1], [2],[3],[4], [5], [6] from (SELECT [row], [value], TheRow FROM @MyMatrixTable UNPIVOT (value FOR TheRow in ([1], [2], [3], [4], [5], [6]))f )Norm PIVOT (Sum(value) FOR [row] IN ([1],[2],[3],[4],[5],[6]) )Transposed ORDER BY TheRow |
Before the PIVOT and UNPIVOT clause arrived, it was a lot clumsier and, in SQL Server 2016, and 2017 at least, takes twice as long.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
SELECT Norm.TheColumn AS row, Sum(CASE WHEN Norm.TheRow = 1 THEN Norm.Value ELSE 0 END) AS [1], Sum(CASE WHEN Norm.TheRow = 2 THEN Norm.Value ELSE 0 END) AS [2], Sum(CASE WHEN Norm.TheRow = 3 THEN Norm.Value ELSE 0 END) AS [3], Sum(CASE WHEN Norm.TheRow = 4 THEN Norm.Value ELSE 0 END) AS [4], Sum(CASE WHEN Norm.TheRow = 5 THEN Norm.Value ELSE 0 END) AS [5], Sum(CASE WHEN Norm.TheRow = 6 THEN Norm.Value ELSE 0 END) AS [6] FROM ( SELECT [@MyMatrixTable].[1], 1, [@MyMatrixTable].row FROM @MyMatrixTable UNION ALL SELECT [@MyMatrixTable].[2], 2, [@MyMatrixTable].row FROM @MyMatrixTable UNION ALL SELECT [@MyMatrixTable].[3], 3, [@MyMatrixTable].row FROM @MyMatrixTable UNION ALL SELECT [@MyMatrixTable].[4], 4, [@MyMatrixTable].row FROM @MyMatrixTable UNION ALL SELECT [@MyMatrixTable].[5], 5, [@MyMatrixTable].row FROM @MyMatrixTable UNION ALL SELECT [@MyMatrixTable].[6], 6, [@MyMatrixTable].row FROM @MyMatrixTable ) AS Norm(Value, TheColumn, TheRow) GROUP BY Norm.TheColumn ORDER BY Norm.TheColumn; |
With JSON, you have a great deal more freedom and, although the code looks messy, the operation is as fast as the Unpivot/Pivot solution.
1 2 3 4 5 6 7 8 9 10 |
DECLARE @JS NVARCHAR(MAX) SELECT @JS=(SELECT [1], [2], [3], [4], [5], [6] FROM @MyMatrixTable FOR JSON AUTO) SELECT [row],[1], [2], [3], [4], [5], [6] FROM (values ( 1, Json_Value(@js,'$[0]."1"'), Json_Value(@js,'$[1]."1"'), Json_Value(@js,'$[2]."1"'), Json_Value(@js,'$[3]."1"'), Json_Value(@js,'$[4]."1"'), Json_Value(@js,'$[5]."1"')), ( 2, Json_Value(@js,'$[0]."2"'), Json_Value(@js,'$[1]."2"'), Json_Value(@js,'$[2]."2"'), Json_Value(@js,'$[3]."2"'), Json_Value(@js,'$[4]."2"'), Json_Value(@js,'$[5]."2"')), ( 3, Json_Value(@js,'$[0]."3"'), Json_Value(@js,'$[1]."3"'), Json_Value(@js,'$[2]."3"'), Json_Value(@js,'$[3]."3"'), Json_Value(@js,'$[4]."3"'), Json_Value(@js,'$[5]."3"')), ( 4, Json_Value(@js,'$[0]."4"'), Json_Value(@js,'$[1]."4"'), Json_Value(@js,'$[2]."4"'), Json_Value(@js,'$[3]."4"'), Json_Value(@js,'$[4]."4"'), Json_Value(@js,'$[5]."4"')), ( 5, Json_Value(@js,'$[0]."5"'), Json_Value(@js,'$[1]."5"'), Json_Value(@js,'$[2]."5"'), Json_Value(@js,'$[3]."5"'), Json_Value(@js,'$[4]."5"'), Json_Value(@js,'$[5]."5"')), ( 6, Json_Value(@js,'$[0]."6"'), Json_Value(@js,'$[1]."6"'), Json_Value(@js,'$[2]."6"'), Json_Value(@js,'$[3]."6"'), Json_Value(@js,'$[4]."6"'), Json_Value(@js,'$[5]."6"')) )f([row], [1], [2], [3], [4], [5], [6]) |
What I’m doing is simply converting the table into its JSON form, and then using this to create a table using the multi-row VALUES syntax which paradoxically allows expressions. The expression I’m using is JSON_Value, which allows me do effectively dictate the source within the table, via that JSON Path expression, and the destination. As it is an expression, I can do all sorts of manipulation as well as a transpose. I could, if I wanted, (in SQL 2017)provide that path parameter as a variable. This sort of technique can be used for several other reporting purposes, and it is well-worth experimenting with it because it is so versatile.
Here are two function that do the rotation. I used them to check on performance. In my own timings they returned similar times when doing 1000 transposes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
IF Object_Id('Dbo.JsonTranspose') IS NOT NULL DROP function Dbo.JsonTranspose GO CREATE FUNCTION Dbo.JsonTranspose /** Summary: > Transposes a six by six matrix by rotating it Author: Phil Factor Date: 11/12/2017 Database: MatrixTransform Examples: - Select * from Dbo.JsonTranspose((SELECT [1], [2], [3], [4], [5], [6] FROM @MyMatrixTable FOR JSON AUTO)) ORDER BY row Returns: > A 6*6 table of integers representing the transposed matrix **/ ( @js NVarchar(max) ) RETURNS TABLE AS RETURN ( SELECT [row],[1], [2], [3], [4], [5], [6] FROM (values ( 1, Json_Value(@js,'$[0]."1"'), Json_Value(@js,'$[1]."1"'), Json_Value(@js,'$[2]."1"'), Json_Value(@js,'$[3]."1"'), Json_Value(@js,'$[4]."1"'), Json_Value(@js,'$[5]."1"')), ( 2, Json_Value(@js,'$[0]."2"'), Json_Value(@js,'$[1]."2"'), Json_Value(@js,'$[2]."2"'), Json_Value(@js,'$[3]."2"'), Json_Value(@js,'$[4]."2"'), Json_Value(@js,'$[5]."2"')), ( 3, Json_Value(@js,'$[0]."3"'), Json_Value(@js,'$[1]."3"'), Json_Value(@js,'$[2]."3"'), Json_Value(@js,'$[3]."3"'), Json_Value(@js,'$[4]."3"'), Json_Value(@js,'$[5]."3"')), ( 4, Json_Value(@js,'$[0]."4"'), Json_Value(@js,'$[1]."4"'), Json_Value(@js,'$[2]."4"'), Json_Value(@js,'$[3]."4"'), Json_Value(@js,'$[4]."4"'), Json_Value(@js,'$[5]."4"')), ( 5, Json_Value(@js,'$[0]."5"'), Json_Value(@js,'$[1]."5"'), Json_Value(@js,'$[2]."5"'), Json_Value(@js,'$[3]."5"'), Json_Value(@js,'$[4]."5"'), Json_Value(@js,'$[5]."5"')), ( 6, Json_Value(@js,'$[0]."6"'), Json_Value(@js,'$[1]."6"'), Json_Value(@js,'$[2]."6"'), Json_Value(@js,'$[3]."6"'), Json_Value(@js,'$[4]."6"'), Json_Value(@js,'$[5]."6"')) )f([row], [1], [2], [3], [4], [5], [6]) ) GO IF Object_Id('dbo.PivotUnpivotTranspose') IS NOT NULL DROP function dbo.PivotUnpivotTranspose GO CREATE FUNCTION dbo.PivotUnpivotTranspose /** Summary: > Use pivot/unpivot to transpose a matrix Author: Phil Factor Date: 11/12/2017 Database: MatrixTransform Examples: - Select [row], [1], [2], [3], [4], [5], [6] FROM dbo.PivotUnpivotTranspose(@MyMatrixTable) ORDER BY row Returns: > A 6*6 table of integers representing the transposed matrix **/ ( @TheMatrix My6x6Matrix readonly ) RETURNS TABLE AS RETURN ( SELECT Transposed.TheRow as [row], [1], [2],[3],[4], [5], [6] from (SELECT [row], [value], TheRow FROM @TheMatrix UNPIVOT (value FOR TheRow in ([1], [2], [3], [4], [5], [6]))f )Norm PIVOT (Sum(value) FOR [row] IN ([1],[2],[3],[4],[5],[6]) )Transposed ) GO |
The JSON_Value and JSON_Modify now, in SQL Server 2017 and in Azure SQL Database, allow you to provide a variable as the value of path, rather than just a literal. This would provide several opportunities for producing reports.
Load comments