The PIVOT Clause is Only for Aggregates
September 4th, 2012 by Stewart Bryson
We briefly interrupt the transactional schemas series for this important message: the PIVOT clause of SELECT is only for aggregates! When Oracle added the PIVOT and UNPIVOT operators in Database 11g, at first, I was hopeful. It certainly scratched an itch that had been around in the Oracle DW space (and especially… the ETL arena) for some time. But once I got my hands on it for a test drive, I was bit disappointed. You see… it only works with aggregate functions. Let me give a quick demonstration. Say we want to look at the Oracle data dictionary and query all the columns for a particular table, we would write the following query:
SQL> SELECT column_name 2 FROM user_tab_columns 3 WHERE table_name='SALES'; COLUMN_NAME -------------------------------- PROD_ID CUST_ID TIME_ID CHANNEL_ID PROMO_ID QUANTITY_SOLD AMOUNT_SOLD 7 rows selected. SQL>
But how would we display this in tabular format? You know… instead of returning 7 rows, return one row with 7 columns. Doing this prior to Oracle 11g, we basically had three options: using DECODE, using CONNECT BY, or using the SQL MODEL clause. At one time or another, I have written versions of all of these. The attached link shows Tom Kyte talking through several approaches, and what’s clear from all of them is this: it’s way too complicated to do in SQL what Excel can do for us quite simply. What we needed was a PIVOT clause… and then “Presto!”, we got one.
But it wasn’t what I expected. The Oracle PIVOT clause is only for aggregate functions… so useful in generating reports with measures in them. For instance, I could easily do a report on how many instances of a particular column exist, but I couldn’t seemingly provide a query that actually shows the literal value. Let me demonstrate:
SQL> SELECT *
2 FROM (
3 SELECT column_name
4 FROM user_tab_columns
5 WHERE table_name='SALES'
6 )
7 pivot
8 ( count(*)
9 FOR column_name IN (
10 'PROD_ID' AS PROD_ID,
11 'CUST_ID' AS CUST_ID,
12 'TIME_ID' AS TIME_ID,
13 'CHANNEL_ID' AS CHANNEL_ID,
14 'PROMO_ID' AS PROMO_ID,
15 'QUANTITY_SOLD' AS QUANTITY_SOLD,
16 'AMOUNT_SOLD' AS AMOUNT_SOLD
17 )
18 );
PROD_ID | CUST_ID | TIME_ID | CHANNEL_ID | PROMO_ID | QUANTITY_SOLD | AMOUNT_SOLD
---------- | ---------- | ---------- | ---------- | ---------- | ------------- | -----------
1 | 1 | 1 | 1 | 1 | 1 | 1
1 row selected.
SQL>
Following directly after the PIVOT keyword, we have to provide an aggregate function: COUNT, SUM, AVG, etc. So if I just wanted to display the actual value of the COLUMN_NAME attribute at the top of the chart, this doesn’t seem to be an option:
SQL> SELECT *
2 FROM (
3 SELECT column_name
4 FROM user_tab_columns
5 WHERE table_name='SALES'
6 )
7 pivot
8 ( column_name
9 FOR column_name IN (
10 'PROD_ID' AS PROD_ID,
11 'CUST_ID' AS CUST_ID,
12 'TIME_ID' AS TIME_ID,
13 'CHANNEL_ID' AS CHANNEL_ID,
14 'PROMO_ID' AS PROMO_ID,
15 'QUANTITY_SOLD' AS QUANTITY_SOLD,
16 'AMOUNT_SOLD' AS AMOUNT_SOLD
17 )
18 );
( column_name
*
ERROR at line 8:
ORA-56902: expect aggregate function inside pivot operation
SQL>
Does anyone see the solution? I didn’t at first, until Pete Tamisin gave me the answer today. When I hear “aggregate function”, my mind usually goes straight toward the NUMBER datatype. Perhaps that’s from years of working with BI Tools. But there are plenty of aggregate functions that accept any datatype… even VARCHARS. For instance… the MAX function. Yep… that’s the answer:
SQL> SELECT * 2 FROM ( 3 SELECT column_name 4 FROM user_tab_columns 5 WHERE table_name='SALES' 6 ) 7 pivot 8 ( MAX(column_name) 9 FOR column_name IN ( 10 'PROD_ID' AS PROD_ID, 11 'CUST_ID' AS CUST_ID, 12 'TIME_ID' AS TIME_ID, 13 'CHANNEL_ID' AS CHANNEL_ID, 14 'PROMO_ID' AS PROMO_ID, 15 'QUANTITY_SOLD' AS QUANTITY_SOLD, 16 'AMOUNT_SOLD' AS AMOUNT_SOLD 17 ) 18 ); PROD_ID | CUST_ID | TIME_ID | CHANNEL_ID | PROMO_ID | QUANTITY_SOLD | AMOUNT_SOLD ---------- | ---------- | ---------- | ---------- | ---------- | -------------- | ------------ PROD_ID | CUST_ID | TIME_ID | CHANNEL_ID | PROMO_ID | QUANTITY_SOLD | AMOUNT_SOLD 1 row selected. SQL>
Of course… you have to ensure that the query is one for which only a single row is returned for each column you want to generate from a row.


September 4th, 2012 at 9:31 pm
“The PIVOT Clause is Only for Aggregates” – yes, but the “prior to 11g option decode” you mentioned is also only for aggregates ?
September 5th, 2012 at 8:10 am
that is not true of course
September 5th, 2012 at 8:47 am
Thank you for the trick.
LISTAGG could be a great alternative if the query returns more than one row for each generated column.
September 5th, 2012 at 10:46 am
@Jerome: LISTAGG would give me a delimited list, which is not exactly what I was going for here… unless there was something else you have in mind.
September 7th, 2012 at 12:42 pm
For your particular case, MAX is perfect. LISTAGG is only a suggestion when you deal with more than 1 row and the varchar2 datatype(in reaction to your last sentence).
February 8th, 2013 at 11:10 pm
Is there any way of generating this list without actually having to type out all of the column names? Your last query is essentially the same as “select (your list of columns/aliases in the “IN” clause) from dual”.
I would like to print out the columns without having to type them out in then “IN” clause. Is this possible?