Skip to content

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
StudentNameSubjectTermMarks
AaravMathsTerm188
AaravScienceTerm176
AaravEnglishTerm191
DiyaMathsTerm167
DiyaScienceTerm185
DiyaEnglishTerm178
KabirMathsTerm195
KabirScienceTerm193
KabirEnglishTerm160

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
StudentNameMathsScienceEnglish
Aarav887691
Diya678578
Kabir959360

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
StudentNameTermMathsScienceEnglish
AaravTerm1887691
AaravTerm2928189
DiyaTerm1678578
DiyaTerm27490NULL
KabirTerm1959360
KabirTerm2978865

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
StudentNameMathsScienceEnglishTotal
Aarav928189262
Diya74900164
Kabir978865250

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
RegionJanFebMarAprMayJun
East995500.0093000.00165450.00331800.00694550.00836150.00
North453780.00857850.001254220.001370250.00296830.00747270.00
South721300.00396500.00439400.00992950.00443500.00488000.00
West438550.00331000.001241500.00528180.00463560.001052600.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
OrderYearDeliveredShippedPendingCancelled
202450004
202523300

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
OrderYearDeliveredShippedPendingCancelledTotal
20245000454
20252330026

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
DepartmentNameMaleFemale
Executive250000.00NULL
Sales152000.00348000.00
Marketing50000.00152000.00
IT443000.00215000.00
HRNULL165000.00
Finance190000.0062000.00
Operations145000.00NULL

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
SegmentAccessoriesDesksDesktopsLaptopsMobilesOffice ChairsStationery
Corporate230340.00184800.001012500.002516000.00773250.00210550.007536.00
Retail198020.0041800.00743750.002357000.00820000.0049300.0010957.50
SMB282925.0099000.00597750.001301750.00873750.00112850.008970.00

Part D: UNPIVOT (columns -> rows)

Wide data

SELECT * FROM dbo.QuarterlyTargets;
Output
RegionQ1TargetQ2TargetQ3TargetQ4Target
East1200000.001300000.00NULL1600000.00
North1500000.001800000.001700000.002200000.00
South2000000.002100000.002300000.002600000.00
West1800000.001900000.002000000.002400000.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
RegionQuarterTarget
EastQ1Target1200000.00
EastQ2Target1300000.00
EastQ4Target1600000.00
NorthQ1Target1500000.00
NorthQ2Target1800000.00
NorthQ3Target1700000.00
NorthQ4Target2200000.00
SouthQ1Target2000000.00
SouthQ2Target2100000.00
SouthQ3Target2300000.00
SouthQ4Target2600000.00
WestQ1Target1800000.00
WestQ2Target1900000.00
WestQ3Target2000000.00
WestQ4Target2400000.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
RegionQuarterTarget
EastQ11200000.00
EastQ21300000.00
EastQ3NULL
EastQ41600000.00
NorthQ11500000.00
NorthQ21800000.00
NorthQ31700000.00
NorthQ42200000.00
SouthQ12000000.00
SouthQ22100000.00
SouthQ32300000.00
SouthQ42600000.00
WestQ11800000.00
WestQ21900000.00
WestQ32000000.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
RegionQtrTargetActualAchievedPct
East11200000.001253950.00104.50
East21300000.001862500.00143.27
North11500000.002565850.00171.06
North21800000.002414350.00134.13
South12000000.001557200.0077.86
South22100000.001924450.0091.64
West11800000.002011050.00111.73
West21900000.002044340.00107.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
StudentNameSubjectMarks
AaravMaths88
AaravScience76
AaravEnglish91
DiyaMaths67
DiyaScience85
DiyaEnglish78
KabirMaths95
KabirScience93
KabirEnglish60

Practice Questions

  1. Pivot Sales : Products as rows, Regions as columns, Quantity summed.
  2. Pivot Orders : Customers as rows, Years (2024, 2025) as columns, order count.
  3. Write a dynamic pivot with one column per MONTH found in the Orders table.
  4. Unpivot the Customers table so Email and Phone become rows (ContactType, ContactValue).
  5. Re-write B1 using conditional aggregation (CASE WHEN).