PIVOT and UNPIVOT in SQL Server
PIVOT : rows -> columns (long table -> wide report)
UNPIVOT : columns -> rows (wide table -> long table, good for analysis)
PIVOT syntax:
SELECT <non-pivoted cols>, [Val1], [Val2], ...
FROM ( <source query with exactly the needed columns> ) AS src
PIVOT ( AGG(<value col>) FOR <column whose values become headers> IN ([Val1], [Val2], ...) ) AS pvt;
Run the sample database script first. Every query below was run on that data, and the Output under each query is the real result.
USE SqlLearningDb;
GO
Topic table 1: StudentMarks (long format)
DROP TABLE IF EXISTS dbo.StudentMarks;
CREATE TABLE dbo.StudentMarks
(
StudentName VARCHAR(30) NOT NULL,
Subject VARCHAR(20) NOT NULL,
Term VARCHAR(10) NOT NULL,
Marks INT NOT NULL
);
INSERT INTO dbo.StudentMarks (StudentName, Subject, Term, Marks) VALUES
('Aarav', 'Maths', 'Term1', 88), ('Aarav', 'Science', 'Term1', 76), ('Aarav', 'English', 'Term1', 91),
('Aarav', 'Maths', 'Term2', 92), ('Aarav', 'Science', 'Term2', 81), ('Aarav', 'English', 'Term2', 89),
('Diya', 'Maths', 'Term1', 67), ('Diya', 'Science', 'Term1', 85), ('Diya', 'English', 'Term1', 78),
('Diya', 'Maths', 'Term2', 74), ('Diya', 'Science', 'Term2', 90), -- English Term2 missing
('Kabir', 'Maths', 'Term1', 95), ('Kabir', 'Science', 'Term1', 93), ('Kabir', 'English', 'Term1', 60),
('Kabir', 'Maths', 'Term2', 97), ('Kabir', 'Science', 'Term2', 88), ('Kabir', 'English', 'Term2', 65);
Messages(17 rows affected)
Topic table 2: QuarterlyTargets (wide format, for UNPIVOT)
DROP TABLE IF EXISTS dbo.QuarterlyTargets;
CREATE TABLE dbo.QuarterlyTargets
(
Region VARCHAR(10) PRIMARY KEY,
Q1Target DECIMAL(12,2),
Q2Target DECIMAL(12,2),
Q3Target DECIMAL(12,2),
Q4Target DECIMAL(12,2)
);
INSERT INTO dbo.QuarterlyTargets (Region, Q1Target, Q2Target, Q3Target, Q4Target) VALUES
('North', 1500000, 1800000, 1700000, 2200000),
('South', 2000000, 2100000, 2300000, 2600000),
('East', 1200000, 1300000, NULL, 1600000), -- Q3 target not set (NULL)
('West', 1800000, 1900000, 2000000, 2400000);
GO
Messages(4 rows affected)
Part A: Basic PIVOT
Long data first
SELECT * FROM dbo.StudentMarks WHERE Term = 'Term1';
Output
| StudentName | Subject | Term | Marks |
| Aarav | Maths | Term1 | 88 |
| Aarav | Science | Term1 | 76 |
| Aarav | English | Term1 | 91 |
| Diya | Maths | Term1 | 67 |
| Diya | Science | Term1 | 85 |
| Diya | English | Term1 | 78 |
| Kabir | Maths | Term1 | 95 |
| Kabir | Science | Term1 | 93 |
| Kabir | English | Term1 | 60 |
Subjects become columns (Term1 only)
SELECT StudentName, [Maths], [Science], [English]
FROM (
SELECT StudentName, Subject, Marks
FROM dbo.StudentMarks
WHERE Term = 'Term1'
) AS src
PIVOT ( SUM(Marks) FOR Subject IN ([Maths], [Science], [English]) ) AS pvt;
Output
| StudentName | Maths | Science | English |
| Aarav | 88 | 76 | 91 |
| Diya | 67 | 85 | 78 |
| Kabir | 95 | 93 | 60 |
Every column in the source that is NOT aggregated or pivoted becomes a GROUP BY column.
Here Term stays -> one row per Student + Term
SELECT StudentName, Term, [Maths], [Science], [English]
FROM (SELECT StudentName, Term, Subject, Marks FROM dbo.StudentMarks) AS src
PIVOT ( SUM(Marks) FOR Subject IN ([Maths], [Science], [English]) ) AS pvt
ORDER BY StudentName, Term;
Output
| StudentName | Term | Maths | Science | English |
| Aarav | Term1 | 88 | 76 | 91 |
| Aarav | Term2 | 92 | 81 | 89 |
| Diya | Term1 | 67 | 85 | 78 |
| Diya | Term2 | 74 | 90 | NULL |
| Kabir | Term1 | 95 | 93 | 60 |
| Kabir | Term2 | 97 | 88 | 65 |
Pivot + total column (and NULL handling with Isnull)
SELECT StudentName,
ISNULL([Maths], 0) AS Maths, ISNULL([Science], 0) AS Science, ISNULL([English], 0) AS English,
ISNULL([Maths], 0) + ISNULL([Science], 0) + ISNULL([English], 0) AS Total
FROM (SELECT StudentName, Subject, Marks FROM dbo.StudentMarks WHERE Term = 'Term2') AS src
PIVOT ( SUM(Marks) FOR Subject IN ([Maths], [Science], [English]) ) AS pvt;
Output
| StudentName | Maths | Science | English | Total |
| Aarav | 92 | 81 | 89 | 262 |
| Diya | 74 | 90 | 0 | 164 |
| Kabir | 97 | 88 | 65 | 250 |
Part B: Business Reports with PIVOT
Monthly sales by Region (months as columns)
SELECT Region, [1] AS Jan, [2] AS Feb, [3] AS Mar, [4] AS Apr, [5] AS May, [6] AS Jun
FROM (
SELECT Region, MONTH(SaleDate) AS SaleMonth, TotalAmount
FROM dbo.Sales
) AS src
PIVOT ( SUM(TotalAmount) FOR SaleMonth IN ([1], [2], [3], [4], [5], [6]) ) AS pvt
ORDER BY Region;
Output
| Region | Jan | Feb | Mar | Apr | May | Jun |
| East | 995500.00 | 93000.00 | 165450.00 | 331800.00 | 694550.00 | 836150.00 |
| North | 453780.00 | 857850.00 | 1254220.00 | 1370250.00 | 296830.00 | 747270.00 |
| South | 721300.00 | 396500.00 | 439400.00 | 992950.00 | 443500.00 | 488000.00 |
| West | 438550.00 | 331000.00 | 1241500.00 | 528180.00 | 463560.00 | 1052600.00 |
Number of orders per Status for each year
SELECT OrderYear, [Delivered], [Shipped], [Pending], [Cancelled]
FROM (SELECT YEAR(OrderDate) AS OrderYear, Status, OrderID FROM dbo.Orders) AS src
PIVOT ( COUNT(OrderID) FOR Status IN ([Delivered], [Shipped], [Pending], [Cancelled]) ) AS pvt;
Output
| OrderYear | Delivered | Shipped | Pending | Cancelled |
| 2024 | 50 | 0 | 0 | 4 |
| 2025 | 23 | 3 | 0 | 0 |
Same report Without the PIVOT keyword: "conditional aggregation"
(works in every database, easier to add totals - very popular in interviews)
SELECT YEAR(OrderDate) AS OrderYear,
SUM(CASE WHEN Status = 'Delivered' THEN 1 ELSE 0 END) AS Delivered,
SUM(CASE WHEN Status = 'Shipped' THEN 1 ELSE 0 END) AS Shipped,
SUM(CASE WHEN Status = 'Pending' THEN 1 ELSE 0 END) AS Pending,
SUM(CASE WHEN Status = 'Cancelled' THEN 1 ELSE 0 END) AS Cancelled,
COUNT(*) AS Total
FROM dbo.Orders
GROUP BY YEAR(OrderDate);
Output
| OrderYear | Delivered | Shipped | Pending | Cancelled | Total |
| 2024 | 50 | 0 | 0 | 4 | 54 |
| 2025 | 23 | 3 | 0 | 0 | 26 |
Salary bill by Department and Gender
SELECT DepartmentName, [M] AS Male, [F] AS Female
FROM (
SELECT d.DepartmentName, e.Gender, e.Salary
FROM dbo.Employees e JOIN dbo.Departments d ON d.DepartmentID = e.DepartmentID
) AS src
PIVOT ( SUM(Salary) FOR Gender IN ([M], [F]) ) AS pvt;
Output
| DepartmentName | Male | Female |
| Executive | 250000.00 | NULL |
| Sales | 152000.00 | 348000.00 |
| Marketing | 50000.00 | 152000.00 |
| IT | 443000.00 | 215000.00 |
| HR | NULL | 165000.00 |
| Finance | 190000.00 | 62000.00 |
| Operations | 145000.00 | NULL |
Part C: Dynamic PIVOT (column list not known in advance)
Revenue per customer segment, with one column per Category (built at run time)
DECLARE @cols NVARCHAR(MAX), @sql NVARCHAR(MAX);
SELECT @cols = STRING_AGG(QUOTENAME(CategoryName), ', ') WITHIN GROUP (ORDER BY CategoryName)
FROM dbo.Categories
WHERE CategoryID IN (SELECT DISTINCT CategoryID FROM dbo.Products);
PRINT @cols; -- see the generated column list in Messages tab
SET @sql = N'
SELECT Segment, ' + @cols + N'
FROM (
SELECT c.Segment, cat.CategoryName, oi.LineTotal
FROM dbo.OrderItems oi
JOIN dbo.Orders o ON o.OrderID = oi.OrderID
JOIN dbo.Customers c ON c.CustomerID = o.CustomerID
JOIN dbo.Products p ON p.ProductID = oi.ProductID
JOIN dbo.Categories cat ON cat.CategoryID = p.CategoryID
) AS src
PIVOT ( SUM(LineTotal) FOR CategoryName IN (' + @cols + N') ) AS pvt;';
EXEC sys.sp_executesql @sql;
GO
Messages, [Desks], [Desktops], [Laptops], [Mobiles], [Office Chairs], [Stationery]
Output
| Segment | Accessories | Desks | Desktops | Laptops | Mobiles | Office Chairs | Stationery |
| Corporate | 230340.00 | 184800.00 | 1012500.00 | 2516000.00 | 773250.00 | 210550.00 | 7536.00 |
| Retail | 198020.00 | 41800.00 | 743750.00 | 2357000.00 | 820000.00 | 49300.00 | 10957.50 |
| SMB | 282925.00 | 99000.00 | 597750.00 | 1301750.00 | 873750.00 | 112850.00 | 8970.00 |
Part D: UNPIVOT (columns -> rows)
Wide data
SELECT * FROM dbo.QuarterlyTargets;
Output
| Region | Q1Target | Q2Target | Q3Target | Q4Target |
| East | 1200000.00 | 1300000.00 | NULL | 1600000.00 |
| North | 1500000.00 | 1800000.00 | 1700000.00 | 2200000.00 |
| South | 2000000.00 | 2100000.00 | 2300000.00 | 2600000.00 |
| West | 1800000.00 | 1900000.00 | 2000000.00 | 2400000.00 |
UNPIVOT: each quarter becomes a row (note: NULL values are Dropped - East Q3 disappears)
SELECT Region, Quarter, Target
FROM dbo.QuarterlyTargets
UNPIVOT ( Target FOR Quarter IN (Q1Target, Q2Target, Q3Target, Q4Target) ) AS unpvt
ORDER BY Region, Quarter;
Output
| Region | Quarter | Target |
| East | Q1Target | 1200000.00 |
| East | Q2Target | 1300000.00 |
| East | Q4Target | 1600000.00 |
| North | Q1Target | 1500000.00 |
| North | Q2Target | 1800000.00 |
| North | Q3Target | 1700000.00 |
| North | Q4Target | 2200000.00 |
| South | Q1Target | 2000000.00 |
| South | Q2Target | 2100000.00 |
| South | Q3Target | 2300000.00 |
| South | Q4Target | 2600000.00 |
| West | Q1Target | 1800000.00 |
| West | Q2Target | 1900000.00 |
| West | Q3Target | 2000000.00 |
| West | Q4Target | 2400000.00 |
Alternative with CROSS APPLY + VALUES: keeps NULLs and allows renaming (recommended)
SELECT t.Region, v.Quarter, v.Target
FROM dbo.QuarterlyTargets t
CROSS APPLY (VALUES ('Q1', t.Q1Target),
('Q2', t.Q2Target),
('Q3', t.Q3Target),
('Q4', t.Q4Target)) AS v(Quarter, Target)
ORDER BY t.Region, v.Quarter;
Output
| Region | Quarter | Target |
| East | Q1 | 1200000.00 |
| East | Q2 | 1300000.00 |
| East | Q3 | NULL |
| East | Q4 | 1600000.00 |
| North | Q1 | 1500000.00 |
| North | Q2 | 1800000.00 |
| North | Q3 | 1700000.00 |
| North | Q4 | 2200000.00 |
| South | Q1 | 2000000.00 |
| South | Q2 | 2100000.00 |
| South | Q3 | 2300000.00 |
| South | Q4 | 2600000.00 |
| West | Q1 | 1800000.00 |
| West | Q2 | 1900000.00 |
| West | Q3 | 2000000.00 |
... showing 15 of 16 rows
Real use: compare Unpivoted targets with actual sales (Q1, Q2 2025)
WITH Targets AS
(
SELECT Region, CAST(RIGHT(LEFT(Quarter, 2), 1) AS INT) AS Qtr, Target
FROM dbo.QuarterlyTargets
UNPIVOT ( Target FOR Quarter IN (Q1Target, Q2Target, Q3Target, Q4Target) ) AS u
),
Actuals AS
(
SELECT Region, DATEPART(QUARTER, SaleDate) AS Qtr, SUM(TotalAmount) AS Actual
FROM dbo.Sales
GROUP BY Region, DATEPART(QUARTER, SaleDate)
)
SELECT t.Region, t.Qtr, t.Target, a.Actual,
CAST(100.0 * a.Actual / t.Target AS DECIMAL(6,2)) AS AchievedPct
FROM Targets t
JOIN Actuals a ON a.Region = t.Region AND a.Qtr = t.Qtr
ORDER BY t.Region, t.Qtr;
Output
| Region | Qtr | Target | Actual | AchievedPct |
| East | 1 | 1200000.00 | 1253950.00 | 104.50 |
| East | 2 | 1300000.00 | 1862500.00 | 143.27 |
| North | 1 | 1500000.00 | 2565850.00 | 171.06 |
| North | 2 | 1800000.00 | 2414350.00 | 134.13 |
| South | 1 | 2000000.00 | 1557200.00 | 77.86 |
| South | 2 | 2100000.00 | 1924450.00 | 91.64 |
| West | 1 | 1800000.00 | 2011050.00 | 111.73 |
| West | 2 | 1900000.00 | 2044340.00 | 107.60 |
Round trip: PIVOT then UNPIVOT back
WITH Wide AS
(
SELECT StudentName, [Maths], [Science], [English]
FROM (SELECT StudentName, Subject, Marks FROM dbo.StudentMarks WHERE Term = 'Term1') s
PIVOT (SUM(Marks) FOR Subject IN ([Maths], [Science], [English])) p
)
SELECT StudentName, Subject, Marks
FROM Wide
UNPIVOT (Marks FOR Subject IN ([Maths], [Science], [English])) u;
Output
| StudentName | Subject | Marks |
| Aarav | Maths | 88 |
| Aarav | Science | 76 |
| Aarav | English | 91 |
| Diya | Maths | 67 |
| Diya | Science | 85 |
| Diya | English | 78 |
| Kabir | Maths | 95 |
| Kabir | Science | 93 |
| Kabir | English | 60 |
Practice Questions
- Pivot Sales : Products as rows, Regions as columns, Quantity summed.
- Pivot Orders : Customers as rows, Years (2024, 2025) as columns, order count.
- Write a dynamic pivot with one column per MONTH found in the Orders table.
- Unpivot the Customers table so Email and Phone become rows (ContactType, ContactValue).
- Re-write B1 using conditional aggregation (CASE WHEN).