PIVOT and UNPIVOT#
PIVOT and UNPIVOT are postfix table operators in the FROM clause. PIVOT turns the values of
one column into columns of its own, aggregating the rows that fall into each of them. UNPIVOT does the
opposite: it turns a list of columns into rows, one row per column and source row.
Both operators require a final alias, and that alias replaces the alias of their input for the rest of the query.
Syntax#
<table_source> PIVOT (
<aggregate_function> ( <value_column> )
FOR <pivot_column> IN ( [ <first_bucket> ] [ ,...n ] )
) [ AS ] <table_alias>
<table_source> UNPIVOT (
<value_column> FOR <name_column> IN ( <source_column> [ ,...n ] )
) [ AS ] <table_alias>
Arguments#
table_source
The input of the rotation: a table, a view, a derived table, or a whole join chain. A rotation may also be applied to the result of another rotation.
aggregate_function
One of SUM, COUNT, COUNT_BIG, AVG, MIN and MAX, in any letter case. The argument is a
single column reference; *, an expression, DISTINCT, ALL and an OVER clause are rejected by
the parser. Any other aggregate name is rejected with error 195, and CHECKSUM_AGG with error 406,
because it is not invariant to NULLs.
pivot_column
The column whose values become output columns. Every value listed in IN is converted to this column’s
type; a value that cannot be converted is rejected with error 8114.
first_bucket
An identifier - usually delimited, as in [2025]. Each entry names one output column and, converted to
the pivot column’s type, selects the rows aggregated into it. Entries may not be qualified with a table
alias (error 254), may not repeat under the catalog collation (error 8156), and may not collide with a
column the rotation keeps (error 265).
value_column and name_column (UNPIVOT)
The two columns the rotation generates: value_column carries the cell value and name_column the name
of the column it came from. Neither identifier may be qualified (error 275), and the two must differ
(error 8156).
source_column
A column of the input to turn into rows. A column may be listed only once (error 277). Every listed column must have exactly the same type - including length, scale and collation (error 8167); there is no common-supertype coercion.
table_alias
Required. Once the rotation is applied, the input’s alias is no longer visible: a query that still references it fails with error 4104.
Remarks#
What PIVOT returns. Every input column that is neither the aggregate argument nor the pivot column
becomes an implicit grouping column, and the result keeps those columns in source order followed by the
IN entries in list order. Generated columns are always nullable, including COUNT and COUNT_BIG
buckets, and take the type the aggregate returns. A bucket that matches no row aggregates to NULL - except
for the counting aggregates, which return 0.
What UNPIVOT returns. The result keeps the columns that were not listed, then the value column, then
the name column. Rows whose cell is NULL are not emitted. The value column takes the exact common type of
the listed columns and is nullable only when at least one of them is; the name column is
nvarchar(128) with the collation of the virtual catalog and carries each source column’s declared
spelling.
Where the rotation runs. Querona executes the operator on the data source whenever the source can run it, and otherwise rewrites it into equivalent standard SQL:
Source |
PIVOT |
UNPIVOT |
|---|---|---|
SQL Server 2012 and later |
Executed by the source |
Executed by the source |
Azure Synapse Analytics |
Federation engine |
Federation engine |
PostgreSQL 16 and later |
Rewritten |
Rewritten |
MySQL and MariaDB |
Rewritten |
Rewritten |
Vertica |
Rewritten |
Rewritten |
StarRocks 4 |
Executed by the source, rewritten when the rotation keeps no grouping column |
Rewritten |
DuckDB |
Executed by the source, rewritten when the rotation keeps no grouping column |
Executed by the source |
Any other source |
Federation engine |
Federation engine |
A rewritten PIVOT becomes conditional aggregation - one SUM(CASE WHEN ... THEN ... END) per bucket
over a GROUP BY of the grouping columns. A rewritten UNPIVOT becomes a UNION ALL of one filtered
branch per listed column. Both rewrites return exactly what the native operator returns.
When neither the source nor the rewrite applies, the rotation runs on the federation engine and only its input is read from the source. If no federation engine can run it either, the query fails with a message naming the operator and the executor that refused it, rather than returning a different result.
Not currently supported
Azure Synapse Analytics executes neither operator, and neither does PostgreSQL before version 16; queries against them fall back to the federation engine.
A rotation whose pivot column or aggregate argument declares a collation different from the virtual catalog’s is refused instead of executed under a guessed collation.
A rotation is not rewritten when its input is not safe to read more than once - for example when the input calls
GETDATE()- or when anUNPIVOTwould need more branches than the configured maximum. In both cases the rotation runs on the federation engine.A rotation that the data source executes itself cannot be combined with data from another source in the same statement - for example a
PIVOTover a SQL Server table joined with a table from a different connection. Such a query is refused with an error naming the operator and its source. Read the rotated data from a single source, or materialize the rotation result first.When a statement produces one of SQL Server’s ordered error pairs, Querona reports the first error of the pair.
Permissions#
Requires SELECT on the objects the rotation reads. The operator itself needs no additional permission.
Examples#
A. Turning years into columns#
SELECT CustomerId, [2025], [2026]
FROM (SELECT CustomerId, FiscalYear, Amount FROM Sales) AS s
PIVOT (SUM(s.Amount) FOR s.FiscalYear IN ([2025], [2026])) AS p
ORDER BY CustomerId ;
CustomerId is the only column left after the aggregate argument and the pivot column, so it becomes the
grouping column. A customer with no rows in a year gets NULL in that year’s column.
B. Turning columns into rows#
SELECT CustomerId, MetricName, MetricValue
FROM Metrics AS m
UNPIVOT (MetricValue FOR MetricName IN (Revenue, Cost, Margin)) AS u
ORDER BY CustomerId, MetricName ;
Each source row yields up to three rows. A metric that is NULL for a customer produces no row at all, so
use a LEFT JOIN against the list of metric names if the gaps matter.
C. Counting instead of summing#
SELECT CustomerId, [2025], [2026]
FROM (SELECT CustomerId, FiscalYear, Amount FROM Sales) AS s
PIVOT (COUNT(s.Amount) FOR s.FiscalYear IN ([2025], [2026])) AS p ;
Counting buckets return 0 where there is nothing to count, while SUM, AVG, MIN and MAX
return NULL.
D. Rotating the result of a rotation#
SELECT *
FROM Sales AS s
PIVOT (SUM(s.Amount) FOR s.FiscalYear IN ([2025], [2026])) AS p
UNPIVOT (Amount FOR FiscalYear IN ([2025], [2026])) AS u ;
The second rotation reads the output of the first one, so it may reference the generated columns [2025]
and [2026] but no longer the alias s.