Oracle SQL PIVOT and UNPIVOT: The Complete Guide
Have you ever needed to convert a set of data from rows into columns? You can do this using the Oracle PIVOT feature (and the reverse using Oracle UNPIVOT).

The Problem
Let’s say you’ve got this set of data in a table called cust_sales:
location | customer_id | sale_amount |
---|---|---|
north | 6 | 875 |
south | 2 | 378 |
east | 5 | 136 |
west | 5 | 236 |
central | 3 | 174 |
north | 1 | 729 |
east | 2 | 147 |
west | 3 | 200 |
north | 6 | 987 |
central | 4 | 584 |
south | 3 | 714 |
east | 1 | 192 |
west | 3 | 946 |
east | 4 | 649 |
south | 2 | 503 |
north | 5 | 399 |
central | 6 | 259 |
east | 3 | 407 |
west | 1 | 545 |
This represents a set of sales for different customers in different regions. What if you wanted to find the SUM of sales for each location and customer? You could write an SQL query using SUM and GROUP BY:
1SELECT location, customer_id, SUM(sale_amount)
2FROM cust_sales
3GROUP BY location, customer_id
4ORDER BY location, customer_id;
This will show the following results:
LOCATION | CUSTOMER_ID | SUM(SALE_AMOUNT) |
---|---|---|
central | 3 | 174 |
central | 4 | 584 |
central | 6 | 259 |
east | 1 | 192 |
east | 2 | 147 |
east | 3 | 407 |
east | 4 | 649 |
east | 5 | 136 |
north | 1 | 729 |
north | 5 | 399 |
north | 6 | 1862 |
south | 2 | 881 |
south | 3 | 714 |
west | 1 | 545 |
west | 3 | 1146 |
west | 5 | 236 |
What if, instead of showing two columns for the location and customer_id, you wanted to show the locations in each row and the customer_id in each column? You would be looking for a result like this:
| location | 1 | 2 | 3 | 4 | 5 | 6 | | central | 0 | 0 | 174 | 584 | 0 | 259 | | east | 192 | 147 | 407 | 649 | 136 | 0 | | north | 729 | 0 | 0 | 0 | 399 | 1862 | | south | 0 | 881 | 714 | 0 | 0 | 0 |
This is often called pivoting, or transposing rows and columns, or transposing columns and rows. It can be done in a couple of ways in SQL, and the easiest is to use the Oracle PIVOT keyword.
The PIVOT Keyword in Oracle SQL
Oracle has the ability to create a result set that transposes or pivots columns and rows to provide a summary. This is done using the SQL PIVOT keyword. This keyword was introduced in Oracle 11g.
This keyword is applied to a SELECT statement, and looks like this:
1SELECT columns
2FROM tables
3PIVOT [XML] (
4 pivot_clause,
5 pivot_for_clause,
6 pivot_in_clause
7);
After the PIVOT keyword, it includes several components:
- XML: This is an optional keyword, and it lets you output data in an XML format. There’s an example of this below.
- pivot_clause: This defines what the query will aggregate the data on, because the PIVOT keyword aggregates data.
- pivot_for_clause: This defines which columns will be grouped and pivoted on.
- pivot_in_clause: This is used to filter the values for the columns in the pivot_for_clause. Each of the values in this clause will be a separate column.
If that’s confusing to you, don’t worry. The examples below will make it much easier to understand.
Simple PIVOT Example
Let’s say you wanted to show the results mentioned earlier: locations in the first column, each customer having a different column, and the SUM of the sale_amount as the values.
The query to show this data in a standard GROUP BY with no pivoting is:
1SELECT location,
2customer_id,
3SUM(sale_amount)
4FROM cust_sales
5GROUP BY location, customer_id
6ORDER BY location, customer_id;
LOCATION | CUSTOMER_ID | SUM(SALE_AMOUNT) |
---|---|---|
central | 3 | 174 |
central | 4 | 584 |
central | 6 | 259 |
east | 1 | 192 |
east | 2 | 147 |
east | 3 | 407 |
east | 4 | 649 |
east | 5 | 136 |
north | 1 | 729 |
north | 5 | 399 |
north | 6 | 1862 |
south | 2 | 881 |
south | 3 | 714 |
west | 1 | 545 |
west | 3 | 1146 |
west | 5 | 236 |
The query to transform this into a pivoted result set looks like this:
1SELECT *
2FROM cust_sales
3PIVOT (
4 SUM(sale_amount)
5 FOR customer_id
6 IN (1, 2, 3, 4, 5, 6)
7);
Let’s break this query down:
- SELECT *. We start with SELECT * FROM cust_sales. Normally I don’t like using SELECT *, but if we list the columns individually, then an error will be shown because it’s the PIVOT clause that is supposed to determine what is shown.
- PIVOT: This indicates that we want to use the SQL pivot functionality to display our output. We open the bracket to include a range of parameters.
- SUM(sale_amount): This is the value to be shown in the middle of the output. It’s a number value that has an aggregate function applied.
- FOR customer_id: This is the column in the table that’s used to display different columns in the output. The values are shown as column headers.
- IN (1, 2, 3, 4, 5, 6): These are the values of the FOR column mentioned above that are used in the output. It’s like a WHERE filter. You don’t have to specify all columns, but I have here.
Any columns not mentioned in the FOR clause are displayed as rows and are grouped. In this case, the location field is used.
The output of this query is:
LOCATION | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
west | 545 | (null) | 1146 | (null) | 236 | (null) |
central | (null) | (null) | 174 | 584 | (null) | 259 |
north | 729 | (null) | (null) | (null) | 399 | 1862 |
south | (null) | 881 | 714 | (null) | (null) | (null) |
east | 192 | 147 | 407 | 649 | 136 | (null) |
This is the result of the PIVOT keyword. The rows are the different locations, the columns are the customer_ids 1 to 6, and the values are the SUM of sale_amount. Any NULL values are because no data exists. For example, there are no sales in the west location for customer_id 2.
Specifying Grouped Columns
When using the PIVOT keyword, any columns not mentioned in the FOR clause are used as part of the Oracle PIVOT GROUP BY. In the above example, the only column was the location column, which was OK.
However, what if your output included another column, called prod_category?
location | prod_category | customer_id | sale_amount |
---|---|---|---|
north | furniture | 2 | 875 |
south | electronics | 2 | 378 |
east | gardening | 4 | 136 |
west | electronics | 3 | 236 |
central | furniture | 3 | 174 |
north | electronics | 1 | 729 |
east | gardening | 2 | 147 |
west | electronics | 3 | 200 |
north | furniture | 4 | 987 |
central | gardening | 4 | 584 |
south | electronics | 3 | 714 |
east | furniture | 1 | 192 |
west | gardening | 3 | 946 |
east | electronics | 4 | 649 |
south | furniture | 2 | 503 |
north | electronics | 1 | 399 |
central | gardening | 3 | 259 |
east | electronics | 3 | 407 |
west | furniture | 1 | 545 |
If you run the same PIVOT query, you’ll get this result. In this example, I’ve used a second table called cust_sales_category and reduced the number of customers..
1SELECT *
2FROM cust_sales_category
3PIVOT (
4 SUM(sale_amount)
5 FOR customer_id
6 IN (1, 2, 3, 4)
7);
The results are:
location | prod_category | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
west | gardening | (null) | (null) | 946 | (null) |
west | furniture | 545 | (null) | (null) | (null) |
east | electronics | (null) | (null) | 407 | 649 |
central | furniture | (null) | (null) | 174 | (null) |
north | furniture | (null) | 875 | (null) | 987 |
east | furniture | 192 | (null) | (null) | (null) |
south | furniture | (null) | 503 | (null) | (null) |
east | gardening | (null) | 147 | (null) | 136 |
north | electronics | 1128 | (null) | (null) | (null) |
west | electronics | (null) | (null) | 436 | (null) |
central | gardening | (null) | (null) | 259 | 584 |
south | electronics | (null) | 378 | 714 | (null) |
Our data is grouped by location and prod_category. This happened because the customer_id was mentioned in the FOR clause, and the statement used all other columns to GROUP BY.
What if we don’t want to group by location and prod_category?
We can try to alter the SELECT query to only select the location column.
1SELECT location, customer_id, sale_amount
2FROM cust_sales_category
3PIVOT (
4 SUM(sale_amount)
5 FOR customer_id
6 IN (1, 2, 3, 4)
7);
The result of this query is:
1ORA-00904: "SALE_AMOUNT": invalid identifier
200904. 00000 - "%s: invalid identifier"
3*Cause:
4*Action:
5Error at Line: 87 Column: 31
You get this error because you can’t specify individual columns as part of the SELECT clause. However, there are a couple of ways to do this: a WITH clause or a subquery.
To specify the columns being grouped as part of a PIVOT query, you can use the Oracle PIVOT with a subquery where the subquery only shows the columns you want:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount)
8 FOR customer_id
9 IN (1, 2, 3, 4)
10);
Notice how instead of selecting directly from the table, I’m performing a SELECT * from a subquery, and the subquery mentions the individual columns. This means the prod_category is ignored, and you get a result like this:
| location | 1 | 2 | 3 | 4 | | --- | --- | --- | --- | | west | 545 | (null) | 1382 | (null) | | central | (null) | (null) | 433 | 584 | | north | 1128 | 875 | (null) | 987 | | south | (null) | 881 | 714 | (null) | | east | 192 | 147 | 407 | 785 |
The result is not grouping the prod_category column at all. If you want to group by the prod_category instead of the location, simply change the field in the subquery:
1SELECT *
2FROM (
3 SELECT prod_category, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount)
8 FOR customer_id
9 IN (1, 2, 3, 4)
10);
location | 1 | 2 | 3 | 4 |
---|---|---|---|---|
furniture | 737 | 1378 | 174 | 987 |
electronics | 1128 | 378 | 1557 | 649 |
gardening | (null) | 147 | 1205 | 720 |
So that’s how you can change the columns being displayed and being grouped by using a PIVOT query: by using Oracle PIVOT with a subquery.
Using the WHERE Clause with PIVOT
In our queries above, the results show the pivoted summary of all data. All records are grouped by a few fields, and the SUM of the sale amount is shown.
What if you wanted to restrict this to only certain rows?
You can use a WHERE clause, just like a normal SELECT query.
However, if you put the WHERE clause in the middle, you’ll get an error:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6WHERE location <> 'south'
7PIVOT (
8 SUM(sale_amount)
9 FOR customer_id
10 IN (1, 2, 3, 4)
11);
1ORA-00933: SQL command not properly ended
200933.00000 - "SQL command not properly ended"
3*Cause:
4*Action:
5Error at Line: 127 Column: 1
This is because the WHERE clause must come after the PIVOT clause. A correct query would look like this:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount)
8 FOR customer_id
9 IN (1, 2, 3, 4)
10)
11WHERE location <> 'south';
The WHERE clause is at the end of the query, after the SQL PIVOT clause. This shows the following results:
location | 1 | 2 | 3 | 4 |
---|---|---|---|---|
west | 545 | (null) | 1382 | (null) |
central | (null) | (null) | 433 | 584 |
north | 1128 | 875 | (null) | 987 |
east | 192 | 147 | 407 | 785 |
The results exclude the records where the location is equal to ‘south’.
If you’re using the subquery method to determine columns, you can also put the WHERE clause inside the subquery:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5 WHERE location <> 'south'
6)
7PIVOT (
8 SUM(sale_amount)
9 FOR customer_id
10 IN (1, 2, 3, 4)
11);
You’ll get the same results.

