An Easier Way of Transposing Query Result in SQL Server

Comments 0

Share to social media

I like Phil Factor’s articles. The latest one, An Easier Way of Pivoting Data in SQL Server, inspired me to start exploring another option to transpose a result from a query.

There are many situations in which the results of a query look better when they are rotated. For example, when you execute the simple T-SQL SELECT * FROM sys.databases to get all databases on the current SQL Server instance, the results look better somehow rotated. In my opinion, the results look better rotated if the number of columns is greater than the number of rows.

In this article, I will show you how to accomplish this task by introducing a SQLCLR stored procedure MATRIX.Transposing that does all the magic with rotation. (NOTE: I am using the words transpose and rotation interchangeably as synonyms throughout the article.)

Therefore, if you would like to transpose the results of your query, you will run a query like that in the listing below once the solution is in place.

The result of SELECT * FROM sys.databases is displayed in the image below. The column on which rotation will take place after running the new code  is outlined with red. (NOTE: The column is name.)

After transposing (executing the query from the listing above ), you will get the results as shown in the image below. The name values now become the column names. The properties of each database are listed vertically instead of horizontally.   

In short, T-SQL is not suitable enough to transpose the query result. There is an option with pivoting which includes aggregation and only moves the values from one column to column headings. It doesn’t flip everything as shown here. However, to accomplish such a task of transposing without aggregation, you have to process your results row by row. That is not what T-SQL is designed for. Yes, you can use a cursor or maybe you can accomplish this task by using an XML transformation. I wrote maybe because, to be honest, I never tested such possibilities. So, in my opinion, there is no easy way to do it.

To run Phil’s solution, you must be running at least SQL Server 2016 version and make sure that the database is in 130 compatibility mode. Yes, I learned a lot about JSON support in SQL Server from his article, but that didn’t keep me from exploring other possibililties.

A SQLCLR Solution

Another possibility, as you might guess, is to use .NET. It means building a SQLCLR project. At first, you must enable CLR if not already enabled. To do this, execute the T-SQL script in the listing below.

For those who do not want to play with .NET source code, there is a ZIP file in the attachment of the article. In the file, you can find two T-SQL scripts. One is for deploying the solution named Setup.sql and the other is for cleaning the environment after playing with the solution named CleanUp.sql.

Open Setup.sql in SSMS, just press F5, and the solution is deployed. I tested the solution on all SQL Server versions, starting with SQL 2005 until SQL 2017. (NOTE: To deploy to 2017, you will have to save the snk file and modify the path and password before running.)

Also, you can download the complete solution from GitHub and deploy the solution through Visual Studio. After deploying the solution from VS or running setup.sql, your database model will be extended with a newly created stored procedure named MATRIX.Transposing (NOTE: MATRIX is the database schema). In either case, you will need to modify the @Path and @Password variables for your environment.

The examples in this article will use the AdventureWorks2014 database. To check your installation, expand the Programmability node to be sure the stored procedure is there as well as the SQLCLR assembly, as shown in the image below.

The stored procedure takes several parameters. In the image below, I outlined the mandatory parameter in red.

Yes, the @Query parameter is the query whose results we would like to transpose.

Stored Procedure Parameters

Attentive readers will notice that some parameters do not show a default value in SSMS, which is opposite to my statement that only one parameter is mandatory. In my opinion, it is a bug in SSMS. Run the following T-SQL query in order to check to see if there is a default value associated with the other parameters. This statement queries sys.objects and sys.parameters filtering by the stored procedure name.

The results are shown in the image below. You can see that the @Query parameter is the only one that does not have a default value.

SSMS performs well when displaying standard T-SQL stored procedure parameters. It seems that SSMS is a little bit confused when has to display SQLCLR stored procedure parameters.

Now back to the main topic: in the table below, each parameter is explained in detail.

Parameter Name

Parameter Description

Parameter Type

Default Value

Can be null

