A grouped query returns data “downwards”: one row per customer and month, for example. But a report usually wants it “sideways”: one row per customer and one column per month, like an Excel pivot table. That rows-to-columns transformation is called pivoting.
SQL Server has the PIVOT operator for it. There is also a standard alternative, using CASE inside an aggregate function, which works in every database and is often easier to read.
The starting point
Sales grouped by customer and month, in vertical format:
| name | month | sales |
|---|---|---|
| Helen | 9 | 4999.00 |
| Janice | 6 | 2449.50 |
| Kimberly | 9 | 899.97 |
| Mavin | 7 | 2199.80 |
| Peter | 7 | 4304.25 |
EXTRACT(MONTH FROM date) is standard SQL and works in the page editor. In SQL Server you write MONTH(date) or DATEPART(month, date).We want the same with one column for June, another for July and another for September.
With PIVOT (SQL Server)
SELECT fixedColumns, [value1], [value2], ...
FROM (sourceQuery) AS source
PIVOT (
aggregateFunction(columnToAggregate)
FOR columnThatBecomesColumns IN ([value1], [value2], ...)
) AS pivotTable;
Applied to our example:
SELECT name, [6] AS june, [7] AS july, [9] AS september
FROM (
SELECT C.name,
MONTH(O.orderDate) AS month,
O.totalPrice
FROM Orders O
INNER JOIN Customers C ON C.customerId = O.customerId
WHERE O.orderDate >= '2022-06-01'
) AS source
PIVOT (
SUM(totalPrice)
FOR month IN ([6], [7], [9])
) AS salesByMonth
ORDER BY name;
Three things to know about PIVOT:
- The
INvalues have to be written by hand. If a new month appears, the query won’t show it until you add it (or generate the query with dynamic SQL). - Every column of the source query that is neither the aggregated one nor the
FORone is used for grouping. That’s why the source should contain only the columns you need. - Cells without data come out as
NULL.
Without PIVOT: CASE inside SUM
The same table in standard SQL. Each column is a sum that only counts the rows of its month. This one you can run:
| name | june | july | september |
|---|---|---|---|
| Helen | NULL | NULL | 4999.00 |
| Janice | 2449.50 | NULL | NULL |
| Kimberly | NULL | NULL | 899.97 |
| Mavin | NULL | 2199.80 | NULL |
| Peter | NULL | 4304.25 | NULL |
The result is identical to the PIVOT one. Advantages of this approach:
- It works in SQL Server, PostgreSQL, MySQL, Oracle and SQLite.
- Each column can have its own condition or function: one column with July’s sum, another with September’s order count, another with the year’s maximum…
- If you add
ELSE 0, empty cells show 0 instead ofNULL.
Another example: units by product family
Pivoting doesn’t have to be by date. Units sold to each customer, split into consoles and everything else:
| name | consoles | other | total |
|---|---|---|---|
| Helen | 0 | 100 | 100 |
| Peter | 0 | 15 | 15 |
| Mavin | 6 | 0 | 6 |
| Kimberly | 3 | 0 | 3 |
| Janice | 0 | 1 | 1 |
UNPIVOT: from columns to rows
UNPIVOT goes the other way: it turns columns into rows. It’s useful when you receive data in spreadsheet format (one column per month) and need it as a table so you can group or join it.
SELECT name, month, sales
FROM salesByMonth
UNPIVOT (
sales FOR month IN (june, july, september)
) AS rowsTable;
The standard alternative is a UNION ALL with one query per column:
SELECT name, 'june' AS month, june AS sales FROM salesByMonth WHERE june IS NOT NULL
UNION ALL
SELECT name, 'july', july FROM salesByMonth WHERE july IS NOT NULL
UNION ALL
SELECT name, 'september', september FROM salesByMonth WHERE september IS NOT NULL;
UNPIVOT drops cells containing NULL, so it doesn’t always give back exactly the original rows.Practice
- Add a column with each customer’s total to the
CASEquery. - Pivot the number of orders (not the amount) by month.
- Make empty cells show 0 instead of
NULL.