Aliasing PIVOT Columns
The queries we’ve looked at so far will display the column headers as the customer_id values that are stored in the table. What if you want to give them a different name? The PIVOT keyword allows you to specify a column alias. This can be done on both the pivot_clause and the pivot_in_clause.
This example gives an alias to the SUM value:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount) AS sales_total
8 FOR customer_id
9 IN (1, 2, 3, 4)
10);
You can see that we have aliased the SUM(sale_amount) using AS sales_total. The results of this query are:
location | 1_SALES_TOTAL | 2_SALES_TOTAL | 3_SALES_TOTAL | 4_SALES_TOTAL |
---|---|---|---|---|
west | 545 | (null) | 1382 | (null) |
central | (null) | (null) | 433 | 584 |
north | 1128 | 875 | (null) | 987 |
south | (null) | 881 | 714 | (null) |
east | 192 | 147 | 407 | 785 |
The Oracle PIVOT column names are now displaying as 1_SALES_TOTAL, 2_SALES_TOTAL, and so on. This has been determined by the <customer_id>_<alias_name>. The first column of 1_SALES_TOTAL is the sales_total value for customer_id 1.
Instead, you could alias the column values inside the IN clause:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount)
8 FOR customer_id
9 IN (1 AS cust1, 2 AS cust2, 3 AS cust3, 4 AS cust4)
10);
location | CUST1 | CUST2 | CUST3 | CUST4 |
---|---|---|---|---|
west | 545 | (null) | 1382 | (null) |
central | (null) | (null) | 433 | 584 |
north | 1128 | 875 | (null) | 987 |
south | (null) | 881 | 714 | (null) |
east | 192 | 147 | 407 | 785 |
The columns values are showing exactly what you aliased them as. Instead of showing 1 it shows CUST1.
Finally, you can combine the pivot_clause alias and the pivot_in_clause alias:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount) AS sales_total
8 FOR customer_id
9 IN (1 AS cust1, 2 AS cust2, 3 AS cust3, 4 AS cust4)
10);
location | CUST1_SALES_TOTAL | CUST2_SALES_TOTAL | CUST3_SALES_TOTAL | CUST4_SALES_TOTAL |
---|---|---|---|---|
west | 545 | (null) | 1382 | (null) |
central | (null) | (null) | 433 | 584 |
north | 1128 | 875 | (null) | 987 |
south | (null) | 881 | 714 | (null) |
east | 192 | 147 | 407 | 785 |
This has concatenated both the alias from the pivot_in_clause (cust1) and the alias from the pivot_clause (sales_total), separating them by an underscore: cust1_sales_total.
Perform Multiple Aggregations
In the examples so far, we performed a single aggregation on a single column: a SUM on the different customer_id combinations. We can expand this in the SQL PIVOT query to do more if we want to.
We can add a second aggregate function to our PIVOT query. For example, let’s say along with showing the SUM, we want to show the COUNT of records in each group. The query would look like this:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount),
8 COUNT(sale_amount)
9 FOR customer_id
10 IN (1, 2, 3, 4)
11);
If we run this query, we get an error:
1ORA-00918: column ambiguously defined
200918. 00000 - "column ambiguously defined"
3*Cause:
4*Action:
To resolve this, we need to give the SUM and COUNT clauses an alias:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount) AS sum_sales,
8 COUNT(sale_amount) AS count_sales
9 FOR customer_id
10 IN (1, 2, 3, 4)
11);
The results of this query are:
location | 1_SUM_SALES | 1_COUNT_SALES | 2_SUM_SALES | 2_COUNT_SALES | 3_SUM_SALES | 3_COUNT_SALES | 4_SUM_SALES | 4_COUNT_SALES |
---|---|---|---|---|---|---|---|---|
west | 545 | 1 | (null) | 0 | 1382 | 3 | (null) | 0 |
central | (null) | 0 | (null) | 0 | 433 | 2 | 584 | 1 |
north | 1128 | 2 | 875 | 1 | (null) | 0 | 987 | 1 |
south | (null) | 0 | 881 | 2 | 714 | 1 | (null) | 0 |
east | 192 | 1 | 147 | 1 | 407 | 1 | 785 | 2 |
You can see that for each value of customer_id, the SUM of the sales is shown, and then the COUNT. The column aliases have included both the customer_id and the alias of either sum_sales or count_sales. This allows us to use Oracle PIVOT on multiple columns.
Group By Multiple Columns
Another way to use multiple columns in your PIVOT query is to group by multiple columns. So far we’ve only been grouping by a customer_id. What if you wanted to group by customer_id and category?
You can do that
1SELECT *
2FROM (
3 SELECT location, prod_category, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount)
8 FOR (customer_id, prod_category)
9 IN (
10 (1, 'furniture') AS furn1,
11 (2, 'furniture') AS furn2,
12 (1, 'electronics') AS elec1,
13 (2, 'electronics') AS elec2
14 )
15);
In this query, the FOR clause includes two columns: the customer_id and the prod_category. This prod_category was also added into the SELECT subquery at the start as it wasn’t included earlier.
Now we’ve specified the two columns in the FOR clause, we need to add both columns into the IN clause. Each of the IN criteria has now been included in brackets, which specifies a value for the customer_id and prod_category (e.g. 1, ‘furniture’). We’ve excluded many of the other records just for this example.
We’ve also given them a column alias to make them easier to read.
If you run this query, this is the result you’ll get:
location | FURN1 | FURN2 | ELEC1 | ELEC2 |
---|---|---|---|---|
west | 545 | (null) | (null) | (null) |
central | (null) | (null) | (null) | (null) |
north | (null) | 875 | 1128 | (null) |
south | (null) | 503 | (null) | 378 |
east | 192 | (null) | (null) | (null) |
The results show the SUM of sales for the furniture and electronics categories and for customer_id 1 and 2. The groups here can be customised in your IN clause to whatever you need.
PIVOT Example with XML
The PIVOT keyword allows you to show your results in an XML format. This is as simple as adding the XML keyword after the PIVOT keyword. Using our earlier example, we can display the output in an XML format.
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT XML (
7 SUM(sale_amount) AS sales_total
8 FOR customer_id
9 IN (1, 2, 3, 4)
10);
If we just add the XML keyword and run this query, we get this error:
1ORA-00905: missing keyword
200905. 00000 - "missing keyword"
3*Cause:
4*Action:
This is because we can’t specify the values inside the IN clause. We’ll need to either use a subquery or use the keyword ANY.
XML with ANY
Here's an example of a query with the XML parameter with the ANY keyword.
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT XML (
7 SUM(sale_amount) AS sales_total
8 FOR customer_id
9 IN (ANY)
10);
Using the keyword ANY means that the grouping is performed on all values of customer_id. The result of this query is:
LOCATION | CUSTOMER_ID_XML |
---|---|
central | (XMLTYPE) |
east | (XMLTYPE) |
north | (XMLTYPE) |
south | (XMLTYPE) |
west | (XMLTYPE) |
By default, SQL Developer will show the value of “XMLTYPE” for these XML columns. It will return the full value if you run it as a script (instead of a grid), or if it’s run as part of an application. If you want to see this in the grid, you can change a setting in SQL Developer:
- Open the Tools menu and click Preferences.
- Expand the Database section and click on Advanced
- Check the box for “Display XML Value in Grid”
- Click OK
Now you can rerun the query and the full value is displayed.
LOCATION | CUSTOMER_ID_XML |
---|---|
central | <PivotSet><item><column name = "CUSTOMER_ID">3</column>... |
east | <PivotSet><item><column name = "CUSTOMER_ID">1</column>... |
north | <PivotSet><item><column name = "CUSTOMER_ID">1</column> |
south | <PivotSet><item><column name = "CUSTOMER_ID">2</column> |
west | <PivotSet><item><column name = "CUSTOMER_ID">1</column> |
The data is translated into an XML format. The values above have been shortened to clean up the table, and a full value looks like this:
1<PivotSet><item><column name = "CUSTOMER_ID">3</column><column name = "SALES_TOTAL">433</column></item><item><column name = "CUSTOMER_ID">4</column><column name = "SALES_TOTAL">584</column></item></PivotSet>
This can be useful if your application is processing XML. However, it makes it a little hard to read compared to a normal grid.
XML with Subquery
Another way to define the columns in a pivot query that uses an XML output is to use a subquery. The query we had earlier used the keyword ANY to define the columns in the IN clause.
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT XML (
7 SUM(sale_amount) AS sales_total
8 FOR customer_id
9 IN (ANY)
10);
Rather than use the ANY keyword, you can use a subquery to define which column values you want to include.
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT XML (
7 SUM(sale_amount) AS sales_total
8 FOR customer_id
9 IN (
10 SELECT customer_id
11 FROM cust_sales_category
12 )
13);
The subquery in the IN clause is:
1SELECT customer_id FROM cust_sales_category
This will find all of the customer_id values from this table. It has the same result as the ANY keyword.
Using a subquery means you have more control over what groups are used. You can add a WHERE clause to this subquery to only show certain customer_id values:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT XML (
7 SUM(sale_amount) AS sales_total
8 FOR customer_id
9 IN (
10 SELECT customer_id
11 FROM cust_sales_category
12 WHERE customer_id <= 3
13 )
14);
This will create column groups for all customer_ids less than or equal to 3.
I haven’t used this XML output much at all, but if you need to use it in your query or application, then it’s quite useful.
Dynamically Specifying Columns
In a normal pivot query, or one that outputs the data as columns and not as XML, you had to specify which columns you wanted to include in the group:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount)
8 FOR customer_id
9 IN (1, 2, 3, 4)
10);
If you want to group by all of these values, is there a way to say that, rather than list all of the values individually?
Unfortunately, there isn’t. You can’t use the ANY keyword or use a subquery in the IN clause unless you have the XML keyword applied.
This query will return an error:
1SELECT *
2FROM (
3 SELECT location, customer_id, sale_amount
4 FROM cust_sales_category
5)
6PIVOT (
7 SUM(sale_amount)
8 FOR customer_id
9 IN (ANY)
10);
1ORA-00936: missing expression
200936. 00000 - "missing expression"
3*Cause:
4*Action:
Transposing Columns into Rows with UNPIVOT
The SQL PIVOT keyword we’ve looked at above will translate rows into columns. Oracle offers an UNPIVOT keyword, which performs the opposite. It transforms columns into rows.
The syntax of the SQL UNPIVOT keyword is:
1SELECT columns
2FROM table
3UNPIVOT [INCLUDE|EXCLUDE NULLS] (
4 unpivot_clause,
5 unpivot_for_clause,
6 unpivot_in_clause
7)
8WHERE criteria;
It has a similar structure to the PIVOT keyword, with a few differences:
- It allows you to treat NULL values in a specific way.
- It does not include the XML keyword.
- It does not de-aggregate rows, as the query has no knowledge of the data behind the aggregation.
There are a few clauses with the UNPIVOT keyword:
- unpivot_clause: This specifies the name of the column for each of the column values from the pivoted data.
- unpivot_for_clause: This specifies the name of the column for the number values shown in the pivoted data.
- unpivot_in_clause: This specifies the list of pivoted columns.
If this sounds confusing, some examples will help to explain it better.
A Simple UNPIVOT Example
To demonstrate the SQL UNPIVOT keyword, we need to have some pivoted data first. We could supply a subquery, but then the UNPIVOT query would be very confusing. So, let’s create a view that shows pivoted data.
We can create a view called pivoted_sales.
1CREATE VIEW pivoted_sales AS
2SELECT *
3FROM (
4 SELECT location, customer_id, sale_amount
5 FROM cust_sales_category
6)
7PIVOT (
8 SUM(sale_amount)
9 FOR customer_id
10 IN (
11 1 AS cust1,
12 2 AS cust2,
13 3 AS cust3,
14 4 AS cust4
15 )
16);
After the view is created, we can select from it to check the data:
1SELECT *
2FROM pivoted_sales;
LOCATION | CUST1 | CUST2 | CUST3 | CUST4 |
---|---|---|---|---|
west | 545 | (null) | 1382 | (null) |
central | (null) | (null) | 433 | 584 |
north | 1128 | 875 | (null) | 987 |
south | (null) | 881 | 714 | (null) |
east | 192 | 147 | 407 | 785 |
Now let’s look at an example of the UNPIVOT keyword.
1SELECT *
2FROM pivoted_sales
3UNPIVOT (
4 total_sales
5 FOR customer_id
6 IN (cust1, cust2, cust3, cust4)
7);
The results of this query are:
LOCATION | CUSTOMER_ID | TOTAL_SALES |
---|---|---|
west | CUST1 | 545 |
west | CUST3 | 1382 |
central | CUST3 | 433 |
central | CUST4 | 584 |
north | CUST1 | 1128 |
north | CUST2 | 875 |
north | CUST4 | 987 |
south | CUST2 | 881 |
south | CUST3 | 714 |
east | CUST1 | 192 |
east | CUST2 | 147 |
east | CUST3 | 407 |
east | CUST4 | 785 |
In this query:
- The unpivot_clause is “total_sales”, which in the results is the name of the last column. This column contains the number values or the SUM of the sales for each customer and region.
- The unpivot_for_clause is “customer_id”. This is the name given to the second column, which is populated from the different column headers in the pivoted data.
- The unpivot_in_clause is each of the column headers from the pivoted data. The columns from the underlying table are cust1, cust2, cust3, and cust4, and these are specified in the IN clause.
Handling NULL Values in UNPIVOT
In the output above, you’ll notice that there are 13 rows returned. However, there are 5 locations and 4 customers. Shouldn’t the query have returned 20 results, which is 5 x 4 and therefore all combinations of locations and customers? Why didn’t the query do that?
It’s because the data we selected from had no values for some combinations of location and customer. The location of "west" had returned rows for customer 1 and 3, so that would mean there are no rows for customer 2 and 4. Let’s query the table and check.
1SELECT *
2FROM pivoted_sales;
LOCATION | CUST1 | CUST2 | CUST3 | CUST4 |
---|---|---|---|---|
west | 545 | (null) | 1382 | (null) |
central | (null) | (null) | 433 | 584 |
north | 1128 | 875 | (null) | 987 |
south | (null) | 881 | 714 | (null) |
east | 192 | 147 | 407 | 785 |
You can see for the row with the location of “west”, there is a value for cust1 and cust3, but not cust2 or cust4.
The Oracle UNPIVOT keyword does not show results where the underlying data is NULL. There are 7 NULL values in this table, so of the 20 possible values, these 7 weren’t shown, resulting in only 13 values.
This behaviour is the default, but it can be changed. If you add the keyword INCLUDE NULLS after the UNPIVOT keyword, you’ll get records with NULL values in your results.
1SELECT *
2FROM pivoted_sales
3UNPIVOT INCLUDE NULLS (
4 total_sales
5 FOR customer_id
6 IN (cust1, cust2, cust3, cust4)
7);
LOCATION | CUSTOMER_ID | TOTAL_SALES |
---|---|---|
west | CUST1 | 545 |
west | CUST2 | (null) |
west | CUST3 | 1382 |
west | CUST4 | (null) |
central | CUST1 | (null) |
central | CUST2 | (null) |
central | CUST3 | 433 |
central | CUST4 | 584 |
north | CUST1 | 1128 |
north | CUST2 | 875 |
north | CUST3 | (null) |
north | CUST4 | 987 |
south | CUST1 | (null) |
south | CUST2 | 881 |
south | CUST3 | 714 |
south | CUST4 | (null) |
east | CUST1 | 192 |
east | CUST2 | 147 |
east | CUST3 | 407 |
east | CUST4 | 785 |
The result shows 20 rows. The original 13 rows are included, as well as the 7 rows that have NULL value for the total_sales. The INCLUDE NULLS keyword that was used here has caused these values to be included.
The other keyword that can be used here is EXCLUDE NULLS. This is the same as the default behaviour, similar to many other parameters such as ORDER BY ASC being the default. If you want something to specifically exclude NULL values and not rely on the default, then it’s best to specify it:
1SELECT *
2FROM pivoted_sales
3UNPIVOT EXCLUDE NULLS (
4 total_sales
5 FOR customer_id
6 IN (cust1, cust2, cust3, cust4)
7);
So that’s how you can handle NULL values in an UNPIVOT query.
Aliases and UNPIVOT
Just like with the PIVOT keyword, we can apply aliases to our data with UNPIVOT. However, we can only do this with the pivot_in_clause. For example:
1SELECT *
2FROM pivoted_sales
3UNPIVOT (
4 total_sales
5 FOR customer_id
6 IN (
7 cust1 AS 'cust 1 sales',
8 cust2 AS 'cust 2 sales',
9 cust3 AS 'cust 3 sales',
10 cust4 AS 'cust 4 sales'
11 )
12);
The result of this query is:
LOCATION | CUSTOMER_ID | TOTAL_SALES |
---|---|---|
west | cust 1 sales | 545 |
west | cust 3 sales | 1382 |
central | cust 3 sales | 433 |
central | cust 4 sales | 584 |
north | cust 1 sales | 1128 |
north | cust 2 sales | 875 |
north | cust 4 sales | 987 |
south | cust 2 sales | 881 |
south | cust 3 sales | 714 |
east | cust 1 sales | 192 |
east | cust 2 sales | 147 |
east | cust 3 sales | 407 |
east | cust 4 sales | 785 |
The values in the customer_id column have been translated to the values specified. They need to be included in single quotes. If they aren’t, you’ll get an error, just like this:
1SELECT *
2FROM pivoted_sales
3UNPIVOT (
4 total_sales
5 FOR customer_id
6 IN (
7 cust1 AS cust_1_sales,
8 cust2 AS cust_2_sales,
9 cust3 AS cust_3_sales,
10 cust4 AS cust_4_sales
11 )
12);
1ORA-56901: non-constant expression is not allowed for pivot|unpivot values
256901. 00000 - "non-constant expression is not allowed for pivot|unpivot values"
3*Cause: Attempted to use non-constant expression for pivot|unpivot values.
4*Action: Use constants for pivot|unpivot values.
5
6Error at Line: 339 Column: 12
This ORA-56901 error (non-constant expression is not allowed for pivot|unpivot values) means you have to use a constant value for your aliases. You can enclose your aliases in single quotes to fix this error.
Can You Use Oracle PIVOT Without an Aggregate?
No, SQL PIVOT only works with aggregate functions. If you wanted to transpose rows to columns without losing the detail in your data, you could try a MAX or MIN function.
Let’s say you had a list of bugs in a bug_result table and different results for each stage:
BUG_ID | STAGE | RESULT |
---|---|---|
1 | Dev | Pass |
1 | Test | Pass |
1 | UAT | Fail |
2 | Dev | Pass |
2 | Test | Fail |
3 | Dev | Pass |
3 | Test | Fail |
3 | UAT | Pass |
If you wanted to use PIVOT on this data set, you can do it using the MAX function on the RESULT column.
1SELECT *
2FROM bug_result
3PIVOT (
4 MAX(result)
5 FOR stage
6 IN ('Dev', 'Test', 'UAT')
7);
The MAX and MIN functions work just as well with VARCHAR2 or text values as they do with dates and numbers. So if you run this query on your database, you’ll get a result like this:
BUG_ID | Dev | Test | UAT |
---|---|---|---|
1 | Pass | Pass | Fail |
2 | Pass | Fail | (null) |
3 | Pass | Fail | Pass |
So that’s how you can generate a pivot table in Oracle SQL without aggregates. It’s great for summarising results like this that don’t have a number value.
Conclusion
The Oracle PIVOT and UNPIVOT features are powerful and are a very useful way to transform your data. They are often used with numerical values to aggregate data but can be used with text and date values as well.
It includes an XML feature to export your data in an XML format. You can also add aliases to the PIVOT rows and columns, add a WHERE clause, and perform multiple aggregations and groups.
Take some time to practice using this handy SQL feature and you’ll be better able to work with your data.
What feature about PIVOT and UNPIVOT do you find most useful? Let me know in the comments below.
