Click here to Skip to main content
15,393,863 members
Articles / Programming Languages / SQL
Posted 24 Nov 2016

Tagged as


5 bookmarked

Using PIVOT with SQL Server

Rate me:
Please Sign up or sign in to vote.
1.95/5 (5 votes)
3 Dec 2016CPOL4 min read
This article intends to help everybody to write queries with PIVOT clause in SQL Server


PIVOT operator is mainly used to convert a result set from a SQL query into another one by rotating unique values in a specified field column throughout the original result set into new column fields in the final result set. This field is known as Spreading Column. These new columns will contain aggregated values according to:

  • Aggregated Column: a previously selected column to make calculations (sumcountavg, etc.) over it.
  • Non-Pivoted Columns: the rest of fields in the original result set.


But, in short, what do we really want to achieve with PIVOT clause? Let's see with a quick example.



It is a result set obtained from a simple query returning a list of orders sold for each employee according to ship countries. In particular, the result set has been obtained from Northwind database over Orders table and any grouping operation has been performed.

And here is what we want and we can obtain for our FINAL RESULT SET just applying PIVOT operation:


And then, what has happened? Well, to begin with distinct values in ShipCountry original field have been pivoting or rotating into new fields in the final result set. Secondly, every new cell value in final result set has been calculated from applying SUM function over Amount original field and grouping by EmployeeID and ShipCountry fields. In this example ShipCountry is the SPREADING COLUMN, Amount is the AGGREGATED COLUMN and EmployeeID is a NON-PIVOTED-COLUMN.

Here is the code executed in Northwind database to obtain previous result by means of PIVOT operator:

;with ORIGINAL_QUERY as (select e.EmployeeID, ShipCountry, Freight
from Orders o
join Employees e on
join Customers c on
select EmployeeID, 
PIVOT (SUM(Freight) FOR ShipCountry IN (
 Germany,Brazil,Belgium,Switzerland)) as PIVOTED_QUERY
order by EmployeeID

Before going to examples let's see the basic syntax for PIVOT operator.

SELECT [Your list of non-pivoted columns],
       [First value in Spreading column],
       [Second value in Spreading column],
       [Last value in Spreading]
FROM (SELECT source query) AS [alias for the source query]
    Aggregation function([Aggregated column])
        [Spreading column or column that contains the values that will become column headers]
    IN ([First value in Spreading column], 
        [Second value in Spreading column],
        [Last value in Spreading column])
) AS 

This article will show you how to generate values for Spreading Column, not only in a static way but dynamically as well. As a consequence it is important to stand out this little piece of code used to concatenate values from a specified column. So, using this piece of code a list of comma delimited values will be obtained from any column that you need.

