
Easily transpose columns as rows in Oracle 11G+, with the UNPIVOT clause in SQL.
A typical scenario:
Customer data is validated against a set of audit checks and the validation results are stored against the Customer Id as Y/N flags. The checks in this example are:
Is name populated? Is name valid?
Is DOB populated? Is DOB valid?
The table:
SQL> desc validation_output Name Null? Type ----------------------- -------- ----------- CUSTID NUMBER(4) NAME_POPULATED VARCHAR2(1) NAME_VALID VARCHAR2(1) DOB_POPULATED VARCHAR2(1) DOB_VALID VARCHAR2(1)
The data:
SQL> select *
2 from validation_output;
CUSTID NAME_POPULATED NAME_VALID DOB_POPULATED DOB_VALID
---------- -------------- ---------- ------------- ---------
100 Y N Y Y
101 N N N N
The requirement is to transpose the columns NAME_POPULATED, NAME_VALID etc as rows — that is, display the result in this form:
CUSTID CHECK_NAME CHECK_RESULT
---------- ---------------- ------------
100 NAME_POPULATED Y
100 NAME_VALID N
100 DOB_POPULATED Y
100 DOB_VALID Y
101 NAME_POPULATED N
101 NAME_VALID N
101 DOB_POPULATED N
101 DOB_VALID N
8 rows selected.
Pre 11G: Using DECODE
Before 11G, the popular solution to the unpivot problem was using a combination of DECODE and the LEVEL pseudocolumn in a hierarchical query on DUAL table.
The SQL:
select custid,
decode(unpivot_row, 1, 'NAME_POPULATED',
2, 'NAME_VALID',
3, 'DOB_POPULATED',
4, 'DOB_VALID',
'N/A') AS check_name,
decode(unpivot_row, 1, NAME_POPULATED,
2, NAME_VALID,
3, DOB_POPULATED,
4, DOB_VALID,
'N/A') AS check_result
FROM validation_output,
(select level as unpivot_row
from dual connect by level <= 4);
When run:
SQL> select custid,
2 decode(unpivot_row, 1, 'NAME_POPULATED',
3 2, 'NAME_VALID',
4 3, 'DOB_POPULATED',
5 4, 'DOB_VALID',
6 'N/A') AS check_name,
7 decode(unpivot_row, 1, NAME_POPULATED,
8 2, NAME_VALID,
9 3, DOB_POPULATED,
10 4, DOB_VALID,
11 'N/A') AS check_result
12 FROM validation_output,
13 (select level as unpivot_row
14 from dual connect by level <= 4);
CUSTID CHECK_NAME CHECK_RESULT
---------- ---------------- ------------
100 NAME_POPULATED Y
101 NAME_POPULATED N
100 NAME_VALID N
101 NAME_VALID N
100 DOB_POPULATED Y
101 DOB_POPULATED N
100 DOB_VALID Y
101 DOB_VALID N
8 rows selected.
In 11G: Using UNPIVOT
With the UNPIVOT syntax, the same query can be written in a more compact, readable form.
The SQL:
select *
from validation_output
unpivot
(
check_result
for check_name in (
NAME_POPULATED
,NAME_VALID
,DOB_POPULATED
,DOB_VALID)
);
When run:
SQL> select *
2 from validation_output
3 unpivot
4 (
5 check_result
6 for check_name in (
7 NAME_POPULATED
8 ,NAME_VALID
9 ,DOB_POPULATED
10 ,DOB_VALID)
11 );
CUSTID CHECK_NAME CHECK_RESULT
---------- ---------------- ------------
100 NAME_POPULATED Y
100 NAME_VALID N
100 DOB_POPULATED Y
100 DOB_VALID Y
101 NAME_POPULATED N
101 NAME_VALID N
101 DOB_POPULATED N
101 DOB_VALID N
8 rows selected.
Notes
1. Aliasing: Column Names Can Be Modified With UNPIVOT
UNPIVOT supports the renaming of column names in the process of converting them to row values.
SQL> select *
2 from validation_output
3 unpivot
4 (
5 check_result
6 for check_name in (
7 NAME_POPULATED as 'NAME_POP'
8 ,NAME_VALID as 'NAME_VAL'
9 ,DOB_POPULATED as 'DOB_POP'
10 ,DOB_VALID as 'DOB_VAL')
11 );
CUSTID CHECK_NAME CHECK_RESULT
---------- ---------------- ------------
100 NAME_POP Y
100 NAME_VAL N
100 DOB_POP Y
100 DOB_VAL Y
101 NAME_POP N
101 NAME_VAL N
101 DOB_POP N
101 DOB_VAL N
8 rows selected.
2. NULL Handling: Exclude NULL By Default
By default, any NULLs in the data are excluded when you transpose columns as rows using UNPIVOT.
Let’s blank out a value and try it out…
SQL> update validation_output
2 set name_valid = ''
3 where custid = 101;
1 row updated.
SQL> select *
2 from validation_output;
CUSTID NAME_POPULATED NAME_VALID DOB_POPULATED DOB_VALID
---------- -------------- ---------- ------------- ---------
100 Y N Y Y
101 N N N
Now use the same SQL that was run earlier:
SQL> select *
2 from validation_output
3 unpivot
4 (
5 check_result
6 for check_name in (
7 NAME_POPULATED
8 ,NAME_VALID
9 ,DOB_POPULATED
10 ,DOB_VALID)
11 );
CUSTID CHECK_NAME CHECK_RESULT
---------- ---------------- ------------
100 NAME_POPULATED Y
100 NAME_VALID N
100 DOB_POPULATED Y
100 DOB_VALID Y
101 NAME_POPULATED N
101 DOB_POPULATED N
101 DOB_VALID N
7 rows selected.
You can see that the SQL did not select the row that had a null value.
To enforce the selection of NULLs, use the clause "INCLUDE NULLS".
SQL> select *
2 from validation_output
3 unpivot include nulls
4 (
5 check_result
6 for check_name in (
7 NAME_POPULATED
8 ,NAME_VALID
9 ,DOB_POPULATED
10 ,DOB_VALID)
11 );
CUSTID CHECK_NAME CHECK_RESULT
---------- ---------------- ------------
100 NAME_POPULATED Y
100 NAME_VALID N
100 DOB_POPULATED Y
100 DOB_VALID Y
101 NAME_POPULATED N
101 NAME_VALID
101 DOB_POPULATED N
101 DOB_VALID N
8 rows selected.
3. DataTypes Must Match!
UNPIVOT expects all columns being used in the UNPIVOT clause to be of the same datatype.
To test this, let’s add a new check PHONE_VALID to the table, but instead of making it a VARCHAR2(1) with Y/N values, define it as NUMBER(1) presumably with 0/1 values.
SQL> alter table validation_output 2 add (phone_valid number(1)); Table altered. SQL> desc validation_output Name Null? Type ----------------------- -------- ----------- CUSTID NUMBER(4) NAME_POPULATED VARCHAR2(1) NAME_VALID VARCHAR2(1) DOB_POPULATED VARCHAR2(1) DOB_VALID VARCHAR2(1) PHONE_VALID NUMBER(1)
In the UNPIVOT clause, include PHONE_VALID too:
select *
from validation_output
unpivot
(
check_result
for check_name in (
NAME_POPULATED
,NAME_VALID
,DOB_POPULATED
,DOB_VALID
,PHONE_VALID)
);
When run:
SQL> select *
2 from validation_output
3 unpivot
4 (
5 check_result
6 for check_name in (
7 NAME_POPULATED
8 ,NAME_VALID
9 ,DOB_POPULATED
10 ,DOB_VALID
11 ,PHONE_VALID)
12 );
,PHONE_VALID)
*
ERROR at line 11:
ORA-01790: expression must have same datatype as
corresponding expression
Oracle throws an error because the datatypes of all columns in the UNPIVOT list are not the same:
ORA-01790: expression must have same datatype as
corresponding expression
Further Reading
A nifty row generation trick that using UNPIVOT: Flexible Row Generator.
The more popular type of transpose — rows to columns — using Oracle’s PIVOT clause.