@Query

Query or stored procedure, which result we will transpose. Calling a stored procedure always should begin with keyword EXEC

Nvarchar(max)

No default value

No

@Params

Query or stored procedure parameters

Nvarchar(4000)

NULL

Yes

@Rco

Rotate column ordinal

Smallint

0

No

@KeyValueOption

Do we specify custom headers with transposing

Smallint

0

No

@ColumnMapping

Custom header(s), column names are separated by a comma.

Nvarchar(4000)

NULL

Yes

@TableName

If we like to save result into permanent table or temp table

Nvarchar(256)

NULL

Yes

Solution Deployment

In order to explore the solution, start up Visual Studio Community Edition (or any kind of licensed edition). I am using Visual Studio 2017, but the solution works fine with Visual Studio 2013 as well. The solution uses SQL Server Database Project as the template.

In the solution, there are several T-SQL scripts. The SQLCLR framework allows you to define only one T-SQL statement for pre-deployment. It must be executed before the assembly is published. Similarly, there is a post-deploy script that is executed after the assembly is published.

The pre-deployment script, named PreDeployment.sql, first creates the MATRIX schema and then depending on SQL Server version (only for SQL Server 2017+), it creates an asymmetric key based on the solution snk file. Then it creates a login and grants the unsafe assembly permission to that login.

In SQL Server 2017, there is a new instance level setting named clr strict security. By default, it is turned on, and it is not recommended to turn it off. This setting requires strong signing assembly, and that was not the case prior SQL Server 2017 version if we marked the project as SAFE. Therefore, to use this solution as a whole, I included support for signing the assembly by using the asymmetric key.

The pre-deployment script is located in PreDeployment.sql. You can see the whole script in the listing below.

The post-deployment script transfers the resulting stored procedure from the default DBO schema to newly created MATRIX schema and creates some defaults. You can see the post-deployment script in the listing below.

The post-deployment script is located in the PostDeployment.sql file. You can see the entire script in the listing below.

The .NET Solution

The query and its parameters are passed as parameters to the MATRIX.Transposing stored procedure. The code uses classic ADO.NET techniques to get an object of type Dataset. For those who are new to .NET, a Dataset should be considered as a table collection. When a dataset is formed, then it is easy to perform many types of transformations. The possible transformations include rotation, e.g., switching rows with columns.

In the solution, getting the data is accomplished in DataAccess.cs. It uses an almost classic ADO.NET approach. The most interesting thing here is the so-called context connection. According to Microsoft documentation, the context connection allows you to execute T-SQL statements in the same context that was used when your code was invoked in the first place. To obtain the context connection, you must use the context connection string keyword. The listing below shows how to get the dataset.

To explain the rest of the parameters, let us take the Phil example. (NOTE: To run it, you must install objects from his article.)

The result will look similar to what is shown in the image below.

In order to get the same result as in Phil’s example, you must call the MATRIX.Transposing stored procedure as shown in the listing bellow.

The key point here is to pass @columnMapping as a new table header and to set @keyValueOption to 1. If you set @keyValueOption to 0, @columnMapping is ignored, and the result will look like the image below.

The first row, from the earlier test, is now the header of our table. In the solution, there are two kinds of transformations which are determined by the parameter @keyValueOption.

If you specify @keyValueOption to 1 and submit @ColumnMapping as NULL, the result is shown in the image below.

The generic header is displayed, highlighted with yellow in the image above. The first column is named Key, and the others are Value, Value1, and so on.

It’s easy to transform the DataTable object as shown in the listing below. All code connected with transposing the DataTable is located in TableManipulation.cs.

When you transpose the original data and produce another table, the code must send the results. Sending results is where SQLCLR magic ‘comes to light.’

The whole task is accomplished by utilizing the SqlPipe class. SqlPipe allows managed stored procedures running in process in a SQL Server database to return results to the caller. This class cannot be inherited.