DECLARE @SpreadingCommaDelimitedValues nvarchar(max)	
DECLARE @ColumnValuesToXml xml;
SELECT @ColumnValuesToXml = (
	  SELECT QuoteName([Spreading Column]) + ','
	  FROM [Table or Query]
SELECT @SpreadingValues = @ColumnValuesToXml.value('.','nvarchar(max)');
SET @SpreadingCommaDelimitedValues=SUBSTRING(@SpreadingCommaDelimitedValues,0,len(@SpreadingCommaDelimitedValues))

If you apply the previous code to ShipCountry field you’ll obtain a nvarchar value that will look like to ([Spain],[United States], [France], ...). This will be very handy for executing dynamic queries!

Finally, I will use the same database explained before in my article Generating Random Int SQL Values between Upper and Lower Limits. So, I suggest you have a look to understand the tables scheme used to write the SQL queries. Anyway, you can download the full source code from this post.

Using the Code

Let's get straight to the point! Here is the explained code to run queries. 

if (OBJECT_ID('tempdb..#cteSales')) is not null
	drop table #cteSales

declare @sql nvarchar(2000)
declare @spreadingValues nvarchar(200)
;with cteSales as
(select Sales.Id, 
	cast(year(OrderDate) as CHAR(4)) 
		+ '-' + replicate('0',2-len(cast(month(OrderDate)as char(2)))) 
		+ cast(month(OrderDate)as char(2)) as OrderYearMonthDate, /* Format YYYY-MM */
	concat(Emp.FName,' ',Emp.LName) as Employee, 
	concat(Cus.FName,' ',Cus.LName) as Customer, 
	Cou.CountryName as CustomerCountry, 
	Pro.Model as ProductModel, 
	Pro.Cost as UnitCost,
	SalesDetails.Quantity * Pro.Cost as TotalCost
from Sales
	join Employees Emp on Emp.EmployeeId=Sales.EmployeeId
	join Customers Cus on Cus.CustomerId=Sales.CustomerId
	join Countries Cou on Cou.CountryId=Cus.CountryId
	join SalesDetails on
	join Products Pro on
/* Load temp table with result set from CTE expression */
select * into #cteSales from cteSales

/* Example 1. STATIC query: Get data grouped by [EMPLOYEE, COUNTRY] 
    and spreading by [COUNTRY]  */
;with cte1 as (select Employee, CustomerCountry /* spreading Column */, 
 Quantity /* Aggregate value */
from #cteSales)
select Employee, 
  Australia, Germany, Spain, India, [United States]
from cte1
pivot (sum(Quantity) for CustomerCountry in (
  Australia, Germany, Spain, India, [United States])) as R	
/* End static Query */

/* Example 2. DYNAMIC query: Get data grouped by [EMPLOYEE, COUNTRY] 
  and spreading by [COUNTRY]  */
/* Same result as previous example */
/* Quit comment to test */
	--DECLARE @CountryNamesToXml xml; 
	--SELECT @CountryNamesToXml = (
	--  SELECT '[' + CountryName + '],'
	--  FROM Countries
	--SELECT @spreadingValues = @CountryNamesToXml.value('.','nvarchar(200)');
	--set @spreadingValues=SUBSTRING(@spreadingValues,0,len(@spreadingValues))

	--set @sql = ';with cte1 as (select Employee, CustomerCountry, Quantity
	--from #cteSales)
	--select Employee,' + @spreadingValues + '
	--from cte1
	--pivot (sum(Quantity) for CustomerCountry in (' + @spreadingValues + ')) as R'
	--exec (@sql)
/* End dynamic Query */

/* Example 3. DYNAMIC query: Get data grouped by [EMPLOYEE, YEAR-MONTH] 
   and spreading by [YEAR-MONTH]  */
/* This example try to solve a question in this site that I saw some days ago */

	DECLARE @MonthYearToXml xml; 
	SELECT @MonthYearToXml = (
	  SELECT distinct quoteName(OrderYearMonthDate)+','
	  FROM #cteSales
	  order by quoteName(OrderYearMonthDate)+','
	SELECT @spreadingValues = @MonthYearToXml.value('.','nvarchar(200)');
	set @spreadingValues=SUBSTRING(@spreadingValues,0,len(@spreadingValues))

	set @sql = ';with cte1 as (select Employee, OrderYearMonthDate, Quantity
	from #cteSales)
	select Employee,' + @spreadingValues + '
	from cte1
	pivot (sum(Quantity) for OrderYearMonthDate in (' + @spreadingValues + ')) as R'
	exec (@sql)
/* End dynamic Query */

/* Example 4. DYNAMIC query: Get data grouped by [EMPLOYEE, COUNTRY] 
   and spreading by [COUNTRY]  */

	DECLARE @ProductNamesToXml xml; 
	SELECT @ProductNamesToXml = (
	  SELECT quoteName(Model) + ','
	  FROM Products
	SELECT @spreadingValues = @ProductNamesToXml.value('.','nvarchar(200)');
	set @spreadingValues=SUBSTRING(@spreadingValues,0,len(@spreadingValues))

	set @sql = ';with cte1 as (select Employee, ProductModel, Quantity
	from #cteSales)
	select Employee,' + @spreadingValues + '
	from cte1
	pivot (sum(Quantity) for ProductModel in (' + @spreadingValues + ')) as R'
	exec (@sql)
/* End dynamic Query */

/* Cleans temp data */
drop table #cteSales

Before explaining the examples let's have a look to next sections There are four result sets, regarding source query, results for example 1 (static version), example 3 and example 4. It should be noted that Example 2 results are not shown because are equal to example 1, but the underlying sql query is dynamic instead of static.

Source Query

It is built using a CTE (Common Table Expression) to show all the sales details stored in the database. As you can see, it returns information about Sales, Employees, Customers, Countries, quantity and cost of ordered items, etc. All examples will use column Quantity as Aggregated Column with SUM as aggregating function and Employee column as Non-Pivoted column in the final result sets. This way, results will show sum of quantity of products sold by each employee according to different cases of Spreading Column (CustomerCountry, a combination of Month and Year and Products)

Initial SQL Result Set

Example 1

It shows sum of quantity of products sold by each employee for each country. Employee is the Non-Pivoted Column, CustomerCountry is the Spreading Column and Quantity is the Aggregated Column.

Example 1 Results

Example 2

The same as previous example but using dynamic SQL to build Spreading Column. This kind of query help us to query all the values in the Spreading Column and therefore you don't have to write statically the spreading values to be converted into new header columns.

Example 3

It shows sum of quantity of products sold by each employee for each month taking the pattern "MM-YYYY" to create the Spreading Column. As in previous examples, Employee is the Non-Pivoted Column and Quantity is the Aggregated Column.

Example 3 Results

Example 4

It shows the sum of quantity of products sold by each employee for each product model. It uses dynamic SQL, too. Employeeis the Non-Pivoted Column, ProductModelis the Spreading Column and Quantity is the Aggregated Column.

Example 4 Results


This article has been tested using Microsoft SQL Server 2012 SP3.


Microsoft TechNet Using PIVOT and UNPIVOT


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Spain Spain
Telecommunication Engineer by University of Zaragoza, Spain.

Passionate Software Architect, MCPD, MCAD, MCP. Over 10 years on the way building software.

Mountain Lover, Popular Runner, 3km 8'46, 10km 31'29, Half Marathon 1h08'50, Marathon 2h27'02. I wish I could be able to improve, but It's difficult by now

Comments and Discussions

-- There are no messages in this forum --