First is invoked SendResultsStart. SendResultStart marks the beginning of a result set to be sent back to the client and uses the record parameter to construct the metadata that describes the result set. For now, ignore the record parameter for a while.

SendResultStart comes in a pair with SendResultEnd. As you probably expect SendResultEnd marks the end of a result set and returns the SqlPipe instance to the initial state. The code that pipes the data table to the client is located in PipeUtilities.cs.

SendResultRow, as the name implies, sends a single row of data back to the client. Also, to conclude the part of sending the result back to the client, SqlDataRecord represents a single row of data and its metadata. This class cannot be inherited.

You are probably asking yourself if the solution limited to newest SQL Server version? To answer this question let’s take a look at the image below. Outlined in red is the project properties. The project uses .NET 2.0. It means that you can use the solution starting with SQL Server 2005!

There are a few things to write about the solution. Help is available at any point by executing

The result is shown in the image below

Solution Limitations

The solution has some limitations. There is a maximum number of columns that you can have per SQL Server table; it could be up to 30000 columns. This solution is limited to 1000. If you try to transpose a table with more than 1000 rows, a warning is displayed in the message window, and you get first 1000 rows transposed. It means you get the table with 1000 columns.

For example, if we execute T-SQL like in the listing below

To transpose table ‘Person.Person’ which has about 20K records, you will get the warning like in the image below.

However, you will get the result, as shown in the image below.

In this spot, I have to make a small digression and describe the term of an application domain. This is extremely important for understanding the performance of the stored procedure.

An application domain is a construct in the Microsoft .NET Framework common language runtime (CLR) that is the unit of isolation for an application. When a user asks for some functionality located in your assembly the first time, an application domain is created. The application domain is a lightweight process used to enforce isolation between running .NET code within the same SQLOS process. SQLCLR uses application domains to isolate execution of .NET code on a per database and per assembly owner basis.

This process of loading an application domain takes time (this is a performance penalty). Therefore, the first execution of your SQLCRL function is always much slower than the next execution.

So, let us get back to the main topic. If the application domain is loaded, I get the result, transpose first 1000 rows in 3 seconds. If the application domain is not loaded everything takes a little bit longer. Maybe, in the future, I will remove this limitation and create some configuration table in which users can decide what is a maximum number of rows to transpose. But in this place, you have to ask yourself why you need a table of thousands of columns?

In the solution, there are some interesting tests which are located in the Test.sql file.

Execute the T-SQL script in the listing below. In the script, the table is filtered passing two parameters and the WHERE clause.

You can see the result in the image below. The filter is applied successfully.

Also, what we should do with our results? SSMS has powerful capabilities to copy the result and paste, for example, into an Excel file. Furthermore, the results could be saved into a temporary table or permanent table as is shown in the listing below.

The key point here was to pass a @tableName parameter. It could be a permanent or temporary table name. The result is shown in the image below. Notice the result of the second query is equal to the result of the first query.

Finally, take a look at the @rco parameter. Execute the T-SQL displayed in the listing below.

The first and the second T-SQL statement differ by the @Rco parameter. In the first statement the @Rco value is 0 and in the second statement is 1. You can notice in the image below that in the first case the header is formed based on the database name. In the second case, the header is formed based on database ID.

If you woul like to clean up your environment after playing with this solution, there is a script for this purpose, located in Setup directory as well as in the attachment of the article.

Summary

Phil’s articles are not just educational but also inspiring. Without Phil, I would never start investigating how to do transposing the query result. In the article, I showed you how to use SQLCLR to accomplish this task. The solution works fine on SQL Server 2005+.

Although SQLCLR seems to be a second-class citizen on SQL Server, there are situations when could replace T-SQL. It is worth exploring and using in solving practical problems.

The solution source code can be found on GitHub here.

Load comments

About the author

Darko Martinović

See Profile

EPAM Systems - Lead Software Engineer (A4)